From 68dde2cfafe70a494a102f42523e0cf2b35122bd Mon Sep 17 00:00:00 2001 From: mavxa Date: Wed, 16 Sep 2026 06:33:11 +0300 Subject: [PATCH] simplify module3 venue workflow --- module3/README.md | 10 +++++----- module3/SIMPLE.md | 28 +++++++++++++++++++------- module3/prepare_dataset.py | 2 +- module3/simple_module_b.py | 33 ++++++++++++++++++------------- module3/simple_prepare_dataset.py | 14 +++++++++---- module3/train.py | 4 ++-- 6 files changed, 58 insertions(+), 33 deletions(-) diff --git a/module3/README.md b/module3/README.md index 6c3ec78..4a8eef5 100644 --- a/module3/README.md +++ b/module3/README.md @@ -130,11 +130,11 @@ wrench Размечайте каждый видимый экземпляр: в штатной сцене две карточки `pliers`, одна `hammer` и одна `wrench`. Классы `box` и `background` не создавайте. -В Roboflow создайте `Object Detection` project, загрузите `dataset/raw/*.jpg`, -разметьте кадры, сделайте split 70/20/10 и экспортируйте в формате YOLO11. -Распакуйте экспорт в `module3/training`: там должны появиться `data.yaml` и -каталоги `train`, `valid`, `test`. Не переставляйте номера классов в `data.yaml`: -они назначаются Roboflow и должны совпадать с ID внутри label-файлов. +В Label Studio создайте проект `Object Detection with Bounding Boxes`, загрузите +`dataset/raw/*.jpg`, разметьте кадры и экспортируйте в формате YOLO. Простой +порядок экспорта, разделения 70/20/10 и обучения описан в [SIMPLE.md](SIMPLE.md). +Не меняйте порядок строк в экспортированном `classes.txt`: номера строк должны +совпадать с ID внутри label-файлов. Локально в `dataset/raw` могут лежать собранные кадры, но Git их намеренно игнорирует: после клонирования датасет нужно перенести отдельно или собрать diff --git a/module3/SIMPLE.md b/module3/SIMPLE.md index c339855..586b62d 100644 --- a/module3/SIMPLE.md +++ b/module3/SIMPLE.md @@ -10,13 +10,23 @@ ffmpeg -i video.mp4 -vf fps=2 module3/dataset/raw/frame_%05d.jpg ## Разметка ```bash -python3 -m venv ~/.venvs/xanylabeling -~/.venvs/xanylabeling/bin/pip install "x-anylabeling-cvhub[cpu]" -~/.venvs/xanylabeling/bin/xanylabeling module3/dataset/raw --labels module3/dataset/classes.txt --autosave +label-studio start ``` -Рамки рисуются клавишей `R`, следующий кадр — `D`. Экспорт: `File -> Export -Annotations -> Export YOLO Annotations -> Detection`. Затем: +Откройте `http://localhost:8080`, создайте проект и загрузите кадры через +`Data Import`. В `Labeling Setup` выберите `Computer Vision -> Object Detection +with Bounding Boxes` и создайте классы `hammer`, `pliers`, `wrench`. + +После разметки выберите `Export -> YOLO` и распакуйте архив так, чтобы получилось: + +```text +module3/dataset/label-studio/ +├── classes.txt +├── images/ +└── labels/ +``` + +Затем: ```bash python3 module3/simple_prepare_dataset.py @@ -41,5 +51,9 @@ module3/.venv/bin/python module3/simple_module_b.py ``` Ввод предмета нужен только для показа. Скрипт выводит YOLO в окне OpenCV, через -5 секунд открывает захват, а через 2 секунды закрывает его. Выход — `Q` или -`Esc`. +5 секунд открывает захват, а через 2 секунды переводит его в положение +`take_cube`. В терминале раз в секунду показываются детекции YOLO. Выход — `Q` +или `Esc`. + +Камера реального RMC1: `/RMC1/arm95/svcam/right/image`. Для проверки в Webots +замените константу `CAMERA` на `/RMC1/arm95/camera_gripper/image_color`. diff --git a/module3/prepare_dataset.py b/module3/prepare_dataset.py index e8f92ef..2fef28e 100644 --- a/module3/prepare_dataset.py +++ b/module3/prepare_dataset.py @@ -8,7 +8,7 @@ HERE = Path(__file__).resolve().parent def arguments(): parser = argparse.ArgumentParser() - parser.add_argument("--dataset", default="training", help="Каталог Roboflow export") + parser.add_argument("--dataset", default="training", help="Каталог YOLO-датасета") return parser.parse_args() diff --git a/module3/simple_module_b.py b/module3/simple_module_b.py index 5aa1622..fefb7c7 100644 --- a/module3/simple_module_b.py +++ b/module3/simple_module_b.py @@ -14,7 +14,7 @@ from ultralytics import YOLO HERE = Path(__file__).resolve().parent -CAMERA = "/RMC1/arm95/camera_gripper/image_color" +CAMERA = "/RMC1/arm95/svcam/right/image" GRIPPER = "/RMC1/arm95/gripper_trajectory_controller/joint_trajectory" @@ -26,10 +26,10 @@ class Demo(Node): self.gripper = self.create_publisher(JointTrajectory, GRIPPER, 10) self.create_subscription(Image, CAMERA, self.on_image, qos_profile_sensor_data) self.started = None - self.opened = False - self.closed = False + self.last_log = -1 + self.step = 0 - def move_gripper(self, position): + def move_gripper(self, position, state): message = JointTrajectory() message.joint_names = ["left_joint", "right_joint"] point = JointTrajectoryPoint() @@ -37,6 +37,7 @@ class Demo(Node): point.time_from_start = Duration(sec=1) message.points = [point] self.gripper.publish(message) + print(f"[GRIPPER] {state}: {position}", flush=True) def on_image(self, message): frame = self.bridge.imgmsg_to_cv2(message, "bgr8") @@ -44,16 +45,19 @@ class Demo(Node): if self.started is None: self.started = time.monotonic() - elapsed = time.monotonic() - self.started + print(f"[CAMERA] {CAMERA}", flush=True) + seconds = int(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.0213) - self.closed = True - print("Захват закрыт") + if seconds != self.last_log: + print(f"[YOLO] {result.verbose().strip()}", flush=True) + self.last_log = seconds + + if seconds >= 5 and self.step == 0: + self.move_gripper(0.04, "OPEN") + self.step = 1 + elif seconds >= 7 and self.step == 1: + self.move_gripper(0.02, "TAKE_CUBE") + self.step = 2 cv2.imshow("YOLO", result.plot()) if cv2.waitKey(1) & 0xFF in (ord("q"), 27): @@ -61,7 +65,8 @@ class Demo(Node): def main(): - input("Предмет (Кисть, Нож или Резаки): ") + target = input("Предмет (Кисть, Нож или Резаки): ") + print(f"[START] {target}", flush=True) rclpy.init() node = Demo() try: diff --git a/module3/simple_prepare_dataset.py b/module3/simple_prepare_dataset.py index f8264af..d37036d 100644 --- a/module3/simple_prepare_dataset.py +++ b/module3/simple_prepare_dataset.py @@ -5,18 +5,24 @@ from pathlib import Path HERE = Path(__file__).resolve().parent -SOURCE = HERE / "dataset/raw" +SOURCE = HERE / "dataset/label-studio" +IMAGES = SOURCE / "images" LABELS = SOURCE / "labels" OUTPUT = HERE / "training_new" IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} def main(): - images = sorted(path for path in SOURCE.iterdir() if path.suffix.lower() in IMAGE_EXTENSIONS) + if not IMAGES.is_dir(): + raise SystemExit(f"Нет каталога {IMAGES}") + images = sorted(path for path in IMAGES.iterdir() if path.suffix.lower() in IMAGE_EXTENSIONS) if not images: - raise SystemExit(f"Нет кадров в {SOURCE}") + raise SystemExit(f"Нет кадров в {IMAGES}") if not LABELS.is_dir(): raise SystemExit(f"Сначала экспортируйте YOLO-разметку в {LABELS}") + classes_file = SOURCE / "classes.txt" + if not classes_file.is_file(): + raise SystemExit(f"Нет {classes_file}") random.Random(42).shuffle(images) test_count = max(1, round(len(images) * 0.1)) if len(images) >= 3 else 0 @@ -44,7 +50,7 @@ def main(): else: destination.touch() - classes = (HERE / "dataset/classes.txt").read_text(encoding="utf-8").splitlines() + classes = classes_file.read_text(encoding="utf-8").splitlines() names = "\n".join(f" {number}: {name}" for number, name in enumerate(classes)) (OUTPUT / "data.yaml").write_text( f"path: {OUTPUT}\ntrain: train/images\nval: valid/images\ntest: test/images\n\nnames:\n{names}\n", diff --git a/module3/train.py b/module3/train.py index b4aa17e..b4a7c05 100755 --- a/module3/train.py +++ b/module3/train.py @@ -31,7 +31,7 @@ def select_device(requested, torch): def arguments(): parser = argparse.ArgumentParser() - parser.add_argument("--data", default="training/data.yaml", help="data.yaml из Roboflow") + parser.add_argument("--data", default="training/data.yaml", help="YOLO data.yaml") parser.add_argument("--model", default="yolo11n.pt", help="Стартовые веса Ultralytics") parser.add_argument("--epochs", type=int, default=30) parser.add_argument( @@ -62,7 +62,7 @@ def main(): data = local_path(args.data) if not data.is_file(): - raise SystemExit(f"Не найден датасет: {data}\nРаспакуйте YOLO11 export из Roboflow в module3/dataset.") + raise SystemExit(f"Не найден датасет: {data}\nПодготовьте YOLO-датасет по module3/SIMPLE.md.") device = select_device(args.device, torch) if device == 0: backend = "ROCm" if getattr(torch.version, "hip", None) else "CUDA"