#3 Нейронные сети для начинающих. Работа с изображениями в OpenCV. Алгоритм Canny Edge Detector

gdazjidy8wjwoonj_m_fee_vchm.png

Здесь должна быть шутка
image


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

  1. Преобразование изображения в оттенки серого.
  2. Уменьшение размерности изображения (в пикселях) в 4 раза.
  3. Нахождение и выделение (рамкой) самого тёмного объекта на изображении.


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

Перед началом работы советую вам прочитать предыдущие статьи для более полного понимания материала:


Код


Итак, приступим. Для начала нам потребуется библиотека OpenCV, которую мы разбирали в предыдущей статье в пункте «Поверхностное знакомство с библиотекой OpenCV». Давайте установим её. Для этого введём в терминале команду:

pip install opencv-python


После этого в нашем файле с расширением .py, мы можем импортировать библиотеку cv2 (краткое название библиотеки OpenCV). Давайте пропишем это:

import cv2
import numpy as np


Также мы импортировали библиотеку NumPy, которая предназначена для математических вычислений. Подробнее о ней вы можете прочитать в предыдущей статье, в пункте «Знакомство с библиотекой NumPy».

Теперь давайте отойдём от кода и вспомним немного теории.

Основные операции с изображениями


Установив OpenCV, пощупаем основные функциональные возможности данной библиотеки.

▍ Вывод изображения на экран


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

Для вывода изображения на экран нам необходимо задать две вещи:

  1. Путь к файлу, в котором содержится изображение (подойдёт как относительный, так и абсолютный путь).
  2. Режим чтения файла (только чтение, запись и т. д.).


Функция, при помощи которой мы считываем изображение, называется cv2.imread (). У неё есть три режима работы:

  1. IMREAD_GRAYSCALE. Как видно из названия, он преобразует изображение в чёрно-белое с оттенками серого.
  2. IMREAD_UNCHANGED, который загружает изображение без обрезания альфа-канала.
  3. IMREAD_COLOR (используемый по умолчанию). Он просто загружает цветное изображение, используя RGB-каналы.


Вот пример кода:

import cv2

my_bike = cv2.imread('bike.png')


Замечание: если в результате выполнения данного кода возникла ошибка, есть три возможных причины для этого. Первая — вы неправильно задали путь к файлу. Вторая — такого файла просто не существует, и третья — тип изображения (jpg/jpeg/png) задан неверно.


Теперь давайте выведем на экран только что загруженное изображение. Для этого используется функция cv2.imshow (). Если вы пользовались Matlab, её работа должна быть вам знакома.

cv2.imshow('my_bike', my_bike)


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

▍ Сохранение изображений


Для сохранения результатов нашей работы с изображениями в библиотеке OpenCV существует функция cv2.imwrite ().

Вот пример её использования:

cv2.imwrite('bike.png', my_bike)


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

▍ Преобразование изображений


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

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

▍ Масштабирование


Попросту говоря, масштабирование — это не что иное, как изменение размеров изображения, его увеличение либо уменьшение. В библиотеке OpenCV для этого существует функция resize. У этой функции, в свою очередь, есть три метода: INTER_CUBIC, INTER_LINEAR и INTER_AREA. Давайте на примере конкретного кода разберём, как это всё работает. Пожалуйста, внимательно изучите код, комментарии к нему и описание ниже.

import cv2
import numpy as np
import matplotlib.pyplot as plt

image = cv2.imread('my_bike.jpg')

# Увеличиваем масштаб/расширяем в 2 раза по ширине и высоте
result_1 = cv2.resize(image, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)

# Уменьшаем масштаб/сжимаем в 2 раза по ширине и высоте
result_2 = cv2.resize(image, None, fx=2, fy=2, interpolation=cv2.INTER_AREA)

# Выводим на экран получившиеся изображения
plt.imshow(result_1)
plt.imshow(result_2)
plt.show()


Здесь в функции resize параметр fx определяет масштаб изменений по ширине, fy — по высоте, а параметр interpolation отвечает за сам способ изменений (то есть расширение или сжатие).

OpenCV cvtColor


Цвета, присутствующие в изображении, представлены цветовыми пространствами в OpenCV. Существует несколько цветовых пространств, каждое из которых имеет свою важность, например, RGB, CMYK и т. д. Всякий раз, когда возникает необходимость преобразовать изображение из одного цветового пространства в другое в OpenCV, мы используем функцию cvtColor (). Всего в OpenCV доступно более 150 кодов преобразования цветового пространства (cv2.COLOR_BGR2GRAY, cv2.COLOR_BGR2HSV и т. д.). Преобразование цветового пространства с помощью функции cvtColor () очень полезно для решения задач в области компьютерного зрения.

Синтаксис для определения функции cvtColor () в OpenCV следующий:

cvtColor(image, code)


Здесь image — это изображение, цветовое пространство которого должно быть преобразовано, а code — это код преобразования цветового пространства.

▍ Как работает функция cvtColor () в OpenCV


Функция cvtColor () в OpenCV принимает два параметра, а именно изображение и код. То есть изображение, цветовое пространство которого должно быть преобразовано в другое цветовое пространство, а также код преобразования цвета. Функция cvtColor () возвращает изображение с изменённым цветовым пространством.

