在將紙本文件或證件影像送入 OCR (光學字元識別) 引擎之前,「二值化 (Binarization)」 —— 也就是將彩色或灰階影像轉換為純黑白(只有 0 與 255 兩種像素值)的過程,是最關鍵的前處理步驟。

二值化的核心概念非常直覺:逐一比對影像中每個像素的灰階數值,若超過某個「門檻值 (Threshold)」就判定為白色背景(255),反之則判定為黑色文字(0)。

對於在均勻光源下掃描的標準紙本申請書,使用一個固定的全局門檻值(例如 127)就能得到極佳的效果。然而,在行動裝置普及的今天,使用者上傳的往往是用手機隨手拍下的證件或收據影像。這類照片常伴隨著局部陰影、反光、光源不均或紙張折痕。如果強行使用固定門檻值,轉換出來的影像經常會出現一大塊漆黑(陰影處全被判定為黑色)或一大塊死白(反光處文字完全消失)的情形,使後續的 OCR 引擎完全失效。

為了在複雜的實際環境中取得理想的二值化結果,本文將深入比較並實作三種主流的影像二值化演算法。

1. 大津二值化 (Otsu’s Binarization)

大津演算法是一種自適應全局門檻值演算法。它不需要手動設定門檻值,而是透過統計影像的灰階直方圖,尋找一個能讓「類間變異量 (Between-Class Variance)」達到最大的最佳門檻值。

簡單來說,大津演算法會假設影像包含兩種類別:背景(通常是較亮的紙張)與前景(通常是較暗的文字)。演算法會自動計算出一個完美的分割線,使得這兩個群體各自的凝聚度最高,彼此的距離最遠。

大津演算法的適用情境與侷限

  • 最佳場景:影像直方圖呈現明顯的「雙峰分佈」(代表前景與背景色差分明),例如光照均勻但文字與紙張對比較低的文件掃描檔。
  • 致命缺點:當影像中存在強烈的漸層光源或嚴重局部陰影時,直方圖的雙峰特徵會被破壞,此時大津演算法找出來的「單一全局門檻值」依然會失效,導致陰影區全黑。

以下是使用 OpenCV 呼叫大津二值化的 Python 程式碼:

import cv2

def otsu_binarization(image_path, output_path):
    # 讀取灰階影像
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    
    # 執行大津二值化 (將門檻值設為 0,並傳入 cv2.THRESH_OTSU)
    # 演算法會自動計算出最佳門檻值,並回傳於 threshold_value 中
    threshold_value, binarized_img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    print(f"[Otsu] 計算出的最佳全局門檻值為: {threshold_value}")
    cv2.imwrite(output_path, binarized_img)

# 使用範例
# otsu_binarization("receipt.jpg", "binarized_otsu.png")

2. 自適應均值門檻值 (Adaptive Mean Thresholding)

當全局二值化(不論是固定門檻值還是大津演算法)因局部光影不均而破功時,局部自適應門檻值 (Adaptive Thresholding) 是最佳的解決方案。

自適應均值門檻值的原理是:影像中的每一個像素,其門檻值都是由它周圍鄰近區域(區塊大小為 $N \times N$)的平均灰階值動態決定的。具體計算方式為:

$$\text{Threshold}(x,y) = \text{Mean}(N \times N \text{ Area}) - C$$

其中 $C$ 是一個使用者自訂的常數,用來排除微小雜訊。因為門檻值是隨著空間位置「因地制宜」地動態調整,所以即使某個區塊處於暗部陰影中,只要文字與紙張在該區域仍有相對的明暗差,文字就能被完美提取出來。

自適應門檻值的適用情境

  • 最佳場景:手機拍照的文件、存摺掃描、含有反光或不均勻陰影的證件影像。這是 OCR 前處理管線中最實用、最穩健的演算法。
  • 缺點:如果設定的區塊大小 $N$ 太小,在沒有文字的純白紙張區塊,演算法會誤將紙張微小的紋路或雜訊判定為文字,進而產生很多細碎的「點狀雜訊」。

以下是 OpenCV 的實作程式碼:

import cv2

