Как найти контуры opencv python
Перейти к содержимому

Как найти контуры opencv python

  • автор:

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 должна вывести все четыре найденных контура. Но есть ещё второй параметр макс_слой, как он будет влиять на вывод? Посмотрим его поведение на анимации:

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

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

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

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

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

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

Тут опять видна путающая всех первая комбинация: контур = -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()

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

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

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

Как найти контуры в определенной области изображения с помощью opencv и Python?

Как я могу поменять эту строку чтобы получить желаемый результат?

вот эта область меня интересует frame[a:a + height, b:left_x + 300] Заранее спасибо!

Отслеживать

задан 15 окт 2021 в 8:56

81 6 6 бронзовых знаков

0

Сортировка: Сброс на вариант по умолчанию

Знаете кого-то, кто может ответить? Поделитесь ссылкой на этот вопрос по почте, через Твиттер или Facebook.

  • python
  • opencv

Обнаружение конгуров с использованием OpenCV

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

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

Контуры в компьютерном зрении ↑

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

  • Обнаружение движения: в видеонаблюдении технология обнаружения движения имеет множество приложений, начиная от внутренней и внешней безопасности, контроля дорожного движения, обнаружения поведения во время занятий спортом, поиск оставленных без присмотра объектов и даже сжатие видео. На рисунке ниже показано, как обнаружение движения людей в видеопотоке может быть полезно в приложении для наблюдения. Обратите внимание, как группа людей, стоящих в левой части изображения, не обнаруживается. Захватываются только те, кто находится в движении. Обратитесь к этой статье для подробного изучения этого подхода.
    Пример обнаружения движущегося объекта, определение движущихся людей. Обратите внимание, что группа людей, стоящяя слева, не обнаруживается
  • Поиск подозрительных объектов. Любой объект, оставленный без присмотра в общественных местах, обычно, считается подозрительным. Эффективным и безопасным решением может быть: (Автоматическое обнаружение объекта посредством формирования контура с использованием вычитания фона).
    Поиск подозрительных объектовИзображение из процитированного документа — фоновая рамка без и с оставленным без присмотра предметом — идентификация и маркировка оставленного без присмотра предмета
  • Сегментация фона/переднего плана: чтобы заменить фон изображения другим, необходимо выполнить извлечение изображения переднего плана (аналогично сегментации изображения). Использование контуров — это один из подходов, который можно использовать для сегментации. Обратитесь к этому сообщению для получения более подробной информации. На следующих изображениях показаны простые примеры такого приложения:
    Пример простого извлечения переднего плана изображения и добавления нового фона к изображению с помощью определения контура

Что такое контуры? ↑

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

  1. findContours()
  2. drawContours()

Также, у них есть два разных алгоритма обнаружения контура:

  1. CHAIN_APPROX_SIMPLE
  2. CHAIN_APPROX_NONE

Рассмотрим их подробно в приведенных ниже примерах. На следующем рисунке показано, как эти алгоритмы могут обнаруживать контуры простых объектов.

Сравните входное изображение и выходное изображение с наложенными контурами

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

Последовательность поиска и отрисовки контуров с помощью OpenCV ↑

OpenCV делает это довольно простой задачей. Просто выполните следующие действия:

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

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

Примечание. Черные пиксели, имеющие значение 0, воспринимаются как пиксели фона и игнорируются.

Здесь может возникнуть один вопрос. Что, если мы будем использовать отдельные каналы, такие как R (красный), G (зеленый) или B (синий), вместо изображений в градациях серого (с пороговыми значениями)? В таком случае алгоритм определения контура не сработает. Как мы обсуждали ранее, алгоритм ищет границы и пиксели аналогичной интенсивности для обнаружения контуров. Двоичное изображение предоставляет эту информацию намного лучше, чем изображение с одним цветовым каналом (RGB). Далее будет показаны результирующие изображения при использовании только одного канала R, G или B вместо изображений в оттенках серого и изображений с пороговыми значениями.

3. Найдите контуры
Используйте функцию findContours() для обнаружения контуров на изображении.

4. Нарисуйте контуры на исходном изображении RGB
После определения контуров используйте функцию drawContours() , чтобы наложить контуры на исходное изображение RGB.

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

Поиск и отрисовка контуров с помощью OpenCV ↑

Начните с импорта OpenCV и чтения входного изображения.

import cv2 # прочитать изображение image = cv2.imread('input/image_1.jpg')

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

# преобразовать изображение в формат оттенков серого img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

Теперь используйте функцию threshold() для применения бинарного порога к изображению. Любому пикселю со значением больше 150 будет присвоено значение 255 (белый). Все оставшиеся пиксели в результирующем изображении будут установлены в 0 (черный). Пороговое значение 150 — это настраиваемый параметр, поэтому вы можете с ним поэкспериментировать.

После установления порога визуализируйте двоичное изображение, используя функцию imshow() , как показано ниже.

