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

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

  • Выявить различные геометрические примитивы (прямые, окружности).

  • Превратить в цепочки точек и уже их отдельно анализировать.

  • Описать как граф и применять к нему алгоритмы на графах.

Продолжим изучать методы предобработки. Например, изображением можно сделать контрастным:

import cv2

img = cv2.imread('MyPhoto.jpg', 1)
cv2.imshow("Original image",img)

# CLAHE (Contrast Limited Adaptive Histogram Equalization)
clahe = cv2.createCLAHE(clipLimit=3., tileGridSize=(8,8))

lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)  # convert from BGR to LAB color space
l, a, b = cv2.split(lab)  # split on 3 different channels

l2 = clahe.apply(l)  # apply CLAHE to the L-channel

lab = cv2.merge((l2,a,b))  # merge channels
img2 = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)  # convert from LAB to BGR
cv2.imshow('Increased contrast', img2)

cv2.waitKey(0)
cv2.destroyAllWindows()

Испытаем данную программу на все той же до боли знакомой из предыдущих уроков картинке с пальмами:

c6a95cac2434750474b63a593761c113.png

Или, вот, например, фото, сделанное в темное время суток:

afb11f6829a56d689dc910eb1270f38f.png

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

# CLAHE (Contrast Limited Adaptive Histogram Equalization)
clahe = cv2.createCLAHE(clipLimit=3., tileGridSize=(8,8))

Далее, для наложения выровненных гистограмм на изображение оно преобразуется  формат LAB, который представляет собой один канал светлоты L и два канала цвета A и B:

lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)  # convert from BGR to LAB color space
l, a, b = cv2.split(lab)  # split on 3 different channels

Наконец, мы можем применить выровненные гистограммы (только к каналу L — светлота):

l2 = clahe.apply(l)  # apply CLAHE to the L-channel

Затем объединяем эти каналы и производим обратное преобразование:

lab = cv2.merge((l2,a,b))  # merge channels
img2 = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)  # convert from LAB to BGR
cv2.imshow('Increased contrast', img2)

Отдельно стоит сказать о параметре clipLimit. Он определят, насколько контрастнее станет фото. Например, поставим его 10 и получим вот это:

6349c7c65a36b683f663fd6f55cce34d.png

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

b9ca2935d594f527c65b182f7a7ce6a3.png

А теперь попробуем у той же картинки выделить контур (без предварительного увеличения контрастности):

6171126da76118890dc240f9d38c073c.png

А если предварительно увеличить контрастность:

8471f4a8a7f7a37362ff786cc77a8be2.png

Как видим, тут более отчетливо проступили силуэты людей.

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

Начнем, пожалуй, с детектора Харриса. Детектор Харриса — это алгоритм поиска углов на изображении. В OpenCV есть такая функция и называется cornerHarris. Давайте испытаем ее:

# Python программа для иллюстрации
# обнаружение угла с
# Метод определения угла Харриса

import cv2
import numpy as np

# путь к указанному входному изображению и
# изображение загружается с помощью команды imread
image = cv2.imread('DSCN1311.jpg')

# конвертировать входное изображение в Цветовое пространство в оттенках серого
operatedImage = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# изменить тип данных
# установка 32-битной плавающей запятой
operatedImage = np.float32(operatedImage)

# применить метод cv2.cornerHarris
# для определения углов с соответствующими
# значения в качестве входных параметров
dest = cv2.cornerHarris(operatedImage, 2, 5, 0.07)

# Результаты отмечены через расширенные углы
dest = cv2.dilate(dest, None)

# Возвращаясь к исходному изображению,
# с оптимальным пороговым значением
image[dest > 0.01 * dest.max()] = [0, 0, 255]

# окно с выводимым изображением с углами
cv2.imshow('Image with Borders', image)

if cv2.waitKey(0) & 0xff == 27:
    cv2.destroyAllWindows()

И вот что получиться, если применить его к изображению:

359ad08937632d5ee96b173814f35a49.png

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

