博客 / 詳情

返回

淺談計算機如何識別圖像2

前言

上篇文章淺談計算機如何識別和存儲圖像中關於計算機如何“看”圖片部分,只是粗糙的一筆帶過,而關於彩色圖是如何轉換成灰度圖不規則的手寫圖片怎麼最終轉換成統一像素大小切換為統一背景 這些原理並未瞭解,通過以下文章來進行探討。

彩色圖轉換為灰度圖

彩色圖為什麼要轉換為灰度圖

對比維度 彩色圖(RGB) 灰度圖(Gray)
通道數 3個通道(R,G,B) 1個通道
每個像素數據 (R,G,B)三個值 單一灰度值
是否包含顏色信息 ✅包含 ❌不包含
是否包含筆畫信息 ✅包含 ✅包含
計算量 大(卷積計算多)
訓練速度
手寫圖像識別中將彩色圖轉換為灰度圖,是因為顏色信息對字符識別幾乎沒有貢獻,而灰度圖能夠完整保留筆畫結構信息,同時降低計算複雜度、減少噪聲並提升模型泛化能力。

彩色圖怎麼轉換為灰度圖

常用公式進行加權平均:

Gray=0.299×R+0.587×G+0.114×B

生理學層面上,人眼的感光細胞分為兩種:

  1. 視杆細胞:負責弱光環境(暗視覺),無色彩感知,對藍綠色波段最敏感,但不參與色覺;
  2. 視錐細胞:負責強光環境(明視覺),感知色彩,分為三種類型(紅敏、綠敏、藍敏),其數量和分佈直接決定色覺敏感度:

    • 綠敏視錐細胞:數量最多(約 60%),主要分佈在視網膜中央的黃斑區(視覺最清晰的區域);
    • 紅敏視錐細胞:數量次之(約 30%),與綠敏視錐細胞分佈重疊;
    • 藍敏視錐細胞:數量最少(約 10%),且大多分佈在黃斑區外圍,中央區域極少。

    數量和核心區域的分佈差異,直接導致綠色信號被大腦接收的強度最高,紅色次之,藍色最弱。

如何解決手寫圖片尺寸不統一問題

下面以一張400 X 300的圖片,轉換為28 X 28的尺寸為例子講解

首先不能直接resize成目標尺寸:直接調整大小會導致 數字被壓扁 / 拉長,筆畫比例失真,模型學到“奇怪形狀”這些問題
而應該從一張大圖片中,把“有意義的內容”提取出來,並用統一規則表達

原圖:0.png

1. 灰度化

  • 彩色圖片是 RGB 三通道,每個通道的像素值不同。
  • 對手寫數字識別來説,我們只關心 亮度信息,不需要顏色信息。
  • 灰度化後,每個像素用 0~255 表示亮度(0=黑,255=白)。

灰度化公式:

Gray = 0.299R + 0.587G + 0.114B

圖片結果:Grayscale_Heatmap.png

2. 二值化

灰度圖後的結果類似於這種: 0, 20, 40, 180, 255...

  • 灰度圖每個像素是 0~255,無法直接表示“筆畫 vs 背景”
  • 通過設定閾值(threshold)把灰度圖轉換為二值圖

二值化的過程:

binary = np.where(gray < 128, 1, 0)
Gray < 128 → 筆畫
Gray ≥ 128 → 背景

圖片結果:Binarized_Heatmap_0_1.png

3.裁減

裁減的原因:

  • 原圖大部分是空白區域,數字只在中間
  • 如果不裁剪,空白太多,模型學到的是“數字在空白裏”,浪費訓練信息

那如何裁減呢?

top, bottom = rows.min(), rows.max()
left, right = cols.min(), cols.max()

找到所有值為1的像素,取行列最小值和最大值作為裁剪邊界,劃分一個矩形。

圖片結果:Resized_Heatmap.png

4.等比例縮放

  • 不同手寫數字區域大小不同,需要統一尺寸
  • 規則:最長邊 → 20 像素,短邊按比例縮放

為什麼不是直接縮放到 28×28?

  • 直接縮放會拉長或壓扁數字
  • 會導致筆畫比例失真,模型學到“奇怪形狀”
以我的圖片為例子,裁減後的尺寸為112 X 134,最長邊為134,
那麼縮放比率為 20/134 = 0.149...
另一邊縮放後的長度為 112 X 0.149 = 16.71 ...

關於如何處理小數問題,一般存在兩種處理方法:

  1. 直接省略小數點:

    • 優點:簡單,常用
    • 缺點:可能會略微縮小圖像,使數字邊緣丟失 1 個像素
    • 適合神經網絡,誤差不大
  2. 四捨五入

    • 優點:更接近真實比例
    • 缺點:仍可能出現 +1 / -1 的偏差
    • 推薦用於追求精確比例的場景

在這裏我採用直接取整的方法:等比例所放後的尺寸為 16 X 20

# binary 是 0/1,把邏輯數組變成可以操作的圖像對象,需要轉回 0~255 才能 resize
digit_img = Image.fromarray((digit_crop * 255).astype(np.uint8))

w, h = digit_img.size