# apply binary thresholding ret, thresh = cv2.threshold(img_gray, 150, 255, cv2.THRESH_BINARY) # визуализировать двоичное изображение cv2.imshow('Binary image', thresh) cv2.waitKey(0) cv2.imwrite('image_thres1.jpg', thresh) cv2.destroyAllWindows()

Посмотрите на изображение ниже! Это двоичное представление исходного изображения RGB. Можно ясно видеть, как перо, границы планшета и телефона все белые. Алгоритм контура будет рассматривать их как объекты и находить точки контура вокруг границ этих белых объектов.

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

Результирующее двоичное изображение после применения пороговой функции

Отрисовка с помощью CHAIN_APPROX_NONE ↑

Теперь давайте найдем и нарисуем контуры с помощью метода CHAIN_APPROX_NONE.

Начнем с функции findContours() . У неё, как показано ниже, есть три обязательных аргумента. Дополнительные аргументы см. На странице документации здесь .

  • image: двоичное входное изображение, полученное на предыдущем шаге.
  • mode: это режим поиска контура. Мы предоставили его как RETR_TREE и это означает, что алгоритм извлечет все возможные контуры из двоичного изображения. Доступны и другие режимы извлечения контуров и их обсудим тоже. Вы можете узнать больше об этих вариантах здесь .
  • method: определяет метод аппроксимации контура. В этом примере мы будем использовать CHAIN_APPROX_NONE . Хотя он немного медленнее, чем CHAIN_APPROX_SIMPLE , здесь для хранения ВСЕХ точек контура будем использовать этот метод.

Здесь надо подчеркнуть, что mode относится к типу извлекаемых контуров, а method относится к тому, какие точки внутри контура сохраняются. Ниже обсудим оба более подробно.

Легко визуализировать и понять результаты различных методов на одном и том же изображении.

Поэтому в примерах кода ниже обязательно делаем копию исходного изображения, а затем демонстрируем методы (не изменяя оригинал).

Затем используйте функцию drawContours() , чтобы наложить контуры на изображение RGB. Эта функция имеет четыре обязательных и несколько необязательных аргументов. Первые четыре аргумента ниже обязательны. Для необязательных аргументов пожалуйста, обратитесь к странице документации здесь .

  • image: это входное изображение RGB, на котором вы хотите нарисовать контур.
  • contours: указывает контуры, полученные с помощью функции findContours().
  • contourIdx: координаты точек контура в пикселях перечислены в полученных контурах. Используя этот аргумент, вы можете указать позицию индекса из этого списка, указывая, какую именно точку контура вы хотите нарисовать. Если указать отрицательное значение, будут нарисованы все точки контура.
  • color: указывает цвет точек контура, которые вы хотите нарисовать. Мы рисуем точки зеленым цветом.
  • thickness: это толщина точек контура.
# обнаруживаем контуры на двоичном изображении с помощью cv2.CHAIN_APPROX_NONE contours, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE) # рисуем контуры на исходном изображении image_copy = image.copy() cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA) # смотрим результаты cv2.imshow('None approximation', image_copy) cv2.waitKey(0) cv2.imwrite('contours_none_image1.jpg', image_copy) cv2.destroyAllWindows()

Выполнение приведенного выше кода приведет к созданию и отображению изображения, показанного ниже. Также сохраняем образ на диск.

Контуры, обнаруженные с помощью CHAIN_APPROX_NONE, наложенные на входное изображение

На следующем рисунке показано исходное изображение (слева), а также исходное изображение с наложенными контурами (справа).

Исходное изображение (слева), а также исходное изображение с наложенными контурами (справа)

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

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

Использование одного канала: красный, зеленый или синий

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

import cv2 # прочитать изображение image = cv2.imread('input/image_1.jpg') # Разделение каналов B, G, R blue, green, red = cv2.split(image) # обнаруживаем контуры по синему каналу и без порога contours1, hierarchy1 = cv2.findContours(image=blue, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE) # рисуем контуры на исходном изображении image_contour_blue = image.copy() cv2.drawContours(image=image_contour_blue, contours=contours1, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA) # смотрим результат cv2.imshow('Contour detection using blue channels only', image_contour_blue) cv2.waitKey(0) cv2.imwrite('blue_channel.jpg', image_contour_blue) cv2.destroyAllWindows() # обнаруживаем контуры с использованием зеленого канала и без порогового значения contours2, hierarchy2 = cv2.findContours(image=green, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE) # рисуем контуры на исходном изображении image_contour_green = image.copy() cv2.drawContours(image=image_contour_green, contours=contours2, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA) # смотрим результат cv2.imshow('Contour detection using green channels only', image_contour_green) cv2.waitKey(0) cv2.imwrite('green_channel.jpg', image_contour_green) cv2.destroyAllWindows() # detect contours using red channel and without thresholding contours3, hierarchy3 = cv2.findContours(image=red, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE) # рисуем контуры на исходном изображении image_contour_red = image.copy() cv2.drawContours(image=image_contour_red, contours=contours3, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA) # смотрим результат cv2.imshow('Contour detection using red channels only', image_contour_red) cv2.waitKey(0) cv2.imwrite('red_channel.jpg', image_contour_red) cv2.destroyAllWindows()

