Уроки компьютерного зрения на Python + OpenCV с самых азов. Часть 8

от автора

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

Сегодня продолжим тему прошлого урока. Вычислим инвариантный вектор новым методом: через отношения длин сторон. Мы начнем обход так же с самой удаленной от центра точки, только будем брать стороны, а не углы межу сторонами. И первая сторона это та, что прилегает к первой точке. То есть она соединяет первую точку и следующую за ней по часовой стрелке. И все эти длины сторон мы разделим на самую длинную сторону. Хотя нет, сделам лучше. Сделаем минимакс нормализацию: вычтем из длины стороны минимум и разделим на разницу между минимумом и максимумом. У нас будет вектор чисел от 0 до 1.

И так, займемся кодингом. Сначала напишем цикл, создающий исходный масcив:

lengths=[] for i in range(size-1):     lengths.append(get_length(polar_coordinates[i],polar_coordinates[i+1])) lengths.append(get_length(polar_coordinates[size-1],polar_coordinates[0])) print(get_normalize_normalize(lengths))

Функция вычисления длины стороны (она просто извекает из структуры, где у нас все храниться, координаты и считает эвклидово расстояние):

#Эвклидово расстояние между двумя элементами def get_length(item1, item2):     _, point1 = item1     _, point2 = item2     x1, y1 = point1     x2, y2 = point2     dx=x1-x2     dy=y1-y2     r=math.sqrt(dx*dx+dy*dy)     return r

Ну и нормализация, конвертим полученный список в numpy массив и делаем минимакс нормализацию:

def get_normalized_vector(list):     arr=np.array(list)     return (arr-arr.min())/(arr.max()-arr.min())

Поехали смотреть, что получилось.

Первый пример:

Вектор: [0.11331868 1.         0.         0.02997931 0.96756226 0.1022278 ]

 

Второй пример:

Вектор: [0.16953268 0.9532099  0.         0.01245409 1.         0.10313678]

 

Как видим, векторы действительно оказались близкие. Причем, в отличие от прошлого варианта, у нас остаются стабильными все элементы вектора.

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

