前言
上篇文章淺談計算機如何識別和存儲圖像中關於計算機如何“看”圖片部分,只是粗糙的一筆帶過,而關於彩色圖是如何轉換成灰度圖,不規則的手寫圖片怎麼最終轉換成統一像素大小,切換為統一背景 這些原理並未瞭解,通過以下文章來進行探討。
彩色圖轉換為灰度圖
彩色圖為什麼要轉換為灰度圖
| 對比維度 | 彩色圖(RGB) | 灰度圖(Gray) |
|---|---|---|
| 通道數 | 3個通道(R,G,B) | 1個通道 |
| 每個像素數據 | (R,G,B)三個值 | 單一灰度值 |
| 是否包含顏色信息 | ✅包含 | ❌不包含 |
| 是否包含筆畫信息 | ✅包含 | ✅包含 |
| 計算量 | 大(卷積計算多) | 小 |
| 訓練速度 | 慢 | 快 |
手寫圖像識別中將彩色圖轉換為灰度圖,是因為顏色信息對字符識別幾乎沒有貢獻,而灰度圖能夠完整保留筆畫結構信息,同時降低計算複雜度、減少噪聲並提升模型泛化能力。
彩色圖怎麼轉換為灰度圖
常用公式進行加權平均:
Gray=0.299×R+0.587×G+0.114×B
生理學層面上,人眼的感光細胞分為兩種:
- 視杆細胞:負責弱光環境(暗視覺),無色彩感知,對藍綠色波段最敏感,但不參與色覺;
-
視錐細胞:負責強光環境(明視覺),感知色彩,分為三種類型(紅敏、綠敏、藍敏),其數量和分佈直接決定色覺敏感度:
- 綠敏視錐細胞:數量最多(約 60%),主要分佈在視網膜中央的黃斑區(視覺最清晰的區域);
- 紅敏視錐細胞:數量次之(約 30%),與綠敏視錐細胞分佈重疊;
- 藍敏視錐細胞:數量最少(約 10%),且大多分佈在黃斑區外圍,中央區域極少。
數量和核心區域的分佈差異,直接導致綠色信號被大腦接收的強度最高,紅色次之,藍色最弱。
如何解決手寫圖片尺寸不統一問題
下面以一張400 X 300的圖片,轉換為28 X 28的尺寸為例子講解
首先不能直接resize成目標尺寸:直接調整大小會導致 數字被壓扁 / 拉長,筆畫比例失真,模型學到“奇怪形狀”這些問題
而應該從一張大圖片中,把“有意義的內容”提取出來,並用統一規則表達
原圖:
1. 灰度化
- 彩色圖片是 RGB 三通道,每個通道的像素值不同。
- 對手寫數字識別來説,我們只關心 亮度信息,不需要顏色信息。
- 灰度化後,每個像素用 0~255 表示亮度(0=黑,255=白)。
灰度化公式:
Gray = 0.299R + 0.587G + 0.114B
圖片結果:
2. 二值化
灰度圖後的結果類似於這種: 0, 20, 40, 180, 255...
- 灰度圖每個像素是 0~255,無法直接表示“筆畫 vs 背景”
- 通過設定閾值(threshold)把灰度圖轉換為二值圖
二值化的過程:
binary = np.where(gray < 128, 1, 0)
Gray < 128 → 筆畫
Gray ≥ 128 → 背景
圖片結果:
3.裁減
裁減的原因:
- 原圖大部分是空白區域,數字只在中間
- 如果不裁剪,空白太多,模型學到的是“數字在空白裏”,浪費訓練信息
那如何裁減呢?
top, bottom = rows.min(), rows.max()
left, right = cols.min(), cols.max()
找到所有值為1的像素,取行列最小值和最大值作為裁剪邊界,劃分一個矩形。
圖片結果:
4.等比例縮放
- 不同手寫數字區域大小不同,需要統一尺寸
- 規則:最長邊 → 20 像素,短邊按比例縮放
為什麼不是直接縮放到 28×28?
- 直接縮放會拉長或壓扁數字
- 會導致筆畫比例失真,模型學到“奇怪形狀”
以我的圖片為例子,裁減後的尺寸為112 X 134,最長邊為134,
那麼縮放比率為 20/134 = 0.149...
另一邊縮放後的長度為 112 X 0.149 = 16.71 ...
關於如何處理小數問題,一般存在兩種處理方法:
-
直接省略小數點:
- 優點:簡單,常用
- 缺點:可能會略微縮小圖像,使數字邊緣丟失 1 個像素
- 適合神經網絡,誤差不大
-
四捨五入
- 優點:更接近真實比例
- 缺點:仍可能出現 +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
展示圖片結果:
背景轉換
在識別手寫圖片中,圖片可能是五顏六色並且有陰影,然後以我們的MINIST數據集所需要的黑底白字為例進行講解。
示例圖片:
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.背景估計
- 核心思想:把筆畫當作“噪聲”,用周圍像素的平均值估計背景
- 結果:得到一張和原圖一樣大小的背景圖
利用局部區域內像素亮度變化緩慢的特性,
用當前像素周圍一塊較大鄰域的平均亮度,
作為該像素的背景亮度估計值。
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)
圖片結果:
3.去陰影
如果不去陰影,後續二值化操作(把像素分成黑白)就會出問題:
- 陰影深的地方 → 背景誤判成筆畫
- 陰影淺的地方 → 筆畫可能被吃掉
方法:
-
減去背景
- 保留筆畫差異
- 抹掉陰影和紙張亮度變化
-
拉伸對比度
- 減掉背景後,筆畫亮度可能還是很接近背景(灰灰的)
- 如果直接二值化,閾值不好選
- 拉伸對比度 = 把最暗的設為 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之間
就像把灰灰的墨水“拉黑拉亮”,讓筆畫更明顯
圖片結果:
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
圖片結果:
5. 統一為黑底白字
目標為黑底白字
binary = 255 - binary
圖片結果:
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 # 把這個點設為黑(刪除)
圖片結果:
結語
關於背景如何進行轉換,規則的手寫圖片怎麼最終轉換成統一像素大小,以及彩色圖如何轉換為灰度圖的瞭解先到這裏,通過簡單的瞭解,對原理有了一定的認識。如有錯誤,請指出!