Посмотрите на результаты обнаружения контура для всех трех отдельных цветовых каналов.

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

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

Отрисовка с помощью CHAIN_APPROX_SIMPLE ↑

Теперь давайте посмотрим, как работает алгоритм CHAIN_APPROX_SIMPLE и чем он отличается от алгоритма CHAIN_APPROX_NONE .

Вот для этого код:

""" Теперь давайте попробуем с `cv2.CHAIN_APPROX_SIMPLE` """ # обнаруживаем контуры на двоичном изображении с помощью cv2.ChAIN_APPROX_SIMPLE contours1, hierarchy1 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # рисуем контуры на исходном изображении для `CHAIN_APPROX_SIMPLE` image_copy1 = image.copy() cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA) # смотрим результат cv2.imshow('Simple approximation', image_copy1) cv2.waitKey(0) cv2.imwrite('contours_simple_image1.jpg', image_copy1) cv2.destroyAllWindows()

Здесь единственная разница заключается в том, что method для findContours() определён, как CHAIN_APPROX_SIMPLE вместо CHAIN_APPROX_NONE .

Алгоритм CHAIN_APPROX_SIMPLE сжимает горизонтальные, вертикальные и диагональные сегменты вдоль контура и оставляет только их конечные точки. Это означает, что любая из точек на прямых путях будет отклонена и останутся только конечные точки. Например, рассмотрим контур по прямоугольнику. Все точки контура, кроме четырех угловых, будут отклонены. Этот метод быстрее, чем CHAIN_APPROX_NONE , потому что алгоритм не сохраняет все точки, использует меньше памяти и, следовательно, требует меньше времени для выполнения. Ниже показаны результаты.

Контуры, обнаруженные с помощью CHAIN_APPROX_SIMPLE, наложенного на входное изображение

Если вы внимательно присмотритесь, почти нет различий между CHAIN_APPROX_NONE и CHAIN_APPROX_SIMPLE .

Итак, почему это так?

Заслуга принадлежит функции drawContours() . Хотя метод CHAIN_APPROX_SIMPLE обычно приводит к меньшему количеству точек, функция drawContours() автоматически соединяет соседние точки, присоединение к ним, даже если их нет в списке контуров.

Итак, как можно подтвердить, что алгоритм CHAIN_APPROX_SIMPLE действительно работает?

Новое изображение, демонстрирующее алгоритм обнаружения контура CHAIN_APPROX_SIMPLE

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

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

  • Первый цикл for проходит по каждой области контура, присутствующей в списке контуров.
  • Второй проходит по каждой из координат в этой области.
  • Затем мы рисуем зеленый кружок в каждой координатной точке, используя функцию circle() из OpenCV.
  • Наконец, мы визуализируем результаты и сохраняем их на диск.
# чтобы визуализировать эффект `CHAIN_APPROX_SIMPLE`, нам нужно правильное изображение image1 = cv2.imread('input/image_2.jpg') img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) ret, thresh1 = cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY) contours2, hierarchy2 = cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) image_copy2 = image1.copy() cv2.drawContours(image_copy2, contours2, -1, (0, 255, 0), 2, cv2.LINE_AA) cv2.imshow('SIMPLE Approximation contours', image_copy2) cv2.waitKey(0) image_copy3 = image1.copy() for i, contour in enumerate(contours2): # loop over one contour area for j, contour_point in enumerate(contour): # loop over the points # draw a circle on the current contour coordinate cv2.circle(image_copy3, ((contour_point[0][0], contour_point[0][1])), 2, (0, 255, 0), 2, cv2.LINE_AA) # смотрим результат cv2.imshow('CHAIN_APPROX_SIMPLE Point only', image_copy3) cv2.waitKey(0) cv2.imwrite('contour_point_simple.jpg', image_copy3) cv2.destroyAllWindows()

Выполнение приведенного выше кода дает следующий результат:

Обратите внимание, что при использовании CHAIN_APPROX_SIMPLE для определения контура на четырех углах книги есть только четыре контурных точки. Полностью игнорируются вертикальные и горизонтальные прямые линии книги

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

Иерархии контуров

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

Родительско-дочерние отношения ↑

Объекты, обнаруженные алгоритмами определения контуров на изображении, могут быть:

  • Отдельными объектами, разбросанными по изображению (как в первом примере), или
  • Объектами и формами друг в друге

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

Взгляните на следующий рисунок, он содержит несколько простых форм, которые помогут продемонстрировать иерархию контуров.

Изображение с простыми линиями и формами. Будем использовать это изображение, чтобы узнать больше о контурных иерархиях

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

