一、先理解 threshold():根据像素亮度分割
假设你有一张农田图片:
cv::Mat image = cv::imread("images/field.jpg");
先将它转换为灰度图:
cv::Mat gray;
cv::cvtColor(
image,
gray,
cv::COLOR_BGR2GRAY
);
这时:
原始 BGR 图像
↓
cv::cvtColor()
↓
单通道灰度图
对于常见的 8 位灰度图,每个像素取值为 0~255。
然后执行:
cv::Mat binary;
cv::threshold(
gray,
binary,
128,
255,
cv::THRESH_BINARY
);
这里五个参数分别代表:
| 参数 | 含义 |
|---|---|
gray |
输入灰度图 |
binary |
输出二值图 |
128 |
判断阈值 |
255 |
满足条件时的输出值 |
cv::THRESH_BINARY |
二值化方式 |
threshold 如何判断一个像素?
对于 THRESH_BINARY,规则是:

这里要特别注意:
threshold() 的 THRESH_BINARY 是大于 阈值才输出 255,而不是大于等于。
假设一张灰度图像只有六个像素:
30 80 120
128 180 230
阈值为 128 时:
0 0 0
0 255 255
因为 128 本身不大于 128。
所以你可以先形成一个直观理解:
THRESH_BINARY相当于根据单通道数值,把图像分成两部分。
对于灰度图,通常表现为将较亮与较暗的像素分开。
二、理解 inRange():判断像素是否落在指定区间
之前已经使用过:
cv::Mat hsv, mask;
cv::cvtColor(
image,
hsv,
cv::COLOR_BGR2HSV
);
cv::inRange(
hsv,
cv::Scalar(35, 40, 40),
cv::Scalar(85, 255, 255),
mask
);
这里的逻辑与灰度阈值不一样。
它并不是简单判断:
像素是否大于某个值?
而是判断:
像素的所有通道数值,是否同时落在指定上下限之间?
对于 HSV 像素 (H,S,V),上述代码对应:

三个条件全部满足才输出 255。
否则输出 0。
例如:
像素 A:(60,170,190)
满足全部条件,结果:
255
而:
像素 B:(60,20,190)
虽然 H 满足,但 S 不满足,结果:
0
一个很重要的区别:
threshold(..., THRESH_BINARY)通常是在一个灰度通道上判断是否大于某个值。inRange()可以在多个通道上同时检查上下限,也可以用于单通道数据。
因此,你的 HSV 绿色植株分割更适合使用inRange()。
三、实验:三张农田图像,两种分割方式
这部分是今天真正需要动手完成的内容。
我建议你选择三种不同光照情况的农田图片:
| 图片 | 拍摄环境 | 重点观察 |
|---|---|---|
| image1 | 光照均匀 | 两种方法能否保留绿色植株 |
| image2 | 阳光强烈、存在高光 | 灰度阈值是否误选明亮土壤 |
| image3 | 阴影明显 | 是否漏掉阴影中的叶片 |
为了保证实验公平,先保持所有图片的原始尺寸,不使用形态学、面积过滤或其他后处理。
这样你比较的才是两种阈值方法本身,而不是后处理带来的改善。
1. 推荐的项目结构
ThresholdDemo/
├── CMakeLists.txt
├── src/
│ └── main.cpp
├── images/
│ ├── image1.jpg
│ ├── image2.jpg
│ └── image3.jpg
├── build/
└── output/
2. 完整 C++ 代码
这份代码会批量读取三张图片,并且自动生成你要求的六张 mask,同时打印每张 mask 保留的像素数及比例。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <iomanip>
namespace fs = std::filesystem;
int main() {
// 在项目根目录执行程序时使用
const std::string inputDir = "images";
const std::string outputDir = "output";
fs::create_directories(outputDir);
std::vector<std::string> imageNames = {
"image1",
"image2",
"image3"
};
const double grayThreshold = 128.0;
const cv::Scalar lowerGreen(35, 40, 40);
const cv::Scalar upperGreen(85, 255, 255);
for (const auto& name : imageNames) {
// 1. 读取图片
fs::path imagePath =
fs::path(inputDir) / (name + ".jpg");
cv::Mat image =
cv::imread(imagePath.string());
if (image.empty()) {
std::cerr
<< "Failed to load: "
<< imagePath << '\n';
continue;
}
// 2. 灰度阈值分割
cv::Mat gray;
cv::Mat grayMask;
cv::cvtColor(
image,
gray,
cv::COLOR_BGR2GRAY
);
cv::threshold(
gray,
grayMask,
grayThreshold,
255,
cv::THRESH_BINARY
);
// 3. HSV 绿色阈值分割
cv::Mat hsv;
cv::Mat hsvMask;
cv::cvtColor(
image,
hsv,
cv::COLOR_BGR2HSV
);
cv::inRange(
hsv,
lowerGreen,
upperGreen,
hsvMask
);
// 4. 保存两种 mask
fs::path grayPath =
fs::path(outputDir) / (name + "_gray_mask.png");
fs::path hsvPath =
fs::path(outputDir) / (name + "_hsv_mask.png");
bool savedGray =
cv::imwrite(grayPath.string(), grayMask);
bool savedHSV =
cv::imwrite(hsvPath.string(), hsvMask);
if (!savedGray || !savedHSV) {
std::cerr << "Failed to save masks for "
<< name << '\n';
}
// 5. 统计白色像素
int grayCount = cv::countNonZero(grayMask);
int hsvCount = cv::countNonZero(hsvMask);
double totalPixels =
static_cast<double>(image.total());
double grayRatio =
100.0 * grayCount / totalPixels;
double hsvRatio =
100.0 * hsvCount / totalPixels;
// 6. 输出统计信息
std::cout << "\nImage: " << name << '\n';
std::cout
<< "Size: "
<< image.cols << " x "
<< image.rows << '\n';
std::cout << std::fixed
<< std::setprecision(2);
std::cout
<< "Gray foreground: "
<< grayCount
<< " (" << grayRatio << "%)\n";
std::cout
<< "HSV foreground: "
<< hsvCount
<< " (" << hsvRatio << "%)\n";
}
return 0;
}
其中有一个你之前没有深入学过的函数:
cv::countNonZero(mask)
它的作用是:
统计单通道图像中非零像素的数量。
因为你生成的是 0/255 二值 mask,所以它统计的就是白色像素的数量。
例如:
图像总像素数:100000
白色像素数:25000
那么前景占比:

