Введение

В этой статье я расскажу, как простая тележка на базе NVIDIA Jetson Nano и Arduino Nano научилась находить единственный проход в кольце фишек-домино, опираясь только на изображение с камеры. Без ультразвука и лидаров — только компьютерное зрение (YOLOv5s → TensorRT), немного геометрии и простая исполнительная логика. Покажу аппаратную сборку, ключевые фрагменты кода, обучение и оптимизацию модели. Этот проект рассчитан в первую очередь на хобби-инженеров, студентов и разработчиков, тем, кто уже имеет общее представление о Jetson/Arduino и установкой Python-библиотек. Если вы абсолютный новичок в Jetson, материалы всё равно будут полезны в образовательных целях, но для запуска понадобятся дополнительные знания по окружению и зависимостям.

Постановка задачи

Представьте круглый «полигон» — по окружности стоят фишки-домино, плотно одна к другой, с шагом примерно 10 см. Наш робот — компактная тележка с камерой — стартует в центре этого круга. При таком расстоянии между фишками тележка не пролезет за пределы круга, но в одном произвольном месте по периметру специально оставлен проход шириной ~20 см — через него робот может выбраться наружу. Задача простая по формулировке и одновременно интересная по сути: научить робота находить и выезжать через этот единственный выход, опираясь только на изображение с камеры. Никаких ультразвуковых датчиков, лидаров или других «подсказок» — только CV.

Рис 1. Зеленой стрелочкой показан единственный выход
Рис 1. Зеленой стрелочкой показан единственный выход

Почему это может быть полезно?

Во-первых, это приближает задачу к тому, как ориентируется человек: глазами и анализом сцены. Во-вторых, такой подход минимизирует аппаратную сложность — достаточно одной камеры и относительно недорогого миникомпьютера типа Jetson Nano — что важно для хобби-проектов и образовательных роботов. Наконец, решение хорошо масштабируется: алгоритмы, которые вычисляют, где есть проход среди любого рода препятствий применимы в других задачах, например движения робота по пересеченной местности, промзоне и т.д.

Что считаем успехом?

Метрика простая: попытка считается успешной, если робот выходит за предел круга через целевой проход без физического контакта с фишками. Для статистики — запускать серию испытаний (например 10), и брать долю успешных попыток.

Граничные случаи и ограничения.

Освещение, наклон домино, ложные детекции, размытие при движении - всё это усложняет задачу. В статье покажем, как устроен алгоритм, какие допуски он использует и какие меры повышают устойчивость к этим эффектам.

Аппаратная часть

Список основных компонентов робота

  • Платформа / шасси (тележка) с двумя ведущими колёсами, парой мотор-редукторов TT (желтых) и опорным третьим поворотным колесом.

  • Драйвер моторов MX1508.

  • Arduino Nano - низкоуровневый контроллер моторов.

  • NVIDIA Jetson Nano (использовался 4 GB RAM) - главный процессор для CV и логики.

  • Камера: CSI-камера (Модуль камеры IMX219 для Jetson Nano 160 градусов 8MP FOV 3280x2464)

  • Питание: отдельная батарея 7.4 В с понижающим преобразователем 5 В / 2 A для моторов и отдельная батарея 7.4 В с преобразователем 5 В / 4 A для Jetson.

  • Wi-Fi-адаптер (usb) для связи с Jetson с внешним компьютером.

    Рис 2. Основные компоненты тележки
    Рис 2. Основные компоненты тележки

Комментарии: Стандартные колеса (покрышки) сильно проскальзывали на полу и поэтому на них были натянуты воздушные шары, что значительно повысило точность движения тележки. На фото пример того, как нужно обрезать воздушный шарик для этих целей. На поворотное колесико, тоже для целей уменьшения трения, наклеен двухсторонний автомобильный скотч. Перед использованием колесико покатали по ткани, чтобы оно наклеило на себя мелкие ворсинки и перестало липнуть к полу.

