Python手写数字识别:图像读取、去噪与标准化处理详解

温以宁
温以宁
发布时间:2026-10-13| 4人浏览过
原创

本文详解Python手写数字识别中图像预处理的核心步骤。通过读取灰度矩阵、设定阈值去噪、切割有效区域及拉伸至100x100标准尺寸,将图片转换为1x10000向量并存入训练矩阵。文章深入解析了GetTrainPicture、CutPicture等关键函数的实现逻辑与代码细节。

写在前面

在上一篇《Python徒手实现手写数字识别—大纲》中,我们已经梳理了整体的实现思路,因此这里不再重复阐述完整流程。

本次重点编写图片读取与处理的代码,逻辑与大纲一致。该部分代码主要包含以下几个处理环节:

  • 读取图片文件;
  • 将图片转换为灰度值矩阵;
  • 去除图片背景噪音;
  • 切割图片,提取手写数字的最小包围矩阵;
  • 对图片进行拉伸或压缩,得到尺寸为100x100的标准矩阵;
  • 将图片展平为1x10000大小的向量,并存入训练矩阵。

接下来,将对上述各个处理函数进行详细解析。

代码分析

基础内容

首先,在代码最前端定义基础变量。

import os
from skimage import io
import numpy as np

##Essential vavriable 基础变量
#Standard size 标准大小
N = 100
#Gray threshold 灰度阈值
color = 100/255

其中,“标准大小”指经过切割和拉伸后最终得到的图片尺寸,记为NxN。“灰度阈值”是指当某像素点的灰度值超过该阈值时,将其标记为1。

接下来是图像处理部分的主函数。

filenames = os.listdir(r"./num/")
pic = GetTrainPicture(filenames)

其中,`filenames`获取num目录下所有文件的名称列表,`pic`则是通过`GetTrainPicture`函数获取的所有训练图像向量组成的矩阵。本文主要围绕该函数展开讲解。

GetTrainPicture函数

GetTrainPicture函数的具体内容如下。

#Read and save train picture 读取训练图片并保存
def GetTrainPicture(files):
 Picture = np.zeros([len(files), N**2+1])
 #loop all pictures 循环所有图片文件
 for i, item in enumerate(files):
 #Read the picture and turn RGB to grey 读取这个图片并转为灰度值
 img = io.imread('./num/'+item, as_grey = True)
 #Clear the noise 清除噪音
 img[img>color] = 1
 #Cut the picture and get the picture of handwritten number
 #将图片进行切割,得到有手写数字的的图像
 img = CutPicture(img)
 #Stretch the picture and get the standard size 100x100
 #将图片进行拉伸,得到标准大小100x100
 img = StretchPicture(img).reshape(N**2)
 #Save the picture to the matrix 将图片存入矩阵
 Picture[i, 0:N**2] = img
 #Save picture's name to the matrix 将图片的名字存入矩阵
 Picture[i, N**2] = float(item[0])
 return Picture

可以看出,该函数包含大量信息,几乎封装了今天涉及的所有处理步骤。因此,图片处理的所有流程均可在此函数中找到对应实现。

提前准备

首先,创建一个用于存放所有图像向量的矩阵`Picture`,其大小为fx10001。其中,f代表训练图片的数量,前10000位代表图片展开后的向量长度,最后一维表示该图片对应的类别标签,例如值为2代表图片上的数字是2。

随后,使用for循环遍历`files`中的每一张图片,计算其向量后存入`picture`矩阵。

循环内部的内容即为对每一张图片执行的具体操作。

读入图片并清除背景噪音

首先调用`io.imread`函数,将图片导出为灰度值矩阵,矩阵中的每个元素对应一个像素点。

接着执行`img[img>color]=1`,这句代码运用了逻辑运算技巧,可拆分为两部分理解。

point = img > color
img[point] = 1

首先是`img>color`,其中`img`是矩阵,`color`是数值。其含义是对`img`中所有元素判断是否大于`color`,并输出一个与`img`同大小的矩阵,对应元素为判断结果(False或True)。若大于该数则为True,否则为False。举例说明如下,不再赘述。

a = np.array([1, 2, 3, 4])
print(a>2)

#以下为输出结果
[False False True True]

随后的`img[point] = 1`说明将所有为True的元素赋值为1。举例说明如下。

a = np.array([1, 2, 3, 4])
p = a > 2
a[p] = 0
print(a)

#以下为输出结果
[1 2 0 0]

通过这种方式,我们清除了与数字颜色差异较大的背景噪音。

切割图像

首先,切割图像的函数命名为`CutPicture`。这里解释一下切割图像的含义。例如,有人写字较小,有人写字较大,如图所示。因此,我们沿图片边缘进行切割,得到如下切割后的图片,使数字占满整个图片,从而具备可比性。

下面贴出代码,详细解释具体实现方法。

#Cut the Picture 切割图象
def CutPicture(img):
 #初始化新大小
 size = []
 #图片的行数
 length = len(img)
 #图片的列数
 width = len(img[0,:])
 #计算新大小
 size.append(JudgeEdge(img, length, 0, [-1, -1]))
 size.append(JudgeEdge(img, width, 1, [-1, -1]))
 size = np.array(size).reshape(4)
 print(size)
 return img[size[0]:size[1]+1, size[2]:size[3]+1]

首先,函数接收的参数仅有一个原图像`img`。

第一步是初始化新的大小,`size`列表将存放四个值:第一个值代表原图片上手写数字图案的最高行,第二个值代表最低行,第三个值代表最左列,第四个值代表最右列。若仍不理解,可参考上方图示,即沿图片边缘进行切割即可。

