zwc3210432055头像
Day 9:腐蚀、膨胀、开运算与闭运算封面图

Day 9:腐蚀、膨胀、开运算与闭运算

一、形态学到底在处理什么?

你 Day 5 得到的 HSV mask 是一张单通道二值图:

0   → 背景
255 → 满足绿色阈值的候选区域

例如:

000000000
001110000
001010000
001110010
000000000

其中可能存在:

  • 大块白色区域:候选植株。
  • 白色区域内部的小黑洞:分割缺失。
  • 背景中的小白点:分割噪声。
  • 两个白色区域之间的狭窄间隙:可能是两株独立植物。
    形态学的作用,是根据某个像素周围的局部结构,重新决定该位置是黑色还是白色。
    这里就需要一个关键工具:结构元素(structuring element),也就是你代码中的 kernel。

二、先理解 kernel 的形状

例如:

cv::Mat kernel = cv::getStructuringElement(
    cv::MORPH_RECT,
    cv::Size(5, 5)
);

它生成一个 5×5 的矩形结构元素。
而:

cv::MORPH_ELLIPSE

会生成椭圆形结构元素。

两种结构元素的区别是:

结构元素特点可能的影响
MORPH_RECT矩形邻域,包含角落位置对水平、垂直及对角区域影响较全面
MORPH_ELLIPSE更接近圆形或椭圆对自然弯曲区域可能更合适

但注意:椭圆结构元素并不一定比矩形更好。最终还要看植株大小、叶片形状、间距以及你希望保留的结构。

三、腐蚀:为什么小白点可能消失?

OpenCV:

cv::erode(mask, eroded, kernel);

对于二值图,腐蚀可以理解为:

将结构元素放在某个位置,只有它覆盖到的有效位置全部属于白色前景,输出位置才保留为白色。

以 3×3 全 1 矩形 kernel 为例:

1 1 1
1 1 1
1 1 1

假设某个局部区域为:

255 255 255
255 255 255
255 255 255

中心经过腐蚀之后仍为 255。
但如果局部区域为:

  0   0   0
  0 255   0
  0   0   0

中心位置就会被腐蚀为 0。
因为 3×3 邻域不是全部白色。
于是孤立白点消失。

为什么会误删小苗?

假设一片叶子在 mask 中只有 2 个像素宽,但你用了 5×5 的结构元素。
腐蚀时,在这条细叶片内部很难找到完整满足结构元素的白色邻域,因此叶片可能整个消失。
这不是 OpenCV 出错,而是你设置的结构元素比真实目标的局部结构还大。

四、膨胀:为什么两株苗可能粘连?

OpenCV:

cv::dilate(mask, dilated, kernel);

对于二值图,膨胀可以理解为:

只要结构元素覆盖的有效邻域中存在白色前景,当前位置就可能变成白色。

所以白色区域会向周围扩张。
例如两块白色区域原本相距几个像素:

███      ███
███      ███

膨胀之后可能变成:

████████████
████████████

于是本来分开的两个区域连成了一片。
这会产生一个非常实际的后果:

原来:
两个白色连通区域

膨胀以后:
一个白色连通区域

以后用 findContours() 或连通域算法进行候选植株统计时,可能把两株植物统计成一株。
所以腐蚀和膨胀具有相反的趋势:腐蚀倾向于收缩前景,膨胀倾向于扩张前景。
但它们不是严格的互逆操作。腐蚀已经消失的小目标,后续膨胀通常无法凭空恢复。

五、开运算和闭运算究竟做了什么?

你之前已经知道:

开运算:腐蚀 → 膨胀
闭运算:膨胀 → 腐蚀

但今天要特别理解:为什么两个看似相反的步骤组合起来,最终仍然能够改变图像?

1. 开运算:先缩小,再恢复
cv::morphologyEx(
    mask,
    opened,
    cv::MORPH_OPEN,
    kernel
);

假设原始 mask 里有一个较大的植株区域,以及一个孤立小白点:

大块白色区域       小白点
    █████
    █████             █
    █████

执行腐蚀后:

大区域缩小         小白点消失
     ███
     ███

再膨胀:

较大区域恢复一部分
    █████
    █████
    █████

但是小白点已经彻底消失,膨胀无法把它恢复。
因此,开运算特别适合去除比结构元素小的孤立前景区域,也可能去掉细小突出结构、断开窄连接。

2. 闭运算:先扩张,再收缩
cv::morphologyEx(
    mask,
    closed,
    cv::MORPH_CLOSE,
    kernel
);

假设植株区域内部存在一个小黑洞:

███████
███ ███
███████

先膨胀时,周围白色区域向黑洞扩张,可能将其填满。
然后腐蚀,使外侧边界恢复一部分。
结果可能是:

███████
███████
███████

但也要注意,闭运算可能连接距离很近的两个白色区域。
因此,闭运算并不是只作用于内部孔洞,它同样可能改变外部形状和目标间距。

3. 开闭运算的区别
运算执行顺序主要作用需要警惕
腐蚀单独腐蚀缩小前景、去除细小区域小苗消失、叶片断裂
膨胀单独膨胀扩大前景、连接近邻多株粘连
开运算腐蚀→膨胀去除小白点、细突出真实小目标被删除
闭运算膨胀→腐蚀填小孔、连狭窄间隙相邻目标被连接

