音视频学习笔记十八——图像处理之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. 高斯滤波(噪声抑制)

  • 目的:消除图像噪声,避免噪声被误检为边缘
  • 原理
    • 使用高斯核进行卷积操作: 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}} 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]
    • 梯度幅值: G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2

    • 梯度方向: θ = arctan ⁡ 2 ( G y , G x ) \theta = \arctan2(G_y, G_x) θ=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);
}
相关推荐
To_OC7 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic8 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志8 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble9 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯10 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司11 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g12 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森12 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http