Числа, показывающие отношения между родителями и детьми между разными фигурами

  • Все отдельные числа, то есть 1, 2, 3 и 4, являются отдельными объектами в соответствии с иерархией контуров и отношениями родитель-потомок.
  • Можно сказать, что 3a — ребенок 3-го. Обратите внимание, что 3а представляет собой внутреннюю часть в контуре 3.
  • Контуры 1, 2 и 4 являются родительскими фигурами без каких-либо связанных дочерних фигур, поэтому их нумерация произвольна. Другими словами, контур 2 можно было бы обозначить как 1 и наоборот.

Контурное представление отношений ↑

Вы видели, что функция findContours() возвращает список контуров и иерархию. Теперь подробно разберемся с выводом иерархии контуров.

Иерархия контуров представлена ​​в виде массива, который, в свою очередь, содержит массивы из четырех значений. Он представлен как:

[Next, Previous, First_Child, Parent]

Итак, что означают все эти значения?

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

  • Для контура 1 следующий контур на том же иерархическом уровне — 2. Здесь Next будет 2.
  • Соответственно, контур 3 не имеет контура на том же иерархическом уровне, что и он сам. Итак, значение Next будет -1.

Previous : обозначает предыдущий контур на том же иерархическом уровне. Это означает, что предыдущее значение контура 1 всегда будет равно -1.

First_Child : обозначает первый дочерний контур контура, который мы сейчас рассматриваем.

  • Контуры 1 и 2 вообще не имеют детей. Таким образом, значение индекса для их First_Child будет -1.
  • Но у контура 3 есть ребенок. Итак, для контура 3 значение позиции First_Child будет индексной позицией 3a.

Parent : обозначает позицию индекса родительского контура для текущего контура.

  • Контуры 1 и 2, как очевидно, не имеют Родительского контура.
  • Для контура 3a его Родителем будет контур 3
  • Для контура 4 родительский контур 3a

Приведенные выше объяснения имеют смысл, но как на самом деле визуализировать эти иерархические массивы? Лучше всего:

  • использовать простое изображение с линиями и формами, как на предыдущем изображении;
  • искать контуры и иерархии, используя различные режимы поиска;
  • затем распечатывать значения, чтобы визуализировать их.

Различные методы извлечения контуров ↑

До сих пор мы использовали один конкретный метод поиска, RETR_TREE , чтобы найти и нарисовать контуры, но в OpenCV есть еще три метода поиска контура, а именно RETR_LIST , RETR_EXTERNAL и RETR_CCOMP .

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

""" Обнаружение контуров и отрисовка с использованием различных режимов извлечения для дополнения понимание иерархий """ image2 = cv2.imread('input/custom_colors.jpg') img_gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) ret, thresh2 = cv2.threshold(img_gray2, 150, 255, cv2.THRESH_BINARY)
RETR_LIST

Метод извлечения контуров RETR_LIST не создает никаких родительских отношений между извлеченными контурами. Таким образом, для всех обнаруженных контурных областей значения позиции индекса First_Child и Parent всегда равны -1.

Все контуры будут иметь соответствующие предыдущие и следующие контуры, как описано выше.

Посмотрите, как метод RETR_LIST реализован в коде.

contours3, hierarchy3 = cv2.findContours(thresh2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) image_copy4 = image2.copy() cv2.drawContours(image_copy4, contours3, -1, (0, 255, 0), 2, cv2.LINE_AA) # смотрим результат cv2.imshow('LIST', image_copy4) print(f"LIST: ") cv2.waitKey(0) cv2.imwrite('contours_retr_list.jpg', image_copy4) cv2.destroyAllWindows()

Выполнение приведенного выше кода дает следующий результат:

LIST: [ [ [ 1 -1 -1 -1] [ 2 0 -1 -1] [ 3 1 -1 -1] [ 4 2 -1 -1] [-1 3 -1 -1] ] ]

Ясно видно, что позиции 3-го и 4-го индексов всех обнаруженных областей контура равны -1, как и ожидалось.

RETR_EXTERNAL

Метод извлечения контура RETR_EXTERNAL действительно интересен. Он обнаруживает только родительские контуры и игнорирует любые дочерние контуры. Таким образом, на всех внутренних контурах, таких как 3a и 4, не будет нарисовано никаких точек.

contours4, hierarchy4 = cv2.findContours(thresh2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) image_copy5 = image2.copy() cv2.drawContours(image_copy5, contours4, -1, (0, 255, 0), 2, cv2.LINE_AA) # смотрим результат cv2.imshow('EXTERNAL', image_copy5) print(f"EXTERNAL: ") cv2.waitKey(0) cv2.imwrite('contours_retr_external.jpg', image_copy5) cv2.destroyAllWindows()

В результате получим:

EXTERNAL: [ [ [ 1 -1 -1 -1] [ 2 0 -1 -1] [-1 1 -1 -1] ] ]

Контуры обнаружены и нарисованы в режиме RETR_EXTERNAL

