3. OpenCV基础(二):图像几何变换、滤波与边缘检测

各位好,欢迎来到第三章。上一章我们把OpenCV的基本IO和像素操作捋了一遍,算是把工具箱打开了。这一章咱们要往里塞几个真正干活用的家伙——几何变换、图像滤波、边缘检测。这三板斧在工业项目里几乎天天见,我自己的经验是,搞懂它们,你就能处理掉80%的图像预处理需求。

本章核心脉络:几何变换解决“图像怎么摆”的问题,滤波解决“图像干不干净”的问题,边缘检测解决“图像里有什么轮廓”的问题。三者层层递进,是后续YOLO训练数据增强和预处理的理论基础。

第3章 知识体系结构 OpenCV图像预处理 几何变换 图像滤波 边缘检测 缩放 旋转 仿射 透视 均值 高斯 中值 双边 Canny Sobel / Laplacian 三者关系:滤波 → 边缘检测 → 几何变换(或按需组合)

3.1 图像几何变换:让图像动起来

几何变换说白了就是改变图像里像素的位置。你想想看,拍照时角度歪了、物体太小了、或者想做个数据增强,都得靠它。OpenCV里最常用的就四种:缩放、旋转、仿射、透视。

3.1.1 缩放与旋转

缩放用 cv2.resize(),这个最简单。但我提醒一句:插值方法选不对,效果天差地别。

场景 推荐插值方法 原因
缩小图像 cv2.INTER_AREA 避免摩尔纹,保留区域特征
放大图像 cv2.INTER_LINEAR 或 cv2.INTER_CUBIC 线性/三次插值,边缘更平滑
YOLO训练数据增强 cv2.INTER_LINEAR 速度与质量平衡,我项目里一直用这个
import cv2
import numpy as np

img = cv2.imread('example.jpg')
# 缩放:指定宽高
resized = cv2.resize(img, (416, 416), interpolation=cv2.INTER_LINEAR)
# 缩放:按比例
h, w = img.shape[:2]
scale_resized = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

# 旋转:先获取旋转矩阵
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)  # 旋转45度,不缩放
rotated = cv2.warpAffine(img, M, (w, h))

我的经验:做YOLO训练时,我习惯把图像统一缩放到416x416或640x640,用INTER_LINEAR。别用INTER_NEAREST,那个锯齿感太强,模型学到的特征会带噪声。

3.1.2 仿射变换与透视变换

这两个容易搞混,我简单说一下区别:

  • 仿射变换:保持平行线平行。平移、旋转、缩放、剪切都属于仿射。需要3个点对来求解变换矩阵。
  • 透视变换:平行线可能不再平行。模拟人眼视角变化,比如把倾斜的文档拍正。需要4个点对。

我曾经在做一个车牌识别项目时,摄像头安装角度不对,拍出来的车牌是梯形的。用仿射变换怎么调都歪,后来换成透视变换,一下就正了。嗯,这里要注意:透视变换的4个点必须按顺序(左上、右上、左下、右下)传入,否则图像会拧成麻花。

# 仿射变换:3个点
pts1 = np.float32([[50,50], [200,50], [50,200]])
pts2 = np.float32([[10,100], [200,50], [100,250]])
M_affine = cv2.getAffineTransform(pts1, pts2)
affine_img = cv2.warpAffine(img, M_affine, (w, h))

# 透视变换:4个点
pts1 = np.float32([[56,65], [368,52], [28,387], [389,390]])
pts2 = np.float32([[0,0], [300,0], [0,300], [300,300]])
M_perspective = cv2.getPerspectiveTransform(pts1, pts2)
perspective_img = cv2.warpPerspective(img, M_perspective, (300, 300))

避坑指南:我曾经在透视变换时没注意坐标顺序,结果图像被翻转了180度。调试了半天才发现是点顺序错了。建议用np.float32确保数据类型,否则OpenCV会报类型错误。

3.2 图像滤波:去噪与保边

滤波的本质是“用邻域像素的某种统计值替换当前像素”。说白了就是让图像变模糊,或者去掉噪点。但不同滤波器的脾气不一样,选错了要么糊成一片,要么噪点纹丝不动。

3.2.1 四种滤波器对比