scale = 20.0 / max(w, h)
new_w = int(w * scale)
new_h = int(h * scale)

digit_resized = digit_img.resize((new_w, new_h), Image.BILINEAR)

5.居中填充到 28×28

當前是16 X 20的小數字圖,但我需要的是28 X 28的比例圖

如果再進行 resize,會造成比例被破壞,數字被橫向或者縱向拉伸。

那麼選擇的方法是居中填充,也就是左邊一半右邊一半,上下也同理。

offset_x = (28 - new_w) // 2
offset_y = (28 - new_h) // 2

展示圖片結果:Final_28x28_Heatmap.png

背景轉換

在識別手寫圖片中,圖片可能是五顏六色並且有陰影,然後以我們的MINIST數據集所需要的黑底白字為例進行講解。

示例圖片: 2.png

1.轉換為灰度圖

只是關心筆畫結構

R = img_np[:, :, 0]
G = img_np[:, :, 1]
B = img_np[:, :, 2]

gray = (0.299 * R + 0.587 * G + 0.114 * B).astype(np.uint8)

圖片結果:
2.2.png

2.背景估計

  • 核心思想:把筆畫當作“噪聲”,用周圍像素的平均值估計背景
  • 結果:得到一張和原圖一樣大小的背景圖
利用局部區域內像素亮度變化緩慢的特性,
用當前像素周圍一塊較大鄰域的平均亮度,
作為該像素的背景亮度估計值。
h, w = gray.shape
// “站在這個像素上,我往上下左右各看 15 個像素,把這一大片的平均亮度當作背景。”
window = 31
pad = window // 2

# 邊緣填充
padded = np.pad(gray, pad, mode='edge')
background = np.zeros_like(gray)

for i in range(h):
    for j in range(w):
        region = padded[i:i+window, j:j+window]
        background[i, j] = np.mean(region)

background = background.astype(np.uint8)

圖片結果:
2.3.png

3.去陰影

如果不去陰影,後續二值化操作(把像素分成黑白)就會出問題:

  • 陰影深的地方 → 背景誤判成筆畫
  • 陰影淺的地方 → 筆畫可能被吃掉

方法:

  1. 減去背景

    • 保留筆畫差異
    • 抹掉陰影和紙張亮度變化
  2. 拉伸對比度

    • 減掉背景後,筆畫亮度可能還是很接近背景(灰灰的)
    • 如果直接二值化,閾值不好選
    • 拉伸對比度 = 把最暗的設為 0,最亮的設為 255,把灰度線性拉開
shadow_removed = np.abs(gray.astype(int) - background.astype(int))

# 拉伸對比度
min_val = shadow_removed.min()
max_val = shadow_removed.max()
shadow_removed = ((shadow_removed - min_val) / (max_val - min_val) * 255).astype(np.uint8)
數學公式意義:
假設 shadow_removed 的最小值是 10,最大值是 60
用公式:\( new=【(old−10)/(60−10)】×255 \)
結果:10 → 0 ,60 → 255 , 所有結果均在0-255之間
就像把灰灰的墨水“拉黑拉亮”,讓筆畫更明顯

圖片結果:
2.4.png

4.二值化

像素值:要麼0(純黑),要麼255(純白)

二值化就是為每個像素設定一個"分數線",分數高於分數線就變成白色(文字),低於分數線就變成黑色(背景),從而把有256種灰度的圖片變成只有純黑純白的圖片。
local_window = 15
pad = local_window // 2
padded = np.pad(shadow_removed, pad, mode='edge')

for i in range(h):
    for j in range(w):
        region = padded[i:i+local_window, j:j+local_window]
        threshold = np.mean(region) - 5  # C = 5
        binary[i, j] = 255 if shadow_removed[i, j] > threshold else 0

圖片結果:2.5.png

5. 統一為黑底白字

目標為黑底白字

binary = 255 - binary

圖片結果:2.6.png

6.簡單去噪

核心思想:如果一個白點(文字)周圍幾乎沒鄰居,那它很可能是噪聲。
for i in range(1, h-1):          # 遍歷每行(跳過最外圈)
    for j in range(1, w-1):      # 遍歷每列(跳過最外圈)
        if binary[i, j] == 255:  # 只檢查白點(可能是文字的點)
            
            # 獲取3×3鄰域(包括自己)
            neighbors = binary[i-1:i+2, j-1:j+2]
            # 相當於9個像素:
            # [i-1,j-1] [i-1,j] [i-1,j+1]
            # [i,j-1]   [i,j]   [i,j+1]
            # [i+1,j-1] [i+1,j] [i+1,j+1]
            
            # 計算鄰居中有多少白點(==255)
            white_count = np.sum(neighbors == 255)
            
            # 如果白點總數 < 3(包括自己在內)
            if white_count < 3:
                clean[i, j] = 0  # 把這個點設為黑(刪除)

圖片結果:
final_binary.png

結語

關於背景如何進行轉換,規則的手寫圖片怎麼最終轉換成統一像素大小,以及彩色圖如何轉換為灰度圖的瞭解先到這裏,通過簡單的瞭解,對原理有了一定的認識。如有錯誤,請指出!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.