OpenCV на python: выделение контуров

Освоив работу с цветовыми фильтрами приступим к изучению ещё одного важного инструмента машинного зрения — функции выделения контуров.

Контур объекта — это его видимый край, который отделяет объект от фона. В действительности, большинство методов анализа изображений работают именно с контурами, а не с пикселями как таковыми. Совокупность методов работы с контурами называется контурным анализом.

Возьмём в качестве подопытного изображения что-нибудь такое, где есть ярко выраженные вложенные контуры, какой-нибудь диск. И попробуем применить к нему стандартные функции OpenCV для поиска и визуализации контуров объектов.

Пончик

1. Функция OpenCV для поиска контуров findContours

В OpenCV для поиска контуров имеется функцией findContours, которая имеет вид:

findContours( кадр, режим_группировки, метод_упаковки [, контуры[, иерархия[, сдвиг]]])

кадр — должным образом подготовленная для анализа картинка. Это должно быть 8-битное изображение. Поиск контуров использует для работы монохромное изображение, так что все пиксели картинки с ненулевым цветом будут интерпретироваться как 1, а все нулевые останутся нулями. На уроке про поиск цветных объектов была точно такая же ситуация.

режим_группировки — один из четырех режимов группировки найденных контуров:

  • CV_RETR_LIST — выдаёт все контуры без группировки;
  • CV_RETR_EXTERNAL — выдаёт только крайние внешние контуры. Например, если в кадре будет пончик, то функция вернет его внешнюю границу без дырки.
  • CV_RETR_CCOMP — группирует контуры в двухуровневую иерархию. На верхнем уровне — внешние контуры объекта. На втором уровне — контуры отверстий, если таковые имеются. Все остальные контуры попадают на верхний уровень.
  • CV_RETR_TREE — группирует контуры в многоуровневую иерархию.

метод_упаковки — один из трёх методов упаковки контуров:

  • CV_CHAIN_APPROX_NONE — упаковка отсутствует и все контуры хранятся в виде отрезков, состоящих из двух пикселей.
  • CV_CHAIN_APPROX_SIMPLE — склеивает все горизонтальные, вертикальные и диагональные контуры.
  • CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS — применяет к контурам метод упаковки (аппроксимации) Teh-Chin.

контуры — список всех найденных контуров, представленных в виде векторов;

иерархия — информация о топологии контуров. Каждый элемент иерархии представляет собой сборку из четырех индексов, которая соответствует контуру[i]:

  • иерархия[i][0] — индекс следующего контура на текущем слое;
  • иерархия[i][1] — индекс предыдущего контура на текущем слое:
  • иерархия[i][2] — индекс первого контура на вложенном слое;
  • иерархия[i][3] — индекс родительского контура.

сдвиг — величина смещения точек контура.

2. Функция OpenCV для отображения контуров drawContours

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

drawContours( кадр, контуры, индекс, цвет[, толщина[, тип_линии[, иерархия[, макс_слой[, сдвиг]]]]])

кадр — кадр, поверх которого мы будем отрисовывать контуры;

контуры — те самые контуры, найденные функцией findContours;

индекс — индекс контура, который следует отобразить. -1 — если нужно отобразить все контуры;

цвет — цвет контура;

толщина — толщина линии контура;

тип_линии — тип соединения точек вектора;

иерархия — информация об иерархии контуров;

макс_слой — индекс слоя, который следует отображать. Если параметр равен 0, то будет отображен только выбранный контур. Если параметр равен 1, то отобразится выбранный контур и все его дочерние контуры. Если параметр равен 2, то отобразится выбранный контур, все его дочерние и дочерние дочерних! И так далее.

сдвиг — величина смещения точек контура.

3. Программа поиска и отображения контуров

Теперь напишем программу, которая будет искать контуры предметов в кадре с пончиком. Прежде всего, следует подготовить изображение. Помним, что функция findContours работает с монохромной картинкой, и нам потребуется обработать наш пончик цветовым фильтром, чтобы сам пончик стал абсолютно белым, а фон — чёрным.

import sys
import numpy as np
import cv2 as cv

# параметры цветового фильтра
hsv_min = np.array((2, 28, 65), np.uint8)
hsv_max = np.array((26, 238, 255), np.uint8)

