Адаптивная бинаризация
Адаптивная бинаризация
Бинаризация – один из ключевых этапов предобработки изображений, особенно в задачах распознавания символов (OCR), анализа документов и компьютерного зрения. Она преобразует полутоновое изображение в чёрно-белое, выделяя объекты переднего плана на фоне. Однако стандартные глобальные пороговые методы, такие как алгоритм Оцу (Otsu), часто дают сбой при наличии неравномерного освещения, градиентных теней или сложного фона. В таких случаях локальные (адаптивные) методы, вычисляющие порог для каждого пикселя на основе окрестности, оказываются значительно эффективнее.
В данном примере с использованием пакетов Images, ImageBinarization, ImageFiltering и ImageMorphology демонстрируется полный конвейер обработки изображения, начиная с синтеза тестового снимка с неравномерным освещением и заканчивая подготовкой бинарного изображения для OCR. Основное внимание уделяется сравнению глобальной бинаризации по Оцу и двух адаптивных методов – на основе локального среднего и гауссовского взвешивания. Далее показано применение морфологических операций (открытие и закрытие) для устранения шума, построение скелета объектов и финальная инверсия, удобная для OCR. Пример наглядно иллюстрирует преимущества адаптивных подходов и показывает, как их комбинация с морфологией позволяет получить качественный результат даже на изображениях со сложным освещением.
# Pkg.add(["ImageBinarization", "ImageMorphology"]
using Images, TestImages, ImageFiltering, ImageBinarization, ImageMorphology
1. Генерация изображения с неравномерным освещением
Исходное изображение «cameraman» искажается градиентной тенью (линейно возрастающей от углов к центру), имитирующей реальные условия съёмки. Такое освещение затрудняет выбор единого глобального порога.
img = testimage("cameraman")
h, w = size(img)
shadow = [Gray(0.3 + 0.7 * (j/w + i/h)/2) for i in 1:h, j in 1:w]
img_uneven = img .* shadow
display(img)
println("Оригинал")
display(img_uneven)
println("С неравномерным освещением")
img_gray = Gray.(img_uneven)
display(img_gray)
println("Градации серого")
2. Глобальная бинаризация (Otsu)
Применение метода Оцу ко всему изображению приводит к значительным потерям: тёмные области превращаются в сплошной фон, а светлые участки, наоборот, могут быть ошибочно отнесены к переднему плану. Результат показывает непригодность глобального порога для данного случая.
img_otsu = binarize(img_gray, Otsu())
display(img_otsu)
println("Глобальная Otsu")
3. Адаптивная бинаризация с локальным средним
Реализована функция, вычисляющая для каждого пикселя среднее значение в скользящем блоке заданного размера (11×11, 21×21, 51×51). Порог определяется как «среднее – константа C». При малом размере блока (11) появляется избыточная детализация и шум; при слишком большом (51) метод приближается к глобальному. Оптимальный размер (21) даёт хорошее разделение объектов без сильного шума.
function adaptive_threshold_mean(img, block_size; C=0.0)
h, w = size(img)
result = similar(img, Bool)
half_block = div(block_size, 2)
for i in 1:h
for j in 1:w
i1 = max(1, i - half_block)
i2 = min(h, i + half_block)
j1 = max(1, j - half_block)
j2 = min(w, j + half_block)
block_mean = mean(img[i1:i2, j1:j2])
result[i, j] = img[i, j] > (block_mean - C)
end
end
return result
end
for bs in [11, 21, 51]
img_adaptive_mean = adaptive_threshold_mean(img_gray, bs, C=0.05)
display(Gray.(img_adaptive_mean))
println("Адаптивная Mean, блок $(bs)x$(bs)")
end
4. Адаптивная бинаризация с гауссовским взвешиванием
Вместо равномерного усреднения используется ядро Гаусса, которое придаёт больший вес центральным пикселям блока. Такой подход более устойчив к выбросам и лучше сохраняет границы объектов. На примере с блоком 21×21 видно, что результат получается чище, чем при использовании простого среднего.
function adaptive_threshold_gaussian(img, block_size; C=0.0, sigma=0.0)
h, w = size(img)
result = similar(img, Bool)
half_block = div(block_size, 2)
if sigma == 0
sigma = 0.3 * ((block_size - 1) * 0.5 - 1) + 0.8
end
x = -half_block:half_block
kernel_1d = [exp(-(i^2)/(2*sigma^2)) for i in x]
kernel_1d = kernel_1d / sum(kernel_1d)
kernel = kernel_1d * kernel_1d'
for i in 1:h
for j in 1:w
i1 = max(1, i - half_block)
i2 = min(h, i + half_block)
j1 = max(1, j - half_block)
j2 = min(w, j + half_block)
block = img[i1:i2, j1:j2]
ki1 = half_block - (i - i1) + 1
ki2 = half_block + (i2 - i) + 1
kj1 = half_block - (j - j1) + 1
kj2 = half_block + (j2 - j) + 1
k = kernel[ki1:ki2, kj1:kj2]
k = k / sum(k)
weighted_mean = sum(block .* k)
result[i, j] = img[i, j] > (weighted_mean - C)
end
end
return result
end
img_adaptive_gauss = adaptive_threshold_gaussian(img_gray, 21, C=0.05)
display(Gray.(img_adaptive_gauss))
println("Адаптивная Gaussian, блок 21x21")
5. Сравнительная визуализация
Создаётся сетка, где одновременно представлены результаты Otsu, адаптивного среднего, адаптивного Гаусса и исходное полутоновое изображение. Разница между методами становится очевидной: адаптивные методы успешно компенсируют градиент освещения, а гауссовский вариант даёт наиболее сбалансированное бинарное представление.
function create_comparison_grid(images, titles)
n = length(images)
h, w = size(images[1])
grid_h = h * 2
grid_w = w * 2
grid = fill(Gray(0.5), grid_h, grid_w)
positions = [(1:h, 1:w), (1:h, w+1:2*w), (h+1:2*h, 1:w), (h+1:2*h, w+1:2*w)]
for (i, (img, pos)) in enumerate(zip(images, positions))
if i <= n
grid[pos...] = Gray.(img)
end
end
return grid
end
comparison = create_comparison_grid(
[img_otsu, adaptive_threshold_mean(img_gray, 21, C=0.05), img_adaptive_gauss, img_gray],
["Otsu", "Adaptive Mean", "Adaptive Gaussian", "Original"]
)
display(comparison)
println("Сравнение: Otsu | Mean | Gaussian | Original")
6. Морфологическая обработка
К лучшему результату (адаптивный Гаусс) применяются операции открытия (opening) и закрытия (closing). Открытие удаляет мелкие шумовые точки, а закрытие заполняет небольшие разрывы в контурах объектов. Это важный этап перед дальнейшим анализом формы.
img_binary = Gray.(img_adaptive_gauss)
img_opened = opening(img_binary)
display(img_opened)
println("После opening")
img_closed = closing(img_opened)
display(img_closed)
println("После closing")
7. Скелетизация
Реализован алгоритм скелетизации (поиск «скелета» – осевых линий объектов). На бинарном изображении после морфологии скелет позволяет выделить топологическую структуру фигур, что может использоваться для распознавания или измерения.
function skeletonize(img)
img_bool = Bool.(img)
skeleton = copy(img_bool)
changed = true
while changed
changed = false
to_remove = []
for i in 2:size(skeleton, 1)-1
for j in 2:size(skeleton, 2)-1
if !skeleton[i, j]
continue
end
p = [
skeleton[i-1, j], skeleton[i-1, j+1], skeleton[i, j+1],
skeleton[i+1, j+1], skeleton[i+1, j], skeleton[i+1, j-1],
skeleton[i, j-1], skeleton[i-1, j-1]
]
nonzero = sum(p)
if nonzero < 2 || nonzero > 6
continue
end
transitions = sum([p[k] == 0 && p[mod1(k+1, 8)] == 1 for k in 1:8])
if transitions != 1
continue
end
if p[1] * p[3] * p[5] == 0 && p[3] * p[5] * p[7] == 0
push!(to_remove, (i, j))
changed = true
end
end
end
for (i, j) in to_remove
skeleton[i, j] = false
end
to_remove = []
for i in 2:size(skeleton, 1)-1
for j in 2:size(skeleton, 2)-1
if !skeleton[i, j]
continue
end
p = [
skeleton[i-1, j], skeleton[i-1, j+1], skeleton[i, j+1],
skeleton[i+1, j+1], skeleton[i+1, j], skeleton[i+1, j-1],
skeleton[i, j-1], skeleton[i-1, j-1]
]
nonzero = sum(p)
if nonzero < 2 || nonzero > 6
continue
end
transitions = sum([p[k] == 0 && p[mod1(k+1, 8)] == 1 for k in 1:8])
if transitions != 1
continue
end
if p[1] * p[3] * p[7] == 0 && p[1] * p[5] * p[7] == 0
push!(to_remove, (i, j))
changed = true
end
end
end
for (i, j) in to_remove
skeleton[i, j] = false
end
end
return skeleton
end
img_skeleton = skeletonize(img_closed)
display(Gray.(img_skeleton))
println("Скелет")
img_overlay = RGB.(img_gray)
skeleton_coords = findall(img_skeleton)
for coord in skeleton_coords
img_overlay[coord] = RGB(1, 0, 0)
end
display(img_overlay)
println("Наложение скелета (красный)")
img_for_ocr = Gray.(.! Bool.(img_closed))
display(Gray.(img_for_ocr))
println("Для OCR (инверсия)")
8. Подготовка для OCR
Финальное изображение инвертируется (чёрные символы на белом фоне) – стандартный формат для большинства систем OCR. Визуализация всего конвейера (от исходного полутонового до готового для OCR) наглядно демонстрирует эффект каждого шага.
function create_pipeline_visualization(images, titles)
n = length(images)
h, w = size(images[1])
result = fill(Gray(0.3), h, w * n + (n-1) * 10)
for (i, img) in enumerate(images)
start_col = (i-1) * (w + 10) + 1
end_col = start_col + w - 1
result[:, start_col:end_col] = Gray.(img)
end
return result
end
pipeline = create_pipeline_visualization(
[img_gray, img_otsu, img_adaptive_gauss, img_closed, img_skeleton, img_for_ocr],
["Gray", "Otsu", "Adaptive", "Denoised", "Skeleton", "OCR Ready"]
)
display(pipeline)
println("Полный pipeline")
Вывод
В ходе работы с примером мы изучили и на практике увидели:
- Ограничения глобальной бинаризации при неравномерном освещении.
- Преимущества адаптивных методов, особенно гауссовского взвешивания, позволяющего учитывать локальный контраст и подавлять артефакты.
- Влияние размера локального блока на результат: слишком малый блок усиливает шум, слишком большой – нивелирует адаптивность.
- Необходимость морфологической постобработки для устранения шума и восстановления целостности объектов.
- Возможность скелетизации для анализа формы и топологии.
- Построение полного конвейера, пригодного для практических задач OCR.
Пример показал, что правильный выбор метода бинаризации и последующая обработка позволяют значительно повысить качество выделения объектов даже на сложных изображениях. Полученные знания могут быть непосредственно применены при разработке систем распознавания текста, анализа документов и других приложений, где важна устойчивость к неоднородному освещению.











