Оглавление.

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

Сегодня продолжим тему прошлого урока. Вычислим инвариантный вектор новым методом: через отношения длин сторон. Мы начнем обход так же с самой удаленной от центра точки, только будем брать стороны, а не углы межу сторонами. И первая сторона это та, что прилегает к первой точке. То есть она соединяет первую точку и следующую за ней по часовой стрелке. И все эти длины сторон мы разделим на самую длинную сторону. Хотя нет, сделам лучше. Сделаем минимакс нормализацию: вычтем из длины стороны минимум и разделим на разницу между минимумом и максимумом. У нас будет вектор чисел от 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()

Комментарии (10)


  1. Qvxb
    12.09.2022 20:38

    Это я так понимаю способ, альтернативный распознаванию с нейросетками, но вместо измерения расстояния от средней точки к остальным не проще ли преобразовать контуры в вектора (прямые/дуги разного радиуса) сразу проблема с шумами и масштабом исчезнет.


    1. megabax Автор
      12.09.2022 22:38

      преобразовать контуры в вектора (прямые/дуги разного радиуса)

      Вы имеете в виду сделать из контура нечто вроде графа, где каждое ребро - это геометрический примитив?


      1. Qvxb
        13.09.2022 00:15

        Ну вот допустим есть контуры квадрата в растровом массиве - преобразовать их в 4 линии, линия это у = кх+б, однако смещение не нужно, так что просто у=кх, однако нужны еще точки начала и конца, тогда квадрат это - 4 линии(у=кх) одинаковой длины у которых концы совпадают и они каждая перпендикулярна следующей и предыдущей


        1. megabax Автор
          13.09.2022 09:39

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


          1. Qvxb
            13.09.2022 12:31

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


            1. megabax Автор
              13.09.2022 13:07

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


  1. Sarjin
    12.09.2022 22:13

    было бы удобно ссылку на все прошлые. чем на 1


    1. megabax Автор
      12.09.2022 22:37
      +2

      Думал об это. Вообще в планах отдельно оглавление вынести, и в каждом уроке делать ссылку на оглавления, как думаете, это удобно будет?


      1. delvin-fil
        13.09.2022 03:32

        Удобно.


      1. Sarjin
        13.09.2022 10:54

        Наверное да