Схема подключения:

  1. Камера → Jetson. CSI-камеру подключаем в CSI-разъём Jetson. На Jetson запускаем GStreamer (nvarguscamerasrc). В коде:

        # Параметры для доступа к камере через GStreamer
        gst_str = (
        "nvarguscamerasrc ! "
        "video/x-raw(memory:NVMM), width=720, height=540, framerate=30/1, format=NV12 ! "
        "nvvidconv flip-method=0 ! "
        "video/x-raw, width=720, height=540, format=BGRx ! "
        "videoconvert ! "
        "video/x-raw, format=BGR ! appsink"
         )
  2. Jetson ↔ Arduino — Serial. Подключение по USB: Jetson видит Arduino как /dev/ttyUSB0. В коде используется Serial на 9600 бод.

        arduino = serial.Serial(
            port='/dev/ttyUSB0',  # Укажите правильный порт, к которому подключен Arduino
            baudrate=9600,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=1
        )
  3. Arduino → Драйвер моторов → Моторы. Arduino управляет входами драйвера через пины: MOTOR1_IN=2, MOTOR1_PWM=3, MOTOR2_IN=4, MOTOR2_PWM=5. PWM пины подключаются к PWM-входам драйвера, IN — к пинам направления. В скетче:

    #define MOTOR1_IN 2
    #define MOTOR1_PWM 3
    #define MOTOR2_IN 4
    #define MOTOR2_PWM 5
    
    .......
    
    void setup() {
        // Инициализация последовательного порта для обмена данными с Jetson
        // Скорость передачи: 9600 бод (бит в секунду)
        Serial.begin(9600);
        
        // Настройка пинов управления двигателем 1:
        pinMode(MOTOR1_IN, OUTPUT);    // Пин направления вращения двигателя 1 (вперед/назад)
        pinMode(MOTOR1_PWM, OUTPUT);   // Пин скорости двигателя 1 (ШИМ-сигнал)
        
        // Настройка пинов управления двигателем 2:
        pinMode(MOTOR2_IN, OUTPUT);    // Пин направления вращения двигателя 2 (вперед/назад)  
        pinMode(MOTOR2_PWM, OUTPUT);   // Пин скорости двигателя 2 (ШИМ-сигнал)
        
        // После setup() выполнится функция loop() для основного цикла управления
    }

    Драйвер цепляем к батарее питания моторов.

  4. Общая «земля». Обязательно связать минусы батарей моторов, Arduino и питания Jetson (GND) — иначе Serial и команды работать не будут.

Схема подключения Arduino к Драйверу моторов и к Jetson

Рис 3.Схема подключения компонентов
Рис 3.Схема подключения компонентов

Питание

В нашем прототипе Jetson запитан от 2S батареи (через понижающий преобразователь) 5 V/4 A; Arduino — от Jetson Nano по USB; моторы — от отдельной батареи через регулятор дающий 5V 2A., затем через драйвер mx1508.

Рекомендуется держать деликатную вычислительную часть (Jetson, Arduino, сенсоры, USB-периферия) и силовую часть (моторы и драйверы) на отдельных источниках питания - лучше на двух отдельных батареях с собственными понижающими преобразователями. Причины следующие: моторы генерируют крупные пусковые токи, импульсные помехи и скачки напряжения, которые легко «садят» общий регулятор и приводят к зависаниям или повреждению чувствительной электроники.

Оптимизированная (TensorRT) модель нашей нейросети с предсказанием раз в 15 кадров не сильно нагружает Jetson Nano, и потребление тока не доходило до 4 А. Тем не менее, запас по току обязателен, так как при высоких нагрузках Jetson Nano может потреблять чуть больше 4 А.

Такая схема удобна для быстрого старта, но для безопасной и стабильной работы мы рекомендуем BMS для батарей, отдельный мощный 5 V регулятор для Jetson (≥4–5 A), конденсаторы/фильтрацию и предохранитель на батарее. Такое решение минимизирует риски просадки напряжения, помех и повреждения электроники. Организовать общую землю (одна «звезда»), толстые провода для питания моторов.

Архитектура системы

Система сделана максимально просто и модульно. Главные компоненты: камера → Jetson Nano → логика (YOLO + модуль кода) → Serial → Arduino Nano → моторы.

Камера снимает кадры, кадр поступает в OpenCV, далее передаётся в детектор (на Jetson работает YOLOv5s, собранный в TensorRT .engine). Детекции (bounding boxes) ограничивающих рамок возвращаются в виде списка: координаты, confidence, класс. На их основе высчитывается геометрия (пара A–B, точка E, смещение EF, длина AB и т.д.). Эта "картина" — единственный источник информации о мире: никаких ультразвуков и лидаров.

Модуль кода на Jetson управляет состояниями робота: сканирование/огибание стены, прицеливание мелкими поворотами, приближение малыми шагами и финальный разворот/выезд. Для экономии ресурсов кадры обрабатываются не каждый, а, например, каждые N-й кадр (в коде — каждые 15), что снижает загрузку Nano и даёт устойчивый отклик.

Команды управления отправляются в Arduino по последовательному порту в формате:

<command>[_<value>]|

Пример кода:

comand = "pravoitog_" + str(ugol) + "|"
arduino.write(comand.encode())

Arduino парсит строку, выполняет заранее запрограммированные манёвры (через motorControl + delay). В скетче:

//Здесь Arduino читает строку из Serial (например, из Jetson Nano) до символа |
String receivedCommand = Serial.readStringUntil('|');

.......
//Здесь ищется символ _. Он разделяет имя команды и значение.
int delimiterIndex = receivedCommand.indexOf('_');

.......

receivedCommand2 = receivedCommand.substring(0, delimiterIndex);  
String valueStr = receivedCommand.substring(delimiterIndex + 1);  
value_v = valueStr.toInt();

//receivedCommand2 = команда 
//value_v = число 

На стороне Arduino простая блокирующая реализация — когда код «останавливается» на выполнение какой-то операции (например, delay(1000)), и пока она не завершится, всё остальное не выполняется. Это удобно и просто для прототипа, потому что поведение очевидное и легко отлаживать. Как развитие проекта можно заменить на неблокирующую для плавности.

