Урок 13
Введение: Мы продолжим тему компьютерного зрения и после получения первичного дискретизированного изображения перейдем к его предобработке. Подход к выбору алгоритмов предобработки и алгоритмов семантического поиска зависит от задачи, которую ставят перед роботом. В нашем случае мы ищем желтый шарик на изображении с камеры. И ключевых слов тут два, "шарик" и "желтый". Т.е. все объекты не желтого цвета, пусть даже и шарики - не подходят под ответ решения семантической задачи. И все желтые объекты не круглой формы - тоже не подходят. И если поиск объектов круглой формы — это достаточно нетривиально, то поиск желтого цвета на цветном изображении достаточно легкая задача. Давайте начнем ее решать. Для начала нам надо понять, что такое желтый цвет. И здесь нам надо погрузиться в основы оптики.
Цвет — это человеческое восприятие светового потока, попадающего в глаз. Говоря простым языком, цвет — это ощущение, которое получает человек при попадании ему в глаз световых лучей. Поток света с одним и тем же спектральным составом вызовет похожие ощущения у разных людей в силу того, что у них похожие физиологические характеристики глаз, и для каждого из них цвет будет слегка разным, но в целом похожим. А что такое "желтый" цвет? Существует популярное представление света как смеси трех основных цветов Красного, Синего и Зеленого. Т.е. любой видимый нами цвет можно представить как совокупность этих трех цветов в различной интенсивности.

К примеру, вот такой вот желтый цвет, можно представить в виде композиции Красного с интенсивностью 247, Зеленого с интенсивностью 255, и Синего нулевой интенсивности. Обычно интенсивность цвета измеряется в градации от 0 до 255. Где 0 это минимальная интенсивность, а 255 максимальная. Данную цветовую модель называют “Модель RGB”, по названиям базовых цветов Red-Green-Blue, Красный-Зеленый-Синий.
Однако люди воспринимают цвет в несколько другой интерпретации, мы не всегда понимаем, какие именно цвета и в каких пропорциях надо смешать, чтобы получить тот или иной цвет, который мы видим. И таким образом нам тяжело сказать, как нам надо изменить соотношение базовых цветов, чтобы получить цвет, который мы хотим. К примеру, какое сочетание цветов RGB будет для Фиолетового? Наверное, там будет много Синего и Красного, а вот сколько там должно быть Зеленого? Не понятно… Или в случае с нашим шариком, допустим мы подберем такое сочетание цветов, которое будет означать желтый цвет именно этого шарика, а если изменится освещение (а оно обязательно изменится), то в какую сторону нам изменять значения цветов, чтобы получить новый, более темный или более светлый желтый?
Именно поэтому для облегчения ориентации в цветовом пространстве часто применяют другую цветовую модель - HSV. Где первое число H отвечает чисто за цвет в спектре радуги, второе Saturation за насыщенность цвета, а третье Value за яркость.
В такой цветовой модели гораздо проще ориентироваться. Например, всем цветам, которые люди могут охарактеризовать как «желтый» соответствует диапазон H от 30 до 70, а яркость и насыщенность у них может быть любой – все равно это будут желтые цвета, просто от темно-желтого - почти черного, до светло-желтого - почти белого.
Таким образом, если в световой модели HSV отфильтровать все цвета, значения H которых лежат вне диапазона от 30 до 70, то оставшиеся пиксели на изображении будут соответствовать только объектам желтого цвета. Т.е. нам надо перевести изображение, которое у нас первоначально было в RGB-модели в HSV-модель и применить фильтр к получившемуся массиву данных.
Подробнее про цветовые модели вы можете почитать здесь). В этой же статье есть формулы, которые надо применить к значениям каждого пикселя изображения в модели RGB, чтобы перевести ее в HSV. Вот они:
Все что нам надо сделать, для нашего RGB массива из прошлого занятия, это пробежать в цикле по всем пикселям, взять значения R, G и B для каждого пикселя, применить формулы для перевода в HSV и получить новый массив уже в новой цветовой модели.
Общий алгоритм решения:
- Возьмем программу прошлого урока как заготовку
- Возьмем массив сконвертированный в RGB 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)
rospy.Subscriber("/front_camera/image_raw/compressed", CompressedImage, cb_video_capture)
rospy.sleep(1)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
Теперь опишем функцию перевода цветовой модели из RGB в HSV, назовем ее RGB2HSV
def RGB2HSV(im):
Переменная im, которая является аргументом нашей функции, содержит тот самый по-пиксельный массив данных нашего изображения, который мы и будем обрабатывать. Это класс многомерного массива библиотеки numpy именно его мы получили применив функции np.frombuffer и cv2.imdecode к данным переданным в функцию обратного вызова подписчика на данные с камеры. Этот класс имеет несколько свойств, и в частности у него есть такое свойство как shape, это кортеж из 3-х элементов в котором указана ширина и высота изображения в пикселях и глубина цвета. В нашем случае это 480 на 640 с глубиной 3. То, что размеры массива нам известны дает нам возможность легко организовать перебор изображения по каждому пикселю каждой строки. Сделаем два цикла и вложим один в другой. В первом будем пробегать по строкам, а во втором по элементам этих строк - пикселям
Если посмотреть на формулу перевода цветовой модели, то видно, что она требует, чтобы значения R,G,B,S,V лежали в пределах от 0 до 1. Давайте вытащим у каждого пикселя значения R,G и B, они находятся в 3-м измерении массива im, и так как максимум эти значений 255 поделим их значения на 255, чтобы частное от деления уложилось в интервал от 0 до 1.
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)
Дальше к каждому из значений применим формулу перевода цветовой модели и получим уже значения H,S и V
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 новые значения. Ведь в массиве не написано, что это именно RBG. А так как размерности совпадают, мы можем также попиксельно перезаписать новые значения.
im[j][i][0] = int(H/2)
im[j][i][1] = int(S*255)
im[j][i][2] = int(V*255)
Далее, чтобы видеть прогресс работы выедем текущее значение обрабатываемой строки и после того как циклы отработают, вернем пересчитанное изображение im.
print(j)
return im
Функция перевода цветовой модели - готова, давайте передадим в нее данные с камеры и сохраним результат в файл .jpg
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)
Здесь, мы подождем одну секунду, чтобы данные с камеры начали поступать в подписчик. Затем передадим их на обработку в нашу функцию, и запишем два файла.
В целом вся программа выглядит так:
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)
rospy.Subscriber("/front_camera/image_raw/compressed", CompressedImage, cb_video_capture)
rospy.sleep(1)
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)
Проверка решения:
Попросите учащихся сохранить программу на роботе и запустить.
После того, как программа отработала, в домашней директории робота должен появиться файл ballJPG.jpg с изображением с камеры и файл ball_HSV.jpg сконвертированный в цветовую модель HSV. Попросите учащихся открыть его в режиме демонстрации экрана в Discord.
Изображение должно выглядеть примерно вот так:
На следующем уроке, мы поговорим почему так странно выглядит наше изображение, произведем наложение маски на изображение и передадим полученную маску в функцию обрабатывающую изображение по алгоритму визуальных моментов, найдем желтый шарик на изображении и определим его центр в координатах изображения.