但这里必须注意:
白色像素比例高,不代表分割准确率高。
例如 HSV 可能把大量杂草也分割出来;灰度阈值可能把明亮土壤识别成前景。所以这个指标只是辅助观察,不是精度指标。
四、结果应该怎么分析?
当你得到六张 mask 后,不建议只说"HSV 看起来更好"。
你需要观察具体出现了哪些差异。
可以按照下面这张表整理实验记录。
| 条件 | 灰度阈值应关注的问题 | HSV 应关注的问题 |
|---|---|---|
| 光照均匀 | 裸土与植株灰度是否重叠 | 绿色叶片是否完整保留 |
| 强烈光照 | 明亮土壤是否误分割 | 高光、低饱和度叶片是否漏检 |
| 阴影明显 | 阴影植株是否被过滤 | 低 V 值植株是否被过滤 |
建议每张图片再统计三种现象:
植株保留情况: 叶片主体是不是被保留下来了?小叶片是否缺失?
背景误分割: 土壤、阴影和其他背景有没有被错误变成白色?
光照鲁棒性: 相同类型的绿色叶片,在亮处和暗处能否都被保留?
如果想更严谨一点,还可以手动挑选若干植株区域和背景区域,统计它们各自被保留的像素比例。
这样就可以把"目测结果"进一步转化为简单的定量分析。
六、再补充两个值得掌握的知识点
1. threshold() 不止一种模式
你今天主要使用:
cv::THRESH_BINARY
但 OpenCV 还提供其他阈值模式:
| 模式 | 作用 |
|---|---|
THRESH_BINARY |
大于阈值输出最大值,否则 0 |
THRESH_BINARY_INV |
大于阈值输出 0,否则最大值 |
THRESH_TRUNC |
超过阈值的数值截断为阈值 |
THRESH_TOZERO |
小于等于阈值的变为 0 |
THRESH_TOZERO_INV |
大于阈值的变为 0 |
今天只需要掌握 THRESH_BINARY,并了解反向二值化即可。
另外,OpenCV 还有 Otsu 自动阈值和自适应阈值等方法,之后学习图像分割时可以再深入。
2. HSV 分割也不总比灰度分割好
这点需要建立正确认识。
例如你希望分割的是:
- 白纸上的黑色文字;
- 光照均匀背景中的黑色物体;
- 具有明显灰度差异的结构。
那么灰度阈值可能已经足够,而且更简单。
但是如果目标是:
从棕色土壤中提取绿色植株。
那么颜色信息通常非常重要,HSV 的 H、S、V 分离表示就更容易设置筛选条件。
因此选择哪一种方法,关键是看:
目标与背景的主要差异到底来自亮度,还是颜色。