На кадр мы выводим наложения (красная линии - AB, синий прицел, зеленый отрезок EF, длина EF - зеленая цифра) — это важно для отладки и подбора параметров.

Рис 4. Основная информация, отображаемая на кадре для контроля.
Рис 4. Основная информация, отображаемая на кадре для контроля.

Общая логика — тактика и стратегия

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

Фазы в коде реализованы через флаги kluh_* и набор функций:

0. Начальное положение - в центре

Рис 5. В центре
Рис 5. В центре
  1. START — начальный подъезд/ориентация. Нужен, чтобы подвести робота из центра к стене в корректное стартовое положение (функция pribligenie_start / levostart). Это положение, при котором робот стоит недалеко стены, примерно под углом 45 градусов к ней. Из такого положения удобно проверять расстояния между домино и двигаться вдоль стены:

    Рис 6. Результат работы фазы START
    Рис 6. Результат работы фазы START
  2. Огибание — основная рабочая фаза: сканируем окружение, ищем соседние домино A и B. Если пара не найдена — движемся вдоль стены (функция ogibanie). Если пара найдена, проверяем, не превышает ли длина линии AB (линия AB - это линия между средней точкой нижней ограничивающей рамки левой домино и средней точкой нижней ограничивающей рамки правой домино) пороговую длину AB_END = 300. Пока не превышает, движемся вдоль стены, корректируя курс по положению точки E (центр синего прицела) и линии AB (красная линия). Идея в том, что нужно корректировать движение робота, подруливая правым и левым колесами, чтобы точка E как бы скользила по красной линии AB. Зеленый отрезок - это EF. Зеленое число показывает положительное или отрицательное отклонение в пикселах от линии AB по горизонтали. На рисунке ниже отклонение EF = -28, это значит, что робот слишком приблизился к стене и правый мотор должен теперь поработать побыстрей, чтобы робот подрулил на лево.

    Рис 7. Процесс огибания стены и поиска (выхода) расстояния AB больше порогового (300)
    Рис 7. Процесс огибания стены и поиска (выхода) расстояния AB больше порогового (300)
  3. Прицеливание — когда найдены две соседние домино с длиной линии AB выше порога (AB > AB_END), значит мы нашли выход и нам нужно к нему подъехать в нужную точку относительно выхода. Для этого начинаем прицеливаться: делаем мелкие пошаговые повороты (levo1 / pravo1), чтобы вертикаль синего прицела совпала не с центром прохода (середина линии AB), а c точкой на линии AB отстоящей на 1/3 от точки A. Для этого вводится поправка popravka = AB / 3. Она учитывает габариты робота. Чтобы он подъехал к выходу в удобную позицию для дальнейшего выезда из лабиринта на следующих фазах. Проще говоря, роботу надо проехать чуть дальше вдоль прохода, чтобы потом развернувшись к нему, оказаться как раз по центу прохода.

    Рис 8. Робот нашел выход и целиться в точку (1/3 от точки A)
    Рис 8. Робот нашел выход и целиться в точку (1/3 от точки A)
  4. Приближение

    После успешного прицеливания робот выполняет подъезд малыми шагами (pribligenie), который работает по разным сценариям в зависимости от количества видимых домино.

    При сохранении видимости обеих домино робот продолжает точное позиционирование, совмещая движение вперед с коррекцией траектории. Он подъезжает до момента, когда вертикальная дистанция до контрольной линии (kontrol_doezda) становится меньше или равна нулю. На практике это означает, что горизонтальная линия синего прицела касается точки B (нижней границы правой домино). В этот момент активируется команда pryamo| - прямое движение на выверенное расстояние без коррекции.

    В случае потери видимости одной из домино, алгоритм переходит в аварийный режим (kluh_1BB). Робот прекращает попытки точного прицеливания и немедленно выполняет заранее запрограммированный финальный маневр, поскольку продолжение коррекции траектории становится невозможным, так как линия AB уже не доступна. Обычно потеря домино происходит как раз в тот момент, когда робот уже находится примерно в нужной позиции для следующего маневра. То есть к тому времени, когда одно домино пропадает из поля зрения, робот уже успевает занять правильное положение для выполнения следующего маневра.

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

    Рис 9. Штатный сценарий - оба домино детектируются. Робот в конце фазы и готов в правому развороту в следующей финальной фазе.
    Рис 9. Штатный сценарий - оба домино детектируются. Робот в конце фазы и готов в правому развороту в следующей финальной фазе.
  5. FINAL / EXIT — финальная последовательность (pravoitog_<угол>) — разворот и выезд через выход. После успешного выезда — переход в STOP.

    Рис 10. Разворот на право уже совершен, дальше выезд.
    Рис 10. Разворот на право уже совершен, дальше выезд.

Комментарий: Количество обрабатываемых кадров, порог AB_END и поправка AB/3 подбирается экспериментально.

Почему такой подход?

