Урок 14
Введение: Данный урок, является продолжением группы уроков на использование компьютерного зрения совместно с нашим роботом. В прошлых уроках, мы получили изображение с камеры, превратили его в массив RGB и затем перевели цветовую модель в HSV. В прошлый раз, мы открыли получившееся изображение и увидели, что оно выглядит довольно странно. Все дело в том, что мы перевели изображение в другую цветовую модель, а открываем его на просмотр при помощи RGB редактора. Т.е. сама программа, которая показывает нам картинку, не знает что картинка в HSV, она думает, что это стандартная RGB картинка и преобразует каждый пиксель этого изображения по правилам RGB, в результате закодированное нами HSV изображение выглядит вот так вот.

Тем не менее, теперь у нас есть массив пикселей, который переведен в цветовую модель HSV и для поиска желтого цвета на изображении, мы применим следующий алгоритм: там где цвета пикселей укладываются в диапазон желтого цвета мы преобразуем эти пиксели в чисто белый цвет, а там где цвета не попали в диапазон - пиксели будут черные. Такое преобразование изображения называется накладыванием цветовой маски. И этот алгоритм превратит цветное изображение в черно-белое.
Теперь давайте найдем цветовые границы, т.е. такой диапазон цвета, вне которого мы будем красить все пиксели в черный. Проще всего это сделать экспериментально.
Подключите rviz к вашему роботу, выведете изображение камеры в rviz и направьте робота при помощи веб-интерфейса так, чтобы камера смотрела строго на шарик. После чего выведете значение пикселя по координатам центра изображения (320х240) это и будет значение желтого цвета от которого мы будем отталкиваться. Мы получим какое-то значение, к примеру, 18, 220, 255 (для желтого цвета). Собственно в этих цветовых координатах мы и будем искать наш шарик. Введем нижнюю и верхнюю границу цвета. Скажем от 14 до 25 - это будет цвет, от 180 до 220 - насыщенность и от 80 до 255 освещенность. Кстати, именно для того, чтобы так легко оперировать цветами мы и переводили изображение в HSV. Согласитесь, так гораздо проще, чем думать, как именно изменить все три компоненты базовых цветов, чтобы получить к примеру чуть другой оттенок желтого. А сейчас мы просто чуть чуть меняем первую компоненту и получаем желтый с зеленым оттенком (салатовый) или желтый с красным (оранжевый).
Далее после того как мы определили основной цвет, и границы, нам надо будет написать алгоритм, аналогичный переводу цветовой модели. Т.е. алгоритм пробежит по всем пикселям и сравнит их значения с цветовыми границами, и если они попадают в границы, то окрасит пиксели чисто-белым, а если нет, то черным.
Подготовка: Данную задачу можно решать в любой комбинации полигона, подойдет и "Офис" и "Дорога". В данном уроке мы не будем передвигать робота, таким образом, нам не нужна какая-то специальная подготовка пространства вокруг. Попросите техника на полигоне положить шарик желтого (или любого другого контрастного цвета) на полигон рядом с роботом.
Задача: Наложить цветовую маску на полученное нами изображение с камеры робота.
Общий алгоритм решения:
- Получить изображение с камеры робота
- Преобразовать его в HSV (взять алгоритм из предыдущего урока)
- Определить цветовые границы
- Наложить цветовую маску - желтого цвета
- Записать получившееся черно-белое изображение в jpg файл
Решение: Возьмем весь код программы прошлого урока, реализующий перевод цветовой модели и сохраним его в новом файле.
import rospy
import cv2
import numpy as np
from sensor_msgs.msg import CompressedImage
rospy.init_node("photographer")
def cb_video_capture(image_msg):
global image_from_ros_camera
np_arr = np.frombuffer(image_msg.data, np.uint8)
image_from_ros_camera = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
def RGB2HSV(im):
for j in range(im.shape[0]):
for i in range(im.shape[1]):
R = float(im[j][i][0]/255)
G = float(im[j][i][1]/255)
B = float(im[j][i][2]/255)
V = max(R,G,B)
if V == 0:
S = 0
else:
S = ((V - min(R,G,B)) / V)
if R == B == G:
H = 0
elif V == R:
H = (60*(G - B))/(V - min(R,G,B))
elif V == G:
H = (120 + 60*(B - R))/(V - min(R,G,B))
elif V == B:
H = (240 + 60*(R - G))/(V - min(R,G,B))
if H < 0:
H += 360
im[j][i][0] = int(H/2)
im[j][i][1] = int(S*255)
im[j][i][2] = int(V*255)
print(j)
return im
rospy.sleep(1)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
HSV = RGB2HSV(image_from_ros_camera)
cv2.imwrite("/home/pi/ball_HSV.jpg", HSV)
Теперь нам надо определить цвет центра изображения, чтобы от него отталкиваться в определении цветовых границ.
Просто выведем значения массива изображения с координатами (320х240) при помощи функции print()
rospy.sleep(1)
HSV = RGB2HSV(image_from_ros_camera)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
cv2.imwrite("/home/pi/ball_HSV.jpg", HSV)
print(HSV[320][240]
Попросите учащихся сохранить программу на роботе, запустить и посмотреть в консоли какое значение HSV у пикселя с координатами 320 на 240. Пусть каждый из учащихся напишет свое получившееся значение в чат.
Обсудите с учащимися почему значения у всех немного разнятся.
Все дело в освещении и калибровке камеры
У нас с нашим освещением и нашим шариком получилось значение [18, 220, 255]. У вас может получится другое значение. Теперь создадим две переменные с цветовыми координатами границ, в соответствии с рассуждениями, которые мы сделали в началe.
import rospy
import cv2
import numpy as np
from sensor_msgs.msg import CompressedImage
rospy.init_node("photographer")
yellowLower = (100, 180, 80) # dark
yellowUpper = (120, 255, 255) # light
Расположим их в начале нашей программы, для того чтобы мы могли быстро их найти и изменить, если мы захотим подстроить цвета под новый шарик или новое освещение.
Напишем теперь функцию наложения маски на изображение. В качестве входного аргумента в функцию используем тот же класс массива данных изображения, что и в функции RGB2HSV, и двойной цикл для пробегания по всем пикселям, скопируем из нее же.
def inRange(im):
for j in range(im.shape[0]):
for i in range(im.shape[1]):
Дальше напишем условия по которым мы будем фильтровать пиксели на изображении. Как мы и определили, все что не попадает в цветовые границы - черное, все что попадает - белое.
def inRange(im):
for j in range(im.shape[0]):
for i in range(im.shape[1]):
if (im[j][i][0] < yellowLower[0] or
im[j][i][0] > yellowUpper[0] or
im[j][i][1] < yellowLower[1] or
im[j][i][1] > yellowUpper[1] or
im[j][i][2] < yellowLower[2] or
im[j][i][2] > yellowUpper[2]):
im[j][i][0] = 0
im[j][i][1] = 0
im[j][i][2] = 0
else:
im[j][i][0] = 255
im[j][i][1] = 255
im[j][i][2] = 255
return im
Дальше, передадим HSV массив на вход нашей функции наложения маски и после того как она отработает, сохраним получившуюся маску в файл.
rospy.sleep(1)
HSV = RGB2HSV(image_from_ros_camera)
mask = inRange(HSV)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
cv2.imwrite("/home/pi/ball_HSV.jpg", HSV)
cv2.imwrite("/home/pi/ball_MASK.jpg", mask)
Проверка решения:
Попросите учащихся сохранить программу на роботе и запустить ее.
После того, как программа отработала в домашней директории робота должен появиться файл ball_JPG.jpg с изображением с камеры, файл ball_HSV.jpg с моделью из предыдущего урока и файл ball_MASK.jpg с наложенной маской нашего изображения. Попросите учащихся открыть его в режиме демонстрации экрана в Discord.
Он должен выглядеть примерно так:

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