diff --git a/module3/README.md b/module3/README.md index 799886f..6c3ec78 100644 --- a/module3/README.md +++ b/module3/README.md @@ -1,8 +1,7 @@ # Модуль В: датасет и YOLO -Максимально простой сценарий на один день находится в [SIMPLE.md](SIMPLE.md). -Он не заменяет полный `module3.py`: там отдельно собраны нарезка видео, -локальная разметка, обучение через CLI, OpenCV-демо и проверка захвата. +Максимально простой сценарий находится в [SIMPLE.md](SIMPLE.md). Один +`simple_module_b.py` показывает YOLO в OpenCV и сам открывает и закрывает захват. Камера Webots: `/RMC1/arm95/camera_gripper/image_color`. На физическом RMC1 новая документация указывает `/RMC1/arm95/svcam/right/image/compressed` с типом diff --git a/module3/SIMPLE.md b/module3/SIMPLE.md index f45859e..c339855 100644 --- a/module3/SIMPLE.md +++ b/module3/SIMPLE.md @@ -1,112 +1,45 @@ -# Модуль В: простой вариант +# Простой модуль В -## 1. Нарезать видео на кадры - -Из корня репозитория: +## Кадры ```bash mkdir -p module3/dataset/raw ffmpeg -i video.mp4 -vf fps=2 module3/dataset/raw/frame_%05d.jpg ``` -Двух кадров в секунду обычно достаточно. Удали смазанные и почти одинаковые -кадры. Оставь хотя бы 30–60 нормальных изображений. - -## 2. Разметить локально - -Один раз установить X-AnyLabeling в отдельное окружение: +## Разметка ```bash python3 -m venv ~/.venvs/xanylabeling -~/.venvs/xanylabeling/bin/pip install -U uv -~/.venvs/xanylabeling/bin/uv pip install "x-anylabeling-cvhub[cpu]" +~/.venvs/xanylabeling/bin/pip install "x-anylabeling-cvhub[cpu]" +~/.venvs/xanylabeling/bin/xanylabeling module3/dataset/raw --labels module3/dataset/classes.txt --autosave ``` -Открыть кадры: - -```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: +Рамки рисуются клавишей `R`, следующий кадр — `D`. Экспорт: `File -> Export +Annotations -> Export YOLO Annotations -> Detection`. Затем: ```bash python3 module3/simple_prepare_dataset.py ``` -Скрипт создаст `module3/training_new` и при повторном запуске пересоздаст только -эту папку. - -## 3. Обучить через Ultralytics CLI +## Обучение ```bash cd module3 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 ``` -Если не хватает видеопамяти, поменяй только `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 source /opt/ros/jazzy/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` снова -открывает. Команды сохраняются в `module3/logs/simple_gripper.log`. - -Если нужно отдельно показать движение всей руки, используй выданный пример: - -```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, а события выводятся в терминал и сохраняются в лог». +Ввод предмета нужен только для показа. Скрипт выводит YOLO в окне OpenCV, через +5 секунд открывает захват, а через 2 секунды закрывает его. Выход — `Q` или +`Esc`. diff --git a/module3/simple_gripper.py b/module3/simple_gripper.py deleted file mode 100644 index 5507ef0..0000000 --- a/module3/simple_gripper.py +++ /dev/null @@ -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() diff --git a/module3/simple_module_b.py b/module3/simple_module_b.py index f1c50df..a9eaff9 100644 --- a/module3/simple_module_b.py +++ b/module3/simple_module_b.py @@ -1,60 +1,76 @@ #!/usr/bin/env python3 -import argparse -from datetime import datetime +import time from pathlib import Path 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 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(): - parser = argparse.ArgumentParser() - parser.add_argument("source", help="Фото, видео, папка с фото или 0 для веб-камеры") - parser.add_argument("--weights", default=str(HERE / "models/latest.pt")) - parser.add_argument("--conf", type=float, default=0.25) - args = parser.parse_args() - - source = int(args.source) if args.source.isdigit() else args.source - model = YOLO(args.weights) - log_path = HERE / "logs/simple_module_b.log" - 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() + input("Предмет (Кисть, Нож или Резаки): ") + rclpy.init() + node = Demo() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + cv2.destroyAllWindows() + if rclpy.ok(): + rclpy.shutdown() if __name__ == "__main__":