浅谈计算机如何识别图像2
前言
上篇文章浅谈计算机如何识别和存储图像中关于计算机如何“看”图片部分,只是粗糙的一笔带过,而关于彩色图是如何转换成灰度图,不规则的手写图片怎么最终转换成统一像素大小,切换为统一背景 这些原理并未了解,通过以下文章来进行探讨。
彩色图转换为灰度图
彩色图为什么要转换为灰度图
| 对比维度 | 彩色图(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 # 把这个点设为黑(删除)
图片结果:
结语
关于背景如何进行转换,规则的手写图片怎么最终转换成统一像素大小,以及彩色图如何转换为灰度图的了解先到这里,通过简单的了解,对原理有了一定的认识。如有错误,请指出!