Привет, Хабр! В предыдущей части мы рассматривали базовые методы цифровой обработки изображений для задачи сегментации спутникового снимка.

В этой статье рассмотрим ещё парочку методов решения этой задачи, всё ещё «классических», то есть без применения машинного обучения или нейросетей. Помогут нам во всём разобраться, как и в прошлый раз, язык программирования Julia и среда технических расчётов Engee!

Постановка задачи (для тех, кто прогулял первую лекцию)

У нас есть «тестовый сигнал», то есть небольшой кусочек спутниковой карты района Москвы, а конкретнее – парка Сокольники и прилегающих жилых районов.

Рисунок 1. Парк Сокольники.
Рисунок 1. Парк Сокольники.

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

У нас пример ознакомительный и достаточно простой, где мы собираемся отделять объекты на условном "переднем плане" (foreground) от объектов условного "фона" (background). Результатом наших трудов должна стать бинарная маска - матрица нулей и единиц, размером с исходное изображение, где элементы 1 соответствуют интересующим нас объектам, а элементы 0 - фону. Не так важно, что мы будем принимать за 1, а что за 0, лес или город, главное, чтобы мы правильно их разграничивали.

Какие же алгоритмы мы рассмотрим в этот раз? В первой части рассматривались:

·         Изменение контраста

·         Бинаризация

·         Морфологические операции

·         Линейные и нелинейные 2-D фильтры

Но очень мало внимания уделялось цветовой составляющей, а ведь снимок-то у нас цветной, и, казалось бы, самым логичным было бы отделять зелёные насаждения от серо-бело-чёрного города. Сперва копнём в этом направлении, а затем посмотрим, как цветовая информация может объединяться с «текстурой» областей изображения (например, границами, выделенными фильтром Собеля) в комплексном алгоритме сегментации, таком как Seeded Region Growing (SRG).

Кратко (прям совсем) о применяемых алгоритмах

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

Рисунок 2. Цветовые пространства.
Рисунок 2. Цветовые пространства.
  • RGB - простое, но чувствительно к освещению.

  • HSV/HSL - отделяет цвет (Hue) от яркости (Value/Lightness), удобно для выделения объектов.

  • Lab - лучше учитывает человеческое восприятие цвета.

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

Цветовое расстояние — это числовая мера различия между двумя цветами в заданном цветовом пространстве (например, RGB, HSV, Lab). Оно используется для сравнения цветов, кластеризации, сегментации изображений и поиска схожих оттенков.

Евклидово расстояние в RGB вычисляется как геометрическое расстояние между точками в RGB-пространстве:

d = \sqrt{(R_1 - R_2)^2 + (G_1 - G_2)^2 + (B_1 - B_2)^2}

Определённый недостаток - RGB не учитывает восприятие цвета человеком (разные комбинации могут давать одинаковое расстояние, но визуально отличаться).

Рисунок 3. Куб RGB.
Рисунок 3. Куб RGB.

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

От цвета снова перейдём к «текстуре». Продолжим знакомится с нелинейными фильтрами.

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

Фильтр использует две матрицы 3×3:

  • Горизонтальное ядро (Gₓ) — выделяет вертикальные границы:

    [ -1  0  1 ]
    [ -2  0  2 ]
    [ -1  0  1 ]
  • Вертикальное ядро (Gᵧ) — выделяет горизонтальные границы:

    [ -1 -2 -1 ]
    [  0  0  0 ]
    [  1  2  1 ]
  1. Изображение обрабатывается обоими ядрами отдельно.

  2. Для каждого пикселя вычисляются градиенты по X (Gₓ) и Y (Gᵧ).

  3. Результирующая оценка границы в точке (x, y):

G = \sqrt{G_x^2 + G_y^2}\   ​
Рисунок 4. Результат применения фильтра Собеля.
Рисунок 4. Результат применения фильтра Собеля.

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

Основные принципы работы:

  • Выбор начальных точек (seeds) Пользователь или автоматический алгоритм выбирает начальные точки (пиксели), которые принадлежат интересующим объектам или регионам.

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

  • Итеративное расширение регионов

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

