simplify module3 venue workflow
This commit is contained in:
+5
-5
@@ -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
@@ -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`.
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user