Робот-танк на Raspberry Pi с OpenCV

Одно время я увлекался сборкой роботов-машинок на Ардуино и Raspberry Pi. Играть в конструктор мне нравилось, но хотелось чего-то большего.

И как-то раз, блуждая по Алиэкспрессу, я набрел на алюминиевое шасси для танка. Выглядело это творение в сравнении с машинками из пластика как Феррари в сравнении с телегой.

Я сделал себе подарок на Новый год, танк приехал, был собран и дальше надо было его оживлять. Я снял с машинки собствено Raspberry, конвертер питания, контроллер мотора и батарею. Все это было поставлено на танк и радостно заработало.

Дальше на питоне был написан нехитрый REST API для руления, а на Андроиде — такая же простая программка, позволяла управлять танком, дергая этот API.

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

image

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

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

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

Средствами OpenCV искались контуры нужного цвета (с допустимой толерантностью), потом среди контуров искалась окружность.

Казалось, что основной проблемой может стать случайный круг нужного цвета на каком-нибудь из предметов.

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

Поиск кружка красного цвета:

import cv2
import numpy as np
import sys

def mask_color(img, c1, c2):
    img = cv2.medianBlur(img, 5)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, c1, c2)
    mask = cv2.erode(mask, None, iterations=2)
    mask = cv2.dilate(mask, None, iterations=2)
    return mask