Преимущества:

  • Простота реализации.

  • Хорошо работает для изображений с чёткими границами и однородными регионами.

  • Позволяет контролировать процесс через выбор начальных точек.

Недостатки:

  • Зависимость от выбора начальных точек.

  • Чувствительность к шуму и неоднородностям.

  • Может сливать близкие регионы, если критерий схожести слишком слабый.

Существуют алгоритмы сегментации, не требующие начальных точек, например алгоритм «водораздела» (watershed). Попробуем рассмотреть его в последующих публикациях.

Подключаемые пакеты Julia для обработки изображений в Engee

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

  • Images.jl — аналог OpenCV + scikit-image, но с более удобным синтаксисом.

  • ImageSegmentation.jl — встроенные алгоритмы:

    • Водораздел (Watershed)

    • Seeded Region Growing

    • Графовые методы (Felzenszwalb, SLIC)

    • Адаптивные пороги

  • Clustering.jl — эффективные методы кластеризации (k-means, DBSCAN).

А ещё удобное комбинировать классические методы и ML: сегментация через k-means → уточнение глубинными сетями (Flux.jl), графовые алгоритмы + CRF (условные случайные поля).

Делаем вывод, что применяемый язык решает, когда требуются:

  • Большие данные (медицинские/спутниковые изображения).

  • Сложные алгоритмы (например, водораздел + кластеризация).

  • Гибкость (хочется свой метод, а не только OpenCV).

  • Скорость (Python тормозит, а C++ сложно).

Наслаждаться всеми преимуществами Julia мы традиционно будем в среде технических расчётов и динамического моделирования Engee, единственном российском аналоге MATLAB/Simulink для самого широкого спектра инженерных задач:

Рисунок 5. Интерфейс среды расчётов Engee.
Рисунок 5. Интерфейс среды расчётов Engee.

Регистрируйтесь по ссылке и получайте бесплатный доступ прямо сейчас – это позволит вам самим интерактивно поизучать описываемые в статьях примеры. Весь код тут:

Сегментация спутникового снимка, часть 1

Сегментация спутникового снимка, часть 2

Переходим в основной скрипт

Подключим необходимые библиотеки Julia для фильтрации, морфологии и сегментации:

using Images, ImageShow, ImageContrastAdjustment, ImageBinarization, ImageMorphology, ImageFiltering, ImageSegmentation

А вот и наше исходное изображение (то же самое, что и в первой части):

I = load("$(@__DIR__)/map_small.jpg")
Рисунок 6. Исходное цветное изображение.
Рисунок 6. Исходное цветное изображение.

Цветовая сегментация

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

(h, w) = size(I);
CV = channelview(I);
[ RGB.(CV[1,:,:], 0.0, 0.0) RGB.(0.0, CV[2,:,:], 0.0) RGB.(0.0, 0.0, CV[3,:,:]) ]
Рисунок 7. Цветовые каналы изображения.
Рисунок 7. Цветовые каналы изображения.

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

midh = Int(round(h/2));
midw = Int(round(w/2));
midpixel = CV[:,midh, midw]

Теперь, используя стандартные логические операции сравнения, бинаризуем каждый из каналов. В красном канале логической единице будут соответствовать пиксели с интенсивностью от 0.15 до 0.25, в зелёном - от 0.2 до 0.3, а в синем - от 0.05 до 0.15.

BIN_RED = (CV[1,:,:] .> 0.15) .& (CV[1,:,:] .< 0.25) ;
BIN_GREEN = (CV[2,:,:] .> 0.2) .& (CV[2,:,:] .< 0.3);
BIN_BLUE = (CV[3,:,:] .> 0.05) .& (CV[3,:,:] .< 0.15);
Рисунок 8. Бинаризация каналов пороговыми значениями.
Рисунок 8. Бинаризация каналов пороговыми значениями.

А теперь объединим три бинарные маски в одну операцией логического "И" - теперь белый пиксель будет только там, где в исходном цветном изображении все три проверяемых условия (диапазона) выполняются:

