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

В рамках данной группы уроков мы закончим с предобработкой изображения, хотя конечно сама тема предобработки изображения не исчерпывается этими двумя приемами (Перевод в HSV и наложение маски). Существует большое количество приемов подготовки изображения к семантическому анализу, и о некоторых из них мы поговорим позже, но большинство вам нужно будет исследовать самостоятельно, если вы хотите углубляться в тему компьютерного зрения.
Итак, раз предобработка закончена и изображение готово к семантическому алгоритму, давайте применим один из самых несложных, как для понимания, так и вычислительно, алгоритмов - поиск Моментов изображения.
Определение моментов из Википедии достаточно точно, но без подготовки и объяснения совершенно непонятно. Мы дополним определение из Википедии, свойствами моментов изображения и через эти свойства вам будет легче понять, что-же такое момент.
Определим, что такое Момент изображения, общими словами. Момент - это некое число, характеризующее изображение. При этом в соответствии с классическим определением, моменты бывают различных порядков. В общем случае момент определяется по этой формуле:

Где (i - i^) и (j - j^) - это отклонение в координатах для каждого пикселя от точки с координатами (i^,j^), относительно которой мы и определяем момент. p и q - это и есть коэффициенты определяющие порядок момента. I(i,j) - интенсивность каждого пикселя. Ну и суммы по обоим коэффициентам i и j - это просто суммы по всем пикселям всего изображения.
Давайте слегка упростим эту формулу, перенеся точку относительно которой мы будем считать момент в начало координат. В итоге получим вот такую формулу:
$$m(p,q) = \sum _x \sum_y x^p y^q I(x,y)$$
Т.е. искомый момент это сумма произведений координат пикселей в соответствующих степенях p и q и интенсивности этих пикселей.
Ну хорошо, чуть понятнее, а зачем нам это нужно? Ответ прост, есть формула связывающая отношение моментов определенных порядков и координаты центра характерных особенностей на картинке, которые эти моменты описывают. Т.е. в нашем случае мы можем произвести над изображением черно-белой маски какие-то алгоритмические преобразования и получить координаты центра массива белых точек т.е. координаты центра шарика на изображении.
Формулы описывающие координаты центра следующие:
x = m10 / m00
y = m01 / m00
Для понимания того как работают эти формулы, давайте рассчитаем координаты центра белого пикселя, вот такого вот изображения:

Это упрощенный пример изображения, которое мы получили после применения маски. Т.е. есть какие-то белые пиксели и есть какие-то черные пиксели, надо определить координаты центра белых пикселей. Давайте для начала посчитаем момент m00, в соответствии с формулой $$m(0,0) = \sum _x \sum_y x^0 y^0 I(x,y)$$ так как $$x^0 y^0$$ всегда равно 1 для любого х и y, то можем написать еще проще
$$m(0,0) = \sum _x \sum_y I(x,y)$$
Будем считать пиксели построчно, начиная с начала координат 0.0. Сначала первую строку с y = 1, и дальше вверх до y = 4, символ ^ означает возведение в степень.
Начнем с первой строчки:
((1^0 * 1^0 * 0 + 2^0 * 1^0 * 0 + 3^0 * 1^0 * 0 + 4^0 * 1^0 * 0) = 0
Очевидно, что суммарный момент по первой строчке равен нулю. Т.к. x в степени i=0, всегда дает единицу, у в степени j=0, тоже всегда дает единицу, а вот I(x,y) всегда равен 0 для черных пикселей и умножение этого коэффициента на любое слагаемое дает 0, ну а сумма 4-х нулей, будет нулем.
Так же понятно, что вторая строчка такая-же. Там тоже все пиксели черные, и нулевая интенсивность даст нулевую сумму.
А вот третью строчку распишем поподробнее:
((1^0 * 1^0 * 0 + 2^0 * 1^0 * 0 + 3^0 * 1^0 * 1 + 4^0 * 1^0 * 0) = 1
Здесь возведение 3 в степень 0, дает 1, возведение 1 в степень 0, тоже дает 1, а вот интенсивность белого пикселя ненулевая. И произведение трех единиц, равно 1.
Четвертая строчка с очевидностью 0, тоже из-за всех черных пикселей нулевой интенсивности.
Таким образом m00 = 1
Теперь давайте посчитаем момент m10, в соответствии с теми же рассуждениями.
первая строчка = ((1^1 * 1^0 * 0 + 2^1 * 1^0 * 0 + 3^1 * 1^0 * 0 + 4^1 * 1^0 * 0) = 0
Не смотря на то, что коэффициенты при x и y, ненулевые, интенсивности черных пикселей превращают нашу первую строку в ноль, так же как вторую и четвертую, а вот третья строка превращается в тройку:
((1^1 * 1^0 * 0 + 2^1 * 1^0 * 0 + 3^1 * 1^0 * 1 + 4^1 * 1^0 * 0) = 3
Т.е. весь m10 = 3.
Таким образом координаты центра по х = m10/m00 = 3 / 1 = 3
Расчет координаты центра по y = m01/m00 = 3, а так же каких-нибудь случаев для двух и более пикселей на тестовом изображении предлагаем провести самостоятельно. В любом случае мы приходим к пониманию, как именно работают моменты изображения. И для того чтобы рассчитать центр нашего шарика нам надо применить этот алгоритм к уже полученной маске.
Подготовка: Данную задачу можно решать в любой комбинации полигона, подойдет и "Офис" и "Дорога". В данном уроке мы не будем передвигать робота, таким образом, нам не нужна какая-то специальная подготовка пространства вокруг. Попросите техника на полигоне положить шарик желтого (или любого другого контрастного цвета) на полигон рядом с роботом.
Задача: Определить координаты центра желтого шарика на изображении и вывести их в консоль.
Общий алгоритм решения:
- Получить изображение с камеры робота
- Преобразовать его в 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)
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
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
print(j)
return im
rospy.sleep(1)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
HSV = RGB2HSV(image_from_ros_camera)
mask = inRange(HSV)
cv2.imwrite("/home/pi/ball_HSV.jpg", HSV)
cv2.imwrite("/home/pi/ball_MASK.jpg", mask)
Теперь нам надо написать функцию применения алгоритма расчета визуальных моментов к полученной маске. Очевидно, что для обхода всех пикселей на изображении, мы можем использовать всё те-же два вложенных цикла, которые использовали в предыдущих уроках. А для самого расчета моментов мы будем пользоваться теми рассуждениями, которые проделывали для упрощенного изображения 4х4 пикселя.
def moments(im):
M10 = 0
M01 = 0
M00 = 0
for j in range(im.shape[0]):
print("Moment :", j)
for i in range(im.shape[1]):
if im[j][i][0] == 255:
M10 += i
M00 += 1
M01 += j
return (int(M10/M00), int(M01/M00))
Поясним. Здесь if im[j][i][0] == 255: идет сравнение значения интенсивности пикселя с белым цветом (255), и если конкретный пиксель белого цвета то соответствующий момент по Х увеличивается на Х, по У на У, а нулевой момент на единицу. Если пиксель черного цвета, то ничего не происходит, т.к. у черных пикселей нулевая интенсивность и прибавлять нули к сумме - не нужно. В итоге, после того как мы пробежимся по всем пикселям, мы будем иметь рассчитанные моменты всех нужных порядков, и все что нам надо для определения координат центра шарика, это рассчитать отношения соответствующих моментов по формулам:
x = m10 / m00
y = m01 / m00
И затем вернуть как результат работы функции.
Оставим все предыдущие шаги в программе на месте, но добавим передачу маски в функцию расчета моментов и последующий вывод полученных координат центра в консоль.
rospy.sleep(1)
cv2.imwrite("/home/pi/ball_JPG.jpg", image_from_ros_camera)
HSV = RGB2HSV(image_from_ros_camera)
mask = inRange(HSV)
moment = moments(mask)
print(moment)
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.
Затем попросите учащихся немного поворачивать робота из стороны в сторону при помощи веб-интерфейса для того чтобы мы могли увидеть, что координаты центра шарика меняются в зависимости от того на какой части изображения он расположен.
Итак, в простых но достаточно эффективных алгоритмах мы получили решение семантической задачи, а именно определение координат центра желтого шарика на изображении. И не смотря на то, что написанная нами программа работает около 30 секунд и ни о какой обработке видеопотока в реальном времени речи не идет, это не значит что сам подход неправильный. На самом деле основная проблема в данном случае это неоптимальность реализации алгоритмов, из-за использования языка Python для того, для чего он не предназначен, а так же в целом мы пожертвовали оптимальностью расчетов ради наглядности самой программы. На следующих уроках, мы заменим написанные нами функции, на функции из библиотеки OpenCV, написанные на языке C++, и это позволит нам реализовать обработку видео в реальном времени.