还有一个重要的理解:
开运算不等于先腐蚀一次、再通过膨胀完全恢复;闭运算同样不等于先膨胀再完全恢复。
因为在第一步中,某些局部结构已经消失或发生了连通性改变。

六、今天的五组正式实验

使用 Day 5 保存的 HSV 绿色 mask,分别进行以下处理:

实验处理方式需要观察
A原始 mask初始噪声和孔洞
B3×3 开运算小白点是否减少
C5×5 开运算细小植株是否损失
D5×5 闭运算小孔洞是否填补
E5×5 先开后闭是否同时改善两类问题

此外,再分别使用 MORPH_RECT 和 MORPH_ELLIPSE 完成相同实验,观察结构元素形状的影响。

完整 C++ 实验代码

这份代码沿用你前面学过的 cv::Mat、vector、函数封装和 C++17 filesystem。


#include <opencv2/opencv.hpp>
#include <iostream>
#include <filesystem>
#include <string>
#include <vector>
#include <iomanip>

namespace fs = std::filesystem;

// 保存mask,并统计白色像素数量
void saveAndPrint(
    const cv::Mat& mask,
    const std::string& name
) {
    std::string path = "output/" + name + ".png";

    if (!cv::imwrite(path, mask)) {
        std::cerr << "Failed to save: " << path << '\n';
        return;
    }

    int whitePixels = cv::countNonZero(mask);

    double percentage =
        100.0 * whitePixels / mask.total();

    std::cout << std::fixed << std::setprecision(2);

    std::cout << name
              << " | White pixels: " << whitePixels
              << " | Foreground: " << percentage << "%\n";
}

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

    // Day 5生成的HSV二值mask
    cv::Mat mask = cv::imread(
        "images/hsv_mask.png",
        cv::IMREAD_GRAYSCALE
    );

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

    // 确保输入是0/255二值图
    cv::threshold(
        mask, mask, 127, 255, cv::THRESH_BINARY
    );

    // A:原始mask
    saveAndPrint(mask, "A_original");

    // 两种结构元素
    std::vector<std::pair<int, std::string>> shapes = {
        {cv::MORPH_RECT, "rect"},
        {cv::MORPH_ELLIPSE, "ellipse"}
    };

    for (const auto& [shape, shapeName] : shapes) {

        cv::Mat kernel3 = cv::getStructuringElement(
            shape, cv::Size(3, 3)
        );

        cv::Mat kernel5 = cv::getStructuringElement(
            shape, cv::Size(5, 5)
        );

        cv::Mat open3, open5, close5, openClose;

        // B:3×3开运算
        cv::morphologyEx(
            mask, open3, cv::MORPH_OPEN, kernel3
        );

        // C:5×5开运算
        cv::morphologyEx(
            mask, open5, cv::MORPH_OPEN, kernel5
        );

        // D:5×5闭运算
        cv::morphologyEx(
            mask, close5, cv::MORPH_CLOSE, kernel5
        );

        // E:先5×5开,再5×5闭
        cv::morphologyEx(
            open5, openClose, cv::MORPH_CLOSE, kernel5
        );

        saveAndPrint(open3, "B_open3_" + shapeName);
        saveAndPrint(open5, "C_open5_" + shapeName);
        saveAndPrint(close5, "D_close5_" + shapeName);
        saveAndPrint(openClose, "E_open_close_" + shapeName);
    }

    return 0;
}

这里的路径 images/hsv_mask.png 是示例路径,运行时需要改成你 Day 5 实际保存的 mask 文件路径。程序假设从项目根目录运行。

为什么要强调 E 组“先开后闭”?

因为:

cv::morphologyEx(
    open5,
    openClose,
    cv::MORPH_CLOSE,
    kernel5
);

这里的输入是 open5,而不是最初的 mask。
流程:

原始 mask
     ↓
开运算
     ↓
去除一部分小前景噪声
     ↓
闭运算
     ↓
填补一部分孔洞和间隙
     ↓
最终结果

如果第二次输入仍然写 mask,那么你得到的就只是独立执行的闭运算结果,不是先开后闭。

七、为什么不能只比较白色像素数量?

你今天计划记录:

cv::countNonZero(mask);

这是很好的初步指标,但还不足以判断形态学处理效果。
例如:

因此,除了 countNonZero(),今天还建议记录几个观察结果:

观察指标需要回答的问题
白色像素数量前景面积增加还是减少?
孤立小白点是否得到有效清理?
细小叶片是否被错误删除?
植株间距两株苗是否发生粘连?
内部黑洞孔洞是否被填补?

等到后面 学习连通域分析时,还可以增加:

cv::connectedComponentsWithStats(...)

统计连通区域数量、面积分布等信息。
这样就能进一步验证今天的猜想:为什么结构元素变大后,连通域数量可能减少?
不过这里要提前注意,连通域数量下降既可能来自去除了小噪声,也可能来自植株粘连,不能单凭数量下降认定效果更好。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/zwc3210432055/article/details/169826047

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--