На приведенном выше изображении показаны только точки, нарисованные на контурах 1, 2 и 3. Контуры 3a и 4 опущены, поскольку они являются дочерними контурами.

RETR_CCOMP

В отличие от RETR_EXTERNAL , RETR_CCOMP извлекает все контуры изображения. Наряду с этим, он также применяет двухуровневую иерархию ко всем формам или объектам на изображении.

  • все внешние контуры будут иметь уровень иерархии 1, а
  • все внутренние контуры будут иметь уровень иерархии 2.

Но что, если у нас есть контур внутри другого контура с уровнем иерархии 2? Точно так же, как у нас есть контур 4 после контура 3a.

  • контур 4, опять же, будет иметь уровень иерархии 1.
  • Если внутри контура 4 есть контуры, то у них будет уровень иерархии 2.

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

Изображение, показывающее различные уровни иерархии в контурах при использовании метода извлечения RETR_CCOMP

На изображении выше показан уровень иерархии как HL-1 или HL-2 для уровней 1 и 2 соответственно. Теперь давайте посмотрим на код и массив выходной иерархии.

contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) image_copy6 = image2.copy() cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA) # смотрим результат cv2.imshow('CCOMP', image_copy6) print(f"CCOMP: ") cv2.waitKey(0) cv2.imwrite('contours_retr_ccomp.jpg', image_copy6) cv2.destroyAllWindows()

В результате выполнение приведенного выше кода получим:

CCOMP: [ [ [ 1 -1 -1 -1] [ 3 0 2 -1] [-1 -1 -1 1] [ 4 1 -1 -1] [-1 3 -1 -1] ] ]

Здесь мы видим, что все отношения Next , Previous , First_Child и Parent поддерживаются в соответствии с методом поиска контуров, поскольку все контуры обнаруживаются. Как и ожидалось, Предыдущее значение первой области контура равно -1. И контуры, у которых нет Родителя, также имеют значение -1.

RETR_TREE

Как и RETR_CCOMP , RETR_TREE также извлекает все контуры. Он также создает полную иерархию с уровнями, не ограниченными 1 или 2. Каждый контур может иметь свою собственную иерархию в соответствии с уровнем, на котором он находится, и соответствующими родительско-дочерними отношениями, которые у него есть.

Уровни иерархии при использовании режима извлечения контура RETR_TREE

Из рисунка выше видно, что:

  • Контуры 1, 2 и 3 находятся на одном уровне, то есть на уровне 0.
  • Контур 3a присутствует на уровне иерархии 1, поскольку он является дочерним элементом контура 3.
  • Контур 4 — это новая область контура, поэтому его уровень иерархии равен 2.

Следующий код использует режим RETR_TREE для извлечения контуров.

contours6, hierarchy6 = cv2.findContours(thresh2, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) image_copy7 = image2.copy() cv2.drawContours(image_copy7, contours6, -1, (0, 255, 0), 2, cv2.LINE_AA) # смотрим резльтат cv2.imshow('TREE', image_copy7) print(f"TREE: ") cv2.waitKey(0) cv2.imwrite('contours_retr_tree.jpg', image_copy7) cv2.destroyAllWindows()

Выполнение приведенного выше кода дает следующий результат:

TREE: [ [ [ 3 -1 1 -1] [-1 -1 2 0] [-1 -1 -1 1] [ 4 0 -1 -1] [-1 3 -1 -1] ] ]

Наконец, давайте посмотрим на полное изображение со всеми контурами, нарисованными при использовании режима RETR_TREE.

Обнаружение контура при использовании режима поиска RETR_TREE

Все контуры нарисованы так, как ожидалось, и участки контура хорошо видны. Вы также делаете вывод, что контуры 3 и 3a — это два отдельных контура, поскольку они имеют разные границы контура и площади. В то же время совершенно очевидно, что контур 3a является потомком контура 3.

Теперь, когда вы знакомы со всеми алгоритмами контуров, доступными в OpenCV, вместе с соответствующими входными параметрами и конфигурациями, поэкспериментируйте и убедитесь сами, как они работают.

Сравнение времени выполнения различных методов поиска контура

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

Метод получения контура Время (в секундах)
RETR_LIST 0,000382
RETR_EXTERNAL 0,000554
RETR_CCOMP 0,001845
RETR_TREE 0,005594

Сравнение скорости вывода разных методов

Из приведенной выше таблицы следует несколько интересных выводов:

  • RETR_LIST и RETR_EXTERNAL занимают наименьшее количество времени для выполнения, поскольку RETR_LIST не определяет никакой иерархии, а RETR_EXTERNAL только извлекает родительские контуры
  • RETR_CCOMP занимает второе по времени выполнение. Он извлекает все контуры и определяет двухуровневую иерархию.
  • RETR_TREE занимает максимальное время для выполнения, так как извлекает все контуры, а также определяет независимый уровень иерархии для каждого родительско-дочернего отношения.

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

