4、图像预处理技术:灰度化、去噪、二值化、倾斜校正、透视变换

各位同学,欢迎来到第四讲。上一章我们聊了图像采集的硬件选型,今天咱们正式进入「让图像变干净」的环节。

说实话,在货架标签识别这个场景里,原始图像的质量往往惨不忍睹。光照不均、角度歪斜、背景杂乱……这些问题如果不处理,后面的OCR识别基本就是白费力气。我最早做这个项目时,就吃过这个亏——采集的图像直接扔进识别模型,结果准确率不到30%。后来老老实实把预处理做扎实,准确率直接飙到95%以上。

好,咱们一个一个来。

4.1 灰度化:去掉颜色,保留结构

为什么要灰度化?说白了,颜色信息对识别标签上的文字和条码来说,基本是冗余的。你想想看,货架标签上的文字是黑白的,条码也是黑白的,保留RGB三个通道只会增加计算量,还容易引入噪声。

常用的灰度化方法有三种:

方法 公式 特点
最大值法 Gray = max(R, G, B) 亮度偏高,细节保留一般
平均值法 Gray = (R + G + B) / 3 均衡,但对比度可能下降
加权法(推荐) Gray = 0.299R + 0.587G + 0.114B 符合人眼感知,效果最好

我个人习惯用加权法。OpenCV里一行代码搞定:

import cv2
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
小提示: 如果你用的是手机拍摄的货架标签,记得先检查图像通道顺序。OpenCV默认是BGR,而很多手机拍出来是RGB。我曾经因为这个顺序搞反,灰度图出来一片漆黑,排查了半天才发现是通道顺序的问题。

4.2 去噪:把「雪花」扫干净

灰度化之后,下一步就是去噪。货架场景下,噪声主要来自两个地方:一是光照反射产生的亮点,二是摄像头传感器本身的热噪声。

常用的去噪方法有:

  • 高斯滤波: 适合去除高斯噪声,对边缘保留较好。我一般用5x5的核。
  • 中值滤波: 对椒盐噪声(就是那种黑白点)效果奇好。我在超市货架项目里遇到过标签上有污渍的情况,中值滤波直接搞定。
  • 双边滤波: 既能去噪又能保边,但速度慢。适合对质量要求极高的场景。

代码示例:

# 高斯滤波
img_gaussian = cv2.GaussianBlur(img_gray, (5, 5), 0)

# 中值滤波
img_median = cv2.medianBlur(img_gray, 5)

# 双边滤波
img_bilateral = cv2.bilateralFilter(img_gray, 9, 75, 75)
注意: 滤波核大小一定要是奇数。我刚开始学的时候,随手写了个(4,4),结果程序直接报错,还以为是OpenCV版本问题。嗯,这种低级错误,你们别犯。

4.3 二值化:让文字「跳」出来

二值化的目标很简单——把图像变成纯黑和纯白。这样文字区域就是黑色,背景就是白色,OCR识别起来就轻松多了。

但这里有个坑:货架标签的光照往往不均匀。比如标签上半部分被灯光直射,下半部分在阴影里。如果用全局阈值,要么上半部分过曝,要么下半部分全黑。

怎么办?用自适应阈值。

# 全局阈值(不推荐用于光照不均场景)
_, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)

# 自适应阈值(推荐)
img_adaptive = cv2.adaptiveThreshold(img_gray, 255, 
                                     cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                     cv2.THRESH_BINARY, 11, 2)

参数说明:11是邻域大小,2是常数修正值。这两个值需要根据实际图像微调。我在项目中一般从11和2开始试,效果不行再调。

核心经验: 二值化之后,如果文字出现断裂,说明阈值太高了;如果文字粘连在一起,说明阈值太低了。调整邻域大小和修正值,直到文字清晰可辨。

4.4 倾斜校正:把歪的标签「掰正」

货架标签很少是正对着摄像头拍的。要么是员工随手一拍,要么是标签贴歪了。倾斜角度一大,OCR识别率直线下降。

怎么做?核心思路是找到图像中的直线,计算角度,然后旋转回来。

具体步骤:

  1. 用Canny边缘检测提取边缘
  2. 用霍夫变换检测直线
  3. 计算直线的平均角度
  4. 用仿射变换旋转图像

代码示例:

import numpy as np

# 边缘检测
edges = cv2.Canny(img_binary, 50, 150, apertureSize=3)

# 霍夫变换检测直线
lines = cv2.HoughLines(edges, 1, np.pi/180, 200)

# 计算平均角度
angles = []
for rho, theta in lines[:, 0]:
    angle = theta * 180 / np.pi - 90
    angles.append(angle)
avg_angle = np.mean(angles)

# 旋转校正
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, avg_angle, 1.0)
img_rotated = cv2.warpAffine(img, M, (w, h), 
                             flags=cv2.INTER_CUBIC, 
                             borderMode=cv2.BORDER_REPLICATE)
避坑指南: 我曾经遇到过一个标签,上面的文字是倾斜的,但标签边框是水平的。霍夫变换检测到的直线全是边框,结果校正完文字更歪了。后来我改用文字区域的轮廓来检测角度,才解决问题。所以,检测区域的选择很关键。

4.5 透视变换:从「斜着拍」到「正着看」

倾斜校正只能处理旋转,但解决不了透视变形。什么叫透视变形?就是你从侧面拍一个标签,它看起来是梯形的,而不是矩形。这时候就需要透视变换。

透视变换的原理很简单:找到原始图像中四个角点,映射到目标矩形的四个角点。

步骤:

  1. 找到标签的四个角点(可以用轮廓检测)
  2. 定义目标矩形的四个角点
  3. 计算透视变换矩阵
  4. 应用变换
# 假设我们已经找到了四个角点 pts_src
# 目标矩形的四个角点
pts_dst = np.array([
    [0, 0],
    [width-1, 0],
    [width-1, height-1],
    [0, height-1]
], dtype='float32')

# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(pts_src, pts_dst)

# 应用变换
img_warped = cv2.warpPerspective(img, M, (width, height))
关键点: 角点检测的精度直接决定透视变换的效果。我建议先用Canny边缘检测,再用轮廓近似找到四个顶点。如果标签有圆角,可以先用膨胀操作把圆角变成直角,再检测轮廓。

4.6 预处理流程总结

好了,五个步骤都讲完了。在实际项目中,我一般按这个流程走:

步骤 操作 目的
1 灰度化 去除颜色冗余,降低计算量
2 去噪 消除光照反射和传感器噪声
3 二值化 分离文字和背景
4 倾斜校正 纠正旋转角度
5 透视变换 纠正透视变形

当然,这个顺序不是死的。比如有些场景下,先做透视变换再做二值化效果更好。我建议你们多试几种组合,找到最适合自己数据的那一套。

下一章,咱们聊聊特征提取和匹配。到时候你会看到,预处理做得好,后面的工作有多轻松。

最后说一句: 预处理没有银弹。每个项目的数据都不一样,多调参数、多看中间结果,才是正道。我当年也是调了上百张图,才摸索出一套稳定的参数组合。别怕麻烦,这一步值得花时间。