simplify module3 venue workflow

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