AnyMath 文档
Notebook

自适应二值化

二值化是图像预处理的关键步骤之一,特别是在字符识别(OCR),文档分析和计算机视觉任务中。 它将灰度图像转换为黑白,突出背景中的前景对象。 然而,标准的全局阈值方法(如Otsu算法)在存在不均匀照明、渐变阴影或复杂背景的情况下通常会失败。 在这种情况下,基于邻域计算每个像素的阈值的局部(自适应)方法变得更有效。

在本例中使用包 Images, ImageBinarization, ImageFilteringImageMorphology 演示了完整的图像处理流程,从合成具有不均匀照明的测试图像开始,到制备用于OCR的二进制图像结束。 主要重点是比较全局ots二值化和基于局部均值和高斯加权的两种自适应方法。 下面展示了形态学操作(打开和关闭)消除噪声的应用,物体骨架的构建和最终的反演,方便OCR。 该示例清楚地说明了自适应方法的优点,并展示了它们与形态学的结合如何使您即使在照明困难的图像中也能获得高质量的结果。

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

1. 具有不均匀照明的图像生成
原始摄像师图像被渐变阴影扭曲(从角落到中心线性增加),模拟真实的拍摄条件。 这样的复盖使得难以选择单个全局阈值。

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)
将Otsu方法应用于整个图像会导致显着的损失:黑暗区域变成坚实的背景,而浅色区域则相反,可能会错误地归因于前景。 结果表明,全局阈值不适合这种情况。

In [ ]:
img_otsu = binarize(img_gray, Otsu())
display(img_otsu)
println("全球大津")
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("自适应均值,块△(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. 高斯加权自适应二值化
而不是均匀平均,使用高斯核,这给块的中心像素更多的权重。 这种方法更能抵抗排放,并更好地保留物体的边界。 使用21x21块的示例,您可以看到结果比使用简单平均值更干净。

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("自适应高斯,块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. 形态学处理
为了获得最佳结果(自适应高斯),应用了打开和关闭操作。 打开可消除小噪声点,关闭可填充物体轮廓中的小间隙。 这是进一步分析表单之前的重要步骤。

In [ ]:
img_binary = Gray.(img_adaptive_gauss)
img_opened = opening(img_binary)
display(img_opened)
println("开业后")
No description has been provided for this image
После opening
In [ ]:
img_closed = closing(img_opened)
display(img_closed)
println("关闭后")
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("完整的管道")
No description has been provided for this image
Полный pipeline

结论

在使用示例的过程中,我们在实践中进行了研究和观察:

-不均匀照明中全局二值化的局限性。
-自适应方法的优点,特别是高斯加权,其允许考虑到局部对比度和抑制伪影。
-局部块的大小对结果的影响:太小的块会增加噪声,太大会降低适应性。
-需要进行形态学后处理以消除噪声并恢复物体的完整性。
-骨架化的可能性,用于形状和拓扑分析。
-构建适合实际OCR任务的完整管道。

该示例表明,正确选择二值化方法和后续处理可以显着提高对象选择的质量,即使在复杂图像中也是如此。 所获得的知识可以直接应用于文本识别系统的开发、文档分析和其他对不均匀照明的抵抗力很重要的应用中。