3. 图像处理基础(下):几何变换、形态学与直方图均衡化
好,咱们接着聊。上一节我们把图像滤波和边缘检测讲透了,这一节我带你搞定三个非常实用的技能:几何变换、形态学操作和直方图均衡化。
这三个东西,说白了就是:
- 几何变换:让图像动起来(缩放、旋转、扭曲)
- 形态学操作:给图像做“整形手术”(去噪、补洞、分离粘连)
- 直方图均衡化:让图像“亮瞎眼”的对比度增强术
我个人习惯把它们放在一起讲,因为实际项目中经常要组合使用。比如做文档OCR预处理,先几何校正,再形态学去噪,最后直方图增强——一套组合拳下来,识别率能提升一大截。
3.1 几何变换:让图像动起来
几何变换,说白了就是改变图像里像素的位置。你拍了一张歪的照片,想把它扶正;或者想把一个logo放大缩小——这些都属于几何变换。
3.1.1 图像缩放
缩放是最简单的几何变换。但这里有个坑:插值方法的选择。
我记得刚入行时,做一个人脸检测项目,直接把图片用最近邻插值缩小了。结果检测率暴跌——人脸五官都变成锯齿了。后来换成双线性插值,效果立马好了。
三种常用插值方法对比:
| 方法 | 速度 | 质量 | 适用场景 |
|---|---|---|---|
| 最近邻插值 | 最快 | 最差(锯齿) | 实时预览、像素艺术 |
| 双线性插值 | 中等 | 较好(平滑) | 通用缩放(推荐) |
| 双三次插值 | 最慢 | 最好(锐利) | 高质量输出、印刷 |
# 图像缩放示例(OpenCV)
import cv2
img = cv2.imread('photo.jpg')
# 方法1:指定目标尺寸
resized = cv2.resize(img, (800, 600), interpolation=cv2.INTER_LINEAR)
# 方法2:按比例缩放(fx, fy为缩放因子)
resized = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_CUBIC)
我的小技巧:做深度学习预处理时,我习惯用双线性插值缩小,双三次插值放大。前者速度快,后者细节保留好。
3.1.2 图像旋转
旋转其实是个数学问题。绕中心点旋转θ角度,每个像素的新位置都要通过矩阵乘法算出来。
嗯,这里要注意:旋转后图像会变大(因为四个角会跑出去)。OpenCV默认会裁剪,但你可以设置参数保留完整图像。
# 图像旋转
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
# 获取旋转矩阵(角度、缩放)
M = cv2.getRotationMatrix2D(center, 45, 1.0) # 顺时针45度
# 执行旋转,保留完整图像
rotated = cv2.warpAffine(img, M, (w, h))
我曾经踩过的坑:旋转后图像边缘会出现黑色区域(填充值默认为0)。如果你做图像拼接,记得把填充值设为边缘像素的平均值,否则拼接处会有黑边。
3.1.3 仿射变换
仿射变换是几何变换的“大杀器”。它不仅能缩放旋转,还能做倾斜、扭曲。说白了,就是保持直线和平行关系的任意变换。
我做过一个车牌识别项目,车牌在照片里是歪的。用仿射变换把四个角点映射到矩形,车牌就正了。识别率从60%直接飙到95%。
# 仿射变换示例
# 原图中的三个点
pts1 = np.float32([[50,50], [200,50], [50,200]])
# 目标图中的三个点
pts2 = np.float32([[10,100], [200,50], [100,250]])
# 计算仿射矩阵
M = cv2.getAffineTransform(pts1, pts2)
# 执行变换
affined = cv2.warpAffine(img, M, (w, h))
核心要点:仿射变换只需要3个点对就能确定变换矩阵。你想想看,3个点确定一个平面,刚好够用。
3.2 形态学操作:图像的“整形手术”
形态学操作,名字听着高大上,其实就是用一个小“结构元素”在图像上滑动,做逻辑运算。它特别擅长处理二值图像。
3.2.1 腐蚀与膨胀
这两个是最基础的操作,理解它们,后面的开闭运算就简单了。
- 腐蚀:让白色区域“瘦身”。结构元素覆盖的区域,只要有一个黑像素,中心就变黑。效果:消除小白点、分离粘连物体。
- 膨胀:让白色区域“增肥”。结构元素覆盖的区域,只要有一个白像素,中心就变白。效果:填补小洞、连接断裂。
我记得做细胞计数项目时,细胞图像里有很多粘连在一起的。先用腐蚀把它们分开,再膨胀恢复大小——这就是开运算的思想。
# 腐蚀与膨胀
kernel = np.ones((5,5), np.uint8)
eroded = cv2.erode(img, kernel, iterations=1)
dilated = cv2.dilate(img, kernel, iterations=1)
避坑指南:迭代次数不是越多越好。我曾经为了去噪,腐蚀了5次,结果把目标物体都腐蚀没了。一般1-2次就够了。
3.2.2 开运算与闭运算
开运算 = 先腐蚀后膨胀。闭运算 = 先膨胀后腐蚀。
你可能会问:为什么不直接腐蚀或膨胀?因为直接操作会改变物体大小。开闭运算能在保持物体大小不变的前提下,完成去噪或补洞。
| 操作 | 顺序 | 效果 | 典型应用 |
|---|---|---|---|
| 开运算 | 腐蚀→膨胀 | 去除小白点、分离粘连 | 去噪、细胞分割 |
| 闭运算 | 膨胀→腐蚀 | 填补小黑洞、连接断裂 | 指纹修复、文字增强 |
# 开运算与闭运算
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
我的经验:做文档OCR预处理时,我通常先用开运算去掉扫描产生的噪点,再用闭运算把断裂的文字笔画连起来。效果立竿见影。
3.3 直方图均衡化:让图像“亮”起来
直方图均衡化,说白了就是把图像的灰度分布“拉平”。让暗的地方变亮,亮的地方变暗,整体对比度提升。
你想想看,一张曝光不足的照片,像素都集中在暗部。均衡化后,像素会均匀分布到整个灰度范围,细节就出来了。
3.3.1 全局直方图均衡化
这是最基础的方法,对整个图像做一次均衡。OpenCV一行代码搞定。
# 全局直方图均衡化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
equalized = cv2.equalizeHist(gray)
注意:全局均衡对彩色图像不友好。它会破坏色彩平衡,让颜色变得怪异。我一般只在灰度图上用,或者对HSV空间的V通道做均衡。
3.3.2 CLAHE:更聪明的均衡化
全局均衡有个问题:它会放大噪声。尤其是图像中既有很亮区域又有很暗区域时,暗部的噪声会被放大得惨不忍睹。
CLAHE(对比度受限自适应直方图均衡化)解决了这个问题。它把图像分成小块,每块单独均衡,同时限制对比度的放大倍数。
我做过一个医学影像项目,X光片里骨骼和软组织亮度差异巨大。用全局均衡,软组织噪声爆炸;换成CLAHE,骨骼清晰、软组织平滑——医生看了直点头。
# CLAHE 示例
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
clahe_img = clahe.apply(gray)
参数调优建议:
clipLimit:对比度限制,默认2.0。值越大,增强越强,但噪声也越明显。我一般设1.5-3.0。tileGridSize:分块大小,默认8×8。块越小,局部细节越丰富,但计算量越大。
3.4 实战组合拳:文档图像预处理
最后,我分享一个实际项目中的组合流程。处理一张歪斜、有噪点、曝光不均的文档照片:
- 几何校正:用仿射变换把文档扶正
- 灰度化:转成灰度图
- CLAHE增强:提升对比度,让文字更清晰
- 开运算去噪:去除扫描产生的椒盐噪声
- 闭运算连接:修复断裂的笔画
- 二值化:得到干净的黑白图像
这一套下来,OCR识别率能从50%提升到98%以上。我亲测有效。
总结一下:几何变换解决“位置不对”,形态学解决“形态不干净”,直方图均衡解决“亮度不均”。三者配合,能应对大部分图像预处理场景。