BIN = BIN_RED .& BIN_GREEN .& BIN_BLUE;
Рисунок 9. Объединённая бинаризация по трём каналам.
Рисунок 9. Объединённая бинаризация по трём каналам.

Сделать из этого пёстрого набора пикселей "ровную" маску нам вновь поможет морфология. В этот раз мы возьмём небольшой структурный элемент (7х7 пикселей) в форме ромба:

se = strel_diamond((7,7))

И оценим результат операции морфологического закрытия:

closeBW = closing(BIN,se);
Рисунок 10. "Промежуточная" маска после закрытия.
Рисунок 10. "Промежуточная" маска после закрытия.

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

openBW = area_opening(closeBW; min_area=500) .> 0;
se2 = Kernel.gaussian(3) .> 0.0025;
MASK_colorseg = closing(openBW,se2);
Рисунок 11. Результирующая бинарная маска для цветовой сегментации.
Рисунок 11. Результирующая бинарная маска для цветовой сегментации.

Наложим инвертированную маску на исходное изображение знакомым способом:

sv_colorseg = StackedView(CV[1,:,:] + (.!MASK_colorseg./3), CV[2,:,:] + 
                            (.!MASK_colorseg./3), CV[3,:,:]);
view_colorseg = colorview(RGB, sv_colorseg)
Рисунок 12. Совмещение маски с исходным изображением.
Рисунок 12. Совмещение маски с исходным изображением.

Фильтр Собеля

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

Временно забываем про цвет и будем работать с картой яркости, то есть с градациями серого:

imgray = Gray.(I)
Рисунок 13. Изображение в градациях серого.
Рисунок 13. Изображение в градациях серого.
# Ядра Собеля для осей X и Y
sobel_x = Kernel.sobel()[1]  # Горизонтальный градиент (вертикальные границы)
sobel_y = Kernel.sobel()[2]  # Вертикальный градиент (горизонтальные границы)

# Применение свертки
gradient_x = imfilter(imgray, sobel_x)
gradient_y = imfilter(imgray, sobel_y)

# Общий градиент (объединение X и Y)
gradient_magnitude = sqrt.(gradient_x.^2 + gradient_y.^2);
imsobel = gradient_magnitude ./ maximum(gradient_magnitude)
Рисунок 14. Границы, выделенные фильтром Собеля.
Рисунок 14. Границы, выделенные фильтром Собеля.

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

BW = binarize(imgray, Otsu());
Рисунок 15. Бинаризация границ методом Отсу.
Рисунок 15. Бинаризация границ методом Отсу.

И немного морфологической магии для получения результирующей маски:

se = Kernel.gaussian(3) .> 0.0035;
closeBW = closing(BW,se);
noobj = area_opening(closeBW; min_area=1000) .> 0;
se2 = Kernel.gaussian(5) .> 0.0027;
smooth = closing(noobj,se2);
smooth_2 = opening(smooth,se2);
MASK_sobel = area_opening(.!smooth_2; min_area=500) .> 0;
Рисунок 16. Результирующая маска после фильтра Собеля.
Рисунок 16. Результирующая маска после фильтра Собеля.
sv_sobel = StackedView(CV[1,:,:] + (.!MASK_sobel./3), CV[2,:,:] + 
                            (.!MASK_sobel./3), CV[3,:,:]);
view_sobel = colorview(RGB, sv_sobel)
Рисунок 17. Наложение маски после фильтра Собеля на исходное изображение.
Рисунок 17. Наложение маски после фильтра Собеля на исходное изображение.

SRG-алгоритм

Мы помним из блока теории, что этот алгоритм требует указания начальных точек. Я примерно прицелюсь в области середины леса и левее, там где застройка. Хочется попасть в тёмно-зелёный и серый цвета. В качестве начальных выберем точки с координатами [200, 50] и [200,300]. Оценим визуально их цвета - от этого сильно зависит успешность работы алгоритма:

[ I[200,50] I[200,300] ]

Устанавливаем координаты начальных точек, и получаем сегменты (результат сегментации) функцией seeded_region_growing, также передав ей исходное цветное изображение. Средние значения цвета внутри сегмента можно узнать функцией segments_mean:

