Математическая морфология: структурирующие элементы, эрозия и дилатация бинарных масок
Математическая морфология: структурирующие элементы, эрозия и дилатация бинарных масок
После пороговой бинаризации или сегментации по цветовым диапазонам перед инженером машинного зрения почти никогда не оказывается идеальная геометрическая маска. На конвейере реального производства бинарная маска детали всегда загрязнена: мелкие пылинки создают ложные белые точки (шум типа «соль»), блики света пробивают черные отверстия внутри сплошного контура («перец»), а контактная тень склеивает две соседние шестерни тонкой перемычкой в один неделимый объект. Если сразу подать такую маску в поиск контуров, алгоритм посчитает две соприкасающиеся детали за одну или насчитает сотни ложных микроконтуров. Для геометрического «исцеления» бинарных структур используется математическая морфология.
Теоретико-множественная природа бинарного растра
Классическая свертка, изученная ранее, оперирует линейной алгеброй — перемножает коэффициенты ядра на яркости пикселей и суммирует результат. Математическая морфология, основы которой заложили Жорж Матерон и Жан Серра в 1960-х годах, опирается на иную парадигму — теорию множеств и нелинейную геометрию.
Бинарное изображение рассматривается не как числовая матрица, а как дискретное множество точек , где точка принадлежит множеству, если соответствующий пиксель равен 255 (передний план, белый), и не принадлежит ему, если пиксель равен 0 (фон, черный). Вся морфологическая обработка сводится к взаимодействию этого исходного множества с малым зондирующим множеством , называемым структурирующим элементом (Structuring Element, SE).
Базовой математической операцией морфологии выступает сложение Минковского. Сложение множеств и определяется как объединение всех попарных векторных сумм их элементов:
Здесь — вектор координат пикселя переднего плана объекта , — вектор смещения точки структурирующего элемента относительно его начала координат (якоря). На практике это означает, что структурирующий элемент последовательно центрируется на каждом пикселе объекта , и результирующая фигура формируется объединением всех таких отпечатков.
Структурирующий элемент: геометрия и якорная точка
Структурирующий элемент — это двоичная матрица малого размера (обычно , или ), которая выступает пространственным зондом. В ней определена опорная точка — якорь (anchor point), которая по умолчанию совпадает с геометрическим центром ядра.
Форма структурирующего элемента напрямую задает геометрический характер трансформации:
- Прямоугольник (
MORPH_RECT): все элементы матрицы равны единице. Воздействует на объект одинаково по осям и диагоналям, сглаживая форму в сторону прямых углов. - Крест (
MORPH_CROSS): единицы расположены строго по центральной вертикали и горизонтали (связность 4). Бережно относится к диагональным скосам. - Эллипс / Диск (
MORPH_ELLIPSE): аппроксимация круга внутри квадратной матрицы. Обеспечивает изотропную (равномерную во всех направлениях) обработку без искажения естественной округлости границ.
В библиотеке OpenCV создание структурирующего элемента выполняется функцией cv2.getStructuringElement:
import cv2
# Прямоугольное ядро 5x5
rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# Эллиптическое ядро 5x5 (изотропное)
ellipse_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# Крестообразное ядро 3x3
cross_kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
Размерность ядра всегда выбирается нечетной (), чтобы якорь лежал строго на центральном целочисленном пикселе.
Эрозия (Erosion): математика сжатия
Эрозия (вычитание Минковского) — операция топологического уменьшения объекта. Она «соскабливает» граничный слой пикселей переднего плана.
Математически эрозия множества по структурирующему элементу определяется как геометрическое место всех таких точек смещения , при которых структурирующий элемент, сдвинутый в точку (обозначается как ), полностью содержится внутри множества :
Если хотя бы один единичный пиксель зонда выходит за пределы белой области (попадает на черный фон), центральный пиксель в результирующем изображении обнуляется.
Ключевой инсайт эрозии: Центральный пиксель сохраняет значение 255 тогда и только тогда, когда ВСЕ пиксели маски, накрытые единицами структурирующего элемента, равны 255. Это эквивалентно локальной логической операции
ANDпо всему окну зонда.
Практические эффекты эрозии:
- Удаление изолированного шума: мелкие белые пятна, размер которых меньше размера ядра , исчезают полностью.
- Разрыв тонких перемычек: узкие паразитные мостики между двумя близко расположенными объектами истончаются и рвутся.
- Уменьшение габаритов: общая площадь объектов сокращается, а внутренние отверстия увеличиваются.
В OpenCV эрозия вызывается методом cv2.erode:
# Базовый вызов эрозии
eroded_mask = cv2.erode(src=binary_mask, kernel=ellipse_kernel, iterations=1)
Дилатация (Dilation): математика расширения
Дилатация (наращивание) — операция, дуальная эрозии. Она наращивает внешний контур объектов переднего плана и затягивает внутренние пустоты.
Математически дилатация множества по структурирующему элементу определяется как геометрическое место точек , при которых сдвинутый структурирующий элемент пересекается с множеством хотя бы по одному элементу:
Если хотя бы один пиксель под структурирующим элементом равен 255, центральный пиксель выходного изображения гарантированно становится белым (255).
Ключевой инсайт дилатации: Выходной пиксель равен 255, если ХОТЯ БЫ ОДИН пиксель под единичными ячейками ядра равен 255. Это эквивалентно локальной логической операции
ORпо всему окну зонда.
Практические эффекты дилатации:
- Заполнение внутренних пустот: мелкие черные дефекты и точечные пробоины внутри объекта исчезают.
- Сращивание фрагментов: разорванные части одного контура расширяются навстречу друг другу и объединяются в связную область.
- Увеличение габаритов: общая площадь объекта возрастает, а наружные границы сглаживаются согласно форме ядра.
Вызов в OpenCV:
# Базовый вызов дилатации
dilated_mask = cv2.dilate(src=binary_mask, kernel=ellipse_kernel, iterations=1)
Сравнение механизмов эрозии и дилатации
Для выбора подходящей операции при очистке масок необходимо четко понимать их дуальные свойства:
| Критерий | Эрозия (cv2.erode) |
Дилатация (cv2.dilate) |
|---|---|---|
| Логическая операция ядра | Логическое AND (строгое совпадение) |
Логическое OR (хотя бы одно совпадение) |
| Влияние на площадь объекта | Уменьшает площадь | Увеличивает площадь |
| Влияние на шум | Уничтожает белый шум на фоне | Уничтожает черный шум внутри объекта |
| Внутренние отверстия | Расширяет | Затягивает / устраняет |
| Связность объектов | Разделяет слабо связанные объекты | Объединяет близко стоящие объекты |
Управление итерациями и граничные условия
Параметр iterations в функциях cv2.erode и cv2.dilate определяет, сколько раз подряд операция применяется к результату предыдущего шага:
Применение эрозии с ядром и параметром iterations=2 математически эквивалентно однократной эрозии с ядром . Однако последовательное применение малого ядра вычислительно эффективнее, так как обходит меньшее число ячеек памяти на каждом проходе.
При скольжении структурирующего элемента по краю кадра ядро неизбежно выходит за границы матрицы. Для корректной работы морфологии используется специальное дополнение краев:
# Явное указание граничных условий
eroded = cv2.erode(
binary_mask,
kernel=rect_kernel,
iterations=2,
borderType=cv2.BORDER_CONSTANT,
borderValue=0
)
Для бинарной морфологии критически важно использовать borderType=cv2.BORDER_CONSTANT с borderValue=0 (для эрозии) или borderValue=0/borderValue=255 в зависимости от того, считается ли край кадра фоном. Если край считается фоном (borderValue=0), эрозия естественным образом отсекает объекты, касающиеся границ изображения.
Практический пример: разделение слипшихся контуров
Рассмотрим реальный конвейерный сценарий. Две круглые металлические шайбы диаметром 60 пикселей упали рядом на конвейерную ленту. В месте касания образовалась паразитная перемычка шириной 4 пикселя. Наша цель — разделить их на два независимых контура для корректного подсчета.
import numpy as np
import cv2
def separate_touching_parts(raw_mask: np.ndarray) -> tuple[int, np.ndarray]:
"""
Разделяет соприкасающиеся детали с помощью адаптивной эрозии
и возвращает количество найденных валидных объектов.
"""
# 1. Создаем изотропный структурирующий элемент радиусом больше перемычки (3x3 или 5x5)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# 2. Выполняем эрозию: перемычка толщиной 4px гарантированно разрушится за 1-2 итерации
eroded_mask = cv2.erode(raw_mask, kernel, iterations=2)
# 3. Извлекаем контуры разделенных объектов
contours, _ = cv2.findContours(eroded_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 4. Фильтруем контуры по площади, отсекая случайный остаточный шум
valid_objects = [cnt for cnt in contours if cv2.contourArea(cnt) > 50]
return len(valid_objects), eroded_mask
В этом примере эрозия уменьшила радиус каждой шайбы, но полностью уничтожила перемычку толщиной 4 пикселя, так как ядро за две итерации «съело» по 4 пикселя с каждой стороны. В результате топологический алгоритм findContours увидел два изолированных замкнутых контура вместо одного слипшегося.
Однако простое применение эрозии уменьшило истинный размер деталей. Чтобы вернуть деталям исходный физический размер, сохранив при этом разделение, базовых операций эрозии и дилатации по отдельности недостаточно — требуется их строго выверенная композиция, составляющая основу продвинутых морфологических фильтров.