def find_contours(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    thresh = cv2.threshold(blurred, 30, 255, cv2.THRESH_BINARY)[1]
    thresh = cv2.bitwise_not(thresh)
    im2, cnts, hierarchy = cv2.findContours(thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    cp_img = img.copy()
    cv2.drawContours(cp_img, cnts, -1, (0,255,0), 3)
    return cp_img

def find_circles(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.medianBlur(gray,5)
    circles = cv2.HoughCircles(blurred,cv2.HOUGH_GRADIENT,1,20,param1=50,param2=30,minRadius=0,maxRadius=0)
    cimg = img
    if circles is not None:
        circles = np.uint16(np.around(circles))
        for i in circles[0,:]:
            cv2.circle(img,(i[0],i[1]),i[2],(255,0,0),2)
            cv2.circle(img,(i[0],i[1]),2,(0,0,255),3)
            print "C", i[0],i[1],i[2]
    return cimg

def find_circle(img, rgb):
    tolerance = 4
    hsv = cv2.cvtColor(rgb, cv2.COLOR_BGR2HSV)
    H = hsv[0][0][0]
    c1 = (H - tolerance, 100, 100)
    c2 = (H + tolerance, 255, 255)
    c_mask = mask_color(img, c1, c2)
    rgb = cv2.cvtColor(c_mask,cv2.COLOR_GRAY2RGB)
    cont_img = find_contours(rgb)
    circ_img = find_circles(cont_img)
    cv2.imshow("Image", circ_img)
    cv2.waitKey(0)

if __name__ == '__main__':
    img_name = sys.argv[1]
    img = cv2.imread(img_name)
    rgb = np.uint8([[[0, 0, 255 ]]])
    find_circle(img, rgb)

Цветовое распознавание стало заходить в тупик, я отвлекся на каскады Хаара, используя танк для фотоохоты на кота. Кот неплохо маскировался, заставляя каскад ошибаться в половине случаев (если кто не знает, OpenCV идет со специально обученным на котиках каскадом Хаара — бери и пользуйся).

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

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

Эксперименты эти проводились на компьютере, и дело осталось за малым — перенести TF на Raspberry Pi. К счастью, на гитхабе живет уникальный человек, который набрался терпения и прорвался через установку всех зависимостей и многочасовое время компиляции — и выложил в общий доступ собранный Tensorflow для Raspberry Pi.

Однако, дальнейшее изучение темы открыло, что OpenCV не стоит на месте и его контрибуторы выпустили модуль DNN (Deep Neural Networks), предлагающий интеграцию с нейросетями, обученными на TensorFlow. Это решение гораздо удобнее в разработке, плюс отпадает необходимость в самом TF. Пришлось немного поколдовать, так как свежая версия Mobile SSD нейросети для TF, уже не подхватывалась последней версией OpenCV. Надо было искать
и проверять рабочую версию Mobile SSD. Плюс к этому, DNN нормально работает только под OpenCV 3.4, а этой версии для Raspberry я не нашел. Пришлось собирать самому, благо это гораздо проще, чем возиться с TensorFlow. При этом собрать OpenCV под последнюю весию Raspbian (Stretch) не удалось, а вот на последней версии предыдущего поколения (Jessie) все взлетело как надо.

Пример кода, использующий DNN и не использующий Tensorflow.

Несколько файлов, отвечающих за имена объектов были вытянуты из TF и зависимость от самого TF убрана (там было только чтение из файла).
Исходный код на гитхабе.


import cv2 as cv
import tf_labels
import sys

DNN_PATH = "---path-to:ssd_mobilenet_v1_coco_11_06_2017/frozen_inference_graph.pb"
DNN_TXT_PATH = "--path-to:ssd_mobilenet_v1_coco.pbtxt"
LABELS_PATH = "--path-to:mscoco_label_map.pbtxt"

tf_labels.initLabels(PATH_TO_LABELS)
cvNet = cv.dnn.readNetFromTensorflow(pb_path, pb_txt)

img = cv.imread(sys.argv[1])
rows = img.shape[0]
cols = img.shape[1]
cvNet.setInput(cv.dnn.blobFromImage(img, 1.0/127.5, (300, 300), (127.5, 127.5, 127.5), swapRB=True, crop=False))
cvOut = cvNet.forward()

for detection in cvOut[0,0,:,:]:
    score = float(detection[2])
    if score > 0.25:
        left = int(detection[3] * cols)
        top = int(detection[4] * rows)
        right = int(detection[5] * cols)
        bottom = int(detection[6] * rows)
        label = tf_labels.getLabel(int(detection[1]))
        print(label, score, left, top, right, bottom)
        text_color = (23, 230, 210)
        cv.rectangle(img, (left, top), (right, bottom), text_color, thickness=2)
        cv.putText(img, label, (left, top), cv.FONT_HERSHEY_SIMPLEX, 1, text_color, 2)

cv.imshow('img', img)
cv.waitKey()

В общем, теперь фотки танка можно распознавать нейросетью, и это очень важный шаг в навигации в плане узнавания ориентиров. Тем не менее, одних картинок для полноценной навигации не хватало, требовалось измерять расстояния до препятствий. Так у робота появился эхолот. Чтобы подключить эхолот к Raspberry, надо немного потрудиться — эхолот возврашает сигнал на 5V, а Raspberry принимает 3.3V. На коленке эту проблему решают в основном резисторами на бредборде, однако мне не хотелось городить такую кустарщину на роботе. В итоге была найдена микросхема Level Shifter, которая делает все, что надо, и размером она с ноготь.

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

image

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

REST интерфейс, который предоставляет робот в качестве базы для дальнейшего использования:

GET /ping
GET /version
GET /name
GET /dist

POST /fwd/on
POST /fwd/off
POST /back/on
POST /back/off
POST /left/on
POST /left/off
POST /right/on
POST /right/off

POST /photo/make
GET /photo/:phid
GET /photo/list

POST /cam/up
POST /cam/down
POST /cam/right
POST /cam/left

POST /detect/haar/:phid
POST /detect/dnn/:phid

Ссылки:


  1. OpenCV DNN
  2. SSD MobileNet совместимая с OpenCV-3.4.1
  3. Tensorflow для Raspberry Pi
  4. Код рест-сервера для робота на гитхабе
  5. Собранная OpenCV 3.4.1 с поддержкой DNN для Raspbian Jessie
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 34
    0
    Исправьте, пожалуйста:
    В итоге была найдем
      +1
      готово. спасибо, что заметили.
      0
      У гусеничных платформ есть свои недостатки. Шумность, вес, цена, скорость движения.
      Эти скользят?

        0
        зависи от производителя и цены. У одних это мягкая резина, у других это жёсткий чёрный пластик который только выглядит как резина
          0
          Колеса не столько скользят, сколько застревают на мягком ковре, так как такие машинки поворачивают при помощи skid steering — т.е колеса одной стороны крутятся вперед, а другой — назад.
            0
            Вопрос чуть не в тему, но может знаете — как искать выбирать двигатели как на фото выше на замену? Получил с китая гусеничный лего вездеход с похожими моторами (только редуктора другие). Один мотор крутится нормально, второй еле ползет. Из за чего вездеход не ездит прямо. Купил в оффлайне 2 визуально таких же мотора на 6-12 вольт — и теперь оба тормозят и вездеход еле едет. Маркировки на моторах нет. «Быстрый» мотор жив, но он один. Редуктора разбирал — все ок, т.е. дело точно в моторе, но как найти «правильный»?
              0
              Эти моторы самые базовые, должно быть много предложений по ним, можно по отзывам выбирать.
                0
                Базовые, то базовые. Но на популярных лотах написано 3-6 вольт (а блок Лего выдает 7,2/9 вольт на АКБ/батарейках). Количество оборотов разнится от лота к лоту и в отзывах иногда пишут что они «слабые».
                Знать бы на какое количество оборотов смотреть, как минимум.
                А так мотор ищется по строке «dc motor 130».
              +1
              я уже наверно с десяток умерших умерших пылесосов rumba выкинул — мне кажется у них отличные колесные узлы.
                0
                Да, на базе румбы очень популярно роботов делать
              0
              У этих другая проблема — они слишком шустрые (125 RPM при 6 вольтах, вроде). В диапазоне 3-6 вольт еще крутятся кое-как, но скорость движения получается довольно высокой. То есть если несколько сантиметров в секунду надо — никак, моторы на низких напряжениях/PWM просто не могут провернуть редуктор.
              0
              А что за шасси? На али нашел похожее за 75$. Это оно? Имхо, дороговато для творчества…
              0
              А я всегда мечтал собрать такое, только с нормальным вооружением ( порох, автомат заряжаниям (револьверный барабан выстрелы на 20) сжатый воздух?) и устраивать дистанционные бои на танках…
                +2
                Вот Вас точно нельзя к котам подпускать :)
                По теме: недавно смотрел всякие бои «роботов» и откровенно скучал. Схватка радиоуправляемых моделек.
                Насколько бы было интереснее запуск автономных ботов управляемых нейросетями — схватка не только железа но и боевых алгоритмов.
                  +1

                  Лазером можно. как в лазертаге устроено. Была у меня такая идея, может и сделаю ещё, только матчасть по оптике надо подтянуть.

                  0

                  Рекомендую всем брать шасси с амортизаторами, иначе робот будет очень жестко ездить по неровностям. (Для экспериментов в доме хватит и обычного).


                  Автор — я веду схожий хобби проект, было бы интересно пообщаться.

                    0
                    Можно пообщаться конечно. По какому каналу?
                    0
                    Автор, спасибо за статью, у меня как раз стоит в режиме «ожидания» проект по TensorFlow на Raspberry Pi. Он уже собран по той инструкции с гитхаба. Моя задача, распознать например цвета (тюльпаны, бутоны… и т.д). Распберри с камерой будет статичной. Сами цветы будут подноситься к нему. На данный момент, модель ТФ обучена и портирована на Малинку, только времени нету чтобы все проверить, и допилить. Ты пишешь, что OpenCV (с модулем DNN) уже развился до уровня, что TensorFlow не нужен. Я правильно понял? Или для моей задачи, все-таки TensorFlow?
                      0
                      Да, если обученная модель по формату подходит версии OpenCV, то можно дальше обойтись без TF на распберри
                        0
                        а на OpenCV тоже обучение происходит на стороннем компьютере, или непосредственно ра Малинке?
                          0
                          на OpenCV нельзя обучить нейросеть. Для этого нужен TF на стороннем компе и помощнее. А OpenCV может использовать обученную.
                      0
                      А как исправляли искажение перспективы при поиске красного круга? Или ждали пока танк сделает фотку прямо перпендикулярно красному кругу?
                        0
                        OpenCV допускает определенный эллипс при поиске окружности.
                          0
                          Что то в документации такого не было. Хафа, а Вы использовали cv2.HoughCircles именно его алгоритм, ищет точно и только круги, ни на какие эллипсы он не реагирует просто потому, что в нем заложена формула окружности, а не эллипса.
                          Что бы методом Хафа искать эллипс нужно менять исходники cv2.HoughCircles — у эллипса и окружности разные, похожие, но всё же разные формулы.
                          И вопрос остался, искажение перспективы тоже.
                            0
                            Только что посмотрел свои тестовые круги — один например оказался 16x19 и контур вокруг был распознан как окружность.
                              0
                              Так какого диаметра окружность распознана? Как 16, 19 или еще как?
                        0
                        Очень интересно, спасибо.
                        А вы не смотрели в сторону AIY Vision Kit от гугла? Там к малинке подцепляется дополнительная платка, которая, как я понимаю, напрямую работает с заранее скомпилированными TF-моделями. По задачам, кажется, есть определённая схожесть, плюс не нужно заморачиваться со сборкой OpenCV нужной версии, да и сама малинка там используется вообще Zero, т.к. вся обработка идёт на внешней плате.
                          0
                          Да, смотрел. Главная проблема — эта плата садится на все пины Raspberry, блокируя что-либо кроме распознавания. А у меня на пинах завязано и движение колес, и управление штативом, и эхолотом.
                            0
                            Там на внешней плате выведено четыре GPIO-контакта для произвольных применений, а у вас на фото, кажется, как раз четыре и используется. ;)
                              0

                              Сейчас используется 6 gpio плюс все пины питания, 3 граунда плюс оба пина i2c.

                          0
                          Спасибо, что поделились своей разработкой, но у меня есть некоторые комментарии по использованию OpenCV на Python:

                          1. Вместо
                          mask = cv2.erode(mask, None, iterations=2)
                          mask = cv2.dilate(mask, None, iterations=2)
                          

                          лучше писать:
                          cv2.erode(mask, dst=mask, kernel=None, iterations=2)
                          cv2.dilate(mask, dst=mask, kernel=None, iterations=2)
                          

                          … а еще лучше:
                          cv2.morphologyEx(mask, dst=mask, op=cv2.MORPH_OPEN, kernel=None, iterations=2)
                          

                          Во-первых, erode() -> dilate() это морфологическая операция open(). Во-вторых, по возможности надо переиспользовать буферы памяти — это экономит не только память, но и время исполнения, поскольку память уже может находиться в кэше.

                          2. Вместо
                          thresh = cv2.bitwise_not(thresh)
                          

                          лучше писать:
                          thresh = ~thresh
                          

                          В сложных выражениях это упрощает понимание. Точно так же NumPy переопределяет побитовые AND (&), OR (|), XOR (^)

                          3. «каскадом Хаара» — на самом деле это детектор Виола-Джонса с Хааровскими фичами.
                            0
                            Спасибо, буду иметь в виду.

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

                          Самое читаемое