▍ Обсудим примеры OpenCV cvtColor


Допустим, что мы пишем программу на OpenCV и языке программирования Python, с помощью которой мы собираемся изменить цветовое пространство изображения на другое цветовое пространство и отобразить полученное изображение с изменённым цветовым пространством в качестве вывода на экран.

Для примера будем использовать следующую картинку:

image

Код программы:

import cv2

# Считывание изображения, которое должно быть преобразовано 
# в цветовое пространство HSV
image1 = cv2.imread('test_img_color.png')

# Преобразование изображения в цветовое пространство HSV 
# с помощью функции cvtColor и сохранение полученного изображения
imageresult = cv2.cvtColor(image1, cv2.COLOR_BGR2HSV)

# Вывод результата 
cv2.imshow("Start image", image1)
cv2.imshow("End image", imageresult)
cv2.waitKey(0)


image

В приведённой выше программе мы импортируем модуль cv2. Затем мы читаем изображение, цветовое пространство которого необходимо преобразовать с помощью функции imread (). Далее используем функцию cvtColor (), указав код преобразования цветового пространства. Полученное изображение отображается как вывод на экране. Результат показан на снимке выше.

Выглядит круто! А теперь давайте поменяем цветовое пространство на серое. Для этого используем следующий код:

import cv2

# Считывание изображения, которое должно быть преобразовано
# в серое цветовое пространство
image1 = cv2.imread('test_img_color.png')

# Преобразование изображения в серое цветовое пространство
# с помощью функции cvtColor и сохранение полученного изображения
imageresult = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)

# Вывод результата 
cv2.imshow("Start image", image1)
cv2.imshow("End image", imageresult)
cv2.waitKey(0)


image

Алгоритм детектирования краёв Canny Edge Detector


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

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

Но что такое рёбра? Края — это особенности изображения, которые можно использовать для оценки и анализа структуры объектов на нём. Они представляют значительные локальные изменения, которые произошли в интенсивности изображения (то есть значение пикселя). Края обычно появляются на границе между двумя разными областями изображения.

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

Небольшая историческая справка: алгоритм детектора краёв Canny назван в честь его изобретателя Джона Ф. Кэнни, который изобрёл алгоритм в 1986 году. Детектор краёв Canny обычно принимает изображение в градациях серого в качестве входных данных и создаёт изображение, показывающее местоположение разрывов интенсивности в качестве выходных данных.


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

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

Затем алгоритм выполняет то, что называется «не максимальным подавлением», где он отслеживает вершину гребней, которые поднимаются от краёв, и устанавливает в ноль пиксели, которые находятся не на вершине гребня, в результате чего получается тонкая линия.

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

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

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

Таким образом, ширина гауссова ядра, используемого для сглаживания входного изображения, и пороговые значения t1 (верхний) и t2 (нижний), используемые трекером, являются параметрами, которые определяют эффект детектора краёв Canny.

▍ Немного кода для понимания реализации Canny Edge Detector с использованием OpenCV


В OpenCV есть функция canny () для применения алгоритма детектора краёв Canny к изображению. Следующий код показывает, как мы можем использовать OpenCV, чтобы найти края в нашем изображении:

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('test_img_color.png')
edges = cv2.Canny(img,25,255,L2gradient=False)
cv2.imshow('Start image', img)
plt.imshow(edges, cmap='gray')
plt.show()
cv2.waitKey(0)


Обратите внимание, что я передал следующее в качестве аргументов функции Canny ():

  • img — имя изображения,
  • lower threshold — 25,
  • upper threshold — 255,
  • L2gradient=False — это означает, что используется L1-норма. Если установлено значение True, будет использоваться L2-норма.


Затем для построения результатов используем библиотеку matplotlib и получаем следующий результат:

image

Более подробно про алгоритм можно почитать здесь (очень советую к прочтению) и здесь.

Возвращение к коду


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

image

image

На первой изображены геометрические фигуры разных цветов (нас будет интересовать самый тёмный объект, то есть чёрный). На второй же мы видим тестовую карту с цветовыми квадратами, разбитыми на сектора. На ней нас также будет интересовать самый тёмный объект, то есть чёрный.

Хорошо, на картинки мы полюбовались, а как же их загрузить?

Из пункта «Основные операции с изображениями» мы знаем, что для загрузки изображения нам нужно прописать команду cv2.imread. Давайте загрузим оба изображения и выведем их с помощью кода:

img_1 = cv2.imread('test_map_1.png')
img_2 = cv2.imread('test_map_2.png')

cv2.imshow('First image', img_1)
cv2.imshow('Second image', img_2)
# функция waitKey() позволяет нам не закрывать выведенные картинки сразу, а дожидаться нажатия любой кнопки, перед тем как всё закрыть 
cv2.waitKey(0)


Вот что нам выведет программа:

image

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

import cv2
import numpy as np

img = cv2.imread('test_map_1.png')

# Создаём копию изначального изображения
img_cont = img.copy()

