
Привет, Хабр! В предыдущей части мы рассматривали базовые методы цифровой обработки изображений для задачи сегментации спутникового снимка.
В этой статье рассмотрим ещё парочку методов решения этой задачи, всё ещё «классических», то есть без применения машинного обучения или нейросетей. Помогут нам во всём разобраться, как и в прошлый раз, язык программирования Julia и среда технических расчётов Engee!
Постановка задачи (для тех, кто прогулял первую лекцию)
У нас есть «тестовый сигнал», то есть небольшой кусочек спутниковой карты района Москвы, а конкретнее – парка Сокольники и прилегающих жилых районов.
Сам парк очень зелёный, в то время как жилые дома не очень. Мы хотим в автоматическом режиме отделять области лесополосы от застройки. В цифровой обработке изображений такая задача называется сегментацией.
У нас пример ознакомительный и достаточно простой, где мы собираемся отделять объекты на условном «переднем плане» (foreground) от объектов условного «фона» (background). Результатом наших трудов должна стать бинарная маска — матрица нулей и единиц, размером с исходное изображение, где элементы 1 соответствуют интересующим нас объектам, а элементы 0 — фону. Не так важно, что мы будем принимать за 1, а что за 0, лес или город, главное, чтобы мы правильно их разграничивали.
Какие же алгоритмы мы рассмотрим в этот раз? В первой части рассматривались:
· Изменение контраста
· Бинаризация
· Морфологические операции
· Линейные и нелинейные 2-D фильтры
Но очень мало внимания уделялось цветовой составляющей, а ведь снимок-то у нас цветной, и, казалось бы, самым логичным было бы отделять зелёные насаждения от серо-бело-чёрного города. Сперва копнём в этом направлении, а затем посмотрим, как цветовая информация может объединяться с «текстурой» областей изображения (например, границами, выделенными фильтром Собеля) в комплексном алгоритме сегментации, таком как Seeded Region Growing (SRG).
Кратко (прям совсем) о применяемых алгоритмах
Для начала поговорим о том, как мы можем сопоставить пиксель изображения какому-либо классу на основе его цвета. Как нам уже известно из предыдущей части, мы можем разложить цветное изображение на отдельные каналы интенсивности. Мы работаем в цветовом пространстве RGB, когда каждая из трёх матриц отвечает за яркость одного из трёх цветов. Но для задач обработки изображений есть и другие цветовые пространства, которые могут подходить для той или иной задачи лучше.
-
RGB — простое, но чувствительно к освещению.
-
HSV/HSL — отделяет цвет (Hue) от яркости (Value/Lightness), удобно для выделения объектов.
-
Lab — лучше учитывает человеческое восприятие цвета.
Про различия между цветовыми пространствами достаточно много открытой информации в сети, и мы для удобства продолжим работать в RGB. Плюс – в этом пространстве легко оценивать цветовое расстояние, самую простую метрику для кластеризации пикселей.
Цветовое расстояние — это числовая мера различия между двумя цветами в заданном цветовом пространстве (например, RGB, HSV, Lab). Оно используется для сравнения цветов, кластеризации, сегментации изображений и поиска схожих оттенков.
Евклидово расстояние в RGB вычисляется как геометрическое расстояние между точками в RGB-пространстве:
Определённый недостаток — RGB не учитывает восприятие цвета человеком (разные комбинации могут давать одинаковое расстояние, но визуально отличаться).
Простыми словами – мы можем задавать пороговые границы численных значений интенсивности в каналах RGB, и проверять условие попадания трёх значений в соответствующие пределы. Чем уже границы – тем ближе цвет должен быть к «эталонному», например, тёмно-зелёному для леса.
От цвета снова перейдём к «текстуре». Продолжим знакомится с нелинейными фильтрами.
Фильтр Собеля — это оператор для выделения границ на изображениях, основанный на вычислении градиента яркости в двух направлениях (по горизонтали и вертикали). Он широко используется в задачах компьютерного зрения, таких как детекция краёв, улучшение контуров и предобработка для других алгоритмов (например, преобразование Хафа).
Фильтр использует две матрицы 3×3:
-
Горизонтальное ядро (Gₓ) — выделяет вертикальные границы:
[ -1 0 1 ] [ -2 0 2 ] [ -1 0 1 ] -
Вертикальное ядро (Gᵧ) — выделяет горизонтальные границы:
[ -1 -2 -1 ] [ 0 0 0 ] [ 1 2 1 ]
-
Изображение обрабатывается обоими ядрами отдельно.
-
Для каждого пикселя вычисляются градиенты по X (Gₓ) и Y (Gᵧ).
-
Результирующая оценка границы в точке (x, y):
Ну и одним из базовых методов объединения текстурных фильтров и цветового расстояния является 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 для самого широкого спектра инженерных задач:
Регистрируйтесь по ссылке и получайте бесплатный доступ прямо сейчас – это позволит вам самим интерактивно поизучать описываемые в статьях примеры. Весь код тут:
Сегментация спутникового снимка, часть 1
Сегментация спутникового снимка, часть 2
Переходим в основной скрипт
Подключим необходимые библиотеки Julia для фильтрации, морфологии и сегментации:
using Images, ImageShow, ImageContrastAdjustment, ImageBinarization, ImageMorphology, ImageFiltering, ImageSegmentation
А вот и наше исходное изображение (то же самое, что и в первой части):
I = load("$(@__DIR__)/map_small.jpg")
Цветовая сегментация
Прицелимся на отделение лесного массива. Наша задача — выделить области, близкие по цвету к наблюдаемому на изображении оттенку зелёного. Мы будем бинаризовать отдельные каналы изображения, а доступ к ним мы получим, как и ранее, при помощи функции 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,:,:]) ]
Типичный «зелёный пиксель» находится, как можно предположить, в центре изображения. Выделим «центральный» пиксель, зная ширину и высоту изображения, и посмотрим на значения его интенсивности в цветовых каналах:
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);
А теперь объединим три бинарные маски в одну операцией логического «И» — теперь белый пиксель будет только там, где в исходном цветном изображении все три проверяемых условия (диапазона) выполняются:
BIN = BIN_RED .& BIN_GREEN .& BIN_BLUE;
Сделать из этого пёстрого набора пикселей «ровную» маску нам вновь поможет морфология. В этот раз мы возьмём небольшой структурный элемент (7х7 пикселей) в форме ромба:
se = strel_diamond((7,7))

