Day 8:Canny边缘检测

已经学习了 Sobel 算子,知道如何计算图像在 X、Y 两个方向上的灰度梯度。今天我们就要解决一个更进一步的问题:

既然 Sobel 已经能找到灰度变化明显的位置,为什么还需要 Canny?

核心原因是:Sobel 计算的是梯度响应,而 Canny 会进一步筛选、细化和连接边缘,最终生成更适合后续处理的二值边缘图。

今天我们主要完成四件事:理解 Canny 的四个步骤、掌握双阈值与滞后连接、完成农田图像对比实验,以及弄清楚 Canny 与 Sobel 的关系。

一、Canny 和 Sobel 到底有什么区别?

先回忆昨天的代码:

复制代码
cv::Sobel(gray, gradX, CV_32F, 1, 0, 3);
cv::Sobel(gray, gradY, CV_32F, 0, 1, 3);

cv::magnitude(gradX, gradY, magnitude);

你得到的是梯度幅值图,亮度越高通常说明局部灰度变化越强。

但直接用梯度幅值作为边缘结果,可能存在三个问题:

  • 一条实际边缘在梯度图里表现为较宽的亮带。
  • 土壤纹理和噪声也会产生大量梯度响应。
  • 只使用一个阈值,可能导致弱叶片边缘断裂。
    而 Canny 就是在梯度信息的基础上增加进一步处理。
对比 Sobel Canny
主要任务 估计图像梯度 提取二值边缘
输出内容 梯度强度、方向分量 通常为 0/255 边缘图
边缘细化 不包含完整的细化流程 使用非极大值抑制
边缘连接 无滞后连接 通过强、弱边缘连接
典型用途 梯度分析、纹理特征 轮廓边界、形状提取

可以简单理解成:

复制代码
Sobel:
这附近的灰度变化有多强?

Canny:
哪些灰度变化应该最终保留为边缘?

二、Canny 的四个核心步骤

1. 高斯平滑

已经学过:

复制代码
cv::GaussianBlur(
    gray,
    blurred,
    cv::Size(5, 5),
    1.0
);

为什么 Canny 常在梯度计算前进行平滑?

因为 Sobel 会对灰度变化产生响应,而噪声本身也可能制造灰度突变。

例如:

复制代码
原来:
100 100 100 100 100

受到噪声干扰:
100 100 240 100 100

中间的 240 可能产生明显梯度,但不是真实物体边缘。

高斯滤波能够减弱部分噪声,从而减少伪边缘。

需要区分算法原理和 OpenCV 接口:经典 Canny 流程包含高斯平滑,但调用 cv::Canny() 并不等于它会自动执行你指定参数的 GaussianBlur()。 因此今天我们明确将其写在 Canny 之前,方便实验。

2. 梯度计算

和昨天的 Sobel 有直接联系:

梯度不仅告诉我们变化的强弱,也告诉我们变化的方向。

梯度方向可由:

得到。

比如一条竖直边缘,灰度主要沿水平方向变化,因此梯度方向大致是水平的。

注意:梯度方向通常垂直于边缘的切线方向。

3. 非极大值抑制:为什么它能细化边缘?

这是今天最值得理解的新概念。

假设某个位置附近的梯度响应为:

复制代码
位置:     1   2   3   4   5
梯度强度: 10  40  90  50  20

如果直接显示梯度图,你可能看到连续几个较亮的像素。

但非极大值抑制会沿梯度方向比较相邻位置,优先保留局部最大值。

在这个简化的一维例子中:

复制代码
原始梯度:10  40  90  50  20

抑制后:   0   0  90   0   0

所以 Canny 的边缘通常比直接把 Sobel 梯度图二值化后得到的结果更细。

不过,并不意味着所有输出边缘都一定是一像素宽的完整曲线,实际结果仍受纹理、离散化和参数影响。

4. 双阈值与滞后连接:为什么需要两个阈值?

你今天使用:

复制代码
cv::Canny(gray, edges, 50, 150);

