跳至正文
-
关注
IT极限技术分享汇

ITmax

IT极限技术分享汇

ITmax

  • 首页
  • 商店
  • 我的帐户
  • 结账
  • 购物车
  • 首页
  • 商店
  • 我的帐户
  • 结账
  • 购物车
关

搜索

技术分享

浅谈计算机如何识别图像2

作者 mumupudding
2026年3月12日 3 分钟阅读
0

前言

上篇文章浅谈计算机如何识别和存储图像中关于计算机如何“看”图片部分,只是粗糙的一笔带过,而关于彩色图是如何转换成灰度图,不规则的手写图片怎么最终转换成统一像素大小,切换为统一背景 这些原理并未了解,通过以下文章来进行探讨。

彩色图转换为灰度图

彩色图为什么要转换为灰度图

对比维度 彩色图(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

结语

关于背景如何进行转换,规则的手写图片怎么最终转换成统一像素大小,以及彩色图如何转换为灰度图的了解先到这里,通过简单的了解,对原理有了一定的认识。如有错误,请指出!

https://segmentfault.com/a/1190000047493718

作者

mumupudding

关注我
其他文章
上一个

从业务开发视角聊聊可观测体系建设

下一个

汽车电子技术和汽车网络

暂无评论!成为第一个。

发表回复 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Copyright 2026 — IT极限技术分享汇. 备案号:浙ICP备17006560号-2