Он прост, прозрачен и хорошо объясним для контроля: зрение даёт картину, геометрия переводит её в цель на кадре (относительное расположение точки E и линии AB), состояния робота превращает цель в серии простых команд. В дальнейшем можно улучшать плавность, реализовать непрерывность движения.

Псевдокод алгоритма для Jetson

Цикл обрабатывает кадры (не каждый), детектит домино, пытается найти пару A/B, по ней вычисляет EF, затем в зависимости от состояния отправляет команды на Arduino.

Упрощенный пвсевдокод для понимания общей логики

// СТРАТЕГИЯ: Движение вдоль дуговой стены по правую руку
// ТАКТИКА: 
//  1. Обнаружение домино-маркеров камерой
//  2. Определение ключевых точек A и B
//  3. Принятие решений на основе геометрических расчетов
// ГЕОМЕТРИЯ: Работа с точками A, B, углами и расстояниями

ОСНОВНОЙ АЛГОРИТМ:
1. Инициализировать камеру и модель распознавания
2. Установить параметры:
   - vertikal = 360 (центр кадра)
   - gorizontal_ef = 230 (горизонтальна линия, пересечение которой с вертикальным центром дают точку E)
   - пороговые значения расстояний и углов

3. Для каждого кадра:
   А. ОБНАРУЖЕНИЕ:
      - Найти все доминошки в кадре
      - Получить их ограничивающие рамки (BB)
   
   Б. ГЕОМЕТРИЧЕСКИЙ АНАЛИЗ:
      - Вычислить средние точки нижних границ BB
      - Найти пару ближайших доминошек слева и справа от центра кадра:
        * Точка A = средняя нижняя точка BB левой доминошки
        * Точка B = средняя левая нижняя точка BB правой доминошки
      - Рассчитать:
        * Длину AB (псевдо расстояние между доминошками)
        * Угол α между AB и горизонталью
        * Расстояния AC и CB до центра кадра
        * Параметр ef для коррекции движения

   В. ЛОГИКА УПРАВЛЕНИЯ:
      ЕСЛИ еще не стартовали (kluh_start = 0):
        pribligenie_start() - двигаться вперед до стартовой позиции
        
      ИНАЧЕ ЕСЛИ нашли выход (AB > порог) И еще не прицелились:
        pricelivanie() - поворачивать чтобы выровнять AC и CB
        ЕСЛИ выровнялись: установить kluh_pricelilis = 1
      
      ИНАЧЕ ЕСЛИ прицелились (kluh_pricelilis = 1):
        pribligenie() - двигаться вперед к выходу
        ЕСЛИ достигли позиции: повернуть на 90° и завершить
        
      ИНАЧЕ (огибание стены):
        ogibanie() - двигаться вдоль стены:
          * Корректировать направление по параметру ef
          * Поддерживать расстояние до стены

ФУНКЦИИ:

pricelivanie() - Точная настройка направления:
  Вычислять разницу между AC и CB
  Поворачивать влево/вправо малыми шагами пока разница > порога

pribligenie_start() - Начальное приближение:
  Двигаться вперед до достижения стартовой дистанции
  Затем повернуть на 45° для начала огибания

pribligenie() - Движение к выходу:
  Совмещать поступательное движение с коррекцией направления
  При достижении целевой позиции выполнить финальный поворот

ogibanie() - Движение вдоль стены:
  Использовать геометрический параметр ef для коррекции траектории
  Регулировать скорость в зависимости от кривизны стены

Геометрия и формула EF

Наша задача, в процессе "огибания" стены из домино — из визуальной сцены, получить одно число, которое подскажет роботу, в какую сторону и насколько сдвинуться по горизонтали. Это число — EF (см. Рис 11. Отрезок EF и отклонение +28 по x).

Рис 11. Отрезок EF и отклонение +28 по x
Рис 11. Отрезок EF и отклонение +28 по x

Основная геометрия:

