231 lines
9.5 KiB
Markdown
231 lines
9.5 KiB
Markdown
|
|
# Модуль В. Система технического зрения с использованием инструментов ИИ
|
|||
|
|
|
|||
|
|
## Описание задачи
|
|||
|
|
|
|||
|
|
На специальной полке на соревновательном поле расположены N > 3 коробок с рисунками инструментов. РМК-1 с манипулятором смотрит на полку сверху (камера на манипуляторе направлена вертикально вниз). Эксперт с помощью генератора случайных чисел выбирает целевую деталь. Алгоритм должен:
|
|||
|
|
|
|||
|
|
1. Распознать все коробки в кадре камеры
|
|||
|
|
2. Определить координаты целевой коробки относительно базы манипулятора (Base_link)
|
|||
|
|
3. Направить манипулятор к коробке, схватить её и поднять в стартовое положение
|
|||
|
|
4. Опустить коробку на свободное место на полке
|
|||
|
|
|
|||
|
|
## Классы объектов
|
|||
|
|
|
|||
|
|
| ID | Название | Описание |
|
|||
|
|
|----|----------|----------|
|
|||
|
|
| 1 | Молоток (hammer) | Коробка с изображением молотка |
|
|||
|
|
| 2 | Гаечный ключ (wrench) | Коробка с изображением гаечного ключа |
|
|||
|
|
| 3 | Пассатижи (pliers) | Коробка с изображением пассатижей |
|
|||
|
|
|
|||
|
|
## Архитектура решения
|
|||
|
|
|
|||
|
|
- **Модель**: YOLOv8n-cls (nano classification) — легковесная модель для классификации изображений
|
|||
|
|
- **Фреймворк**: Ultralytics / PyTorch
|
|||
|
|
- **Размер входа**: 224x224 px
|
|||
|
|
- **Размер модели**: ~5.3 MB (PyTorch), ~5.5 MB (ONNX)
|
|||
|
|
- **Скорость инференса**: ~1.6 мс на CPU
|
|||
|
|
|
|||
|
|
### Пайплайн обработки кадра
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Кадр с камеры манипулятора
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Сегментация коробок (OpenCV: пороговая фильтрация + контурный анализ)
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Классификация каждой коробки (YOLOv8n-cls)
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Поиск целевой коробки (по ID класса + порог уверенности > 0.7)
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Пересчёт пиксельных координат в координаты Base_link
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Отправка координат манипулятору (MoveIt2 API)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Структура файлов
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
module_v/
|
|||
|
|
├── images/ # Исходные изображения инструментов
|
|||
|
|
│ ├── Hammer.jpg
|
|||
|
|
│ ├── Wrench.jpg
|
|||
|
|
│ └── Pliers.jpg
|
|||
|
|
├── dataset/ # Сгенерированный датасет
|
|||
|
|
│ ├── train/ # 900 изображений (300 на класс)
|
|||
|
|
│ │ ├── hammer/
|
|||
|
|
│ │ ├── wrench/
|
|||
|
|
│ │ └── pliers/
|
|||
|
|
│ └── val/ # 180 изображений (60 на класс)
|
|||
|
|
│ ├── hammer/
|
|||
|
|
│ ├── wrench/
|
|||
|
|
│ └── pliers/
|
|||
|
|
├── runs/module_v_cls/weights/ # Результаты обучения
|
|||
|
|
│ ├── best.pt
|
|||
|
|
│ └── best.onnx
|
|||
|
|
├── best.pt # Обученная модель (копия)
|
|||
|
|
├── generate_dataset.py # Скрипт генерации датасета
|
|||
|
|
├── train_model.py # Скрипт обучения модели
|
|||
|
|
├── detect_box.py # Основной скрипт инференса + ROS2
|
|||
|
|
├── test_detect.py # Тестовый скрипт с визуализацией
|
|||
|
|
├── README.md # Общая инструкция по симулятору
|
|||
|
|
└── README_MODULE_V.md # Данный файл
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Установка зависимостей
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pip install ultralytics opencv-python-headless Pillow albumentations
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Генерация датасета
|
|||
|
|
|
|||
|
|
Датасет генерируется из 3 исходных изображений с помощью аугментаций:
|
|||
|
|
- Композитинг инструментов на различные фоны (имитация полки)
|
|||
|
|
- Повороты, отражения, перспективные искажения
|
|||
|
|
- Изменение яркости, контраста, насыщенности
|
|||
|
|
- Размытие, шум, JPEG-компрессия
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3 generate_dataset.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Результат: 900 train + 180 val изображений (по 300/60 на класс).
|
|||
|
|
|
|||
|
|
## Обучение модели
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3 train_model.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Параметры обучения:
|
|||
|
|
- Базовая модель: `yolov8n-cls.pt` (предобученная на ImageNet)
|
|||
|
|
- Эпохи: до 80 (с early stopping, patience=15)
|
|||
|
|
- Размер изображения: 224x224
|
|||
|
|
- Batch size: 32
|
|||
|
|
|
|||
|
|
Результат: **100% top-1 accuracy** на валидации.
|
|||
|
|
|
|||
|
|
Модель автоматически экспортируется в ONNX формат.
|
|||
|
|
|
|||
|
|
## Запуск
|
|||
|
|
|
|||
|
|
### Тест на отдельном изображении
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3 detect_box.py --target 1 --image path/to/image.jpg
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Где `--target` — ID целевой детали (1=молоток, 2=ключ, 3=пассатижи).
|
|||
|
|
|
|||
|
|
### Тест на всех исходных изображениях
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3 detect_box.py --target 2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Тест с визуализацией на кадре с камеры
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3 test_detect.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Результат сохраняется в `test_result.png` с аннотированными рамками и подписями классов.
|
|||
|
|
|
|||
|
|
### Запуск в режиме ROS2
|
|||
|
|
|
|||
|
|
Предварительно запустите симуляцию модуля В:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Терминал 1: запуск симулятора
|
|||
|
|
ros2 launch ar_webots_fms_ros2 module3.launch.py
|
|||
|
|
|
|||
|
|
# Терминал 2: запуск детектора (указать целевую деталь)
|
|||
|
|
python3 detect_box.py --target 1 --ros
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Скрипт подписывается на топик `/RMC1/arm95/camera_gripper/image_color`, распознаёт коробки и выводит координаты целевой коробки относительно Base_link.
|
|||
|
|
|
|||
|
|
## Результаты тестирования
|
|||
|
|
|
|||
|
|
### На исходных изображениях
|
|||
|
|
|
|||
|
|
| Изображение | Предсказание | Уверенность |
|
|||
|
|
|-------------|-------------|-------------|
|
|||
|
|
| Hammer.jpg | hammer (молоток) | 99.98% |
|
|||
|
|
| Wrench.jpg | wrench (гаечный ключ) | 99.99% |
|
|||
|
|
| Pliers.jpg | pliers (пассатижи) | 100.00% |
|
|||
|
|
|
|||
|
|
### На тестовом кадре с камеры (test.png)
|
|||
|
|
|
|||
|
|
5 коробок на полке (молоток, ключ, 2 пассатижей + объект на краю):
|
|||
|
|
|
|||
|
|
| Коробка | Класс | Уверенность |
|
|||
|
|
|---------|-------|-------------|
|
|||
|
|
| Box 1 | wrench | 99.51% |
|
|||
|
|
| Box 2 | pliers | 100.00% |
|
|||
|
|
| Box 3 | wrench | 65.81% (край кадра, фильтруется порогом) |
|
|||
|
|
| Box 4 | hammer | 79.42% |
|
|||
|
|
| Box 5 | pliers | 99.93% |
|
|||
|
|
|
|||
|
|
Рекомендуемый порог уверенности для фильтрации: **> 0.7**
|
|||
|
|
|
|||
|
|
## Интеграция с манипулятором
|
|||
|
|
|
|||
|
|
Пример полного цикла (распознавание + захват):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import rclpy
|
|||
|
|
from detect_box import load_model, find_target_box, pixel_to_base_link
|
|||
|
|
from geometry_msgs.msg import PoseStamped
|
|||
|
|
from tf_transformations import quaternion_from_euler
|
|||
|
|
from moveit.planning import MoveItPy
|
|||
|
|
|
|||
|
|
# Инициализация
|
|||
|
|
rclpy.init()
|
|||
|
|
model = load_model("best.pt")
|
|||
|
|
arm95 = MoveItPy(node_name="moveit_py", name_space="/RMC1/arm95")
|
|||
|
|
arm95_arm = arm95.get_planning_component("arm95_group")
|
|||
|
|
gripper = arm95.get_planning_component("gripper")
|
|||
|
|
|
|||
|
|
# 1. Получить кадр с камеры (через ROS2 subscriber)
|
|||
|
|
# frame = ... (cv2 image from /RMC1/arm95/camera_gripper/image_color)
|
|||
|
|
|
|||
|
|
# 2. Найти целевую коробку
|
|||
|
|
target_id = 1 # молоток
|
|||
|
|
result = find_target_box(model, frame, target_id)
|
|||
|
|
if result:
|
|||
|
|
cx, cy, conf, bbox = result
|
|||
|
|
h, w = frame.shape[:2]
|
|||
|
|
wx, wy, wz = pixel_to_base_link(cx, cy, w, h)
|
|||
|
|
|
|||
|
|
# 3. Переместить манипулятор к коробке
|
|||
|
|
q = quaternion_from_euler(3.14, 0.0, 1.57)
|
|||
|
|
pose_goal = PoseStamped()
|
|||
|
|
pose_goal.header.frame_id = "Base_link"
|
|||
|
|
pose_goal.pose.orientation.x = q[0]
|
|||
|
|
pose_goal.pose.orientation.y = q[1]
|
|||
|
|
pose_goal.pose.orientation.z = q[2]
|
|||
|
|
pose_goal.pose.orientation.w = q[3]
|
|||
|
|
pose_goal.pose.position.x = wx
|
|||
|
|
pose_goal.pose.position.y = wy
|
|||
|
|
pose_goal.pose.position.z = 0.3 # высота над коробкой
|
|||
|
|
|
|||
|
|
arm95_arm.set_start_state_to_current_state()
|
|||
|
|
arm95_arm.set_goal_state(pose_stamped_msg=pose_goal, pose_link="gripper_base")
|
|||
|
|
plan_result = arm95_arm.plan()
|
|||
|
|
if plan_result:
|
|||
|
|
arm95.execute(plan_result.trajectory, controllers=[])
|
|||
|
|
|
|||
|
|
# 4. Захватить коробку
|
|||
|
|
gripper.set_goal_state(configuration_name="closed")
|
|||
|
|
plan_result = gripper.plan()
|
|||
|
|
if plan_result:
|
|||
|
|
arm95.execute(plan_result.trajectory, controllers=[])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Важно**: координаты `pixel_to_base_link()` требуют калибровки параметров камеры (FOV, высота) под конкретную установку.
|