refactor(module3): reduce simple demo to one script

This commit is contained in:
2026-09-15 22:40:00 +03:00
parent 6c5e23c86c
commit 6bc567ea9e
4 changed files with 79 additions and 203 deletions
+2 -3
View File
@@ -1,8 +1,7 @@
# Модуль В: датасет и YOLO # Модуль В: датасет и YOLO
Максимально простой сценарий на один день находится в [SIMPLE.md](SIMPLE.md). Максимально простой сценарий находится в [SIMPLE.md](SIMPLE.md). Один
Он не заменяет полный `module3.py`: там отдельно собраны нарезка видео, `simple_module_b.py` показывает YOLO в OpenCV и сам открывает и закрывает захват.
локальная разметка, обучение через CLI, OpenCV-демо и проверка захвата.
Камера Webots: `/RMC1/arm95/camera_gripper/image_color`. На физическом RMC1 новая Камера Webots: `/RMC1/arm95/camera_gripper/image_color`. На физическом RMC1 новая
документация указывает `/RMC1/arm95/svcam/right/image/compressed` с типом документация указывает `/RMC1/arm95/svcam/right/image/compressed` с типом
+15 -82
View File
@@ -1,112 +1,45 @@
# Модуль В: простой вариант # Простой модуль В
## 1. Нарезать видео на кадры ## Кадры
Из корня репозитория:
```bash ```bash
mkdir -p module3/dataset/raw mkdir -p module3/dataset/raw
ffmpeg -i video.mp4 -vf fps=2 module3/dataset/raw/frame_%05d.jpg ffmpeg -i video.mp4 -vf fps=2 module3/dataset/raw/frame_%05d.jpg
``` ```
Двух кадров в секунду обычно достаточно. Удали смазанные и почти одинаковые ## Разметка
кадры. Оставь хотя бы 30–60 нормальных изображений.
## 2. Разметить локально
Один раз установить X-AnyLabeling в отдельное окружение:
```bash ```bash
python3 -m venv ~/.venvs/xanylabeling python3 -m venv ~/.venvs/xanylabeling
~/.venvs/xanylabeling/bin/pip install -U uv ~/.venvs/xanylabeling/bin/pip install "x-anylabeling-cvhub[cpu]"
~/.venvs/xanylabeling/bin/uv pip install "x-anylabeling-cvhub[cpu]" ~/.venvs/xanylabeling/bin/xanylabeling module3/dataset/raw --labels module3/dataset/classes.txt --autosave
``` ```
Открыть кадры: Рамки рисуются клавишей `R`, следующий кадр — `D`. Экспорт: `File -> Export
Annotations -> Export YOLO Annotations -> Detection`. Затем:
```bash
~/.venvs/xanylabeling/bin/xanylabeling module3/dataset/raw \
--labels module3/dataset/classes.txt --autosave
```
Классы уже лежат в `dataset/classes.txt`:
```text
hammer
pliers
wrench
```
Нажми `R`, обведи каждый видимый инструмент прямоугольником, выбери класс,
затем `D` для следующего кадра. После разметки выбери `File -> Export
Annotations -> Export YOLO Annotations -> Detection`, укажи
`dataset/classes.txt`. В `dataset/raw/labels` должны появиться TXT-файлы.
Подготовить train/valid/test:
```bash ```bash
python3 module3/simple_prepare_dataset.py python3 module3/simple_prepare_dataset.py
``` ```
Скрипт создаст `module3/training_new` и при повторном запуске пересоздаст только ## Обучение
эту папку.
## 3. Обучить через Ultralytics CLI
```bash ```bash
cd module3 cd module3
source .venv/bin/activate source .venv/bin/activate
yolo detect train model=yolo11n.pt data=training_new/data.yaml epochs=30 patience=7 imgsz=512 batch=8 device=0 workers=4 project=training_new/runs name=tools yolo detect train model=yolo11n.pt data=training_new/data.yaml epochs=30 patience=7 imgsz=512 batch=8 device=0 project=training_new/runs name=tools exist_ok=True
cp training_new/runs/tools/weights/best.pt models/latest.pt cp training_new/runs/tools/weights/best.pt models/latest.pt
``` ```
Если не хватает видеопамяти, поменяй только `batch=8` на `batch=4`. ## Запуск
## 4. Показать работу модели в OpenCV
На тестовых фотографиях:
```bash
python simple_module_b.py training_new/test/images
```
На исходном видео:
```bash
python simple_module_b.py /путь/к/video.mp4
```
На обычной USB-камере:
```bash
python simple_module_b.py 0
```
На изображении будут bounding box, класс и confidence. Результаты также пишутся
в `logs/simple_module_b.log`. `Q` или `Esc` закрывает окно.
## 5. Открыть и закрыть захват
После запуска симулятора и контроллеров:
```bash ```bash
source /opt/ros/jazzy/setup.bash source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash source ~/ros2_ws/install/setup.bash
python3 module3/simple_gripper.py cd ~/scripts/mvch
module3/.venv/bin/python module3/simple_module_b.py
``` ```
Скрипт сначала открывает захват, по `Enter` закрывает и ещё по `Enter` снова Ввод предмета нужен только для показа. Скрипт выводит YOLO в окне OpenCV, через
открывает. Команды сохраняются в `module3/logs/simple_gripper.log`. 5 секунд открывает захват, а через 2 секунды закрывает его. Выход — `Q` или
`Esc`.
Если нужно отдельно показать движение всей руки, используй выданный пример:
```bash
ros2 launch ar_webots_fms_ros2 moveit_api_example.launch.py
```
## Что сказать эксперту
«Я нарезал видео на кадры, разметил три класса bounding box-ами, разделил
датасет на train/valid/test и обучил YOLO11n через Ultralytics CLI. В OpenCV
показываю класс, confidence и bounding box. Управление захватом отправляет ROS 2
команду контроллеру RMC1, а события выводятся в терминал и сохраняются в лог».
-72
View File
@@ -1,72 +0,0 @@
#!/usr/bin/env python3
from datetime import datetime
from pathlib import Path
import time
import rclpy
from builtin_interfaces.msg import Duration
from control_msgs.action import GripperCommand
from rclpy.action import ActionClient
from rclpy.node import Node
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
ACTION = "/RMC1/arm95/gripper_controller/gripper_cmd"
TOPIC = "/RMC1/arm95/gripper_trajectory_controller/joint_trajectory"
HERE = Path(__file__).resolve().parent
class Gripper(Node):
def __init__(self):
super().__init__("simple_gripper")
self.client = ActionClient(self, GripperCommand, ACTION)
self.publisher = self.create_publisher(JointTrajectory, TOPIC, 10)
def set_state(self, opened):
state = "open" if opened else "closed"
if self.client.wait_for_server(timeout_sec=2.0):
goal = GripperCommand.Goal()
goal.command.position = 0.04 if opened else 0.0
goal.command.max_effort = 20.0
future = self.client.send_goal_async(goal)
rclpy.spin_until_future_complete(self, future)
handle = future.result()
if not handle.accepted:
raise RuntimeError("Контроллер отклонил команду")
result = handle.get_result_async()
rclpy.spin_until_future_complete(self, result)
else:
message = JointTrajectory()
message.joint_names = ["left_joint", "right_joint"]
point = JointTrajectoryPoint()
value = 0.04 if opened else 0.0213
point.positions = [value, value]
point.time_from_start = Duration(sec=1)
message.points = [point]
for _ in range(5):
self.publisher.publish(message)
rclpy.spin_once(self, timeout_sec=0.1)
line = f"{datetime.now().isoformat(timespec='seconds')} | gripper={state}"
print(line)
with (HERE / "logs/simple_gripper.log").open("a", encoding="utf-8") as log:
log.write(line + "\n")
time.sleep(1)
def main():
rclpy.init()
node = Gripper()
try:
node.set_state(True)
input("Захват открыт. Enter — закрыть: ")
node.set_state(False)
input("Захват закрыт. Enter — снова открыть: ")
node.set_state(True)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
+61 -45
View File
@@ -1,60 +1,76 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import time
from datetime import datetime
from pathlib import Path from pathlib import Path
import cv2 import cv2
import rclpy
from control_msgs.action import GripperCommand
from cv_bridge import CvBridge
from rclpy.action import ActionClient
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import Image
from ultralytics import YOLO from ultralytics import YOLO
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
CAMERA = "/RMC1/arm95/camera_gripper/image_color"
GRIPPER = "/RMC1/arm95/gripper_controller/gripper_cmd"
class Demo(Node):
def __init__(self):
super().__init__("simple_module_b")
self.model = YOLO(str(HERE / "models/latest.pt"))
self.bridge = CvBridge()
self.gripper = ActionClient(self, GripperCommand, GRIPPER)
self.gripper.wait_for_server()
self.create_subscription(Image, CAMERA, self.on_image, qos_profile_sensor_data)
self.started = None
self.opened = False
self.closed = False
def move_gripper(self, position):
goal = GripperCommand.Goal()
goal.command.position = position
goal.command.max_effort = 20.0
self.gripper.send_goal_async(goal)
def on_image(self, message):
frame = self.bridge.imgmsg_to_cv2(message, "bgr8")
result = self.model(frame, conf=0.25, verbose=False)[0]
if self.started is None:
self.started = time.monotonic()
elapsed = time.monotonic() - self.started
if elapsed >= 5 and not self.opened:
self.move_gripper(0.04)
self.opened = True
print("Захват открыт")
elif elapsed >= 7 and not self.closed:
self.move_gripper(0.0)
self.closed = True
print("Захват закрыт")
cv2.imshow("YOLO", result.plot())
if cv2.waitKey(1) & 0xFF in (ord("q"), 27):
rclpy.shutdown()
def main(): def main():
parser = argparse.ArgumentParser() input("Предмет (Кисть, Нож или Резаки): ")
parser.add_argument("source", help="Фото, видео, папка с фото или 0 для веб-камеры") rclpy.init()
parser.add_argument("--weights", default=str(HERE / "models/latest.pt")) node = Demo()
parser.add_argument("--conf", type=float, default=0.25) try:
args = parser.parse_args() rclpy.spin(node)
except KeyboardInterrupt:
source = int(args.source) if args.source.isdigit() else args.source pass
model = YOLO(args.weights) finally:
log_path = HERE / "logs/simple_module_b.log" node.destroy_node()
log_path.parent.mkdir(exist_ok=True)
still_images = False
if isinstance(source, str):
path = Path(source)
still_images = path.is_dir() or path.suffix.lower() in {
".jpg",
".jpeg",
".png",
".bmp",
".webp",
}
with log_path.open("a", encoding="utf-8") as log:
results = model.predict(source=source, conf=args.conf, stream=True, verbose=False)
for result in results:
now = datetime.now().isoformat(timespec="seconds")
detections = []
if result.boxes is not None:
for box in result.boxes:
name = result.names[int(box.cls.item())]
confidence = float(box.conf.item())
detections.append(f"{name} {confidence:.2f}")
line = f"{now} | {', '.join(detections) if detections else 'ничего не найдено'}"
print(line)
log.write(line + "\n")
log.flush()
cv2.imshow("YOLO: q или Esc для выхода", result.plot())
key = cv2.waitKey(0 if still_images else 1) & 0xFF
if key in (ord("q"), 27):
break
cv2.destroyAllWindows() cv2.destroyAllWindows()
if rclpy.ok():
rclpy.shutdown()
if __name__ == "__main__": if __name__ == "__main__":