Pull to refresh

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

Reading time 6 min
Views 35K
Одно время я увлекался сборкой роботов-машинок на Ардуино и 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
Tags:
Hubs:
+33
Comments 34
Comments Comments 34

Articles