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()
⚠️ 避坑指南: 我曾经在项目里犯过一个低级错误——直接用OpenCV读取图像,然后用matplotlib显示,结果颜色全偏了。后来才想起来,OpenCV默认是BGR通道顺序,而matplotlib用的是RGB。这个问题排查了我整整一下午,嗯,从那以后我再也没忘过这个转换。

另外,读取图像时要注意路径问题。中文路径、特殊字符,都可能导致读取失败。我建议统一用英文路径,省心。

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    # 常数,从均值中减去的值
)
💡 经验之谈: 邻域大小这个参数很关键。设得太小,容易把文字内部的细节当成背景;设得太大,又失去了自适应的意义。我一般从11开始试,根据文字大小调整。如果是小字体,可以设到7或9;大字体,15或21。

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)
⚠️ 注意: 我曾经在项目里对一张模糊的发票做开运算,结果把“—”这个横线给去掉了。为什么?因为横线太细,腐蚀的时候直接没了。所以,结构元素的大小要根据文字笔画粗细来调整。笔画细的,用3x3;笔画粗的,可以试试5x5。

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

这条流水线,我用了好几年,在大多数场景下都能拿到不错的效果。当然,具体参数要根据你的图像来调。没有万能药,只有不断试错。

嗯,这一章的内容就到这里。下一章,我们会讲图像增强和几何变换,到时候你会看到更多有意思的技巧。记住,预处理做得好,后面识别就轻松。别偷懒,多试试不同的组合。