Здесь должна быть шутка
Сегодня я хочу представить вам третью статью из серии «Нейронные сети для начинающих». Мы научимся обрабатывать изображения и сохранять результаты в отдельные файлы. Вот небольшой список задач, которые будут разобраны ниже:
- Преобразование изображения в оттенки серого.
- Уменьшение размерности изображения (в пикселях) в 4 раза.
- Нахождение и выделение (рамкой) самого тёмного объекта на изображении.
Для работы нам понадобится любой редактор кода. Можете выбрать ваш любимый, я же буду использовать PyCharm. Подробнее про работу с PyCharm вы можете почитать на официальной странице редактора или же посмотреть в кратком гайде.
Перед началом работы советую вам прочитать предыдущие статьи для более полного понимания материала:
- #1 Нейронные сети для начинающих. Решение задачи классификации Ирисов Фишера
- #2 Нейронные сети для начинающих. NumPy. MatplotLib. Операции с изображениями в OpenCV
Код
Итак, приступим. Для начала нам потребуется библиотека OpenCV, которую мы разбирали в предыдущей статье в пункте «Поверхностное знакомство с библиотекой OpenCV». Давайте установим её. Для этого введём в терминале команду:
pip install opencv-python
После этого в нашем файле с расширением .py, мы можем импортировать библиотеку cv2 (краткое название библиотеки OpenCV). Давайте пропишем это:
import cv2
import numpy as np
Также мы импортировали библиотеку NumPy, которая предназначена для математических вычислений. Подробнее о ней вы можете прочитать в предыдущей статье, в пункте «Знакомство с библиотекой NumPy».
Теперь давайте отойдём от кода и вспомним немного теории.
Основные операции с изображениями
Установив OpenCV, пощупаем основные функциональные возможности данной библиотеки.
▍ Вывод изображения на экран
Процесс вывода изображения на экран состоит из двух шагов. Сначала мы должны загрузить изображение, а после вывести его на экран. Эти операции выполняются последовательно, и для каждой из них предназначена отдельная функция.
Для вывода изображения на экран нам необходимо задать две вещи:
- Путь к файлу, в котором содержится изображение (подойдёт как относительный, так и абсолютный путь).
- Режим чтения файла (только чтение, запись и т. д.).
Функция, при помощи которой мы считываем изображение, называется cv2.imread(). У неё есть три режима работы:
- IMREAD_GRAYSCALE. Как видно из названия, он преобразует изображение в чёрно-белое с оттенками серого.
- IMREAD_UNCHANGED, который загружает изображение без обрезания альфа-канала.
- 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, с помощью которой мы собираемся изменить цветовое пространство изображения на другое цветовое пространство и отобразить полученное изображение с изменённым цветовым пространством в качестве вывода на экран.
Для примера будем использовать следующую картинку:
Код программы:
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)
В приведённой выше программе мы импортируем модуль 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)
Алгоритм детектирования краёв 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 и получаем следующий результат:
Более подробно про алгоритм можно почитать здесь (очень советую к прочтению) и здесь.
Возвращение к коду
Надеюсь, вы не забыли, что мы учимся обрабатывать изображения и сохранять результаты в отдельные файлы, поэтому давайте продолжим. После импорта необходимых библиотек нам нужно считать нашу тестовую картинку. У меня их будет две:
На первой изображены геометрические фигуры разных цветов (нас будет интересовать самый тёмный объект, то есть чёрный). На второй же мы видим тестовую карту с цветовыми квадратами, разбитыми на сектора. На ней нас также будет интересовать самый тёмный объект, то есть чёрный.
Хорошо, на картинки мы полюбовались, а как же их загрузить?
Из пункта «Основные операции с изображениями» мы знаем, что для загрузки изображения нам нужно прописать команду 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)
Вот что нам выведет программа:
Далее мы выполняем следующий код (внимательно прочитайте комментарии к нему):
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».
Изначальное изображение:
Изображение в сером цветовом канале:
Уменьшенное в 4 раза изображение:
Изображение с детектированными контурами самого тёмного элемента:
То же самое давайте посмотрим для «test_map_2.png».
Изначальное изображение:
Изображение в сером цветовом канале:
Уменьшенное в 4 раза изображение:
Изображение с детектированными контурами самого тёмного элемента:
Приведу здесь весь код для тех, кому не хочется собирать его по крупицам:
Всё очень просто!
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.
А теперь вопрос к вам! Давайте поразмышляем, где можно применить данные технологии? Как, преобразовав задачу и усовершенствовав код, все эти знания можно использовать в реальных проектах? Жду ваших комментариев!
Telegram-канал с полезностями и уютный чат