Ограничения

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

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

  • Когда объекты на изображении сильно контрастируют с их фоном, вы можете четко определить контуры, связанные с каждым объектом. Но что, если у вас есть изображение, как на рисунке ниже. У него не только яркий объект (щенок), но и фон, загроможденный тем же значением (яркостью), что и объект интереса (щенок). Вы обнаружите, что контуры на правом изображении даже не завершены. Кроме того, в области фона есть несколько нежелательных контуров.
  • Обнаружение контура также может не сработать, если фон объекта на изображении заполнен линиями.

Резюме ↑

Мы начали с определения контуров и научились реализовывать это в OpenCV. Увидели, как приложения используют контуры для обнаружения и сегментации движения. Затем мы продемонстрировали использование четырех различных режимов восстановления и двух методов контурной аппроксимации. Мы также научились рисовать контуры, закончили обсуждением контурных иерархий и как различные режимы поиска контуров влияют на прорисовку контуров на изображении.

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

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

Print Friendly, PDF & Email

Обнаружение конгуров с использованием OpenCV , опубликовано К ВВ, лицензия — Creative Commons Attribution-NonCommercial 4.0 International.
Респект и уважуха

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

Для отправки комментария вам необходимо авторизоваться.

Ограничение ответственности

Информация на сайте предоставляется «как есть», без всяких гарантий, включая гарантию применимости в определенных целях, коммерческой пригодности и т.п. В текстах могут быть технические неточности и ошибки. Автор не может гарантировать полноты, достоверности и актуальности всей информации, не несет ответственности за последствия использования сайта третьими лицами. Автор не делает никаких заявлений, не дает никаких гарантий и оценок относительно того, что результаты, размещенные на сайте и описанные в заявлениях относительно будущих результатов, будут достигнуты. Автор не несет ответственности за убытки, возникшие у пользователей или третьих лиц в результате использования ими сайта, включая упущенную выгоду. Автор не несет ответственности за убытки, возникшие в результате действий пользователей, явно не соответствующих обычным правилам работы с информацией в сети Интернет. Пользуясь сайтом, вы принимаете и соглашаетесь со всеми нашими правилами, включая «Ограничение ответственности».

Рекомендую

Link’s QR code

Время, где сейчас

До восхода не будить и при пожаре выносить в первую очередь, а после заката звонить только в экстренных случаях:

Скоро, скоро Новый год

Рубрики

Отче наш

Отче наш, Иже еси́ на небесе́х! Да святи́тся имя Твое́, да прии́дет Ца́рствие Твое, да будет воля Твоя, я́ко на небеси́ и на земли́. Хлеб наш насу́щный даждь нам днесь; и оста́ви нам до́лги наша, я́коже и мы оставля́ем должнико́м нашим; и не введи́ нас во искушение, но изба́ви нас от лука́ваго

Под контролем

  1. Академия студенчества
  2. ИЛИМК
  3. Офис академического письма
  4. МАЛиМК
  5. НОЦ Лингво-инновационных технологий
  6. Партнерская сеть «Институт Пушкина»
  7. Центр делового образования
  8. Центр «Лингва»
  9. Управление инновациями и инвестициями
  10. Финансовый менеджмент
  11. Кафедра ИТЭ
  12. Кафедра ЦЭИИТ
  13. Бизнес-информатика + Кванториум
  14. ВКР: Бизнес-информатика
  15. Бизнес-информатика on-line
  16. Бизнес-информатика — драйв цифровой трансформации
  17. ЭУ-160
  18. ЭУ-235
  19. ЭУ-320
  20. ЭУ-442
  21. ЭУ-419 (архив)
  22. ЭУ-444, 461 (архив)
  23. ЭУ-459 (архив)
  24. ЭУ-434 (архив)
  25. Магистры Бизнес-информатики
  26. Магистратура ВШЭУ
  27. Технологическая площадка
  28. Digital Experience Workshop
  29. Cтэк технологий Web-разработки: шаг за шагом
  30. Абитуриент ВШЭУ — 2018
  31. Школа бизнеса ВШЭУ
  32. Кибер-студент
  33. Зеркало сайта
  34. ИТ-марафон
  35. Покори свой Олимп
  36. Digital Experience
  37. ООО «ЮЖУРАЛСЕРВИС»
  38. Школа перспективных технологий
  39. Бизнес‑информатике 15 лет

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

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

  • пытались найти по цвету (чаще всего так делать не надо);
  • пытались найти круглый знак посредством функции HoughCircles (иногда работает);
  • а еще мы изучили морфологические операции (открытие закрытие).

Сегодняшний урок будет более глубоко посвящен работе с контурами, так как часто контур помогает выдели фичи на изображения, а так же области интересов (благодаря контуру, мы можем охватить форму объекта).

Для начала вспомним, как находить контуры:

