refactor(module3): reduce simple demo to one script
This commit is contained in:
+2
-3
@@ -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
@@ -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, а события выводятся в терминал и сохраняются в лог».
|
|
||||||
|
|||||||
@@ -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
@@ -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__":
|
||||||
|
|||||||
Reference in New Issue
Block a user