Глава 10. Применение нейросети

1. Цель занятия и обзор плана

На этом занятии мы научимся применять обученную модель YOLO для автономного управления катамараном. Мы разберём, какие данные возвращает нейросеть, как их фильтровать, как рассчитывать азимут на объект и как интегрировать всё это с регулятором и машиной состояний.

После завершения занятия вы сможете:

  • получать и интерпретировать результаты детекции YOLO;

  • фильтровать объекты по уверенности (confidence) и по классам;

  • выбирать лучший объект на класс;

  • рассчитывать азимут на обнаруженный объект;

  • наводить катамаран на ворота с помощью регулятора;

  • объединять всё в машину состояний для автономного прохождения задания.

1.1. Обзор плана занятия

  1. Данные от YOLO — что мы получаем.

  2. Фильтрация по уверенности (confidence).

  3. Фильтрация по классам.

  4. Форматы bounding box.

  5. Расчёт азимута на объект.

  6. Азимут на ворота — средний курс.

  7. Регулятор.

  8. Интеграция с машиной состояний.

2. Теоретический материал

2.1. Данные от YOLO

Модель YOLO возвращает для каждого обнаруженного объекта три вещи:

  • Bounding box (рамка) — координаты прямоугольника, описывающего объект.

  • Confidence (уверенность) — число от 0 до 1, показывающее, насколько модель уверена в обнаружении. Чем ближе к 1 — тем увереннее.

  • Class ID — идентификатор класса объекта. Через словарь model.names его можно преобразовать в текстовое имя (например, «red buoy»).

results = model.predict(frame, verbose=False)
boxes = results[0].boxes

for i in range(len(boxes)):
    cls_id = int(boxes.cls[i].item())
    cls_name = model.names[cls_id]        # имя класса, например "red_buoy"
    conf = boxes.conf[i].item()           # уверенность, например 0.87
    xywh = boxes.xywh[i].tolist()         # [center_x, center_y, width, height] в пикселях

    print(f"{cls_name}: {conf:.2f}, bbox: {xywh}")

2.2. Форматы bounding box

YOLO поддерживает несколько форматов координат рамки:

  • xyxy — координаты верхнего левого (x1, y1) и нижнего правого (x2, y2) углов в пикселях.

  • xywh — центр (x, y) и размеры (w, h) в пикселях.

  • xywhn — то же, что xywh, но нормализовано (от 0 до 1). Нормализованные значения удобнее: они не зависят от разрешения изображения. Чтобы получить нормализованные координаты, достаточно добавить n в конце: boxes.xywhn.

На практике для расчёта азимута удобнее всего использовать xywhn — нормализованную координату центра по оси X.

2.3. Фильтрация по уверенности (confidence)

Не все обнаруженные объекты достоверны. Модель может «увидеть» буй там, где его нет — просто с низкой уверенностью. Поэтому устанавливается пороговое значение: все обнаружения с confidence ниже порога отбрасываются.

Обычно порог начинают с 0.5 (50%). Это означает: «если модель уверена менее чем наполовину — считаем это ложным обнаружением».

results = model.predict(
    frame,
    conf=0.5,          # пороговое значение уверенности
    verbose=False
)

Порог можно регулировать через Dynamic Reconfigure в RQT. Запустите пример:

$ ros2 launch lesson_10 main.launch.py lesson_num:=1

Попробуйте увеличить порог до 0.8 — заметите, что объекты пропадают и появляются только когда модель очень уверена (обычно при приближении). Уменьшите до 0.3 — появится больше обнаружений, но и больше ложных срабатываний.

Чем выше порог, при котором модель стабильно работает — тем лучше она обучена.

2.4. Фильтрация по классам

Если нас интересуют только определённые объекты (например, только буи для прохождения ворот), можно ограничить детекцию нужными классами:

# Список нужных классов
allowed_classes = ['red_buoy', 'yellow_buoy']

# Получаем ID этих классов
allowed_ids = [key for key, value in model.names.items() if value in allowed_classes]

results = model.predict(
    frame,
    conf=0.5,
    classes=allowed_ids,    # детектировать только указанные классы
    verbose=False
)

Теперь модель будет возвращать только красные и жёлтые буи, игнорируя все остальные объекты — даже если они есть в кадре.

Выбор лучшего объекта на класс

Если модель обнаружила несколько объектов одного класса (например, два красных буя), полезно оставить для каждого класса только тот, в котором модель больше всего уверена:

def filter_best_per_class(results):
    boxes = results[0].boxes
    best_idx = []
    for cls in boxes.cls.unique():
        cls_mask = boxes.cls == cls
        cls_conf = boxes.conf[cls_mask]
        cls_indices = cls_mask.nonzero(as_tuple=True)[0]
        best_idx.append(int(cls_indices[cls_conf.argmax()]))
    results[0].boxes = boxes[best_idx]

После этой фильтрации у нас остаётся по одному объекту каждого класса — с максимальной уверенностью.

3. Практическое занятие

3.1. Расчёт азимута на объект