import cv2 import numpy as np my_photo = cv2.imread('DSCN1311.JPG') filterd_image = cv2.medianBlur(my_photo,7) img_grey = cv2.cvtColor(filterd_image,cv2.COLOR_BGR2GRAY) #set a thresh thresh = 100 #get threshold image ret,thresh_img = cv2.threshold(img_grey, thresh, 255, cv2.THRESH_BINARY) #find contours contours, hierarchy = cv2.findContours(thresh_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #create an empty image for contours img_contours = np.uint8(np.zeros((my_photo.shape[0],my_photo.shape[1]))) cv2.drawContours(img_contours, contours, -1, (255,255,255), 1) cv2.imshow('origin', my_photo) # выводим итоговое изображение в окно cv2.imshow('res', img_contours) # выводим итоговое изображение в окно cv2.waitKey() cv2.destroyAllWindows() 

Обратите внимание, что перед выделением контуров мы используем фильтрацию. Вот что у нас получилось:

Без фильтрации у нас бы получилось вот что (для сравнения, справа без фильтра, слева с фильтром):

Теперь посмотрим, а что именно у нас возвращает findContours и как с этим работать:

contours, hierarchy = cv2.findContours(thresh_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) print(type(contours),type(hierarchy))

Мы получили вывод:

Таким образом, сам контур – это обыкновенный тьюпл, а второе возвращенное значение массив numpy. Если мы посмотрим этот тьюпл отладчиком, то увидим, что элементами этого тьюпла являются массив numpy:

Иными словами, функция возвращает целое множество контуров. По идее, можно работать с каждым из контуров по отдельности. Давайте, например, выведем на экран четвертый (он на самом деле будет под номером 3, считаем же с нуля) контур:

img_contours = np.uint8(np.zeros((my_photo.shape[3],my_photo.shape[1])))

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

Можно вывести сразу несколько контуров:

sel_countours=[] sel_countours.append(contours[3]) sel_countours.append(contours[7]) sel_countours.append(contours[8]) cv2.drawContours(img_contours, sel_countours, -1, (255,255,255), 1)

Вот что мы увидим:

Найдем самый большой контур:

max=0 sel_countour=None for countour in contours: if countour.shape[0]>max: sel_countour=countour max=countour.shape[0] cv2.drawContours(img_contours, [sel_countour], -1, (255,255,255), 1)

Надо сказать, что контур может храниться как в виде точек, так и в виде отрезков, в зависимости от установлено параметра аппроксимации:

contours, hierarchy = cv2.findContours(thresh_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

В нашем случае установлен Simple, значит, контур храниться в виде отрезков, если мы нарисуем по точкам, то контура не получиться:

for point in sel_countour: y=int(point[0][1]) x=int(point[0][0]) img_contours[y,x]=255

Но если вы укажете функции findContours что надо искать контуры без аппроксимации:

contours, hierarchy = cv2.findContours(thresh_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

То контур будет как на предыдущей картинке.

С другой стороны, если у вас аппроксимация включена, то вы можете нарисовать контур, соединив точки линиями:

last_point=None for point in sel_countour: curr_point=point[0] if not(last_point is None): x1=int(last_point[0]) y1=int(last_point[1]) x2=int(curr_point[0]) y2=int(curr_point[1]) cv2.line(img_contours, (x1, y1), (x2, y2), 255, thickness=1) last_point=curr_point

Получится то же самое, что и на первой картинке.

И так, функция findContours возвращает сгруппированные наборы точек, которые являются точками контура (или концами отрезков контура, в зависимости от типа аппроксимации).

Полученный контур мы можем и далее аппроксимировать:

import cv2 import numpy as np import os img = cv2.imread("DSCN1311.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) max=0 sel_countour=None for countour in contours: if countour.shape[0]>max: sel_countour=countour max=countour.shape[0] # calc arclentgh arclen = cv2.arcLength(sel_countour, True) # do approx eps = 0.0005 epsilon = arclen * eps approx = cv2.approxPolyDP(sel_countour, epsilon, True) # draw the result canvas = img.copy() for pt in approx: cv2.circle(canvas, (pt[0][0], pt[0][1]), 7, (0,255,0), -1) cv2.drawContours(canvas, [approx], -1, (0,0,255), 2, cv2.LINE_AA) img_contours = np.uint8(np.zeros((img.shape[0],img.shape[1]))) cv2.drawContours(img_contours, [approx], -1, (255,255,255), 1) cv2.imshow('origin', canvas) # выводим итоговое изображение в окно cv2.imshow('res', img_contours) # выводим итоговое изображение в окно cv2.waitKey() cv2.destroyAllWindows()

Давайте посмотрим, что получиться:

Мы можем управлять точность аппроксимации, меняя значение переменной eps. Поставим, например, вместо 0.0005 значение 0.005 и картинка будет уже совсем другой:

А теперь более внимательно рассмотрим кусок кода, ответственный за аппроксимацию:

# calc arclentgh arclen = cv2.arcLength(sel_countour, True) # do approx eps = 0.0005 epsilon = arclen * eps approx = cv2.approxPolyDP(sel_countour, epsilon, True)

Функция arcLength возвращает длину дуги контура. Давайте попробуем посмотреть длины разных контуров. Только давайте сначала отсортируем контуры в порядке уменьшения их длин. Для этого определим кастмную сортировочную функцию:

def custom_sort(countour): return -countour.shape[0]

Теперь мы можем отсортировать контуры:

contours=list(contours) contours.sort(key=custom_sort)

Самый длинный контур будет первым:

sel_countour=contours[0] # calc arclentgh arclen = cv2.arcLength(sel_countour, True) print(arclen)

Остальные контуры будут поменьше, например, вот контур под индексом 5:

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

Аппроксимируемый контур – это, по сути те же точки, соединенные отрезками, так что его можно вывести и так:

last_point=None for point in approx: curr_point=point[0] if not(last_point is None): x1=int(last_point[0]) y1=int(last_point[1]) x2=int(curr_point[0]) y2=int(curr_point[1]) cv2.line(img_contours, (x1, y1), (x2, y2), 255, thickness=1) last_point=curr_point

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

Сейчас мы попробуем создать такое инвариантное описание объекта. Пусть это будет обыкновенная шариковая ручка:

Логично предположить, что надо работать с самым длинным контуром. Найдем, его, это мы уже умеем:

Нет, не угадали, придется перебирать. К счастью, контур оказался второй по длине:

contours,_ = cv2.findContours(thresh_img,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) contours=list(contours) contours.sort(key=custom_sort) sel_countour=contours[1]

Как оказалось, при значении eps=0.005 контур имеет всего 7 элементов:

eps = 0.005 epsilon = arclen * eps approx = cv2.approxPolyDP(sel_countour, epsilon, True) print(len(approx))

Посмотрим, как будет выделен контур в других положениях:

В последнем случае мы получили, кстати, не 7, а 9 элементов. Короче, тут засада с тенью. В общем, надо как-то избавиться от мелких деталей. Но как? Поднять порог аппроксимации? Давайте сделаем 0.01:

Количество элементов стало 6. На других фотографиях, тоже кстати 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)))