import cv2 import numpy as np import math import os img = cv2.imread("Samples/1.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) thresh = 100  def custom_sort(countour):     return -countour.shape[0]  def polar_sort(item):     return item[0][0]  def get_normalized_vector(list):     arr=np.array(list)     return (arr-arr.min())/(arr.max()-arr.min())  #Эвклидово расстояние между двумя элементами def get_length(item1, item2):     _, point1 = item1     _, point2 = item2     x1, y1 = point1     x2, y2 = point2     dx=x1-x2     dy=y1-y2     r=math.sqrt(dx*dx+dy*dy)     return r  def get_cos_edges(edges):     dx1, dy1, dx2, dy2=edges     r1 = math.sqrt(dx1 * dx1 + dy1 * dy1)     r2 = math.sqrt(dx2 * dx2 + dy2 * dy2)     return (dx1*dx2+dy1*dy2)/r1/r2  def get_polar_coordinates(x0,y0,x,y,xc,yc):     #Первая координата в полярных координатах - радиус     dx=xc-x     dy=yc-y     r=math.sqrt(dx*dx+dy*dy)      #Вторая координата в полярных координатах - узел, вычислим относительно начальной точки     dx0=xc-x0     dy0=yc-y0     r0 = math.sqrt(dx0 * dx0 + dy0 * dy0)     scal_mul=dx0*dx+dy0*dy     cos_angle=scal_mul/r/r0     sgn=dx0*dy-dx*dy0 #опредедляем, в какую сторону повернут вектор     if cos_angle>1:         if cos_angle>1.0001:             raise Exception("Что-то пошло не так")         cos_angle=1     angle=math.acos(cos_angle)     if sgn<0:         angle=2*math.pi-angle     return angle,r  def get_coords(item1, item2, item3):     _, point1 = item1     _, point2 = item2     _, point3 = item3     x1, y1 = point1     x2, y2 = point2     x3, y3 = point3     dx1=x1-x2     dy1=y1-y2     dx2=x3-x2     dy2=y3-y2     return dx1,dy1,dx2,dy2  #get threshold image ret,thresh_img = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY)  # find contours without approx contours,_ = cv2.findContours(thresh_img,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) contours=list(contours) contours.sort(key=custom_sort) sel_countour=contours[1]  # calc arclentgh arclen = cv2.arcLength(sel_countour, True)  # do approx eps = 0.01 epsilon = arclen * eps approx = cv2.approxPolyDP(sel_countour, epsilon, True)  sum_x=0.0 sum_y=0.0 for point in approx:     x = float(point[0][0])     y = float(point[0][1])     sum_x+=x     sum_y+=y xc=sum_x/float(len((approx))) yc=sum_y/float(len((approx)))  max=0 beg_point=-1 for i in range(0,len(approx)):     point=approx[i]     x = float(point[0][0])     y = float(point[0][1])     dx=x-xc     dy=y-yc     r=math.sqrt(dx*dx+dy*dy)     if r>max:         max=r         beg_point=i  polar_coordinates=[] x0=approx[beg_point][0][0] y0=approx[beg_point][0][1]  for point in approx:     x = int(point[0][0])     y = int(point[0][1])     angle,r=get_polar_coordinates(x0,y0,x,y,xc,yc)     polar_coordinates.append(((angle,r),(x,y)))  polar_coordinates.sort(key=polar_sort)  img_contours = np.uint8(np.zeros((img.shape[0],img.shape[1]))) size=len(polar_coordinates) for i in range(1,size):     _ , point1=polar_coordinates[i-1]     _, point2 = polar_coordinates[i]     x1,y1=point1     x2,y2=point2     cv2.line(img_contours, (x1, y1), (x2, y2), 255, thickness=i) _ , point1=polar_coordinates[size-1] _, point2 = polar_coordinates[0] x1,y1=point1 x2,y2=point2 cv2.line(img_contours, (x1, y1), (x2, y2), 255, thickness=size)  cv2.circle(img_contours, (int(xc), int(yc)), 7, (255,255,255), 2)  coses=[] coses.append(get_cos_edges(get_coords(polar_coordinates[size-1],polar_coordinates[0],polar_coordinates[1]))) for i in range(1,size-1):     coses.append(get_cos_edges(get_coords(polar_coordinates[i-1], polar_coordinates[i],polar_coordinates[i+1]))) coses.append(get_cos_edges(get_coords(polar_coordinates[size-2], polar_coordinates[size-1],polar_coordinates[0]))) print(coses)  lengths=[] for i in range(size-1):     lengths.append(get_length(polar_coordinates[i],polar_coordinates[i+1])) lengths.append(get_length(polar_coordinates[size-1],polar_coordinates[0])) print(get_normalized_vector(lengths))    point=approx[beg_point] x = float(point[0][0]) y = float(point[0][1]) cv2.circle(img_contours, (int(x), int(y)), 7, (255,255,255), 2)  cv2.imshow('origin', img) # выводим итоговое изображение в окно cv2.imshow('res', img_contours) # выводим итоговое изображение в окно  cv2.waitKey() cv2.destroyAllWindows()

Наш следующий шаг в освоении OpenCV – это поиск предмета на изображении. Искать мы будет все ту же ручку, но теперь на изображении у нас будут другие предметы:

Здесь мы точно так же выделим контуры на изображении, а потом каждый из контуров будем проверять на соответствии заданному шаблону – шесть граней и вектор инвариантного описания близок к исходному. Насколько близок? Это определим эмпирическим путем, подбирая порог.

И так, вот программа:

import cv2 import numpy as np import math  template_vector=np.array([0.16953268, 0.9532099, 0, 0.01245409, 1, 0.10313678]) distance_thresh=0.1  img = cv2.imread("Samples/objects.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) thresh = 100  def polar_sort(item):     return item[0][0]  def get_normalized_vector(list):     arr=np.array(list)     return (arr-arr.min())/(arr.max()-arr.min())  #Эвклидово расстояние между двумя элементами def get_length(item1, item2):     _, point1 = item1     _, point2 = item2     x1, y1 = point1     x2, y2 = point2     dx=x1-x2     dy=y1-y2     r=math.sqrt(dx*dx+dy*dy)     return r  def get_polar_coordinates(x0,y0,x,y,xc,yc):     #Первая координата в полярных координатах - радиус     dx=xc-x     dy=yc-y     r=math.sqrt(dx*dx+dy*dy)      #Вторая координата в полярных координатах - узел, вычислим относительно начальной точки     dx0=xc-x0     dy0=yc-y0     r0 = math.sqrt(dx0 * dx0 + dy0 * dy0)     scal_mul=dx0*dx+dy0*dy     cos_angle=scal_mul/r/r0     sgn=dx0*dy-dx*dy0 #опредедляем, в какую сторону повернут вектор     if cos_angle>1:         if cos_angle>1.0001:             raise Exception("Что-то пошло не так")         cos_angle=1     angle=math.acos(cos_angle)     if sgn<0:         angle=2*math.pi-angle     return angle,r   #get threshold image ret,thresh_img = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY)  # find contours without approx contours,_ = cv2.findContours(thresh_img,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) for sel_countour in contours:     # calc arclentgh     arclen = cv2.arcLength(sel_countour, True)      # do approx     eps = 0.01     epsilon = arclen * eps     approx = cv2.approxPolyDP(sel_countour, epsilon, True)      #Обрабатываем только контуры длиной 6 углов     if len(approx)==6:          # вычислим центр тяжести контура         sum_x = 0.0         sum_y = 0.0         for point in approx:             x = float(point[0][0])             y = float(point[0][1])             sum_x += x             sum_y += y         xc = sum_x / float(len((approx)))         yc = sum_y / float(len((approx)))          #найдем начальную точку         max = 0         beg_point = -1         for i in range(0, len(approx)):             point = approx[i]             x = float(point[0][0])             y = float(point[0][1])             dx = x - xc             dy = y - yc             r = math.sqrt(dx * dx + dy * dy)             if r > max:                 max = r                 beg_point = i          #Вычислми полярные координаты         polar_coordinates=[]         x0=approx[beg_point][0][0]         y0=approx[beg_point][0][1]         for point in approx:             x = int(point[0][0])             y = int(point[0][1])             angle, r = get_polar_coordinates(x0, y0, x, y, xc, yc)             polar_coordinates.append(((angle, r), (x, y)))          #Создадим вектор описание         polar_coordinates.sort(key=polar_sort)         size = len(polar_coordinates)         lengths = []         for i in range(size - 1):             lengths.append(get_length(polar_coordinates[i], polar_coordinates[i + 1]))         lengths.append(get_length(polar_coordinates[size - 1], polar_coordinates[0]))         descr=get_normalized_vector(lengths)          #Вычислим эвклидово расстояние         square = np.square(descr - template_vector)         sum_square = np.sum(square)         distance = np.sqrt(sum_square)         if distance<distance_thresh:             for i in range(1, size):                 _, point1 = polar_coordinates[i - 1]                 _, point2 = polar_coordinates[i]                 x1, y1 = point1                 x2, y2 = point2                 cv2.line(img, (x1, y1), (x2, y2), (0,0,255), thickness=4)             _, point1 = polar_coordinates[size - 1]             _, point2 = polar_coordinates[0]             x1, y1 = point1             x2, y2 = point2             cv2.line(img, (x1, y1), (x2, y2), (0,0,255), thickness=size)   cv2.imshow('origin', img) # выводим итоговое изображение в окно  cv2.waitKey() cv2.destroyAllWindows()

Порог подобран 0.1, шаблон – вектор ко второй картинке, где искомый объект повернут в другую сторону, чем тот, что на картинке:

template_vector=np.array([0.16953268, 0.9532099, 0, 0.01245409, 1, 0.10313678]) distance_thresh=0.1

И вот что у нас получилось:

Найден всего один объект. Если порог увеличить до 0.4, то будет найдет и второй объект, но еще несколько «левых» объектов:

Избавить мы можем от них, просто введя критерий размера (не имеет смысл рассматривать слишком малые объекты):

… for sel_countour in contours:     # calc arclentgh     arclen = cv2.arcLength(sel_countour, True)     if arclen<20:         continue …

И вот что мы получим теперь:

Но кто сказал, что надо вообще аппроксимировать контур и считать углы? Мы можем просто обойти контур по кругу, который разделим на определенное кол-во секторов:

count=100 full_angle=2*math.pi i=1 end_angle = float(i) * full_angle / float(count) summ=0.0 count_angles=0.0 signature=[] for item_coord in polar_coord:     angle,r=item_coord     if angle>end_angle:         signature.append((angle,summ/count_angles))         i+=1         end_angle = float(i) * full_angle / float(count)         summ=0         count_angles=0     summ+=r     count_angles+=1 signature.append((angle,summ/count_angles)) print(signature)

Для того, чтобы проверить правильность  формирования сигнатуры, переведем ее опять в декартовы координаты и отобразим. Функция перевода полярных координат в декартовы:

def polar_to_decart(angle,r):     x=math.sin(angle)*r     y=math.cos(angle)*r     return x,y

И вот таким образом мы отобразим сигнатуру:

img_contours = np.zeros((img.shape[0],img.shape[1],3), np.uint8) # np.uint8(np.zeros((img.shape[0],img.shape[1]))) cv2.drawContours(img_contours, [sel_countour], -1, (255,0,0), 1)  for i in range(1,len(signature)):     angle1,r1=signature[i-1]     angle2,r2=signature[i]     x1, y1=polar_to_decart(angle1,r1)     x2, y2 = polar_to_decart(angle2, r2)     cv2.line(img_contours, (int(x1+xc), int(y1+yc)), (int(x2+xc), int(y2+yc)), (0,0,255), thickness=1) angle1,r1=signature[len(signature)-1] angle2,r2=signature[0] x1, y1=polar_to_decart(angle1,r1) x2, y2 = polar_to_decart(angle2, r2) cv2.line(img_contours, (int(x1+xc), int(y1+yc)), (int(x2+xc), int(y2+yc)), (0,0,255), thickness=1)

Да, я специально не стал делать поправку на угол, чтобы совмещать контуры, пусть красный контур (сигнатура) будет повернут, чтобы было лучше видно:

Можно отобразить эту сигнатуру на графике:

x=[] y=[] for item in signature:     angle,r=item     x.append(angle)     y.append(r)  plt.plot(x,y) plt.show()

Заметим, что неважно, какой угол поворота, графики будут выглядеть одинаково:

Ну, или почти одинаково:

Сравнить мы его сможет так же, как и вектора. Тем более, что теперь у нас контур приведен к единоразмерной сигнатуре.

Попробуем другой предмет:

Как видим, в случае круглого предмета получился шум вокруг определенного уровня — радиуса этого круга. В идеале должна, конечно, получиться прямая, но ничего в этом мире нет идеального.  

Еще один предмет:

Еще сигнатуру можно нормировать, тогда мы получим инвариантный к размеру вектор. В прошлый раз мы использовали минимакс, но есть и другие способы, например, можно разделить на радиус или делить разницу между текущим и средним значением на среднеквадратическое отклонение. Но обсуждение способов нормирования уже выходит за рамки статьи.

В заключении полный текст программы.

Файл SignLib.py:

import math   def custom_sort(countour):     return -countour.shape[0]  def get_center(countour):     # вычислим центр тяжести контура     sum_x = 0.0     sum_y = 0.0     for point in countour:         x = float(point[0][0])         y = float(point[0][1])         sum_x += x         sum_y += y     xc = sum_x / float(len((countour)))     yc = sum_y / float(len((countour)))     return xc,yc  def get_beg_point(countour,xc,yc):     max = 0     beg_point = -1     for i in range(0, len(countour)):         point = countour[i]         x = float(point[0][0])         y = float(point[0][1])         dx = x - xc         dy = y - yc         r = math.sqrt(dx * dx + dy * dy)         if r > max:             max = r             beg_point = i         return beg_point  def get_polar_coordinates(x0,y0,x,y,xc,yc):     #Первая координата в полярных координатах - радиус     dx=xc-x     dy=yc-y     r=math.sqrt(dx*dx+dy*dy)      #Вторая координата в полярных координатах - узел, вычислим относительно начальной точки     dx0=xc-x0     dy0=yc-y0     r0 = math.sqrt(dx0 * dx0 + dy0 * dy0)     scal_mul=dx0*dx+dy0*dy     cos_angle=scal_mul/r/r0     sgn=dx0*dy-dx*dy0 #опредедляем, в какую сторону повернут вектор     if cos_angle>1:         if cos_angle>1.0001:             raise Exception("Что-то пошло не так")         cos_angle=1     angle=math.acos(cos_angle)     if sgn<0:         angle=2*math.pi-angle     return angle,r  def polar_to_decart(angle,r):     x=math.sin(angle)*r     y=math.cos(angle)*r     return x,y  def polar_sort(item):     return item[0]  def get_polar_coordinates_list(countour,xc,yc,beg_point):     polar_coordinates = []     x0 = countour[beg_point][0][0]     y0 = countour[beg_point][0][1]     for point in countour:         x = int(point[0][0])         y = int(point[0][1])         angle, r = get_polar_coordinates(x0, y0, x, y, xc, yc)         polar_coordinates.append((angle, r))      # Создадим вектор описание     polar_coordinates.sort(key=polar_sort)      return polar_coordinates

 Основной файл программы:

import math import matplotlib.pyplot as plt import cv2 import numpy as np  from SignLib import custom_sort, get_center, get_beg_point, get_polar_coordinates_list, polar_to_decart  img = cv2.imread("Samples/battery.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) thresh = 100  #get threshold image ret,thresh_img = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY)  # find contours without approx contours,_ = cv2.findContours(thresh_img,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) contours=list(contours) contours.sort(key=custom_sort) sel_countour=contours[1] xc,yc=get_center(sel_countour) beg_point=get_beg_point(sel_countour,xc,yc) polar_coord=get_polar_coordinates_list(sel_countour,xc,yc,beg_point) count=100 full_angle=2*math.pi i=1 end_angle = float(i) * full_angle / float(count) summ=0.0 count_angles=0.0 signature=[] for item_coord in polar_coord:     angle,r=item_coord     if angle>end_angle:         signature.append((angle,summ/count_angles))         i+=1         end_angle = float(i) * full_angle / float(count)         summ=0         count_angles=0     summ+=r     count_angles+=1 signature.append((angle,summ/count_angles)) print(signature)  img_contours = np.zeros((img.shape[0],img.shape[1],3), np.uint8) # np.uint8(np.zeros((img.shape[0],img.shape[1]))) cv2.drawContours(img_contours, [sel_countour], -1, (255,0,0), 1)  for i in range(1,len(signature)):     angle1,r1=signature[i-1]     angle2,r2=signature[i]     x1, y1=polar_to_decart(angle1,r1)     x2, y2 = polar_to_decart(angle2, r2)     cv2.line(img_contours, (int(x1+xc), int(y1+yc)), (int(x2+xc), int(y2+yc)), (0,0,255), thickness=1) angle1,r1=signature[len(signature)-1] angle2,r2=signature[0] x1, y1=polar_to_decart(angle1,r1) x2, y2 = polar_to_decart(angle2, r2) cv2.line(img_contours, (int(x1+xc), int(y1+yc)), (int(x2+xc), int(y2+yc)), (0,0,255), thickness=1)  cv2.imshow('origin', img) # выводим итоговое изображение в окно cv2.imshow('res', img_contours) # выводим итоговое изображение в окно  x=[] y=[] for item in signature:     angle,r=item     x.append(angle)     y.append(r)  plt.plot(x,y) plt.show()  cv2.waitKey() cv2.destroyAllWindows()


ссылка на оригинал статьи https://habr.com/ru/post/687864/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *