Сегментация изображений с использованием цветовых пространств в OpenCV + Python

Оглавление

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

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

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

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

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

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

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

Color RGB value
Red 255, 0, 0
Orange 255, 128, 0
Pink 255, 153, 255

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

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

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

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

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

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

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

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

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

Если вы не знакомы с NumPy или Matplotlib, вы можете прочитать о них в официальном руководстве по NumPy и отличной статье Брэда Соломона о Matplotlib.

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

Сначала вам нужно будет настроить свое окружение. В этой статье предполагается, что в вашей системе установлен Python 3.x. Обратите внимание, что хотя текущая версия OpenCV - 3.x, имя пакета для импорта по-прежнему cv2:

>>> import cv2

Если вы не установили OpenCV на свой компьютер, импорт не удастся, пока вы не сделаете это. Вы можете найти удобный учебник по установке на различные операционные системы здесь, а также собственное руководство OpenCV по установке. После успешного импортирования OpenCV вы можете просмотреть все преобразования цветового пространства, которые предоставляет 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 для некоторых манипуляций с изображениями. Если у вас еще не установлены Matplotlib или NumPy, вам нужно pip3 install matplotlib и pip3 install numpy, прежде чем приступать к импорту:

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

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

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

OpenCV uses BGR by default

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

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

BGR to RGB

Теперь Немо гораздо больше похож на себя.

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

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

Для построения графика вам понадобится еще несколько библиотек Matplotlib:

>>> from mpl_toolkits.mplot3d import Axes3D
>>> from matplotlib import cm
>>> from matplotlib import colors

Эти библиотеки предоставляют функциональные возможности, необходимые для построения сюжета. Вы хотите поместить каждый пиксель в свое местоположение на основе его компонентов и раскрасить его по цвету. OpenCV split() здесь очень удобен: он разбивает изображение на составляющие его каналы. Эти несколько строк кода разделяют изображение и настраивают 3D-сюжет:

>>> r, g, b = cv2.split(nemo)
>>> fig = plt.figure()
>>> axis = fig.add_subplot(1, 1, 1, projection="3d")

Теперь, когда вы настроили сюжет, необходимо задать цвета пикселей. Чтобы окрасить каждый пиксель в соответствии с его истинным цветом, необходимо немного изменить форму и нормализацию. Выглядит это сложно, но по сути вам нужно, чтобы цвета, соответствующие каждому пикселю изображения, были сплющены в список и нормализованы, чтобы их можно было передать в параметр facecolors в Matplotlib scatter().

Нормализация означает лишь сжатие диапазона цветов от 0-255 до 0-1, как это требуется для параметра facecolors. И наконец, facecolors хочет получить список, а не массив NumPy:

>>> pixel_colors = nemo.reshape((np.shape(nemo)[0]*np.shape(nemo)[1], 3))
>>> norm = colors.Normalize(vmin=-1.,vmax=1.)
>>> norm.autoscale(pixel_colors)
>>> pixel_colors = norm(pixel_colors).tolist()

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

>>> axis.scatter(r.flatten(), g.flatten(), b.flatten(), facecolors=pixel_colors, marker=".")
>>> axis.set_xlabel("Red")
>>> axis.set_ylabel("Green")
>>> axis.set_zlabel("Blue")
>>> plt.show()

Вот цветная диаграмма рассеяния для изображения Nemo в RGB:

3D scatter plot of image in RGB

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

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

Мы видели Немо в пространстве RGB, теперь давайте посмотрим на него в пространстве HSV и сравним.

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

HSV color space cylinder

Изображение: Википедия

Чтобы преобразовать изображение из RGB в HSV, вы можете использовать cvtColor():

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

Теперь hsv_nemo сохраняет представление Nemo в HSV.

Код для отображения изображения в HSV такой же, как и для RGB. Обратите внимание, что вы используете ту же переменную pixel_colors для раскраски пикселей, поскольку Matplotlib ожидает, что значения будут в RGB:

>>> h, s, v = cv2.split(hsv_nemo)
>>> fig = plt.figure()
>>> axis = fig.add_subplot(1, 1, 1, projection="3d")

>>> axis.scatter(h.flatten(), s.flatten(), v.flatten(), facecolors=pixel_colors, marker=".")
>>> axis.set_xlabel("Hue")
>>> axis.set_ylabel("Saturation")
>>> axis.set_zlabel("Value")
>>> plt.show()

3D scatter plot of image in HSV

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

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

Давайте порогуем Немо, основываясь на простом ассортименте апельсинов. Вы можете выбрать диапазон, ориентируясь на график выше или используя приложение для подбора цвета онлайн, например, вот это RGB to HSV tool. В качестве образцов здесь выбраны светло-оранжевый и более темный оранжевый, почти красный:

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

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

>>> from matplotlib.colors import hsv_to_rgb

Затем постройте маленькие 10x10x3 квадраты, заполненные соответствующим цветом. Вы можете использовать NumPy, чтобы легко заполнить квадраты цветом:

>>> lo_square = np.full((10, 10, 3), light_orange, dtype=np.uint8) / 255.0
>>> do_square = np.full((10, 10, 3), dark_orange, dtype=np.uint8) / 255.0

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

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(hsv_to_rgb(do_square))
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(hsv_to_rgb(lo_square))
>>> plt.show()

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

The light and dark orange range

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

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

Mask and original with mask imposed

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

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

После того как вы задали цветовой диапазон, вы можете просмотреть выбранные цвета:

White range

 

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

>>> lw_square = np.full((10, 10, 3), light_white, dtype=np.uint8) / 255.0
>>> dw_square = np.full((10, 10, 3), dark_white, dtype=np.uint8) / 255.0

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(hsv_to_rgb(lw_square))
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(hsv_to_rgb(dw_square))
>>> plt.show()

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

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

Mask and original for white stripes

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

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

Final combined mask and original

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

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

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

Final segmented Nemo with blur

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

Для интереса давайте посмотрим, насколько хорошо эта техника сегментации подходит для других изображений рыб-клоунов. В хранилище имеется подборка из шести изображений рыб-клоунов от Google, лицензированных для публичного использования. Изображения находятся в подкаталоге и индексируются nemoi.jpg, где i - индекс от 0-5.

Сначала загрузите всех родственников Немо в список:

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)

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

Вот функция segment_fish():

def segment_fish(image):
    ''' Attempts to segment the clownfish out of the provided image '''

    # Convert the image into HSV
    hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)

    # Set the orange range
    light_orange = (1, 190, 200)
    dark_orange = (18, 255, 255)

    # Apply the orange mask 
    mask = cv2.inRange(hsv_image, light_orange, dark_orange)

    # Set a white range
    light_white = (0, 0, 200)
    dark_white = (145, 60, 255)

    # Apply the white mask
    mask_white = cv2.inRange(hsv_image, light_white, dark_white)

    # Combine the two masks
    final_mask = mask + mask_white
    result = cv2.bitwise_and(image, image, mask=final_mask)

    # Clean up the segmentation using a blur
    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()

Nemo's friend 1

У рыбы-клоуна на переднем плане оранжевые оттенки темнее, чем у нас.

Nemo's friend 2

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

Nemo's friend 3 Nemo's friend 4 Nemo's friend 5

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

Заключение

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

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

Вернуться на верх