接下来的`length`和`width`分别代表原图片的行数与列数,作用见下文。此外,创建了一个`JudgeEdge`函数,用于输出其行数或列数的边界数字。第一个`append`操作将两个行序号(最高行和最低行)放入`size`列表,第二个`append`操作将两个列序号(最左列和最右列)放入`size`列表。接下来分析`JudgeEdge`函数的功能。

def JudgeEdge(img, length, flag, size):
 for i in range(length):
 #Cow or Column 判断是行是列
 if flag == 0:
  #Positive sequence 正序判断该行是否有手写数字
  line1 = img[img[i,:]<color]
  #Negative sequence 倒序判断该行是否有手写数字
  line2 = img[img[length-1-i,:]<color]
 else:
  line1 = img[img[:,i]<color]
  line2 = img[img[:,length-1-i]<color]
 #If edge, recode serial number 若有手写数字,即到达边界,记录下行
 if len(line1)>=1 and size[0]==-1:
  size[0] = i
 if len(line2)>=1 and size[1]==-1:
  size[1] = length-1-i
 #If get the both of edge, break 若上下边界都得到,则跳出
 if size[0]!=-1 and size[1]!=-1:
  break
 return size

`JudgeEdge`函数的参数`flag`用于判断是处理行还是列。当`flag=0`时,说明以行为基础开始循环;当`flag=1`时,说明以列为基础进行循环。因此,传递参数`length`时即为行数和列数。接下来的for循环根据`length`的大小进行迭代。

当处理行时,这里使用`line1`和`line2`作为指针。即在第i行时,`line1`的内容是该行拥有非白色底(数值为1)的像素个数;`line2`的作用则是反向的,即计算倒数第i行非白色像素个数。这样做的目的是能够更快地同时从上下两端开始寻找,而不必分别遍历整张图片,大大节省了时间。

寻找该行拥有非白色底像素个数的语句同样运用了逻辑判断,原理与上文去噪部分完全一致。

当`line`中有数值时,说明已到达手写数字的边缘。此时记录该位置,且不再改变。当两个`line`都不等于初始值-1时,说明已找到两个边缘,此时跳出循环并返回结果。

该函数逻辑即如此。因此,切割图像的完整代码如上所示。接下来,需将切割后大小不一的图像拉伸为标准大小100x100。

拉伸图像

由于切割后的图像大小不一,一张图片可能是21x39(例如数字2),另一张可能是4x40(例如数字1)。为了统一大小至100x100,我们需要对图像进行拉伸处理。

大致效果如图所示。实际图案可能更复杂,因此下面展示相关代码。

#Stretch the Picture 拉伸图像
def StretchPicture(img):
 newImg = np.ones(N**2).reshape(N, N)
 ##Stretch/Compress each cows/columns 对每一行/列进行拉伸/压缩
 #The length of each cows after stretching 每一行拉伸/压缩的步长
 step1 = len(img[0])/100
 #Each columns blabla 每一列拉伸/压缩的步长
 step2 = len(img)/100
 #Operate on each cows 对每一行进行操作
 for i in range(len(img)):
 for j in range(N):
  newImg[i, j] = img[i, int(np.floor(j*step1))]
 #Operate on each columns 对每一列进行操作
 for i in range(len(img[0])):
 for j in range(N):
  newImg[j, i] = img[int(np.floor(j*step2)), i]
 return newImg

首先初始化一个新的图片矩阵,大小为标准尺寸100x100。接下来是核心部分。这里采用的方法较为简单基础,但可能仍具有一定难度。

首先定义两个步长`step1`和`step2`,分别代表拉伸/压缩行与列时的步长。其原理是将原长度平均分成100份,然后将这100个像素点分别对应到原有的像素点。

如下图所示,假设图像1为原图像,图像2为标准图像,我们需要将图像1转化为图像2,其中每个点代表一个像素点。即图像1有五个像素点,图像2有四个像素点。

因此,我的思路是直接让图像2的像素点的值等于距离它最近的图像1的像素点。

为方便起见,在此定义一种语法:图像2的第三个数据点可表示为2_3。

因此,2_1对应1_1,2_2对应1_2,2_3对应1_4,2_4对应1_5。通过这种方式,我们可以得到图像2的所有数据点。

用数学形式表达,假设图像1长度为l_1,图像2长度为l_2,则令图像2的步长为l_1/l_2。也就是说,当图像2的第一个像素点对应图像1的第一个像素点,图像2的最后一个像素点对应图像1的最后一个像素点。然后,图像2第二个像素点的位置即为2*l_1/l_2,对应图像1第floor(2*l_1/l_2)个像素点。依此类推即可。因此,再回看那段代码,是否更容易理解?

之后对行与列分别执行此操作,即可得到标准的图片大小。随后返回至`GetTrainPicture`函数。

在`GetTrainPicture`函数中,使用了`reshape`函数将原本100x100大小的图片拉伸成为1x10000大小的向量,然后存入矩阵中,并将该图片的类别存入矩阵最后一维。

以上即为图片处理的所有内容。

小结

以上介绍了将一张图片经过处理后存入矩阵的全过程。

本文已被收录到专题《python图片处理操作》,欢迎大家点击学习更多精彩内容。

本文中的所有算法与代码均由本人独立构思,因此可能存在不足之处。由于未系统学习过图像相关知识,且非计算机专业背景,理论上可能存在不符合常规之处。如有错误,欢迎一起讨论,谢谢。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。