题记:前文介绍OpenCV的进阶操作,本节会继续介绍OpenCV检测相关。图像处理需要用到很多专业的算法,本人业余学习略知皮毛,只是庶竭驽钝叙其所得,在音视频学习Demo有一些的示例。文章或代码若有错误,也希望大佬不吝赐教。
1. 边缘检测
1.1. 梯度
梯度(Gradient)用于描述图像中像素值变化的强度和方向,在边缘检测、特征提取等任务中至关重要。梯度实际上就是像素周围的点差异,如Sobel算子,X方向右-左
,Y方向下-上
,一般相邻像素点的比重大于对角线权重。Sobel算子如下
css
Gx = [-1, 0, 1] Gy = [-1, -2, -1]
[-2, 0, 2] [ 0, 0, 0]
[-1, 0, 1] [ 1, 2, 1]
代码如下,Sobel算子结果取绝对值,再把两个方向相加。
css
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
cv::Mat grad_x, grad_y;
cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3);
cv::Sobel(gray, grad_y, CV_16S, 0, 1, 3);
cv::convertScaleAbs(grad_x, grad_x);
cv::convertScaleAbs(grad_y, grad_y);
cv::Mat combined;
cv::addWeighted(grad_x, 0.5, grad_y, 0.5, 0, combined);
Sobel效果:
1.2. Canny检测
Canny代码:
css
// 高斯模糊降噪
cv::GaussianBlur(gray, blurred, cv::Size(5,5), 0);
// Canny边缘检测
cv::Canny(blurred, edges, 80, 150);
Canny检测效果:
Canny在梯度基础上增加检测,相关步骤如下:
1.2.1. 高斯滤波(噪声抑制)
- 目的:消除图像噪声,避免噪声被误检为边缘
- 原理 :
- 使用高斯核进行卷积操作: <math xmlns="http://www.w3.org/1998/Math/MathML"> G ( x , y ) = 1 2 π σ 2 e − x 2 + y 2 2 σ 2 G(x,y) = \frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}} </math>G(x,y)=2πσ21e−2σ2x2+y2
- 核大小通常为5×5(OpenCV中可通过
cv2.Canny()
的apertureSize
参数调整)
- 效果 :
- σ值越大,平滑效果越强
- 在保留边缘的同时有效抑制噪声
1.2.2. 梯度计算(边缘强度与方向)
-
目的:找出图像中灰度变化最大的区域
-
原理:
-
使用Sobel算子计算水平和垂直梯度:
cssGx = [-1 0 1] Gy = [-1 -2 -1] [-2 0 2] [ 0 0 0] [-1 0 1] [ 1 2 1]
-
梯度幅值: <math xmlns="http://www.w3.org/1998/Math/MathML"> G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} </math>G=Gx2+Gy2
-
梯度方向: <math xmlns="http://www.w3.org/1998/Math/MathML"> θ = arctan 2 ( G y , G x ) \theta = \arctan2(G_y, G_x) </math>θ=arctan2(Gy,Gx)(角度范围:0°-180°)
-
-
方向量化:
- 将方向分为4个区间:0°, 45°, 90°, 135°
- 例如:22.5°~67.5° → 45°方向
1.2.3. 非极大值抑制(NMS)
- 目的:细化边缘,使边缘宽度变为单像素
- 原理 :
- 沿梯度方向比较当前像素与相邻像素
- 仅保留梯度值最大的像素(局部最大值)
- 操作示例 :
- 若当前像素梯度方向为90°(垂直)
- 比较其上、下相邻像素的梯度值
- 仅当当前像素梯度值 > 上下像素时保留
1.2.4. 双阈值检测
- 目的:区分真实边缘与噪声
- 原理 :
- 设置高低阈值:
threshold_low
和threshold_high
- 分类像素:
- 强边缘 :梯度值 >
threshold_high
- 弱边缘 :
threshold_low
< 梯度值 ≤threshold_high
- 非边缘 :梯度值 ≤
threshold_low
- 强边缘 :梯度值 >
- 设置高低阈值:
- 阈值选择经验 :
threshold_high ≈ 2-3 × threshold_low
- OpenCV默认比例:2:1(高阈值:低阈值)
1.2.5. 边缘连接(滞后阈值处理)
- 目的:连接断开的边缘,形成连续轮廓
- 原理 :
- 保留所有强边缘像素
- 仅保留与强边缘相连的弱边缘像素
- 孤立弱边缘视为噪声丢弃
- 实现方式 :
- 使用深度优先搜索(DFS)或连通区域分析
- 检查弱边缘像素的8邻域是否有强边缘
2. 轮廓检测
轮廓检测和边缘检测区别在于:
- 边缘:孤立的像素点或线段,表示局部像素的突变。
- 轮廓 :由边缘连接而成的连续、封闭的曲线,是对物体边界的整体描述(需要算法将离散边缘 "连接" 起来)。
2.1 效果
原图 | 轮廓 |
---|---|
![]() |
![]() |
OpenCV代码:
c
// 转换为灰度图
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
// 二值化处理
cv::Mat binary;
cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY);
// 寻找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// 绘制轮廓(使用随机颜色)
cv::Mat result(mat.size(), mat.type());
for (size_t i = 0; i < contours.size(); i++) {
cv::Scalar color(rand() % 256, rand() % 256, rand() % 256);
cv::drawContours(result, contours, i, color, 2, cv::LINE_8, hierarchy, 0);
}
return result;
2.2 原理
2.2.1 预处理
由于轮廓检测依赖边缘,需先通过预处理增强边缘的对比度:
- 灰度化:将彩色图像转为灰度图。
- 二值化 :通过阈值分割(如
cv2.threshold
)将灰度图转为黑白二值图,使物体(前景)和背景的边界更清晰(非黑即白,边缘处像素值突变更显著)。 - (可选)去噪 :若图像有噪声,可先用高斯模糊(
cv2.GaussianBlur
)平滑图像,避免噪声被误判为边缘。
2.2.2 查找
OpenCV 中cv2.findContours()
函数是轮廓检测的核心,其底层原理基于 **"轮廓跟踪" 算法 **,大致流程如下:
- 遍历像素:从二值图像的左上角开始,逐行扫描像素,寻找第一个非零像素(即物体的起点)。
- 跟踪边界 :以起点为基准,按照一定规则(如顺时针或逆时针)跟踪相邻的非零像素,直到回到起点,形成一个封闭的轮廓。
- 边界判断:跟踪时,始终沿着 "前景与背景的交界" 移动。例如,当前像素是前景(非 0),则寻找其邻域中 "从背景(0)到前景(非 0)" 的过渡点,作为下一个轮廓点。
- 避免重复:每跟踪一个像素,就标记为 "已处理",防止同一像素被多次计入轮廓。
- 区分层次 :若轮廓内部还有其他轮廓(如 "回" 字的外框和内框),算法会记录它们的嵌套关系(即
hierarchy
层次信息)。
轮廓近似:简化冗余点
原始轮廓可能包含大量冗余像素点(例如一条直线上的所有点),cv2.findContours()
通过Douglas-Peucker 算法(道格拉斯 - 普克算法)进行轮廓近似,原理如下:
- 对轮廓上的点,找到距离当前线段最远的点,若距离大于阈值,则保留该点并递归分割线段;否则,用两端点连接的线段替代原曲线。
- 例如:
cv2.CHAIN_APPROX_SIMPLE
模式会删除直线上的冗余点,只保留端点(如矩形轮廓仅保留 4 个角点),大大减少计算量。
返回值:
contours
:轮廓列表,每个轮廓是一个 numpy 数组(形状为(N, 1, 2)
,存储像素坐标)。hierarchy
:轮廓层次信息(用于描述轮廓之间的嵌套关系)。
常用模式(mode) :
cv2.RETR_EXTERNAL
:只检测最外层轮廓。cv2.RETR_LIST
:检测所有轮廓,不建立层次关系。cv2.RETR_CCOMP
:检测所有轮廓,建立两层层次(外层和内层)。cv2.RETR_TREE
:检测所有轮廓,建立完整的层次树。
常用近似方法(method) :
cv2.CHAIN_APPROX_NONE
:存储所有轮廓点(精确但冗余)。cv2.CHAIN_APPROX_SIMPLE
:压缩水平 / 垂直 / 对角线方向的冗余点(保留端点,更高效)。
2.2.3 画线
cv::drawContours
是 OpenCV 中用于在图像上绘制轮廓的核心函数,可将 cv::findContours
提取的轮廓可视化。它支持绘制单个或多个轮廓,并可自定义颜色、线宽等参数
进阶操作:轮廓特征分析 提取轮廓后,可通过 OpenCV 函数计算轮廓的关键特征:
- 面积 :
cv2.contourArea(cnt)
- 周长 :
cv2.arcLength(cnt, closed=True)
(closed=True
表示闭合轮廓) - 边界矩形 :
x, y, w, h = cv2.boundingRect(cnt)
(外接矩形) - 最小外接圆 :
(x, y), radius = cv2.minEnclosingCircle(cnt)
- 凸包 :
hull = cv2.convexHull(cnt)
3. 角点检测
角点是图像中两个边缘的交点 ,或灰度值在多个方向上发生剧烈变化的点。例如:
- 棋盘格的交叉点(x 方向和 y 方向均有灰度突变);
- 矩形物体的四个拐角(水平和垂直方向均有突变)。
原图 | 角点 |
---|---|
![]() |
![]() |
代码如下:
ini
cv::Mat result = mat.clone();
// 将图像转换为灰度图
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
// 设置参数
int maxCorners = 100;
double qualityLevel = 0.01;
double minDistance = 10;
int blockSize = 3;
bool useHarrisDetector = false;
double k = 0.04;
// 检测角点
std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(
gray, // 输入灰度图
corners, // 输出角点
maxCorners, // 最大角点数量
qualityLevel, // 角点质量阈值
minDistance, // 角点间最小距离
cv::Mat(), // 掩码
blockSize, // 邻域大小
useHarrisDetector, // 是否使用Harris检测器
k // Harris检测器参数
);
// 在原始图像上绘制检测到的角点
for (size_t i = 0; i < corners.size(); i++) {
cv::circle(result, corners[i], 5, cv::Scalar(0, 0, 255), -1);
}
4. 直线检测
直线检测的核心算法是 霍夫变换(Hough Transform) ,它能从图像中提取具有直线特征的像素集合。霍夫变换通过将图像空间中的直线转换到参数空间进行检测,对噪声和部分遮挡有较强的鲁棒性。以下是直线检测的原理、常用方法及实现:
4.1. 霍夫变换的基本原理
在直角坐标系中,直线可表示为 y = kx + b
(k
为斜率,b
为截距),但斜率 k
在直线垂直时会无穷大,不便计算。霍夫变换采用极坐标表示:ρ=xcosθ+ysinθ其中:
(x, y)
是图像中的像素坐标;ρ
(rho)是原点到直线的垂直距离;θ
(theta)是垂线与 x 轴的夹角(范围通常为[-90°, 90°]
或[0°, 180°]
)。
每个像素 (x, y)
对应极坐标中一条正弦曲线(ρ
随 θ
变化),多条曲线的交点 (ρ, θ)
即对应图像中多条直线的参数。
4.2. 霍夫变换的检测流程
- 边缘检测:先通过 Canny 等算法提取图像边缘(直线由边缘像素构成)。
- 参数空间投票 :为每个边缘像素,在
(ρ, θ)
参数空间中对应的曲线上 "投票"(累加计数)。 - 阈值筛选 :投票数超过阈值的
(ρ, θ)
即为检测到的直线参数。
原图 | 直线检测 |
---|---|
![]() |
![]() |
代码 cv::Mat result = mat.clone();
c
// 将图像转换为灰度图
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
// 高斯模糊降噪
cv::Mat blurred;
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
// Canny边缘检测
cv::Mat edges;
cv::Canny(blurred, edges, 50, 150, 3);
// 使用HoughLines检测直线(标准霍夫变换)
std::vector<cv::Vec2f> lines;
cv::HoughLines(
edges, // 输入边缘图
lines, // 输出直线集合
1, // 距离分辨率(像素)
CV_PI / 180, // 角度分辨率(弧度)
200, // 累加器阈值
0, // srn
0 // stn
);
// 绘制检测到的直线
for (size_t i = 0; i < lines.size(); i++) {
float rho = lines[i][0];
float theta = lines[i][1];
double a = cos(theta);
double b = sin(theta);
double x0 = a * rho;
double y0 = b * rho;
// 计算直线的两个端点
cv::Point pt1(cvRound(x0 + 1000 * (-b)), cvRound(y0 + 1000 * (a)));
cv::Point pt2(cvRound(x0 - 1000 * (-b)), cvRound(y0 - 1000 * (a)));
// 绘制红色直线
cv::line(result, pt1, pt2, cv::Scalar(0, 0, 255), 2);
}