seeds = [(CartesianIndex(200,50),1), (CartesianIndex(200,300),2)]
segments = seeded_region_growing(I, seeds);
sm = segment_mean(segments);
[ sm[1] sm[2] ]

Вот такие усреднённые цвета у нас получились для двух классов. Посмотрим, как отработал алгоритм:

simshow(map(i->segment_mean(segments,i), labels_map(segments)))
Рисунок 18. Сегменты после алгоритма SRG.
Рисунок 18. Сегменты после алгоритма SRG.

Бинарную матрицу мы получим из матрицы лейблов. Нас интересуют пиксели с лейблом 1:

lmap = labels_map(segments);
BW_smart = lmap .== 1;
Рисунок 19. Бинаризация сегментов SRG.
Рисунок 19. Бинаризация сегментов SRG.

Ну и немного простой морфологии:

se = Kernel.gaussian(2) .> 0.004;
closeBW = closing(BW_smart,se);
MASK_smart = area_opening(.!closeBW; min_area=500) .> 0;
Рисунок 20. Результирующая маска после алгоритма SRG.
Рисунок 20. Результирующая маска после алгоритма SRG.
sv_smart = StackedView(CV[1,:,:] + (.!MASK_smart./3), CV[2,:,:] + 
                            (.!MASK_smart./3), CV[3,:,:]);
view_smart = colorview(RGB, sv_smart)
Рисунок 21. Наложение маски после алгоритма SRG на исходное изображение.
Рисунок 21. Наложение маски после алгоритма SRG на исходное изображение.

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

Заключение

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

Весь код, как и в прошлый раз, доступен по ссылке. А больше примеров из области обработки изображений - в Сообществе.

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

Комментарии (8)


  1. Spaceoddity
    25.06.2025 17:13

    Как только название статьи прочитал - первая мысль «дык это, просто смотрите значение каналов a и b в Lab, меняя диапазон - варируем "похожесть" цвета».


    1. Skykharkov
      25.06.2025 17:13

      Да, "расстояние" в RGB тут вообще никак не поможет. Однажды "скрещивал" Google Maps и OSM. Ну если OSM еще хоть как-то, ну например, определить вода или нет по этим координатам, просто из "слоя воды" то в Google такого API нет. Задачка была простая - из определенной точки получить расстояние до ближайшей воды. Как ни извращался всегда то в лес упрешься, то в штриховку какую-то. Так с гуглом с цветами и не получилось. А разница, кстати, там существенная. Между OSM и Google - легко разница в метров пятьдесят может быть.


      1. MaratUss Автор
        25.06.2025 17:13

        Расстояние здесь не в смысле расстояния между объектами на карте. Мы в этой части вообще никак не привязывались к единицам измерения типа метров. Цветовое расстояние в RGB, о котором идёт речь - это чистая математика. Евклидово расстояние в трёхмерном пространстве, где по осям координат откладываются градиенты цветов.


        1. Skykharkov
          25.06.2025 17:13

          Да, я про то-же. Криво сформулировал. :) Я про то что пытался определить "разность" между цветами точек на карте. Для разных цветов "расстояние" может быть одинаковым. Даже не "расстояние", это то понятно. А "разность". В RGB на это никак ориентироваться нельзя. особенно если нужно определить "похожесть" цветов.


  1. slupoke
    25.06.2025 17:13

    Сделать из этого пёстрого набора пикселей "ровную" маску нам вновь поможет морфология. В этот раз мы возьмём небольшой структурный элемент (7х7 пикселей) в форме ромба:

    Почему именно метрика Манхэттена использовалась для таких данных?


    1. MaratUss Автор
      25.06.2025 17:13

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


      1. slupoke
        25.06.2025 17:13

        Форма структурного элемента здесь, думаю, никак не связана с расстоянием городских кварталов.

        Ромб это как раз и есть шар в метрике L1 (расстояние городских кварталов). В данном случае получается ромб радиусом 3 (Расстояние из центра до границ структурного элемента по метрике Манхэттена)


  1. Licemery
    25.06.2025 17:13

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