Документация Engee
Notebook

Адаптивная бинаризация

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

В данном примере с использованием пакетов Images, ImageBinarization, ImageFiltering и ImageMorphology демонстрируется полный конвейер обработки изображения, начиная с синтеза тестового снимка с неравномерным освещением и заканчивая подготовкой бинарного изображения для OCR. Основное внимание уделяется сравнению глобальной бинаризации по Оцу и двух адаптивных методов – на основе локального среднего и гауссовского взвешивания. Далее показано применение морфологических операций (открытие и закрытие) для устранения шума, построение скелета объектов и финальная инверсия, удобная для OCR. Пример наглядно иллюстрирует преимущества адаптивных подходов и показывает, как их комбинация с морфологией позволяет получить качественный результат даже на изображениях со сложным освещением.

In [ ]:
# Pkg.add(["ImageBinarization", "ImageMorphology"]
using Images, TestImages, ImageFiltering, ImageBinarization, ImageMorphology

1. Генерация изображения с неравномерным освещением
Исходное изображение «cameraman» искажается градиентной тенью (линейно возрастающей от углов к центру), имитирующей реальные условия съёмки. Такое освещение затрудняет выбор единого глобального порога.

In [ ]:
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("С неравномерным освещением")
No description has been provided for this image
Оригинал
No description has been provided for this image
С неравномерным освещением
In [ ]:
img_gray = Gray.(img_uneven)
display(img_gray)
println("Градации серого")
No description has been provided for this image
Градации серого

2. Глобальная бинаризация (Otsu)
Применение метода Оцу ко всему изображению приводит к значительным потерям: тёмные области превращаются в сплошной фон, а светлые участки, наоборот, могут быть ошибочно отнесены к переднему плану. Результат показывает непригодность глобального порога для данного случая.

In [ ]:
img_otsu = binarize(img_gray, Otsu())
display(img_otsu)
println("Глобальная Otsu")
No description has been provided for this image
Глобальная Otsu

3. Адаптивная бинаризация с локальным средним
Реализована функция, вычисляющая для каждого пикселя среднее значение в скользящем блоке заданного размера (11×11, 21×21, 51×51). Порог определяется как «среднее – константа C». При малом размере блока (11) появляется избыточная детализация и шум; при слишком большом (51) метод приближается к глобальному. Оптимальный размер (21) даёт хорошее разделение объектов без сильного шума.

In [ ]:
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
No description has been provided for this image
Адаптивная Mean, блок 11x11
No description has been provided for this image
Адаптивная Mean, блок 21x21
No description has been provided for this image
Адаптивная Mean, блок 51x51

4. Адаптивная бинаризация с гауссовским взвешиванием
Вместо равномерного усреднения используется ядро Гаусса, которое придаёт больший вес центральным пикселям блока. Такой подход более устойчив к выбросам и лучше сохраняет границы объектов. На примере с блоком 21×21 видно, что результат получается чище, чем при использовании простого среднего.

In [ ]:
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")
No description has been provided for this image
Адаптивная Gaussian, блок 21x21

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

In [ ]:
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")
No description has been provided for this image
Сравнение: Otsu | Mean | Gaussian | Original

6. Морфологическая обработка
К лучшему результату (адаптивный Гаусс) применяются операции открытия (opening) и закрытия (closing). Открытие удаляет мелкие шумовые точки, а закрытие заполняет небольшие разрывы в контурах объектов. Это важный этап перед дальнейшим анализом формы.

In [ ]:
img_binary = Gray.(img_adaptive_gauss)
img_opened = opening(img_binary)
display(img_opened)
println("После opening")
No description has been provided for this image
После opening
In [ ]:
img_closed = closing(img_opened)
display(img_closed)
println("После closing")
No description has been provided for this image
После closing

7. Скелетизация
Реализован алгоритм скелетизации (поиск «скелета» – осевых линий объектов). На бинарном изображении после морфологии скелет позволяет выделить топологическую структуру фигур, что может использоваться для распознавания или измерения.

In [ ]:
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("Скелет")
No description has been provided for this image
Скелет
In [ ]:
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("Наложение скелета (красный)")
No description has been provided for this image
Наложение скелета (красный)
In [ ]:
img_for_ocr = Gray.(.! Bool.(img_closed))
display(Gray.(img_for_ocr))
println("Для OCR (инверсия)")
No description has been provided for this image
Для OCR (инверсия)

8. Подготовка для OCR
Финальное изображение инвертируется (чёрные символы на белом фоне) – стандартный формат для большинства систем OCR. Визуализация всего конвейера (от исходного полутонового до готового для OCR) наглядно демонстрирует эффект каждого шага.

In [ ]:
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")
No description has been provided for this image
Полный pipeline

Вывод

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

  • Ограничения глобальной бинаризации при неравномерном освещении.
  • Преимущества адаптивных методов, особенно гауссовского взвешивания, позволяющего учитывать локальный контраст и подавлять артефакты.
  • Влияние размера локального блока на результат: слишком малый блок усиливает шум, слишком большой – нивелирует адаптивность.
  • Необходимость морфологической постобработки для устранения шума и восстановления целостности объектов.
  • Возможность скелетизации для анализа формы и топологии.
  • Построение полного конвейера, пригодного для практических задач OCR.

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