Сегментация изображения с OpenCV и Python

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

Из этого материала вы узнаете, как просто сегментировать объект из изображения на основе цвета с помощью Python и OpenCV. Популярная библиотека компьютерного зрения, написанная на C/C++ и связанная с Python, OpenCV, предлагает простые способы манипулирования цветовыми пространствами.

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

Что такое цветовые пространства?

В наиболее распространенном цветовом пространстве, RGB (Красный Зеленый Синий) цвета представлены с точки зрения их составляющих красного, зеленого и синего. Если говорить в более техническом плане, RGB описывает цвет как кортеж трех компонентов. Каждый из них может быть значением от 0 до 255, где (0, 0, 0) представляет черный цвет, а (255, 255, 255) — белый.

RGB считается «аддитивным» цветовым пространством, где цвета являются произведением красного, синего и зеленого света на черный фон.

Вот еще несколько примеров цветов RGB:

ЦветЗначение RGB
Красный255, 0, 0
Оранжевый255, 128, 0
Розовый255, 153, 255

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

В издательском деле используется CMYK, потому что оно описывает комбинации, которые необходимы для получения цвета на белом фоне. Если в RGB кортеж 0 — это черный, то в CMYK — это белый. Таким образом принтеры содержат картриджи с голубым, пурпурным, желтым и черным цветами.

В определенных областях медицины сканируются стеклянные слайды с образцами окрашенной ткани. Они сохраняются в виде изображений. Такие анализируются в пространстве HED — представлении насыщенности типов краски — гематоксилина, эозина и 3,3’-Диаминобензидина.

HSV и HSL — это описания оттенка, насыщенности и яркости. Они подходят для определения контрастности на изображениях. Такие пространства часто используются для выбора цвета в ПО для веб-дизайна.

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

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

Простая сегментация с помощью цветовых пространств

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

Ключевые требуемые библиотеки: передовой пакет для научных вычислений NumPy, инструмент построения графиков Matplotlib и, конечно, OpenCV. В этом примере используются версии OpenCV 3.2.0, NumPy 1.12.1 и Matplotlib 2.0.2. Но небольшие отличия в версиях не должны сильно повлиять на понимание самой концепции работы.

Цветовые пространства и чтение изображений в OpenCV

В первую очередь необходимо настроить пространство. Python 3.x уже должен быть установлен в вашей системе. Обратите внимание, что несмотря на использование OpenCV 3.x, импортировать все равно необходимо cv2:

import cv2

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

flags = [i for i in dir(cv2) if i.startswith('COLOR_')]

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

>>> len(flags)
258
>>> flags[40]
'COLOR_BGR2RGB'

Первые символы после COLOR обозначают оригинальное цветовое пространство, а символы после 2 — целевое. Этот флаг представляет преобразование из BGR (Blue, Green, Red) в RGB. Эти пространства очень похожи. У них лишь отличаются значения первого и последнего каналов.

Для просмотра изображений потребуется matplotlib.pyplot, а также NumPy. Если они не установлены, то используйте команды pip3 install matplotlib и pip3 install numpy перед импортами:

Архив с изображениями

import matplotlib.pyplot as plt
import numpy as np

Теперь можно загружать и изучать изображение. Обратите внимание, что если вы работаете из командной строки или терминала, изображения появятся во всплывающем окне. Если это Jupyter Notebook или что-то другое, то они будут отображаться ниже. Вне зависимости от настройки вы увидите изображение, сгенерированное командой show():

nemo = cv2.imread('./images/nemo0.jpg')
plt.imshow(nemo)
plt.show()

изображение, сгенерированное show()

А это точно Немо… или Дори? Похоже, синий и красный каналы перемешались. Дело в том, что цветовым пространством по умолчанию в OpenCV является BGR. Чтобы исправить это, используйте cvtColor(image, flag) и рассмотренный выше флаг:

nemo = cv2.cvtColor(nemo, cv2.COLOR_BGR2RGB)
plt.imshow(nemo)
plt.show()

изображение, сгенерированное show()

Теперь Немо похож на себя.

Визуализация Немо в цветовом пространстве RGB

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

Вот график цветового рассеивания изображения Немо в RGB:

график цветового рассеивания изображения

Здесь можно видеть, что оранжевые элементы изображения распределены по всем значениям красного, зеленого и синего. Таким образом сегментировать Немо в пространстве RGB на основе значений RGB — совсем не простая задача.

Визуализация Немо в цветовом пространстве HSV

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


Как уже упоминалось, HSV — это Hue, Saturation и Value (оттенок, насыщенность и яркость). Это цилиндрическое цветовое пространство. Цвета, или оттенки, меняются при движении по кругу цилиндра. Вертикальная ось отвечает за яркость: от темного (0 в нижней части) до светлого сверху. Третья ось, насыщенность, определяет тени оттенков при движении от центра к краю вдоль радиуса цилиндра (от менее к более насыщенному)

Для конвертации из RGB в HSV можно использовать cvtColor():

hsv_nemo = cv2.cvtColor(nemo, cv2.COLOR_RGB2HSV)