这里:

复制代码
50  → 低阈值
150 → 高阈值

经过梯度分析和非极大值抑制之后,可以将候选响应分成三类:

梯度响应 分类 处理方式
低于低阈值 非边缘 丢弃
位于两个阈值之间 弱候选边缘 检查是否与强边缘连通
高于高阈值 强边缘 保留

阈值相等处的具体处理边界这里暂不展开,重点理解三类候选与连通关系。

假设一条叶片边缘上的梯度响应是:

复制代码
180 --- 120 --- 90 --- 65 --- 160

其中:

复制代码
180、160 → 强边缘

120、90、65 → 弱候选边缘

如果这些像素沿着边缘连通,中间的弱候选可能被保留下来。

而远处单独出现的弱候选,即使数值也是 120,也可能被丢弃,因为它没有与强边缘连接。

这里与上一天的 threshold() 有一个本质区别:

Canny 的高低阈值判断的是梯度响应,不是原始灰度值。

比如原图中某个区域灰度为 200,但周围灰度也是 200,那么这个位置没有明显的局部变化,仍然可能不是边缘。

反过来,灰度为 30 和 100 的区域之间,也可能产生明显边缘。

三、正式学习 cv::Canny() 的参数

完整一些的调用方式:

复制代码
cv::Canny(
    blurred,
    edges,
    50,
    150,
    3,
    false
);
参数 作用 这里的设置
blurred 输入单通道 8 位图像 高斯平滑结果
edges 输出二值边缘图 由函数写入
50 低阈值 50
150 高阈值 150
3 Sobel 梯度核大小 apertureSize 3
false 是否使用 L2 梯度范数 默认 false

这里有一个值得连接昨天 Sobel 知识的细节。

昨天你使用的真实梯度幅值是:

但是 cv::Canny() 默认 L2gradient=false,使用的梯度强度近似是:

因此,即使是同一张图,Canny 阈值 50/150 也不能不加区分地直接套用到昨天的 L2 梯度幅值图上。

今天先使用默认设置就可以。以后需要更精细对比,再尝试 L2gradient=true。

四、完整 C++ 实验代码

建议你使用同一张具有明显苗行结构的农田图像,并且固定输入尺寸、灰度转换方式,只改变 Canny 的阈值。

下面的代码还会对比直接执行 Canny 和 先进行 GaussianBlur 再执行 Canny 的差别。

复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <filesystem>
#include <string>
#include <vector>

namespace fs = std::filesystem;

int main() {
    fs::create_directories("output");

    cv::Mat image = cv::imread("images/field.jpg");

    if (image.empty()) {
        std::cerr << "Failed to load image\n";
        return 1;
    }

    // 1. 转换成灰度图
    cv::Mat gray;
    cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);

    // 2. 高斯平滑
    cv::Mat blurred;
    cv::GaussianBlur(
        gray,
        blurred,
        cv::Size(5, 5),
        1.0
    );

    // 3. 比较不同阈值组合
    std::vector<std::pair<int, int>> thresholds = {
        {30, 90},
        {50, 150},
        {100, 200}
    };

    for (const auto& [low, high] : thresholds) {
        cv::Mat edges;

        cv::Canny(
            blurred,
            edges,
            low,
            high
        );

        std::string filename =
            "output/canny_" +
            std::to_string(low) + "_" +
            std::to_string(high) + ".png";

        cv::imwrite(filename, edges);

        // 统计边缘像素数量
        int edgeCount = cv::countNonZero(edges);

        double edgeRatio =
            100.0 * edgeCount / image.total();

        std::cout
            << "Thresholds: "
            << low << ", " << high
            << " | Edge pixels: " << edgeCount
            << " | Ratio: " << edgeRatio << "%\n";
    }

    // 4. 比较是否进行显式高斯平滑
    cv::Mat edgesNoBlur, edgesBlur;

    cv::Canny(gray, edgesNoBlur, 50, 150);
    cv::Canny(blurred, edgesBlur, 50, 150);

    cv::imwrite(
        "output/canny_no_blur.png",
        edgesNoBlur
    );

    cv::imwrite(
        "output/canny_with_blur.png",
        edgesBlur
    );

    std::cout << "No blur edge count: "
              << cv::countNonZero(edgesNoBlur) << '\n';

    std::cout << "With blur edge count: "
              << cv::countNonZero(edgesBlur) << '\n';

    return 0;
}