if __name__ == '__main__':
    print(__doc__)

    fn = 'image.jpg' # путь к файлу с картинкой
    img = cv.imread(fn)

    hsv = cv.cvtColor( img, cv.COLOR_BGR2HSV ) # меняем цветовую модель с BGR на HSV 
    thresh = cv.inRange( hsv, hsv_min, hsv_max ) # применяем цветовой фильтр
    # ищем контуры и складируем их в переменную contours
    _, contours, hierarchy = cv.findContours( thresh.copy(), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

    # отображаем контуры поверх изображения
    cv.drawContours( img, contours, -1, (255,0,0), 3, cv.LINE_AA, hierarchy, 1 )
    cv.imshow('contours', img) # выводим итоговое изображение в окно

    cv.waitKey()
    cv.destroyAllWindows()

В результате работы программы мы получим пончики с выделенными внешними и вложенными границами.

OpenCV на python: поиск контуров

Теперь разберёмся как параметры кадр и макс_слой влияют на отображаемые контуры.

Алгоритм findContours нашел у пончиков четыре замкнутых контура. Если вывести иерархию в консоль с помощью обычного print, то мы увидим следующую таблицу:

[ 2 -1  1 -1] - внешний контур первого бублика
[-1 -1 -1  0] - дырка первого бублика
[-1  0  3 -1] - внешний контур второго бублика
[-1 -1 -1  2] - дырка второго бублика

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

В программе параметр контур = -1, следовательно drawContours должна вывести все четыре найденных контура. Но есть ещё второй параметр макс_слой, как он будет влиять на вывод? Посмотрим его поведение на анимации:

OpenCV на python, поиск контуров, параметр maxLevel

Примечание! На верхнем бегунке contour = 0, хотя мы почему-то говорим про -1. Это не ошибка! На самом деле в этом положении бегунка в функцию поступает параметр контур = -1. Это несоответствие возникло из-за особенностей бегунка в OpenCV — он не может принимать отрицательные значения, поэтому в программе из значения бегунка contour каждый раз принудительно вычитается единица.

Вернёмся к параметрам.

При макс_слой = 0 отображается первый попавшийся контур на верхнем уровне иерархии. Такая комбинация параметров вообще нетипичная  и бесполезная, она эквивалентна комбинации контур = 0, макс_слой=0.

При макс_слой = 1 отобразятся все контуры на самом верхнем уровне иерархии — это уже полезно. Так мы сможем увидеть все бублики в кадре.

Наконец, при макс_слой = 2 отобразятся контуры на верхнем уровне и все контуры на следующем уровне — то есть дырки.

Теперь наоборот, зафиксируем макс_слой = 0, и будем менять контур в диапазоне от 0 до 3.

OpenCV на python, поиск контуров, drawContours, параметр contour

Тут опять видна путающая всех первая комбинация: контур = -1, макс_слой = 0, игнорируем её. Но затем всё становится логично. Как и ожидалось, мы просто перебираем контуры на всех слоях, внутренних и внешних.

Чтобы самостоятельно поэкспериментировать с параметрами можно написать программу, которая добавит в окно два бегунка для изучаемых параметров. Подобное мы делали на уроке про цветовые фильтры.

import sys
import numpy as np
import cv2 as cv

hsv_min = np.array((2, 28, 65), np.uint8)
hsv_max = np.array((26, 238, 255), np.uint8)

if __name__ == '__main__':
    fn = 'donuts.jpg'
    img = cv.imread(fn)

    hsv = cv.cvtColor( img, cv.COLOR_BGR2HSV )
    thresh = cv.inRange( hsv, hsv_min, hsv_max )
    _, contours0, hierarchy = cv.findContours( thresh.copy(), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

    index = 0
    layer = 0

    def update():
        vis = img.copy()
        cv.drawContours( vis, contours0, index, (255,0,0), 2, cv.LINE_AA, hierarchy, layer )
        cv.imshow('contours', vis)

    def update_index(v):
        global index
        index = v-1
        update()

    def update_layer(v):
        global layer
        layer = v
        update()

    update_index(0)
    update_layer(0)
    cv.createTrackbar( "contour", "contours", 0, 7, update_index )
    cv.createTrackbar( "layers", "contours", 0, 7, update_layer )

    cv.waitKey()
    cv.destroyAllWindows()

К размышлению

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

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


Изменено: