音视频学习笔记十八——图像处理之OpenCV检测

题记:前文介绍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算子计算水平和垂直梯度:

      css 复制代码
      Gx = [-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_lowthreshold_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 + bk 为斜率,b 为截距),但斜率 k 在直线垂直时会无穷大,不便计算。霍夫变换采用极坐标表示:ρ=xcosθ+ysinθ其中:

  • (x, y) 是图像中的像素坐标;
  • ρ(rho)是原点到直线的垂直距离;
  • θ(theta)是垂线与 x 轴的夹角(范围通常为 [-90°, 90°][0°, 180°])。

每个像素 (x, y) 对应极坐标中一条正弦曲线(ρθ 变化),多条曲线的交点 (ρ, θ) 即对应图像中多条直线的参数

4.2. 霍夫变换的检测流程

  1. 边缘检测:先通过 Canny 等算法提取图像边缘(直线由边缘像素构成)。
  2. 参数空间投票 :为每个边缘像素,在 (ρ, θ) 参数空间中对应的曲线上 "投票"(累加计数)。
  3. 阈值筛选 :投票数超过阈值的 (ρ, θ) 即为检测到的直线参数。
原图 直线检测

代码 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);
}
相关推荐
摸着石头过河的石头2 小时前
从零开始玩转前端:一站式掌握Web开发基础知识
前端·javascript
sniper_fandc3 小时前
关于Mybatis-Plus的insertOrUpdate()方法使用时的问题与解决—数值精度转化问题
java·前端·数据库·mybatisplus·主键id
10岁的博客3 小时前
技术博客SEO优化全攻略
前端
南屿im4 小时前
别再被引用坑了!JavaScript 深浅拷贝全攻略
前端·javascript
想要一辆洒水车4 小时前
vuex4源码分析学习
前端
sophie旭4 小时前
一道面试题,开始性能优化之旅(6)-- 异步任务和性能
前端·javascript·性能优化
年少不知有林皇错把梅罗当球王4 小时前
vue2、vue3中使用pb(Base64编码)
前端
FanetheDivine4 小时前
常见的AI对话场景和特殊情况
前端·react.js
sophie旭4 小时前
一道面试题,开始性能优化之旅(5)-- 浏览器和性能
前端·面试·性能优化