# Переводим изначальное изображение img в серый канал (с этим методом
# мы познакомились выше) и сохраняем в переменной gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Производим изменение размерности в 4 раза (изменение размерности производится
# в пикселях) относительно изначальной картинки img и сохраняем полученное 
# изображение в переменной img_resize
img_resize = cv2.resize(img, (int(img.shape[1] / 4), int(img.shape[0] / 4)))

# Далее идёт большой блок кода, в котором мы создаём 
# алгоритм детектирования краёв Canny Edge Detector (с этим методом
# мы познакомились выше) 
canny_1 = 200
canny_2 = 225
canny = cv2.Canny(img, canny_1, canny_2)
contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
min_black = 255
cnt_black = []

for cnt in contours:
    c_area = cv2.contourArea(cnt) + 1e-7
    if cv2.contourArea(cnt) + 1e-7 > 500:
        cv2.drawContours(img_cont, [cnt], -1, 3)
        mask = np.zeros_like(gray)
        cv2.drawContours(mask, [cnt], -1, (255,255,255), -1)
        temp_mask = cv2.bitwise_and(gray, mask)
        temp_col = np.sum(temp_mask).real/(cv2.contourArea(cnt)+1e-7)
        if (temp_col < min_black) or (len(cnt_black) == 0):
            cnt_black = cnt
            min_black = temp_col

if len(cnt_black)!=0:
    cv2.drawContours(img_cont, [cnt_black], -1, (0,0,255), 3)


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

# Сохранение результатов работы нашей программы в папку

# Сохранение изображения с детектированными контурами
cv2.imwrite('img_contour_1_1.jpg', img_cont)
# Сохранение изображения в сером цветовом канале
cv2.imwrite('img_gray_channel_1_1.png', gray)
# Сохранение уменьшенного в 4 раза изображения 
cv2.imwrite('img_resize_1_1.png', img_resize)

# Вывод на экран изначального изображения
cv2.imshow('Basic image', img)
# Вывод на экран изображения с детектированными контурами
cv2.imshow('Contour image', img_cont)
# Вывод на экран изображения в сером цветовом канале
cv2.imshow('Gray channel image', gray)
# Вывод на экран уменьшенного в 4 раза изображения 
cv2.imshow('Resize image', img_resize)

# Режим ожидания нажатия кнопки
cv2.waitKey(0)


Это код выполняет преобразование первой картинки с именем «test_map_1». Чтобы преобразовать вторую, достаточно в строке img = cv2.imread ('test_map_1.png') поменять название файла с «test_map_1.png», на «test_map_2.png».

Теперь давайте посмотрим результаты работы нашей программы для «test_map_1.png».

Изначальное изображение:

image

Изображение в сером цветовом канале:

image

Уменьшенное в 4 раза изображение:

image

Изображение с детектированными контурами самого тёмного элемента:

image

То же самое давайте посмотрим для «test_map_2.png».

Изначальное изображение:

image

Изображение в сером цветовом канале:

image

Уменьшенное в 4 раза изображение:

image

Изображение с детектированными контурами самого тёмного элемента:

image

Приведу здесь весь код для тех, кому не хочется собирать его по крупицам:
import cv2
import numpy as np

img = cv2.imread('test_map_1.png')

img_cont = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_resize = cv2.resize(img, (int(img.shape[1] / 4), int(img.shape[0] / 4)))
canny_1 = 200
canny_2 = 225
canny = cv2.Canny(img, canny_1, canny_2)
contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
min_black = 255
cnt_black = []

for cnt in contours:
    c_area = cv2.contourArea(cnt) + 1e-7
    if cv2.contourArea(cnt) + 1e-7 > 500:
        cv2.drawContours(img_cont, [cnt], -1, 3)
        mask = np.zeros_like(gray)
        cv2.drawContours(mask, [cnt], -1, (255,255,255), -1)
        temp_mask = cv2.bitwise_and(gray, mask)
        temp_col = np.sum(temp_mask).real/(cv2.contourArea(cnt)+1e-7)
        if (temp_col < min_black) or (len(cnt_black) == 0):
            cnt_black = cnt
            min_black = temp_col

if len(cnt_black)!=0:
    cv2.drawContours(img_cont, [cnt_black], -1, (0,0,255), 3)
   
cv2.imwrite('img_basic_1_1.jpg', img)
cv2.imwrite('img_contour_1_1.jpg', img_cont)
cv2.imwrite('img_gray_channel_1_1.png', gray)
cv2.imwrite('img_resize_1_1.png', img_resize)

cv2.imshow('Basic image', img)
cv2.imshow('Contour image', img_cont)
cv2.imshow('Gray channel image', gray)
cv2.imshow('Resize image', img_resize)
cv2.waitKey(0)

Всё очень просто!


Все файлы для повторения кода из статьи, включая код и картинки, вы можете найти на моём GitHub.

А теперь вопрос к вам! Давайте поразмышляем, где можно применить данные технологии? Как, преобразовав задачу и усовершенствовав код, все эти знания можно использовать в реальных проектах? Жду ваших комментариев!

RUVDS | Community в telegram и уютный чат

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru