# Модуль В. Система технического зрения с использованием инструментов ИИ

## Описание задачи

На специальной полке на соревновательном поле расположены 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, высота) под конкретную установку.