最终会生成五张边缘图:

复制代码
output/
├── canny_30_90.png
├── canny_50_150.png
├── canny_100_200.png
├── canny_no_blur.png
└── canny_with_blur.png

代码中:

复制代码
std::vector<std::pair<int, int>>

表示存储多组整数对,刚好可以用来管理低阈值和高阈值。

而:

复制代码
for (const auto& [low, high] : thresholds)

是 C++17 的结构化绑定写法,能够直接取出每组阈值的两个数。你的 CMake 项目需要启用 C++17。

五、三个阈值方案应该怎样比较?

方案 低阈值 高阈值 主要观察
A 30 90 是否有大量土壤纹理、杂散边缘
B 50 150 噪声与叶片边缘能否取得平衡
C 100 200 小叶片、弱边缘是否开始丢失

通常降低阈值容易产生更多边缘响应,提高阈值则会抑制一部分较弱的响应。

但边缘数量多并不意味着检测效果好。

对于你的无人机农田图像,我建议重点观察三个区域:单株幼苗的叶片边界、植株之间的土壤,以及沿苗行方向连续出现的边缘结构。

另外,比较高斯滤波前后效果时,注意两种方法使用同一个 Canny 阈值组合。这样才能看出显式平滑对噪声和细小叶片边缘的影响。

六、今天再连接一下 findContours()

你之前已经学习过:

复制代码
cv::findContours(
    cleanMask,
    contours,
    cv::RETR_EXTERNAL,
    cv::CHAIN_APPROX_SIMPLE
);

那今天能不能把 Canny 的 edges 直接传给 findContours()?

技术上可以,但要注意结果的含义。

之前:

复制代码
HSV → inRange → mask → findContours

你的 mask 白色区域代表候选绿色植株,所以提取的是这些区域的边界。

今天:

复制代码
GRAY → Canny → edges → findContours

白色像素代表灰度变化明显的边缘。

因此,一株植物可能产生很多轮廓:叶缘、叶脉、阴影分界、土壤纹理等。它们不一定对应独立的植株。

所以后续如果你想进行植株计数,不能简单把 Canny 轮廓数量当成植株数量。

相关推荐
kaixin_啊啊1 小时前
【零基础学AI】第 21 章——神经网络怎样工作
人工智能·深度学习·神经网络
郝学胜-神的一滴1 小时前
游戏设计:原型与开发 19:U3D工位布置与性格说明书
人工智能·算法·unity·unreal engine·游戏策划·游戏设计
Data-Miner1 小时前
AI和Excel怎么结合才能真提效
人工智能·excel
七夜zippoe1 小时前
Spring AI 函数调用:让 Java 方法变成 Agent 的工具(实战全流程)
java·人工智能·spring·ai·agent·函数调用
wflynn1 小时前
DeepSeek 开源 Super-agent 架构:AI 主动发现 Bug 并提交 PR
人工智能·ai
rolt2 小时前
[鉴赏]02 DHH-摇身一变鼓吹软件工程
人工智能·软件工程·uml
天天代码码天天2 小时前
我用 C# 做了个拍照测量小工具:贴一张标记,量长度和面积
人工智能
聪明蛋子哟2 小时前
高扩展性AI应用架构指南:融合RAG检索、Agent编排与OpenAPI/RPC工具网关的设计模式
人工智能·rpc·架构
一水鉴天2 小时前
决策树、三支决策与随机森林:从精确值到能所双亡双活 20261030(元宝)
人工智能·机器学习