Модель дает все BB домино на кадре. Ищем подходящую пару BB (левой и правой домино относительно vertikal ) и забираем их координаты (точка A (нижняя средняя точка BB левой домино и точка B (нижняя средняя точка правой домино)):

xa, ya, xb, yb = poisk_ab(spisok_bb_x, spisok_bb_x_pravo, spisok_bb_y_pravo, spisok_bb_x_levo, spisok_bb_y_levo, vertikal) 

Вычислим EF смещение от центра кадра по x:

b1b = abs(xb - vertikal)
eb1 = abs(yb - gorizontal_ef)
aa1 = abs(vertikal - xa)
a1b1 = abs(yb - ya)

ef = b1b - (eb1 * (aa1 + b1b) / a1b1)

Вычислим длину отрезка AB:

AD = yb - ya
DB = xb - xa
AB = (AD * AD + DB * DB) ** 0.5 # длина AB

Вычисляем, необходимые для центрирования, отрезки AC и CB

ac = abs(vertikal - xa)
cb = abs(xb - vertikal) AB

Почему 2D достаточно для нашей задачи в 3D-мире?

В нашем прототипе камера жёстко закреплена: фиксирован угол (≈45°) и высота над полом. Такое положение даёт хороший обзор ближней и дальней зоны и формирует перспективную проекцию, где вертикальная координата в кадре коррелирует (хоть и не линейно) с расстоянием в реальном пространстве. Маркеры (домино) стандартизованы и лежат на плоском полу.

При этих условиях вертикальная позиция объекта в кадре монотонно зависит от дальности, а горизонтальная — от бокового смещения. Поэтому практичный и вычислительно лёгкий подход — оперировать результатами детектора в 2D (в пикселях) и использовать относительные соотношения (AB, EF) вместо полного 3D-восстановления. Все пороги и поправки (например, AB_END, AB/3, gorizontal_ef) подобраны эмпирически под нашу установку и требуют перекалибровки при смене камеры, разрешения или крепления. Такой компромисс даёт простую, быструю и надёжную систему для задачи «найти выход из лабиринта» в контролируемой среде.

Ключевые параметры и за что они отвечают

Ниже — сводная таблица с ключевыми параметрами, их смыслом, рекомендованными стартовыми значениями (исходя из текущего кода и входного разрешения 720×540) и краткими советами по настройке.

Параметр

Что делает

Рекомендация (старт)

Как настраивать / советы

FRAME_SKIP (такой перемeнной в коде нет, но менять можно вот здесь:

if frame_count% 15!= 0:

Обрабатывать каждый N-й кадр (уменьшает нагрузку)

15

Уменьшайте для большей отзывчивости (→ больше CPU), увеличивайте для стабилизации детекций.

vertikal

X центра кадра (вертикальная ось)

360 px (для ширины 720 px)

Подстройте под реальную разметку кадра; проверьте на отрисовке.

gorizontal_ef

Y контрольной горизонтали (пересечение vertikal и gorizontal_ef дает точку E)

230 px

От этой горизонтали зависит положение цента прицела E - которая должна скользить по отрезку AB. Меняя значение можно регулировать фактическое расстояние до стены домино во время огибания.

AB_END

Порог длины AB (когда начинать фазу Прицеливания)

300 px

Определите эмпирически: измерьте AB (в px) выхода из лабиринта в поле зрения.

distanziy_start

Порог определяет момент завершения стартового приближения к домино с последующим поворотом налево для перехода к огибанию стены.

250 px

Меняйте, чтобы стартовый этап корректно завершался перед сканированием и робот встал в удобную позицию для огибания - не слишком далеко и не слишком близко к стене.

d_ac_cb

Допуск при прицеливании (разница ac/cb)

30 px

Больше — мягче прицеливание; меньше — жестче (частые мелкие повороты).

popravka (AB/3)

Компенсация ориентации выхода при прицеливании в связи с габаритами робота

AB/3 (по умолч.)

Попробуйте AB/4..AB/2 при сильной наклонности выхода из лабиринта

conf (детектор)

Порог уверенности YOLO

0.6

Снижение → больше ложных срабатываний; повышение → может пропускать домино, но меньше ложных срабатываний

Модель, обучение и деплой

Использованная модель

Для детекции домино была произведена разметка домино, обучена модель на версии YOLOv5 — yolov5s. Модель обучалась на одной метке (один класс — d), после тренировки веса экспортировались и были подготовлены к inference на Jetson Nano.

Разметка

Для обучения нейросетевой модели использовался размеченный датасет, подготовленный в среде Roboflow. Датасет доступен по ссылке. Разметка производилась нами.

Датасет состоит из 579 тренировочных изображений и 48 вариационных. Для большей универсальности модели была произведена средствами Roboflow аугментация:

  • Случайный поворот в диапазоне от -15 до +15 градусов

  • Случайный сдвиг (сдвиг) от -10° до +10° по горизонтали и от -10° до +10° по вертикали

Комментарий: В среде Roboflow есть удобный инструмент (умный полигон), который быстро и очень точно сам при клике на объект рисует его маску, затем в контекстном меню эту маску можно перевести в ограничивающую рамку.

Обучение

Обучение провели в Google Colab. После скачивания датасета с Roboflow и загрузки его в Google Colab нужно клонировать и установить yolov5 c Github:

!git clone https://github.com/ultralytics/yolov5  # clone
%cd yolov5
%pip install -qr requirements.txt comet_ml  # install

import torch
import utils
display = utils.notebook_init()  # checks

В зависимости от того, куда вы загрузили датасет, необходимо в файле data.yaml дописать путь до папки, в которой у вас лежит датасета. В итоге файл data.yaml должен выглядеть примерно вот так:

path: /content/yolov5/datasets
train: ../train/images
val: ../valid/images
test: ../test/images

nc: 1
names: ['d']

roboflow:
  workspace: realmetalru
  project: domino_pol
  version: 1
  license: CC BY 4.0
  url: https://universe.roboflow.com/realmetalru/domino_pol/dataset/1

Запуск обучения для размера 640 на 640, батч 16, 300 эпох на модели yolov5s

!python train.py --img 640 --batch 16 --epochs 300 --data /content/yolov5/datasets/data.yaml --weights yolov5s.pt --cache

Результаты обучения

Модель хорошо обучилась: mAP50 на валидационных данных 0.955. Упрощенно это значит что модель обнаруживает и правильно идентифицирует домино в 95.5% случаев.

mAP50 = .955
mAP50 = .955

График Полнота - Уверенность

  • Recall: Показывает, какой процент всех нужных объектов (например, домино) находит наша модель.

  • Confidence: Показывает, насколько уверена модель в своих предсказаниях. Чем правее, тем больше она "верит", что не ошибается.

Рис 12. Полнота-Уверенность
Рис 12. Полнота-Уверенность

График Recall–Confidence (полнота → порог уверенности) показывает, как меняется полнота при повышении порога confidence. В нашей модели полнота держится примерно на уровне 0.97 при порогах до ~0.9. Это означает, что при порогах ≤ 0.9 детектор находит почти все объекты. При дальнейшем повышении порога (>0.9) полнота начинает падать — то есть остаются только очень «уверенные» предсказания, и часть реальных объектов перестаёт учитываться.

Выбор между двумя крайностями

Низкий порог → много предсказаний → много ложных срабатываний, мало пропусков. Высокий порог → мало предсказаний → мало ложных срабатываний, много пропусков.

Взвешенный компромиссный подход

conf = 0.5–0.7 и мультикадровая проверка ( это когда применяется правило: считать детекцию «истинной» только если она появляется несколько раз подряд или в N из K последних обработанных кадров) — даёт хорошую recall и низкий уровень ложных тревог. Если после подтверждения надо сразу действовать — можно делать «мягкий» манёвр (медленнее), если подтверждение ещё не окончательное.

В нашем прототипе нет мультипроверки и мягких маневров. Это как раз то куда можно развивать проект. Мы взяли значение conf = 0.6, которое показало себя хорошо именно в реальных тестах.

Рис 13. Основные показатели ошибок а метрик при обучении
Рис 13. Основные показатели ошибок а метрик при обучении

Комментарии к графикам: Как и должно быть - ошибки падают (loss) на обучающем и валидационном наборах от эпохи к эпохе, а метрики metrics/mAP_0.5 и metrics/mAP_0.5:0.95 растут.

Оптимизация

Чтобы Jetson проще справлялась с моделью yolov5s (не самой маленькой) необходимо перевести ее в формат TensorRT .engine

Для конвертации YOLOv5 из .pt.wts → в TensorRT-движок (.engine) и для запуска (деплоя) на Jetson мы использовали код из публичного репозитория JetsonYolov5, где подробно показаны шаги сборки .engine и сопутствующих утилит.

В коде Jetson мы применяем обёрточный класс YoloTRT из репозитория JetsonYolov5, который принимает путь к .engine. Пример использования в коде:

from yoloDet import YoloTRT

model = YoloTRT(library="yolov5/build/libmyplugins.so", engine="yolov5/build/best_domino_pol_050524.engine", conf=0.6, yolo_ver="v5")

При предикте в detections складываются координаты BB домино:

detections, t = model.Inference(frame)

Затем происходит парсинг координат:

for obj in detections:
            boxxx = obj['box']
            spisok_bb_y_levo.append(boxxx[3]) # y левого угла рамок
            spisok_bb_y_pravo.append(boxxx[3]) # y правого угла рамок
            spisok_bb_x_levo.append(boxxx[0])  # x левого угла рамок
            spisok_bb_x_pravo.append(boxxx[2])  # x правого угла рамок

Преимущество методов из репозитория JetsonYolov5:

  1. Подготовка кадра. Сначала YoloTRT берёт кадр с камеры и приводит его к виду, который ждала модель во время обучения: изменяет размер, при необходимости добавляет «поля» (чтобы не искажать картинку) и нормализует пиксели. Это стандартный приём, называемый letterbox.

  2. Запуск инференса. Подготовленное изображение подаётся в TensorRT-движок — это быстрый, оптимизированный вариант вашей обученной YOLOv5-модели, упакованный в файл best_domino_pol_050524.engine. Процесс идёт на GPU Nano и выполняется значительно быстрее, чем чистый PyTorch.

  3. Постобработка и привязка к оригиналу. Результат модели (сырые предсказания) проходит фильтрацию (отсекаем слабые срабатывания, делаем NMS), а затем координаты боксов автоматически переводятся обратно в пиксели исходного кадра (таким образом вы получаете x,y,w,h в тех же координатах, что и изображение с камеры).

  4. Удобный вывод. На выходе вы получаете список объектов: класс (d), confidence и box — готовую координату, которую можно рисовать на кадре или использовать для геометрии (в нашем случае — вычисления A, B, E, EF и тд.).

Преимущество такого подхода в том, что основная логика «подготовка → inference → обратный перевод координат» спрятана в одном месте (yoloDet.py). В основном скрипте остаётся только удобный вызов detections, t = model.Inference(frame) и работа с уже «правильными» координатами. Если нужно поменять модель — достаточно заменить .engine файл и, при необходимости, порог conf.

Arduino: приём команд и управление моторами

Arduino в системе — простой, надёжный «исполнитель»: он не принимает решения, а лишь выполняет короткие заранее прописанные манёвры по командам от Jetson. Команды приходят по Serial в формате:

<command>[_<value>]|

Примеры: pryamo|, levee_700|, pravoitog_20|.

На приёме Arduino читает строку до '|', парсит часть до '_' как имя команды, а часть после — как опциональный параметр (скорость/угол).

Мы используем пины: MOTOR1_IN=2, MOTOR1_PWM=3, MOTOR2_IN=4, MOTOR2_PWM=5.

Функция motorControl(val, IN, PWM) преобразует значение val (диапазон примерно -1023..1023) в PWM и выставляет направление через IN-пин.

Текущее поведение — блокирующее: для каждого манёвра используются delay(...), после чего моторы останавливаются (stopMotors()). Это простая и понятная схема для прототипа.

Основные элементы скетча Arduino

Мы ранее уже рассмотрели используемые пины, инициализацию и парсинг команд, а теперь кратко посмотрим, что происходит на стороне Arduino в основной логике в loop()

Основная логика в loop() — запуск исполнения

// Если есть команда — выполняем (посылается в executeCommand)
if (lastCommand != "") {
    executeCommand(lastCommand);
    Serial.println("ok");
    lastCommand = ""; // сбрасываем после выполнения
}

Комментарии: команды выполняются последовательно; после выполнения поле очищается. Это простая блокирующая схема — весь цикл ждёт выполнения команд (включая delay внутри executeCommand).

Выполнение команд — набор "манёвров"

void executeCommand(String command) {
    // Примеры команд: levee, pravee, pryamo, levo1, pravo1, pribligenie, pravoitog и т.д.
    if (command == "levee") {
        // движение влево: смешанная скорость для поворота
        motorControl(400, MOTOR1_IN, MOTOR1_PWM);
        motorControl(-value_v, MOTOR2_IN, MOTOR2_PWM);
        delay(60); // блокирующая задержка
        stopMotors();
    }
    else if (command == "pryamo") {
        // движение прямо
        motorControl(900, MOTOR1_IN, MOTOR1_PWM);
        motorControl(-600, MOTOR2_IN, MOTOR2_PWM);
        delay(90);
        stopMotors();
    }
    // ... остальные варианты (levostart, pravo1, pribligenie и т.д.)
}

Комментарии каждая команда — предопределённый набор вызовов motorControl() + delay() + stopMotors(). Это даёт простое и предсказуемое поведение, но блокирует выполнение других задач во время delay().

Функция управления мотором motorControl

void motorControl(int val, byte pinIN, byte pinPWM) {
  // val ожидается примерно в диапазоне -1023..1023, приводим к PWM -255..255
  val = map(val, -1023, 1023, -255, 255);

  if (val > 0) {              // ВПЕРЁД
    analogWrite(pinPWM, val);
    digitalWrite(pinIN, LOW);
  } else if (val < 0) {       // НАЗАД (инверсия PWM)
    analogWrite(pinPWM, 255 + val); // val отрицательное => 255+val корректирует PWM
    digitalWrite(pinIN, HIGH);
  } else {                    // СТОП
    digitalWrite(pinIN, LOW);
    digitalWrite(pinPWM, LOW);
  }
}

Комментарии: функция преобразует логическую скорость в сигналы направления и ШИМ. Поддерживается как вперёд, так и назад, и стоп.

Остановка моторов

void stopMotors() {
    motorControl(0, MOTOR1_IN, MOTOR1_PWM);
    motorControl(0, MOTOR2_IN, MOTOR2_PWM);
}

Комментарии: единая точка для безопасной остановки — удобно для поддержки.

Ключевые особенности системы

  • Модульная архитектура - разделение на обработку команд и управление моторами

  • Универсальный драйвер двигателей - единая функция для всех моторов

  • Прецизионное управление - точные временные задержки и ШИМ-сигналы

  • Безопасность - автоматическая остановка моторов после каждой команды

  • Гибкость - поддержка параметризованных команд формата "команда_значение"

Такой подход делает Arduino надёжным приводом, а все «интеллектуальные» решения остаются на Jetson, что упрощает отладку и масштабирование проекта.

Запуск и отладка робота

При разработке Jetson подключался к отдельному монитору клавиатуре, мышке. Для этого в Jetson nano есть usb входы и hdmi выход. Для соединения с интернетом использовался Wi-Fi-адаптер.

При запуске и отладке использовалось Wi-Fi соединение. На Jetson был установлен сервис xrdp (Remote Desktop Protocol), который позволяет подключаться удаленно с внешнего компьютера к рабочему столу Jetson. Чтобы подключиться к нему нужно выяснить ip адрес Jetson:

ifconfig

На внешнем компьютере c Windows подключиться к удаленному рабочему столу Jetson ( Win+R -> mstsc -> Подключение к удаленному рабочему столу (здесь как раз прописать ip адрес Jetson) -> вести логин и пароль Jetson), чтобы видеть рабочий стол Jetson и работать на нем:

Рис 14. Подключение к Jetson через удаленный рабочий стол
Рис 14. Подключение к Jetson через удаленный рабочий стол

Эксперименты и результаты

Цель экспериментов — объективно проверить, насколько надёжно (Jetson+YOLOv5s → логика → Arduino → моторы) находит и проезжает через единственный проход в кольце домино исключительно по изображению с камеры.

Результаты

  1. Из 10 запусков 8 успешных выездов без контакта с домино. Два не успешных.

    Баг номер 1: Выезд не в проход. Причина: не детектировал одно домино в центре между двумя домино слева и справа, поэтому посчитал AB расстояние не между соседними домино, а через одного. AB сразу получилось большим и запустилась следующая фаза - выезда, что привело к падению домино. Решение такого бага: мультикадровая проверка.

    Баг номер 2: При выезде робот сдвинул домино: вместо поворота на 90° робот повернулся примерно на 120°. Причина — нестабильность третьего опорного поворотного колеса. Его конструкция допускает произвольную ориентацию при следующем движении: колесо может оказаться развернуто неудачно и создать дополнительное боковое усилие при повороте, что даёт ошибку траектории. Решение — сделать третью опору более предсказуемой .

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

  3. Ложные срабатывания. Когда в кадре появляются предметы на заднем плане, они иногда ложна детектируются как домино. Необходимы эксперименты порогом conf, чтобы исключить такие ложные срабатывания.

Что не получилось в проекте?

  1. Не получилось запустить на Jetson YOLOv8. Использовали YOLOv5

  2. Не получилось на Jetson перевести модель из .pt в .wts. Использовали Google Colab.

Заключение

В этой статье мы показали, что простая мобильная платформа с одной камерой и небольшим алгоритмом компьютерного зрения может решать нетривиальную задачу — найти и проехать через единственный проход в кольце домино. Подход «только CV» оказался жизнеспособным: комбинация не тяжелой модели (yolov5s → TensorRT), простой геометрии (отрезок AB → точка E → EF) и простого состояния робота на Jetson с исполнительной логикой на Arduino даёт понятную, отлаживаемую систему для экспериментов и обучения.

Основные выводы

1. Архитектура Jetson + Arduino — удобна и масштабируема

2. Ключ к надёжности — фильтрация детекций, мультикадровая проверка и эмпирический тюнинг параметров (AB_END, FRAME_SKIP, conf);

3. Блокирующая реализация Arduino проста в прототипе, но для стабильности и плавности стоит перейти на неблокирующий контроль.

Возможное развитие проекта

Обучение робота находить выход из стены домино произвольной формы, когда в кадр попадают не только рядом стоящие, но и домино на заднем плане. Для оценки глубины и взаимного расположения объектов может потребоваться стереокамера. Также возможны эксперименты с более сложной 3D-геометрией реального мира и её точной проекцией на 2D-кадр с учётом высоты и угла наклона камеры.

Приложения

Материалы по проекту можно посмотреть на Github :

  • app.py — основной скрипт для Jetson

  • labirint.ino — скетч для Arduino Nano

  • models/best_domino_pol_050524.engine - модель

  • models/libmyplugins.so — плагин

Комментарии (4)


  1. el_mago
    01.10.2025 22:56

    Это эксперимент для ВКР? У вас опечатка там, где настройки gstreamer. Обычно, вместо постоянных подруливаний, выходят на линию, перпендикулярную к цели и двигаются вдоль этой линии. В развитии вашего проекта стереокамера, где требуется находить объекты в помещении на малом расстоянии, вам не подойдет. Лучше справится, какая-нибудь ToF камера глубины. В целом, тоже самое, но более быстро, менее вычислительно затратно и без нейросетей можно сделать на дешевом одноплатнике с недорогим лидаром.


    1. Iskatel-potenzialov Автор
      01.10.2025 22:56

      Это эксперимент в рамках хобби. Опечатка ок. Спасибо за идею с ToF-камерой глубины.


  1. user-book
    01.10.2025 22:56

    Круто было бы реально только на ардуине решать такое)

    Впрочем если будет дальнометры или даже лидар то ардуинвы хватит. Чисто камерой я хз, хватит ли камня даже если самому написать а не использовать готовые либы как у вас которые ради своей простоты создают оверхед


    1. Iskatel-potenzialov Автор
      01.10.2025 22:56

      Конечно, с камерой и CV у меня совсем НЕ оптимальный вариант. Здесь по оптимальности победит любой робот-пылесос с дешевым 2d-лидаром и маломощным чипом, без «камней» уровня Jetson. Это больше исследовательский кейс о возможностях CV для будущих задач, например, езды по пересеченной местности, где, помимо ориентации с помощью 2D-лидара и энкодеров, полезно определять, что за препятствие находится перед роботом.