Ладно, выделили мы особые точки. А что дальше? А дальше, примерно то же, что и с контурами:

  • Составить из точек различеные геометрические фигуры, например, треугольники.

  • Превратить в цепочки точек и уже их отдельно анализировать.

  • Описать как граф и применять к нему алгоритмы на графах.

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

# Возвращаясь к исходному изображению,
# с оптимальным пороговым значением
image[dest > 0.05 * dest.max()] = [0, 0, 255]

030a925bb8529d947f0e02e9c64c8bd0.png

Теперь, для наглядности, уберем картинку, оставив только одни точки:

# Возвращаясь к исходному изображению,
# с оптимальным пороговым значением
img_blank = np.zeros(image.shape)
img_blank[dest > 0.05 * dest.max()] = [0, 0, 255]

# окно с выводимым изображением с углами
cv2.imshow('Image with Borders', img_blank)

bf8e348a78dc5286af97a03f81c0a4e2.png

Как нам эти точки превратить в граф? Для начала вспомним, как работать с такими структурами, как граф. Об этом можно почитать в статье: Алгоритмы обхода графов на Python и C# — Библиотека разработчика Programming Store (programstore.ru). Если кратко, то граф можно представить в виде списка вершин (точек) и их соединений (ребер). Еще можно представить граф в виде матрицы соединений. Как будем представлять граф мы, решим попозже, для начала, давайте попробуем перебрать все точки и сосчитать их количество:

heigh=img_blank.shape[0]
width=img_blank.shape[1]
count=0
for x in range(0,width):
    for y in range(0,heigh):
        if img_blank[y,x,2]==255:
            print(x,y)
            count+=1
print("Всего",count,"точек")

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

1dae77bdeaaa3a6438e97d41b9136a52.png

Как это точки можно проредить? Один из способов — это объединить близкие точки в одну, центр которой будет неким средним арифметическим вот этих вот близких точек. Но тут одна проблема. Если мы будем вычислять расстояние каждой точки с каждой, то это будет количество точек в квадрате. Число 2829 в степени 2 — это чуть больше 8 млн. А если точек будет еще больше? Явно подход нерациональный. Как вариант, можно при обходе сразу собирать близкие точки в кластер, а уже потом рассчитать их средние координаты.

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

#Получить последнюю точку в списке списочков и одиночных точек
def get_last_point(ls):
    lls = len(ls)
    if lls>0:
        item=ls[lls - 1]
        if type(item)==list:
            if len(item)>0:
                x,y=item[len(item)-1]
            else:
                return 0, 0, False
        else:
            x,y=item
        return x,y,True
    return 0,0,False

#Вычислить расстояние между точками
def get_distance(p1,p2):
    x1,y1=p1
    x2,y2=p2
    l = math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))  # Евклидово расстояние между точками
    return l

#Добавить точку в кластер 
def add_point_to_claster(x,y,ls):
    lls = len(ls)
    item = ls[lls - 1]
    if type(item) == list:
        item.append((x,y))
    else:
        x1,y1=item
        item=[(x1,y1)]
        item.append((x,y))
        ls[lls-1]=item

Идея состоит в том, что точка у нас представлена в виде кортежа из двух чисел (ее координат). Мы проходим по растру, определяем, если там точка и если есть то добавляем ее в список. Но если эта точка близка к последней точке списка, то мы превращаем эту последнюю точку в список, состоящий последней и текущей точки. Далее, если следующая точка окажется близкой, мы тоже добавим ее в этот список. Если нет — начнем строить новый список. В итоге у нас получиться список списков и одиночных точек (если они будут). Можно, конечно, было сделать проще, список списков, а если это одиночная точка то подпсписок имеет длину 1. Но, с дургой стороны, из этого примера мы еще научимся, как в Python определять тип переменной.

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

Вот как мы используем эти функции:

heigh=img_blank.shape[0]
width=img_blank.shape[1]
points=[]
for x in range(0,width):
    for y in range(0,heigh):
        if img_blank[y,x,2]==255:
            x1, y1, point_is = get_last_point(points)
            if point_is:
                l=get_distance((x1,y1),(x,y))
                if l<3:
                    add_point_to_claster(x,y,points)
                    continue
            points.append((x,y))
for point in points:
    print(point)

Вот что в итоге получилось:

04b6a0daf3c4ef3785b872852ce8e535.png

В итоге, количество элементов в списке стало 591. Теперь осталось превратить их в точки:

 def calk_center(ls):
    ix=0
    iy=0
    l=float(len(ls))
    for point in ls:
        x,y=point
        ix+=x
        iy+=y
    return round(ix/l),round(iy/l)

Выведем эти точки на экран:

centers=[]
for point in points:
    if type(point)==list:
        centers.append(calk_center(point))
    else:
        centers.append(point)

img_blank1 = np.zeros(image.shape)
for point in centers:
    print(point)
    x,y=point
    img_blank1[y,x,2]=255

Вот что в итоге получилось:

af30893581a2dbca5af34fa54beb226f.png

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

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

# Python программа для иллюстрации
# обнаружение угла с
# Метод определения угла Харриса

import cv2
import numpy as np
import math

#Получить последнюю точку в списке списоков и одиночных точек
def get_last_point(ls):
    lls = len(ls)
    if lls>0:
        item=ls[lls - 1]
        if type(item)==list:
            if len(item)>0:
                x,y=item[len(item)-1]
            else:
                return 0, 0, False
        else:
            x,y=item
        return x,y,True
    return 0,0,False

#Вычслить расстояние между точками
def get_distance(p1,p2):
    x1,y1=p1
    x2,y2=p2
    l = math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))  # Евклидово расстояние между последней и текущей точкой
    return l

#Добавить точку в кластер
def add_point_to_claster(x,y,ls):
    lls = len(ls)
    item = ls[lls - 1]
    if type(item) == list:
        item.append((x,y))
    else:
        x1,y1=item
        item=[(x1,y1)]
        item.append((x,y))
        ls[lls-1]=item

def calk_center(ls):
    ix=0
    iy=0
    l=float(len(ls))
    for point in ls:
        x,y=point
        ix+=x
        iy+=y
    return round(ix/l),round(iy/l)

# путь к указанному входному изображению и
# изображение загружается с помощью команды imread
image = cv2.imread('DSCN1311.jpg')

# конвертировать входное изображение в Цветовое пространство в оттенкахсерого
operatedImage = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# изменить тип данных
# установка 32-битной плавающей запятой
operatedImage = np.float32(operatedImage)

# применить метод cv2.cornerHarris
# для определения углов с соответствующими
# значения в качестве входных параметров
dest = cv2.cornerHarris(operatedImage, 2, 5, 0.07)

# Результаты отмечены через расширенные углы
dest = cv2.dilate(dest, None)

# Возвращаясь к исходному изображению,
# с оптимальным пороговым значением
img_blank = np.zeros(image.shape)
img_blank[dest > 0.05 * dest.max()] = [0, 0, 255]

heigh=img_blank.shape[0]
width=img_blank.shape[1]
points=[]
for x in range(0,width):
    for y in range(0,heigh):
        if img_blank[y,x,2]==255:
            x1, y1, point_is = get_last_point(points)
            if point_is:
                l=get_distance((x1,y1),(x,y))
                if l<3:
                    add_point_to_claster(x,y,points)
                    continue
            points.append((x,y))
centers=[]
for point in points:
    if type(point)==list:
        centers.append(calk_center(point))
    else:
        centers.append(point)

img_blank1 = np.zeros(image.shape)
for point in centers:
    print(point)
    x,y=point
    img_blank1[y,x,2]=255

# окно с выводимым изображением с углами
cv2.imshow('Image with Borders', img_blank1)

if cv2.waitKey(0) & 0xff == 27:
    cv2.destroyAllWindows()

© Habrahabr.ru