Отобразим центр после вывода контура:

cv2.circle(img_contours, (int(xc), int(yc)), 7, (255,255,255), 2)

Найдем точку, наиболее удаленную от центра:

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
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)

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

Полярные координаты вычислим вот такой вот функцией:

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 #опредедляем, в какую сторону повернут вектор angle=math.acos(cos_angle) if sgn

Здесь мы задаем точку начала отчета, искомую точку и наш центр. Первая координата, это радиус, его мы вычислим по теореме Пифагора. Угол найдем через скалярное произведение. Тут, правда, есть засада. Через скалярное произведение мы вычислим угол между векторами, но не направление. Чтобы его вычислить, нам надо найти определить матрицы векторов. Знак это и будет направление вращения. Но нам надо не просто отрицательный угол, иначе при сортировке первая точка будет не начало отчета, а точка с самым отрицательным углом. Поэтому если направление в другую сторону, то вычтем этот угол из угла 2 пи радиан (360 градусов).

Если не понятно, то я сейчас наглядно продемонстрирую проблему. Но, давайте сначала отсортируем:

polar_coordinates=[] x0=approx[beg_point][0][0] y0=approx[beg_point][0][1] print(x0,y0) 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))) print(polar_coordinates) 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)

Смотрим, что получилось:

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

А теперь уберем из функции перевода в полярные координаты наши манипуляции с определением направления вращения:

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 #опредедляем, в какую сторону повернут вектор angle=math.acos(cos_angle) #if sgn

И вот тогда какая ерунда получится:

Так что, вернем что закоментили на место и продолжим.

Приступим к инвариантному описанию. Углы между гранями контура. Здесь мы будем исходить из того, что углы положительны и меньше 180 градусов, то есть не будем делать тех манипуляций с определением направление. Хотя… лучше даже определить не углы а косинусы углов, они примут значения от 0 до 1. По сути, это уже будет обычный вектор, который мы можем подать на вход какого-нибудь алгоритма классификации, например, нейросеть.

И так, функция вычисления косинуса угла между гранями (. ):

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_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

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

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)

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

[0.8435094506704439, -0.9679482843035412, -0.7475204740128089, 0.12575426475263257, -0.7530074822433576, -0.9513518107379842]

Посмотрим в другом положении:

[0.8997284651496198, -0.9738348113021638, -0.886281044605172, 0.6119832801209469, -0.9073303511520623, -0.9760783176138438]

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

Для чистоты эксперимента, еще в одном положении:

[0.8447017514267182, -0.968529494204698, -0.20124730714807806, -0.4685934718394871, -0.7702667523702886, -0.9517100095171195]

Видим аналогичную ситуацию.

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

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_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 sgnmax: 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) 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
  • python
  • cv
  • computer vision
  • компьютерное зрение
  • выделение контуров
  • работа с изображениями
  • Python
  • Программирование
  • Обработка изображений

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

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