def adaptive_mean_binarization(image_path, output_path):
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    
    # 執行自適應均值二值化
    # Block Size 設為 11 (代表 11x11 像素區塊),常數 C 設為 2
    binarized_img = cv2.adaptiveThreshold(
        img, 255, 
        cv2.ADAPTIVE_THRESH_MEAN_C, 
        cv2.THRESH_BINARY, 
        11, 2
    )
    
    cv2.imwrite(output_path, binarized_img)

# 使用範例
# adaptive_mean_binarization("receipt.jpg", "binarized_adaptive.png")

3. Floyd-Steinberg 抖動演算法 (Dithering)

Floyd-Steinberg 是一種基於誤差擴散 (Error Diffusion) 概念的二值化演算法,主要用於圖像的「半色調 (Halftone)」處理。

它的基本概念是:當我們將一個灰階像素(數值 0~255)強行轉換成純黑(0)或純白(255)時,會產生一個「量化誤差」(例如灰階值 120 被轉成 0,誤差就是 +120)。為了補償這個誤差以保持視覺上的灰階漸層感,演算法會按照特定的比例,將這個誤差擴散傳遞給其右方、下方、右下方與左下方的鄰近像素:

$$\begin{aligned} \text{Pixel}[x+1][y] &\leftarrow \text{Pixel}[x+1][y] + \text{Error} \times \frac{7}{16} \ \text{Pixel}[x-1][y+1] &\leftarrow \text{Pixel}[x-1][y+1] + \text{Error} \times \frac{3}{16} \ \text{Pixel}[x][y+1] &\leftarrow \text{Pixel}[x][y+1] + \text{Error} \times \frac{5}{16} \ \text{Pixel}[x+1][y+1] &\leftarrow \text{Pixel}[x+1][y+1] + \text{Error} \times \frac{1}{16} \end{aligned}$$

這種做法可以在只有黑白兩色的媒介上,透過點陣疏密程度「模擬」出灰階的漸層效果。

Floyd-Steinberg 的適用情境與 OCR 警告

  • 最佳場景:電子書閱讀器 (E-ink)、熱感式發票列印機、復古像素風圖像處理。
  • OCR 禁用警告此演算法絕對不可以用於 OCR 預處理。因為它會將原本平滑的文字邊緣打碎成無數的網點雜訊,這會使 OCR 引擎(如 Tesseract 或 PaddleOCR)的特徵提取完全失效,導致辨識率降為零。

由於 OpenCV 沒有內建 Floyd-Steinberg 函數,以下是我們使用 NumPy 進行矩陣加速的完整演算法實作:

import cv2
import numpy as np

def floyd_steinberg_dithering(image_path, output_path):
    # 讀取影像並轉換為浮點數矩陣以進行精確誤差計算
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE).astype(float)
    h, w = img.shape
    
    for y in range(h):
        for x in range(w):
            old_val = img[y, x]
            # 二值化量化
            new_val = 255 if old_val > 127 else 0
            img[y, x] = new_val
            
            # 計算量化誤差
            err = old_val - new_val
            
            # 將誤差向鄰近像素擴散
            if x + 1 < w:
                img[y, x + 1] += err * 7 / 16
            if y + 1 < h:
                if x - 1 >= 0:
                    img[y + 1, x - 1] += err * 3 / 16
                img[y + 1, x] += err * 5 / 16
                if x + 1 < w:
                    img[y + 1, x + 1] += err * 1 / 16
                    
    # 轉回 8 位元無符號整數並儲存
    cv2.imwrite(output_path, img.clip(0, 255).astype(np.uint8))

# 使用範例
# floyd_steinberg_dithering("receipt.jpg", "binarized_dithering.png")

三種二值化演算法綜合比較

演算法名稱決策範圍核心優勢OCR 適用度最佳應用場景
大津二值化全局 (Global)自動計算門檻值,免手動調整★★☆☆☆ (限均勻光源)光源均勻之紙本掃描文件
自適應均值門檻值局部 (Local)抗陰影、抗反光,穩健度極高★★★★★ (最佳)手機拍照證件、收據前處理
Floyd-Steinberg局部誤差擴散能在黑白媒介上完美模擬漸層灰階☆☆☆☆☆ (禁用)電子紙、熱感式發票列印