И оценим результат операции морфологического закрытия:
closeBW = closing(BIN,se);
Результирующий вид маски мы получим, удалив небольшие «блобы», а также осуществив операцию закрытия ещё раз, теперь уже для сглаживания границ основных «крупных» областей объединённых белых пикселей:
openBW = area_opening(closeBW; min_area=500) .> 0; se2 = Kernel.gaussian(3) .> 0.0025; MASK_colorseg = closing(openBW,se2);
Наложим инвертированную маску на исходное изображение знакомым способом:
sv_colorseg = StackedView(CV[1,:,:] + (.!MASK_colorseg./3), CV[2,:,:] + (.!MASK_colorseg./3), CV[3,:,:]); view_colorseg = colorview(RGB, sv_colorseg)
Фильтр Собеля
Применяемый нелинейный фильтр — это оператор, используемый в обработке изображений для обнаружения границ (краёв объектов) на основе вычисления градиента яркости в горизонтальном и вертикальном направлениях. Это позволит отделить «резкую» текстуру города от практически не изменяющегося по градиенту парка.
Временно забываем про цвет и будем работать с картой яркости, то есть с градациями серого:
imgray = Gray.(I)
# Ядра Собеля для осей 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)
Бинаризуем результат фильтрации методом Отсу без дополнительных аргументов:
BW = binarize(imgray, Otsu());
И немного морфологической магии для получения результирующей маски:
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;
sv_sobel = StackedView(CV[1,:,:] + (.!MASK_sobel./3), CV[2,:,:] + (.!MASK_sobel./3), CV[3,:,:]); view_sobel = colorview(RGB, sv_sobel)
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)))
Бинарную матрицу мы получим из матрицы лейблов. Нас интересуют пиксели с лейблом 1:
lmap = labels_map(segments); BW_smart = lmap .== 1;
Ну и немного простой морфологии:
se = Kernel.gaussian(2) .> 0.004; closeBW = closing(BW_smart,se); MASK_smart = area_opening(.!closeBW; min_area=500) .> 0;
sv_smart = StackedView(CV[1,:,:] + (.!MASK_smart./3), CV[2,:,:] + (.!MASK_smart./3), CV[3,:,:]); view_smart = colorview(RGB, sv_smart)
SRG-алгоритм, хоть и является относительно «умным», не относится к техниками машинного обучения. Это классический алгоритм, в котором нет этапов обучения и автоматического выделения признаков непосредственно из данных. Впрочем, его можно объединять с техниками машинного обучения, например, для автоматического выбора начальных точек.
Заключение
Во второй части мы немного углубились в сегментацию на основе цветового расстояния, познакомились с ещё одним фильтром, позволяющим выделить текстуру, а также на примере алгоритма SRG посмотрели, как цвет и текстуру можно объединять в рамках комплексного алгоритма сегментации.
Весь код, как и в прошлый раз, доступен по ссылке. А больше примеров из области обработки изображений — в Сообществе.
В последующих публикациях мы двинемся в сторону машинного обучения. Оставайтесь с нами!
ссылка на оригинал статьи https://habr.com/ru/articles/921784/
Добавить комментарий