Структурирующий элемент
Пример эрозии
Функцию эрозии erode
в простейшем одномерном случае можно определить так:
Поскольку выходное значение в позиции зависит не только от собственного пикселя A[p]
, но и от его соседних значений A[p-1]
и A[p+1]
, такая операция называется преобразованием окрестности изображения.
Возникает вопрос: что делать, если нужно обобщить функцию erode
? Следует обобщить понятие «окрестность».
Два представления окрестности
Утверждение «$\Omega_p$ является окрестностью » можно выразить в виде обычного кода Julia как p in Ωₚ
. Для повышения производительности Ωₚ
обычно генерируется из пары (p, Ω)
. p
— это центральная точка, которая изменяется во время итерации, а Ω
— это обычно предопределенные и неизменные данные, которые содержат информацию об окрестности и форме. Это Ω
называется структурирующим элементом. Ω
обычно выражается двумя способами:
-
смещение: список объектов
CartesianIndex
, определяющий смещение относительно центральной точкиp
; -
маска связи: маска в виде массива логических значений, определяющая связь с центральной точкой
p
.
Например, в следующем блоке кода создается связь C4 для двухмерного случая:
# смещение
Ω_offsets = [
CartesianIndex(-1, 0),
CartesianIndex(0, -1),
CartesianIndex(0, 1),
CartesianIndex(1, 0),
]
# маска связи
Ω_bool = Bool[
0 1 0
1 1 1
0 1 0
]
Если p=CartesianIndex(3, 3)
, то известно, что p=CartesianIndex(3, 4)
находится в Ωₚ
.
Вернемся к примеру эрозии. На основе представления в виде смещения довольно легко реализовать простейшую универсальную версию erode
:
# Только для иллюстрации; производительность можно значительно повысить, используя итерацию, чтобы не выделять лишнюю память
function my_erode(A, Ω)
out = similar(A)
R = CartesianIndices(A)
for p in R
Ωₚ = filter!(q->in(q, R), Ref(p) .+ Ω)
# здесь не предполагается, что p входит в Ωₚ
out[p] = min(A[p], minimum(A[Ωₚ]))
end
return out
end
using ImageMorphology
using ImageBase
using TestImages
img = Gray.(testimage("morphology_test_512"))
img = Gray.(img .< 0.8)
img_e = my_erode(img, Ω_offsets)
mosaic(img, img_e; nrow=1)
Как вы, возможно, уже поняли, представление в виде смещения удобно использовать при реализации алгоритмов, но сложно визуализировать. Напротив, представление в виде маски связи не так удобно использовать при реализации алгоритмов, но легко визуализировать. Например, такой СЭ очень легко понять с первого взгляда:
3×3 Matrix{Bool}: 1 1 1 1 1 0 0 0 0
Но не такой:
4-element Vector{CartesianIndex{2}}: CartesianIndex(-1, -1) CartesianIndex(0, -1) CartesianIndex(-1, 0) CartesianIndex(-1, 1)
Функция strel
Этот пакет поддерживает преобразование между различными представлениями СЭ с помощью вспомогательной функции strel
. strel
— это сокращение от STRucturing ELement (структурирующий элемент).
Преобразовать представление в виде маски связи в представление в виде смещения можно так:
Ω_mask = Bool[1 1 1; 1 1 0; 0 0 0] |> centered
Ω_offsets = strel(CartesianIndex, Ω_mask)
4-element Vector{CartesianIndex{2}}:
CartesianIndex(-1, -1)
CartesianIndex(0, -1)
CartesianIndex(-1, 0)
CartesianIndex(-1, 1)
Предполагается, что массив маски отцентрирован относительно нуля. Это означает, что оси маски размером 3×3
Функция |
и также экспортируется вместе с пакетом ImageMorphology.
Преобразовать представление в виде смещения обратно в представление в виде маски связи можно так:
strel(Bool, Ω_offsets)
3×3 OffsetArray(::BitMatrix, -1:1, -1:1) with eltype Bool with indices -1:1×-1:1:
1 1 1
1 1 0
0 0 0
Совсем не сложно, да? Таким образом, чтобы сделать функцию my_erode
более универсальной, достаточно добавить всего одну строку:
function my_erode(A, Ω)
out = similar(A)
+ Ω = strel(CartesianIndex, Ω)
R = CartesianIndices(A)
Симметричность
Из всех структурирующих элементов на практике чаще всего используются симметричные — они обладают лучшими свойствами, а их симметрия позволяет определенным образом оптимизировать реализацию.
Для представления в виде маски mask = strel(Bool, se)
симметричность СЭ определяется относительно центральной точки: se
симметричен, если условие mask[I] == mask[-I]
соблюдается для I ∈ CartesianIndices(mask)
. По этой же причине представление в виде маски требует массива centered
.
se = centered(Bool[
1 0 1
0 1 0
1 0 1
])
ImageMorphology.is_symmetric(se) # true
se = centered(Bool[
1 1 1
0 1 0
1 0 1
])
ImageMorphology.is_symmetric(se) # false
se = [CartesianIndex(1), CartesianIndex(-1)]
ImageMorphology.is_symmetric(se) # true
se = [CartesianIndex(1)]
ImageMorphology.is_symmetric(se) # false
Удобные конструкторы
Помимо прочих возможностей СЭ, этот пакет предоставляет конструкторы для двух распространенных случаев:
-
конструктор СЭ ромбовидной формы
strel_diamond
; -
конструктор СЭ прямоугольной формы
strel_box
.
julia> strel_diamond((3, 3)) # смежные элементы: связь C4
3×3 ImageMorphology.StructuringElements.SEDiamondArray{2, 2, UnitRange{Int64}, 0} with indices -1:1×-1:1:
0 1 0
1 1 1
0 1 0
julia> strel_diamond((3, 3), (1, )) # по первому измерению
3×3 ImageMorphology.StructuringElements.SEDiamondArray{2, 1, UnitRange{Int64}, 1} with indices -1:1×-1:1:
0 1 0
0 1 0
0 1 0
julia> strel_box((3, 3)) # все соседние элементы: связь C8
3×3 SEBoxArray{2, UnitRange{Int64}} with indices -1:1×-1:1:
1 1 1
1 1 1
1 1 1
julia> strel_box((3, 3), (1, ))
3×3 SEBoxArray{2, UnitRange{Int64}} with indices -1:1×-1:1:
0 1 0
0 1 0
0 1 0
Используя эти конструкторы, можно предоставить более простой в использовании интерфейс my_erode(A, [dims])
, добавив еще один метод:
my_erode(A, dims::Dims=ntuple(identity, ndims(A))) = my_erode(A, strel_diamond(A, dims))
Для структурирующего элемента |
Оптимизация производительности и функция strel_type
Благодаря механизму множественной диспетчеризации в Julia можно реализовать все приемы оптимизации, не усложняя интерфейс для пользователя. Это можно сделать программно с помощью функции strel_type
. Например, если известна очень эффективная реализация erode
для СЭ со связью C4, ее можно добавлять последовательно:
using ImageMorphology: MorphologySE, SEDiamond
my_erode(A, dims::Dims) = my_erode(A, strel_diamond(A, dims))
my_erode(A, Ω) = _my_erode(strel_type(Ω), A, Ω)
# универсальная реализация, приведенная выше
function _my_erode(::MorphologySE, A, Ω)
...
end
# оптимизированная реализация для СЭ, созданного функцией `strel_diamond`
function _my_erode(::SEDiamond, A, Ω)
...
end
# ...и другие оптимизированные версии, если они есть
По сути, strel_type
— это функция-типаж, помогающая при диспетчеризации и проектировании кода:
julia> strel_type(Ω_mask)
SEMask{2}()
Она возвращает внутренний объект SEMask{2}()
. Поначалу это может пугать, но вот такая простая сопоставительная таблица, в которой отражены приведенные выше рассуждения, упрощает понимание:
Представление | Тип элементов | strel_type |
---|---|---|
Смещение |
|
|
Маска связи |
|
|
|
|
|
|
|