3、图像预处理基础:图像读取与显示、灰度化、二值化(OTSU/自适应)、去噪(高斯/中值)、形态学操作
各位同学,欢迎来到第三章。
这一章,我们正式进入图像预处理的世界。说实话,这是整个OCR流水线里最“脏”最“累”的活,但也是最能体现功力的地方。我做了这么多年OCR,见过太多模型在干净数据集上跑得飞起,一到真实场景就崩盘——十有八九是预处理没做好。
你想想看,摄像头拍出来的照片,光照不均匀、有噪声、文字歪歪扭扭,你直接把这种图扔给模型,它能认出来才怪。所以,预处理的目的就一个:把“烂图”变成“好图”,让后续的识别模块能轻松干活。
3.1 图像读取与显示
一切从读取开始。在Python里,我们最常用的就是OpenCV和PIL。我个人习惯用OpenCV,因为它速度快、功能全,而且底层是C++写的,处理大图也不虚。
import cv2
import matplotlib.pyplot as plt
# 读取图像
img = cv2.imread('test.jpg')
# OpenCV默认读取的是BGR格式,不是RGB
# 显示的时候要注意转换
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.axis('off')
plt.show()
另外,读取图像时要注意路径问题。中文路径、特殊字符,都可能导致读取失败。我建议统一用英文路径,省心。
3.2 灰度化
为什么要灰度化?说白了,彩色图像有三个通道,信息冗余大,计算量也大。对于文字识别来说,颜色信息其实没那么重要,我们更关心的是文字的轮廓和对比度。
灰度化就是把三通道的彩色图变成单通道的灰度图。每个像素的值从0到255,0代表纯黑,255代表纯白。
# 灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 或者直接以灰度模式读取
gray = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
这里有个小细节:直接以灰度模式读取,比先读取再转换要快一点点。但如果你需要对原图做其他处理,还是先读取彩色图再转换更灵活。
3.3 二值化
灰度图还是太“丰富”了,每个像素有256种可能。二值化就是把这256种可能压缩成两种:0和255,也就是黑和白。这样,文字区域和非文字区域就彻底分开了。
3.3.1 全局阈值(OTSU)
OTSU算法,也叫大津法。它的核心思想是:自动找一个阈值,让前景和背景的类间方差最大。说白了,就是让黑白分得最开。
# OTSU二值化
_, binary_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
我在项目中遇到过一个问题:当图像光照不均匀时,OTSU的效果会变差。比如一张图左边亮右边暗,OTSU只能找到一个全局阈值,结果左边文字被吃掉,右边背景被染黑。这时候就需要自适应阈值了。
3.3.2 自适应阈值
自适应阈值,说白了就是“因地制宜”。它把图像分成一个个小区域,每个区域单独计算阈值。这样,亮的地方阈值高,暗的地方阈值低,效果就好多了。
# 自适应阈值
binary_adaptive = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
11, # 邻域大小,必须是奇数
2 # 常数,从均值中减去的值
)
3.4 去噪
二值化之后,图像上经常会有一些“小点点”——噪声。这些噪声可能是摄像头传感器产生的,也可能是纸张本身的纹理。去噪的目的就是把这些干扰去掉,同时尽量保留文字的边缘。
3.4.1 高斯滤波
高斯滤波,说白了就是“加权平均”。它用一个高斯核去扫描图像,每个像素的值被替换成它周围像素的加权平均值。权重由高斯分布决定,离中心越近,权重越大。
# 高斯滤波
gaussian_blur = cv2.GaussianBlur(gray, (5, 5), 0)
高斯滤波对高斯噪声(也就是那种随机分布的噪声)效果很好。但它的缺点是会让图像变模糊,边缘也会被平滑掉。所以,核的大小要控制好,我一般不超过7x7。
3.4.2 中值滤波
中值滤波就简单粗暴多了——它取邻域内所有像素的中位数作为当前像素的值。这招对椒盐噪声(就是那种黑白点)特别有效。
# 中值滤波
median_blur = cv2.medianBlur(gray, 5)
我记得有一次做身份证OCR,图像上有明显的扫描条纹。高斯滤波搞不定,但中值滤波一上去,条纹全没了。为什么?因为条纹是孤立的亮线,中值滤波直接把它当成异常值剔除了。
| 滤波方法 | 适用噪声 | 边缘保留 | 速度 |
|---|---|---|---|
| 高斯滤波 | 高斯噪声 | 较差 | 快 |
| 中值滤波 | 椒盐噪声 | 较好 | 中等 |
3.5 形态学操作
形态学操作,听起来很高大上,其实就是对二值图像做“膨胀”和“腐蚀”。这两个操作是基础,组合起来能玩出很多花样。
3.5.1 腐蚀与膨胀
腐蚀:让白色区域“瘦”一圈。说白了,就是去掉边缘的孤立点。
膨胀:让白色区域“胖”一圈。说白了,就是填补内部的空洞。
# 定义结构元素
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 腐蚀
erosion = cv2.erode(binary, kernel, iterations=1)
# 膨胀
dilation = cv2.dilate(binary, kernel, iterations=1)
3.5.2 开运算与闭运算
开运算 = 先腐蚀后膨胀。作用是去掉小的白色噪点,同时保持文字大小不变。
闭运算 = 先膨胀后腐蚀。作用是填补文字内部的黑色空洞,同时保持文字大小不变。
# 开运算
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
3.6 预处理流水线实战
好了,知识点都讲完了。我们来串一条完整的预处理流水线。这是我个人在项目中常用的套路:
def preprocess_pipeline(img_path):
# 1. 读取图像
img = cv2.imread(img_path)
# 2. 灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 3. 去噪(先中值后高斯,效果更佳)
denoised = cv2.medianBlur(gray, 3)
denoised = cv2.GaussianBlur(denoised, (3, 3), 0)
# 4. 自适应二值化
binary = cv2.adaptiveThreshold(
denoised, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
11, 2
)
# 5. 形态学操作(闭运算填补空洞)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
return cleaned
这条流水线,我用了好几年,在大多数场景下都能拿到不错的效果。当然,具体参数要根据你的图像来调。没有万能药,只有不断试错。
嗯,这一章的内容就到这里。下一章,我们会讲图像增强和几何变换,到时候你会看到更多有意思的技巧。记住,预处理做得好,后面识别就轻松。别偷懒,多试试不同的组合。