Как использовать распределение Гаусса для сегментации изображений

Сегментация изображений по цвету - чрезвычайно полезный навык. Ранее я писал статью о том, как это сделать с помощью цветовых пространств RGB и HSV. Однако еще один эффективный способ сегментировать изображения на основе их цвета - использовать RG Chromaticity и Gaussian Distribution. В этой статье мы обсудим, как это сделать.

Давай начнем!

Как всегда, сначала импортируйте необходимые библиотеки Python.

import numpy as np
import matplotlib.pyplot as plt
from skimage.io import imread, imshow
from numpy import ndarray
from matplotlib.patches import Rectangle
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib import colors

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

budapest = imread('budapest.png')
plt.figure(num=None, figsize=(8, 6), dpi=80)
imshow(budapest);

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

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

def rgb_splitter(image):
    rgb_list = ['Reds','Greens','Blues']
    fig, ax = plt.subplots(1, 3, figsize=(17,7), sharey = True)
    for i in range(3):
        ax[i].imshow(image[:,:,i], cmap = rgb_list[i])
        ax[i].set_title(rgb_list[i], fontsize = 22)
        ax[i].axis('off')
    fig.tight_layout()
rgb_splitter(budapest)

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

Теперь давайте обсудим RG Chromaticity.

RG Chromaticity - это просто двумерная нормализованная версия цветового пространства RGB. Мы можем преобразовать наше изображение в это цветовое пространство, разделив значение каждого цветового канала на суммарное значение этого конкретного пикселя. С точки зрения программирования это будет выглядеть так, как показано ниже.

budapest_r = budapest[:,:,0] /budapest.sum(axis=2)
budapest_g = budapest[:,:,1] /budapest.sum(axis=2)

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

one_matrix = np.ones_like(float,shape=budapest_r.shape)
budapest_b = one_matrix- (budapest_r +budapest_g)

Теперь вы можете спросить себя: «Как выглядит это изображение?». Что ж, давайте посмотрим, что случилось с изображением.

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

Конечно, можем ли мы визуализировать изображение или нет, не имеет значения, истинная сила RG Chromaticity заключается в возможности представить все значения пикселей на 2-осевом графике (помните, что синий канал может быть получен).

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

def RG_Chroma_plotter(red,green):
    p_color = [(r, g, 1-r-g) for r,g in 
               zip(red.flatten(),green.flatten())]
    norm = colors.Normalize(vmin=0,vmax=1.)
    norm.autoscale(p_color)
    p_color = norm(p_color).tolist()
    fig = plt.figure(figsize=(10, 7), dpi=100)
    ax = fig.add_subplot(111)
    ax.scatter(red.flatten(), 
                green.flatten(), 
                c = p_color, alpha = 0.40)
    ax.set_xlabel('Red Channel', fontsize = 20)
    ax.set_ylabel('Green Channel', fontsize = 20)
    ax.set_xlim([0, 1])
    ax.set_ylim([0, 1])
    plt.show()
RG_Chroma_plotter(budapest_r,budapest_g)

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

что нам нужно сделать в первую очередь, это выбрать патч на изображении.

patch = budapest[500:510,50:60]
imshow(patch);

Посмотрим, где находится наш патч на графике цветности RG.

patch_r = patch[:,:,0] /patch.sum(axis=2)
patch_g = patch[:,:,1] /patch.sum(axis=2)
RG_Chroma_plotter(patch_r,patch_g)

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

Ниже представлена ​​полезная функция, которая делает это за нас.

def gaussian(p,mean,std):
    return np.exp(-(p-mean)**2/(2*std**2))*(1/(std*((2*np.pi)**0.5)))
def rg_chroma_patch(image, patch_coor, mean = 1, std = 1):
patch = image[patch_coor[0]:patch_coor[1],
                  patch_coor[2]:patch_coor[3]]
    
    image_r = image[:,:,0] /image.sum(axis=2)
    image_g = image[:,:,1] /image.sum(axis=2)
    
    patch_r = patch[:,:,0] / patch.sum(axis=2)
    patch_g = patch[:,:,1] / patch.sum(axis=2)
    
    std_patch_r = np.std(patch_r.flatten())
    mean_patch_r = np.mean(patch_r.flatten())
    std_patch_g = np.std(patch_g.flatten())
    mean_patch_g = np.mean(patch_g.flatten())
    masked_image_r = gaussian(image_r, mean_patch_r, std_patch_r)
    masked_image_g = gaussian(image_g, mean_patch_g, std_patch_g)
    final_mask = masked_image_r * masked_image_g
    fig, ax = plt.subplots(1,2, figsize=(15,7))
    ax[0].imshow(image)
    ax[0].add_patch(Rectangle((patch_coor[2], patch_coor[0]), 
                               patch_coor[1] - patch_coor[0], 
                               patch_coor[3] - patch_coor[2], 
                               linewidth=2,
                               edgecolor='b', facecolor='none'));
    ax[0].set_title('Original Image with Patch', fontsize = 22)
    ax[0].set_axis_off()
    
    #clean the mask using area_opening
    ax[1].imshow(final_mask, cmap = 'hot');
    ax[1].set_title('Mask', fontsize = 22)
    ax[1].set_axis_off()
    fig.tight_layout()
    
    return final_mask
final_mask = rg_chroma_patch(budapest, [500,510,50,60])

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

binarized_mask = final_mask > final_mask.mean()
plt.figure(num=None, figsize=(8, 6), dpi=80)
imshow(binarized_mask)

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

def apply_mask(image,mask):
    yuv_image = rgb2yuv(image)
    yuv_image[:,:,0] = yuv_image[:,:,0] * mask 
    
    masked_image = yuv2rgb(yuv_image)
    
    fig, ax = plt.subplots(1,2, figsize=(15,7))
    ax[0].imshow(image)
    ax[0].set_title('Original Image', fontsize = 22)
    ax[0].set_axis_off()
    
    ax[1].imshow(masked_image);
    ax[1].set_title('Masked Image', fontsize = 22)
    ax[1].set_axis_off()
    fig.tight_layout()

Мы видим, что маска отлично справляется со своей задачей. Все, кроме поля, почернело. Обратите внимание, как мы использовали цветовое пространство Y’UV, потому что цветовое пространство Y’UV имеет канал, предназначенный для яркости.

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

singapore = imread('singapore_street.png')
plt.figure(num=None, figsize=(8, 6), dpi=80)

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

singapore_r = singapore[:,:,0] /singapore.sum(axis=2)
singapore_g = singapore[:,:,1] /singapore.sum(axis=2)
RG_Chroma_plotter(singapore_r,singapore_g)

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

Давайте теперь попробуем выбрать патч и получить соответствующую маску.

final_mask_singapore = rg_chroma_patch(singapore, [125,150,290,310])

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

binarized_mask_singapore = final_mask_singapore >      
                           final_mask_singapore.mean()
apply_mask(singapore,binarized_mask_singapore)

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

В заключение

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