一、形态学到底在处理什么?
你 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 | 初始噪声和孔洞 |
| B | 3×3 开运算 | 小白点是否减少 |
| C | 5×5 开运算 | 细小植株是否损失 |
| D | 5×5 闭运算 | 小孔洞是否填补 |
| E | 5×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