Сейчас hsv_nemo хранит представление Немо в HSV. С помощью уже знакомой техники можно посмотреть на график изображения в HSV:

Немо в пространстве HSV

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

Выбор диапазона

Попробуем разбить Немо на основе простого диапазона значений оранжевого цвета. Можно выбрать диапазон, внимательно рассмотрев график или воспользоваться онлайн-инструментом, например, RGB в HSV. Здесь выбраны образцы яркого и темного оранжевого цветов, близких к красному.

light_orange = (1, 190, 200)
dark_orange = (18, 255, 255)

Когда необходимый цветовой диапазон выбран, можно использовать cv2.inRange(), чтобы разбить Немо. inRange() принимает три параметра: изображение, нижнее и верхнее значения диапазона. Она возвращает бинарную маску (ndarray из единиц и нулей) размером с оригинальное изображение. Единицы обозначают значения в пределах диапазона, а нули — вне его:

mask = cv2.inRange(hsv_nemo, light_orange, dark_orange)

Чтобы наложить маску поверх оригинального изображения, используется cv2.bitwise_and(). Она сохраняет каждый пиксель изображения, если соответствующее значение маски равно 1:

result = cv2.bitwise_and(nemo, nemo, mask=mask)

Чтобы увидеть, что именно произошло, стоит рассмотреть саму маску и оригинальное изображение с ее наложением:

plt.subplot(1, 2, 1)
plt.imshow(mask, cmap="gray")
plt.subplot(1, 2, 2)
plt.imshow(result)
plt.show()

маска поверх оригинального изображения

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

light_white = (0, 0, 200)
dark_white = (145, 60, 255)

После определения диапазона необходимо взглянуть на выбранные цвета:

выбранные цвета

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

mask_white = cv2.inRange(hsv_nemo, light_white, dark_white)
result_white = cv2.bitwise_and(nemo, nemo, mask=mask_white)

plt.subplot(1, 2, 1)
plt.imshow(mask_white, cmap="gray")
plt.subplot(1, 2, 2)
plt.imshow(result_white)
plt.show()

маска поверх оригинального изображения

Неплохо. Теперь можно объединить маски. В таком случае единицы будут в тех местах, где значения — белый или оранжевый, что и требуется для получения результата. Соединим маски и посмотрим на результат:

final_mask = mask + mask_white

final_result = cv2.bitwise_and(nemo, nemo, mask=final_mask)
plt.subplot(1, 2, 1)
plt.imshow(final_mask, cmap="gray")
plt.subplot(1, 2, 2)
plt.imshow(final_result)
plt.show()

сегментация Немо в пространстве HSV


По сути, грубая сегментация Немо в пространстве HSV уже готова. Все еще есть «блуждающие» пиксели вдоль границы сегментации, но их можно исправить с помощью гауссовского размытия.

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

blur = cv2.GaussianBlur(final_result, (7, 7), 0)
plt.imshow(blur)
plt.show()

Гауссовское размытие Немо

Обобщает ли эта сегментация родственников Немо?

Ради интереса посмотрим, как эта сегментация обобщает все изображения рыб-клоунов. Имеется набор картинок из Google, лицензированных для общего использования.

В первую очередь загрузим их все в список:

path = "./images/nemo"

nemos_friends = []
for i in range(6):
   friend = cv2.cvtColor(cv2.imread(path + str(i) + ".jpg"), cv2.COLOR_BGR2RGB)
   nemos_friends.append(friend)

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

def segment_fish(image):
    ''' Cегментация рыбы-клоуна из предоставленного изображения '''

    # Конвертация изображения в HSV
    hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)

    # Установка оранжевого диапазона
    light_orange = (1, 190, 200)
    dark_orange = (18, 255, 255)

    # Применение оранжевой маски
    mask = cv2.inRange(hsv_image, light_orange, dark_orange)

    # Установка белого диапазона
    light_white = (0, 0, 200)
    dark_white = (145, 60, 255)

    # Применение белой маски
    mask_white = cv2.inRange(hsv_image, light_white, dark_white)

    # Объединение двух масок
    final_mask = mask + mask_white
    result = cv2.bitwise_and(image, image, mask=final_mask)

    # Сглаживание сегментации с помощью размытия
    blur = cv2.GaussianBlur(result, (7, 7), 0)
    return blur

Таким образом сегментация превращается в одну строку:

results = [segment_fish(friend) for friend in nemos_friends]

Посмотрим на результаты в цикле:

for i in range(1, 6):
    plt.subplot(1, 2, 1)
    plt.imshow(nemos_friends[i])
    plt.subplot(1, 2, 2)
    plt.imshow(results[i])
    plt.show()

Сегментации рыбы, пример 1

У рыбы на переднем плане оранжевые оттенки темнее выбранного диапазона.

Сегментации рыбы, пример 2

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

Сегментации рыбы, пример 3
Сегментации рыбы, пример 4
Сегментации рыбы, пример 5

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

Выводы

Из этого руководства вы увидели, какие бывают цветовые пространства, как изображение распределяется с RGB и HSV, и как использовать OpenCV для конвертации между пространствами и сегментирования диапазонов.

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

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript