Все животные, включая людей, очень любят следить за движущимися объектами. Этот встроенный в живых существ механизм, позволяет им не терять вещь из виду и позиционировать её в пространстве. Этот проект посвящен созданию робота-наблюдателя, который при помощи OpenCV будет следить за теннисным мячиком.
Итак, за каким объектом будет следить робот? По началу, в качестве такого объекта я выбрал обычный теннисный шарик. Шарик этот имеет классический ярко-зеленый цвет, и теоретически должен был легко выделяться из явно не зеленого окружения. Однако, в дальнейшем пришлось заменить большой зеленый шар на маленький оранжевый шарик для пинг-понга. Причина замены будет описана ниже.
В проекте были использованы два самых простых сервомотора sg90, драйвер сервомоторов Pololu и вебкамера Microsoft LifeCam HD 3000.
Программа для детектирования окружности
Писать программу было решено на языке python. Алгоритм работы python-скрипта для распознавания состоит из шести основных шагов:
- преобразование кадра в формат HSV;
- фильтрация в заданном диапазоне HSV;
- морфологическое преобразование;
- размывание;
- детектирование окружностей;
- передача управляющих сигналов на сервоприводы.
Преобразование в HSV
hsv = cv2.cvtColor(img, cv.CV_BGR2HSV )
Здесь всё просто. Даем изображение с веб-камеры, получаем — конвертированное в HSV изображение. Почему HSV? Потому что в HSV проще создать правильную маску для выделения нужного цвета.
Фильтрация по цвету
thresh = cv2.inRange(hsv, h_min, h_max)
Очень важный шаг. Функция inRange преобразует цветную картинку в черно-белую маску. В этой маске, все пиксели, попадающие в заданный диапазон — становятся белыми. Прочие — черными. Результат работы inRange представлен на второй картинке.
Морфологическое преобразование
st1 = cv2.getStructuringElement(cv2.MORPH_RECT, (21, 21), (10, 10)) st2 = cv2.getStructuringElement(cv2.MORPH_RECT, (11, 11), (5, 5)) thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, st1) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, st2)
Данная процедура нужна для того, чтобы убрать из кадра мелкий мусор и замазать возможные дефекты в выделяемом объекте. Например, морфологическое преобразование позволяет убрать из теннисного шарика прожилку, которая имеет отличный цвет. Либо, как в моем случае, можно убрать надпись и засвеченные участки шарика.
Размывание
thresh = cv2.GaussianBlur(thresh, (5, 5), 2)
Банальное размывание методом Гаусса. Как и предыдущая процедура, размывание необходимо для сглаживания шероховатостей.
Детектирование окружностей
circles = cv2.HoughCircles( thresh, cv.CV_HOUGH_GRADIENT, 2, mcr, np.array([]), cet1, cet2, mcs, xcs)
Собственно, само детектирование. Процедура HoughCircles находит на изображении все окружности, используя при этом преобразование Хофа. Важными параметрами здесь являются:
- mcr — минимальное расстояние между окружностями (h/4);
- cet1 и cet2 — параметры оператора Кэнни, используемого для построения границ объекта (80 и 50);
- mcs, xcs — минимальный и максимальный радиус окружностей (5 и 0).
Для наглядности, поверх изображения накладывается окружность синего цвета, а центр этой окружности обозначается зеленой точкой.
cv2.circle(img, (x, y), 3, (0,255,0), -1) cv2.circle(img, (x, y), maxRadius, (255,0,0), 3)
Также в углу кадра отображается время между двумя кадрами.
text = '%0.1f' % (1./dt) cv2.putText( img, text, (20, 20), cv2.FONT_HERSHEY_PLAIN, 1.0, (0, 110, 0), thickness = 2)
Результат:
На последнем шаге, на основе координат обнаруженной окружности, рассчитываются углы поворота сервоприводов.
sctrl.shift(0, (x*1./w)*20-10) sctrl.shift(1, -((y*1./h)*20-10))
Полный код программы детектирования окружности
import sys, time
from threading import Thread
import cv2.cv as cv
import cv2
import numpy as np
import video
from servo import Servo
COM = 'COM4'
MORPH_ON = 1
HOUGH_ON = 1
SERVO_ON = 1
BLUR_ON = 1
SHOW_MAIN = 1
SHOW_THRESH = 1
FLIP = 1
COLOR_RANGE={
'ball_light': (np.array((20, 70, 170), np.uint8), np.array((40, 170, 255), np.uint8)),
'ball_dark': (np.array((0, 170, 120), np.uint8), np.array((20, 240, 255), np.uint8)),
}
def nothing( *arg ):
pass
def createPath( img ):
h, w = img.shape[:2]
return np.zeros((h, w, 3), np.uint8)
class Tracker(Thread):
def __init__(self, color, color_2=None, flag=0):
Thread.__init__(self)
self.color = color
self.path_color = cv.CV_RGB(0,110,0)
self.lastx = 0
self.lasty = 0
self.time = 0
self.h_min = COLOR_RANGE[color][0]
self.h_max = COLOR_RANGE[color][1]
if color_2:
self.h_min_2 = COLOR_RANGE[color_2][0]
self.h_max_2 = COLOR_RANGE[color_2][1]
self.flag = flag
if self.flag:
cv2.namedWindow( self.color )
def poll(self,img):
par1 = 80#40
par2 = 50#67
h, w = img.shape[:2]
dt = time.clock() - self.time
self.time = time.clock()
hsv = cv2.cvtColor(img, cv.CV_BGR2HSV )
thresh = cv2.inRange(hsv, self.h_min, self.h_max)
thresh_2 = cv2.inRange(hsv, self.h_min_2, self.h_max_2)
thresh = cv2.bitwise_or(thresh, thresh_2)
if MORPH_ON:
st1 = cv2.getStructuringElement(cv2.MORPH_RECT, (21, 21), (10, 10))
st2 = cv2.getStructuringElement(cv2.MORPH_RECT, (11, 11), (5, 5))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, st1)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, st2)
if BLUR_ON:
thresh = cv2.GaussianBlur(thresh, (5, 5), 2)
circles = None
if HOUGH_ON:
circles = cv2.HoughCircles( thresh,
cv.CV_HOUGH_GRADIENT, 2, h/4,
np.array([]),
par1, par2, 5, 0)
if circles is not None:
maxRadius = 0
x = 0
y = 0
found = False
for c in circles[0]:
found = True
if c[2] > maxRadius:
maxRadius = int(c[2])
x = int(c[0])
y = int(c[1])
if found:
cv2.circle(img, (x, y), 3, (0,255,0), -1)
cv2.circle(img, (x, y), maxRadius, (255,0,0), 3)
xspeed = abs(x - self.lastx)/dt;
yspeed = abs(y - self.lasty)/dt;
self.lastx = x
self.lasty = y
if SERVO_ON:
yaw = (x*1./w)*50.0 - 25.0
pitch = (y*1./h)*39.0 - 19.5
sctrl.shift(0, (x*1./w)*20-10)
sctrl.shift(1, -((y*1./h)*20-10))
if FLIP:
img = cv2.flip(img,0)
thresh = cv2.flip(thresh,0)
if self.flag:
cv2.imshow(self.color, thresh)
text = '%0.1f' % (1./dt)
cv2.putText( img, text, (20, 20),
cv2.FONT_HERSHEY_PLAIN,
1.0, (0, 110, 0), thickness = 2)
if SHOW_MAIN:
cv2.imshow('result', img)
if __name__ == '__main__':
if SHOW_MAIN:
cv2.namedWindow( "result", cv.CV_WINDOW_AUTOSIZE )
green = Tracker('ball_light', 'ball_dark', SHOW_THRESH)
green.start()
if SERVO_ON:
sctrl = Servo( com=COM )
sctrl.setpos(0,45)
sctrl.setpos(1,45)
cap = video.create_capture(1)
flag, img = cap.read()
green.path = createPath(img)
while True:
flag, img = cap.read()
try:
green.poll(img)
except:
cap = None
green = None
if SERVO_ON:
sctrl.stop()
raise
green.join()
ch = cv2.waitKey(5)
if ch == 27:
break
if SERVO_ON:
sctrl.setpos(0,45)
sctrl.setpos(1,45)
sctrl.stop()
green = None
cap = None
cv2.destroyAllWindows()
Программа для управления сервомоторами на Python
В представленной выше программе класс Servo отвечает за передачу управляющих команд для сервомоторов в специальный драйвер фирмы Pololu. Полный код класса:
import serial
import sys
MODE_256 = 0
MODE_QUARTER = 1
def clamp( val, mmin, mmax ):
if val > mmax:
val = mmax
elif val < mmin:
val = mmin
return val
class Servo:
ser = None
def __init__( self, com='COM6', baud = 9600, mode=MODE_256 ):
self.ser = serial.Serial(com)
self.ser.baudrate = baud
self.mode = mode
self.coords = [0,0]
def stop( self ):
self.ser.close()
def shift( self, n, angle ):
angle = clamp( self.coords[n] + angle, 0, 90)
self.setpos( n, angle )
def setpos( self, n, angle ):
angle = clamp(angle, 0, 90)
self.coords[n] = angle
if self.mode == MODE_256:
pos = int( 254 * angle/90.0 )
bud=chr(0xFF)+chr(n)+chr(pos)
elif mode == MODE_QUARTER:
pos = int( 1000 * angle/90.0 ) + 1000
lo = pos & 0x7F
hi = ( pos >> 7 ) & 0x7F
bud=chr(0x84)+chr(n)+chr(lo)+chr(hi)
self.ser.write(bud)
self.ser.flush()
Проблемы
Несмотря на кажущуюся простоту проекта, по ходу его реализации возникли достаточно неприятные проблемы.
Камера
Изначально, я хотел использовать старенькую 300-килопиксельную веб-камеру. Я даже вытащил её из родного корпуса и прикрутил к монтировке (см. предыдущий пост). Однако, после попытки провести распознавание, меня ждало разочарование. Цвета изображения были каким-то блеклыми, и зеленый шарик выглядел совсем не зеленым. Никак не получалось подобрать маску для отделения этого шарик от фона. Кроме того, изрядно тормозил видеопоток, вероятно из-за встроенного алгоритма автоматический экспозиции, который старался высветлить темную картинку.
Ввиду такого безобразия, я решил заменить камеру на что-то более качественное и настраиваемое. Посмотрев различные ролики на ютубе, мой выбор пал на доступную Microsoft LifeCam HD 3000. Подключив эту камеру к роботу, я снова немного разочаровался 🙁 Опять тормоза и непонятная цветопередача. Благо, отключение автоматической экспозиций решило проблему с тормозами, а отключение TrueColor и авто-баланса белого — немного стабилизировало цветопередачу.
Инфракрасное излучение
Несмотря на новую камеру, мне по-прежнему долго не удавалось настроить маску для выделения нужного цвета. Лишь спустя множество попыток, я понял что камера видит этот мир совсем не так как я. Смотря на оранжевый шарик, я вижу оранжевый шарик. Когда на этот же шарик смотрит веб-камера — она видит чертовски яркий оранжевый шарик. Такой яркий, что он уже не оранжевый, а желтый. Как это обычно со мной бывает, причина явления лежала на поверхности. Дело в том, что матрица камеры очень хорошо чувствует ближнее инфракрасное излучение (Near-infrared). Эти ИК лучи в больших количествах излучаются галогеновыми (и не только) лампами и солнцем.
Как показала практика, настройка цветовой маски в условиях ИК засветки является весьма непростой задачей. Решением здесь может стать ИК фильтр, который устанавливается в хороших фотоаппаратах для обеспечения адекватной цветопередачи. Но проблему можно попытаться решить и немного иначе, хоть и не так качественно как с фильтром. Для выделения засвеченного шарика я попробовал применить сразу два фильтра. Один — для засвеченного участка шарика, другой — для затененного. Получилось сносно, но наверняка есть и другие варианты.
Настройка цветовой маски
Как уже говорилось, для каждого объекта требуется специально настраивать цветовую маску (даже две). Для настройки этой маски, а именно параметров h_min, h_max функции inRange пришлось сделать отдельное приложение с кучей бегунков. Чтобы выделить нужный цвет необходимо подобрать границы компонента H (Hue). Параметр S (Saturation) отвечает за насыщенность цвета. Например, кожа человека имеет оранжево-желтый цвет (H) как у шарика, но слабую насыщенность. Наконец V (Value) определяет яркость цвета. Затененный шарик будет иметь низкое значение V.
Для моего оранжевого шарика я подобрал такие маски:
светлый шарик — (20, 70, 170) — (40, 170, 255)
темный шарик — (0, 170, 120) — (20, 240, 255)
Несмотря на возникшие трудности, робот-наблюдатель всё-таки ожил. Следующий этап — установка ИК фильтра и распознавание лиц. Небольшое видео представлено ниже.
а что с проектом? Почему нет новых материалов?