滤波器 原理 适用场景 缺点
均值滤波 邻域像素取平均 简单去噪,要求不高时 边缘模糊严重
高斯滤波 按高斯权重加权平均 通用去噪,保留一定边缘 对椒盐噪声效果一般
中值滤波 取邻域像素中位数 椒盐噪声、脉冲噪声 计算慢,对大噪声效果有限
双边滤波 空间距离+像素值差异双重权重 保边去噪,美颜、预处理 参数敏感,速度慢
# 四种滤波一行代码
img_blur = cv2.blur(img, (5, 5))                    # 均值
img_gaussian = cv2.GaussianBlur(img, (5, 5), 1.5)   # 高斯
img_median = cv2.medianBlur(img, 5)                 # 中值
img_bilateral = cv2.bilateralFilter(img, 9, 75, 75) # 双边

核心建议:做边缘检测前,我通常先用高斯滤波去噪。双边滤波虽然保边效果好,但参数调起来很费时间。如果你只是做YOLO训练前的预处理,高斯滤波+中值滤波的组合就够用了。

3.2.2 参数怎么调?

我个人的习惯是:

  • 核大小:必须是奇数。3x3、5x5、7x7最常见。核越大,模糊越强,但细节丢失也越多。
  • 高斯滤波的sigma:一般设为0,OpenCV会自动根据核大小计算。手动设的话,sigma越大越模糊。
  • 双边滤波的sigmaColor和sigmaSpace:sigmaColor控制像素值差异的敏感度,sigmaSpace控制空间距离的影响。我一般从sigmaColor=50、sigmaSpace=50开始调。

你想想看,如果sigmaColor设得太大,双边滤波就跟高斯滤波没区别了,保边效果就没了。反过来设得太小,噪点根本去不掉。这个平衡点需要根据实际图像试。

3.3 边缘检测:Canny算子

边缘检测是计算机视觉的基石。YOLO虽然不直接依赖边缘检测,但做数据预处理、背景分割、ROI提取时,Canny几乎是标配。

3.3.1 Canny的四个步骤

Canny不是简单的算子,而是一个流程。我当年面试时被问过好几次,其实记住四个步骤就行:

  1. 高斯滤波去噪——先让图像干净点
  2. 计算梯度幅值和方向——用Sobel算子算水平和垂直梯度
  3. 非极大值抑制——把不是边缘的像素去掉,只保留局部梯度最大的点
  4. 双阈值检测与边缘连接——高阈值确定强边缘,低阈值保留弱边缘,但只有与强边缘相连的弱边缘才保留
# Canny边缘检测:一行代码搞定
edges = cv2.Canny(img, 100, 200)  # 低阈值100,高阈值200

# 如果想先滤波再检测,可以这样
blurred = cv2.GaussianBlur(img, (5, 5), 1.5)
edges = cv2.Canny(blurred, 50, 150)

3.3.2 双阈值怎么设?

这是Canny里最让人头疼的地方。我分享一个经验:

  • 高阈值:设为图像最大梯度值的70%-80%。可以用np.max()先看看梯度图的范围。
  • 低阈值:设为高阈值的1/2到1/3。比如高阈值200,低阈值就设100或66。
  • 经验值:对于光照均匀的图片,我常用(50, 150)或(100, 200)。对于光照变化大的场景,我会用(30, 100)。

调试技巧:我曾经在调试Canny参数时,写了个滑动条回调函数,实时调整阈值看效果。你可以用cv2.createTrackbar()快速搭建一个调参工具,比硬编码改值高效得多。

3.3.3 Canny vs Sobel vs Laplacian

简单说两句:Sobel和Laplacian是基础算子,直接算梯度。Canny是它们的“升级版”,加了非极大值抑制和双阈值,所以边缘更细、更干净。我个人的建议是:除非你有特殊需求,否则直接用Canny。Sobel和Laplacian更适合做梯度特征提取,而不是最终边缘输出。

# Sobel和Laplacian作为补充
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
laplacian = cv2.Laplacian(img, cv2.CV_64F)

注意:Sobel和Laplacian输出的是浮点数,直接显示会全黑或全白。记得用cv2.convertScaleAbs()转成8位无符号整数再显示。我刚开始学时就踩过这个坑,显示出来一片黑,还以为代码写错了。

3.4 实战组合:滤波+边缘检测+几何变换

最后给一个实际项目中的组合用法。比如你要从一张照片里提取某个物体的轮廓,然后把它摆正:

# 1. 读取图像
img = cv2.imread('object.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 高斯滤波去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)

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

# 4. 找轮廓(下一章会细讲)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 5. 假设找到了一个矩形轮廓,做透视变换摆正
# 这里省略轮廓筛选和点提取的细节
# 最终用 cv2.warpPerspective() 把物体摆正

这个流程我在做工业零件检测时用过很多次。先滤波去噪,再边缘检测找轮廓,最后用透视变换把零件摆正,方便后续测量或识别。每一步的参数都需要根据实际图像微调,但整体框架是通用的。