Зная горизонтальный угол обзора камеры (FOV) и нормализованную координату центра объекта, можно рассчитать азимут — угол отклонения объекта от оптической оси камеры.

В нашем симуляторе FOV = 90°. Для углов до 90° можно использовать линейную аппроксимацию:

# x_center_n — нормализованная координата центра (0 = левый край, 1 = правый)
# fov — горизонтальный угол обзора камеры в градусах

azimuth_deg = (x_center_n - 0.5) * fov

Результат: 0° — объект в центре кадра. Отрицательные значения — объект левее. Положительные — правее.

В коде это выглядит так:

fov = camera_info.fov  # получаем FOV камеры

for box in results[0].boxes:
    x_center_n = box.xywhn[0][0].item()  # нормализованная X-координата центра
    cls_name = model.names[int(box.cls.item())]
    azimuth = (x_center_n - 0.5) * fov

    data.append((cls_name, azimuth, box.conf.item()))

Запуск примера:

$ ros2 launch lesson_10 main.launch.py lesson_num:=2

На обработанном изображении в левом верхнем углу будет отображаться азимут. Поворачивайте катамаран и наблюдайте, как меняется угол: объект слева — минус, справа — плюс, в центре — около нуля.

3.2. Азимут на ворота

Ворота — это не один объект, а два буя (красный и жёлтый). Чтобы катамаран двигался в центр ворот, нужно рассчитать средний азимут:

def eval_gate_azimuth(data):
    if len(data) < 2:  # нужны оба буя
        return None
    azimuth_sum = sum([azimuth for _, azimuth, _ in data])
    return azimuth_sum / len(data)

Суммируем азимуты всех найденных буёв и делим на их количество — получаем направление на центр ворот.

Запуск примера:

$ ros2 launch lesson_10 main.launch.py lesson_num:=3

В RQT через Topic Monitor можно наблюдать значение целевого азимута в топике target_azimuth. Когда вы смотрите левее ворот — азимут отрицательный (нужно повернуть влево). Когда правее — положительный. Когда ворота по центру — близок к нулю.

3.3. Регулятор

На основе целевого азимута можно реализовать регулятор: если азимут отрицательный (ворота левее) — поворачиваем влево; если положительный — вправо. Одновременно катамаран движется вперёд.

Это дискретный регулятор, аналогичный тому, что мы использовали в предыдущих занятиях. Он стремится свести ошибку по азимуту к нулю:

$ ros2 launch lesson_10 main.launch.py lesson_num:=4

Через Dynamic Reconfigure в RQT можно настроить параметры регулятора:

  • линейная скорость — скорость движения вперёд. Увеличите — катамаран быстрее поедет к цели.

  • коэффициент реакции — чувствительность к ошибке азимута. Увеличите — катамаран будет резче реагировать на отклонение, вплоть до колебаний (синусоидальная траектория). Уменьшите — реакция будет плавнее, но медленнее.

Там же есть выключатель регулятора. Остановили — катамаран замирает. Включили — продолжает движение.

Подбирайте параметры так, чтобы катамаран двигался к воротам плавно, без резких рывков. Возможно, стоит реализовать ПИД-регулятор из предыдущих работ — он даст более плавное и точное управление.

3.4. Интеграция с машиной состояний

На финальном шаге все компоненты объединяются: YOLO-детекция, расчёт азимута, регулятор и машина состояний из урока 08. Миссия выглядит примерно так:

  1. WaitStart — ждём команду на старт.

  2. ResetMotion — сбрасываем скорости.

  3. MoveForward — двигаемся вперёд некоторое время.

  4. SearchGate — начинаем искать ворота (поворачиваемся, пока не обнаружим буи).

  5. Autopilot — двигаемся в режиме автопилота в сторону ворот.

Запуск:

$ ros2 launch lesson_10 main.launch.py lesson_num:=5

Загрузите сцену соревнования в симуляторе. Для старта отправьте True в топик start через RQT Message Publisher.

Катамаран начнёт движение, проедет некоторое расстояние вперёд, начнёт поворачиваться в поисках ворот, найдёт их и двинется в их сторону. Его траектория может быть не идеальной — можно настроить параметры регулятора и миссию, чтобы он двигался оптимальнее.

4. Домашнее задание

  1. Реализуйте автономное прохождение ворот с использованием вашей нейросети из урока 09.

  2. Модифицируйте регулятор для более плавного движения (например, реализуйте ПИД-регулятор).

  3. Задача со звёздочкой: реализуйте автономную парковку в гараж с использованием нейросети. По сути, всё выглядит точно так же, но требует небольших модификаций машины состояний.

5. Дополнительные материалы

Документация Ultralytics по детекции

https://docs.ultralytics.com/tasks/detect/

https://docs.ultralytics.com/tasks/detect/

Документация Ultralytics по предсказанию

https://docs.ultralytics.com/modes/predict/

https://docs.ultralytics.com/modes/predict/

Документация Ultralytics: работа с результатами

https://docs.ultralytics.com/reference/engine/results/

https://docs.ultralytics.com/reference/engine/results/