OpenCV 工业视觉学习笔记(二):固定阈值、OTSU 与自适应阈值
前言
在上一篇文章中,学习了 OpenCV 中的 Mat、图像读取、显示、保存以及灰度化。
将彩色图像转换为灰度图之后,下一步通常是把目标区域从背景中分离出来。
在传统图像处理中,最常用、最基础的图像分割方法之一就是阈值分割。
阈值分割的核心思想是:
根据像素灰度值,将图像中的像素划分为目标和背景两部分。
例如,一张灰度图中:
- 背景灰度较亮;
- 缺陷区域灰度较暗。
那么可以设置一个阈值,将低于阈值的像素划分为缺陷,高于阈值的像素划分为背景。
本篇主要学习以下内容:
- 什么是二值图像;
- 固定阈值分割;
threshold()函数的参数;- 常见阈值类型;
- OTSU 自动阈值;
- Triangle 三角阈值;
- 自适应阈值;
- 全局阈值与自适应阈值的区别;
- 阈值分割在工业视觉中的应用;
- 常见问题和参数选择方法。
一、什么是二值图像
二值图像通常只有两种像素值:
0
255
其中:
- 0 表示黑色;
- 255 表示白色。
例如,一张原始灰度图中包含不同亮度的像素:
20、50、80、120、180、220
设置阈值为 100 后,可以将图像转换为:
0、0、0、255、255、255
这样,原本包含大量灰度级别的图像,就被划分成了黑色区域和白色区域。
二值化之后,图像结构会更加简单,便于进行后续处理,例如:
- 连通域分析;
- 轮廓提取;
- 缺陷面积计算;
- 目标计数;
- 形态学处理;
- 孔洞检测;
- 尺寸测量;
- 区域筛选。
二、固定阈值分割
OpenCV 使用 threshold() 函数进行固定阈值分割。
基本写法如下:
Mat binary;
threshold(gray, binary, 100, 255, THRESH_BINARY);
其中:
gray:输入灰度图;binary:输出二值图;100:设置的阈值;255:满足条件后赋予的最大值;THRESH_BINARY:阈值处理类型。
完整示例:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat binary;
threshold(gray, binary, 100, 255, THRESH_BINARY);
imshow("原始图像", source);
imshow("灰度图", gray);
imshow("固定阈值结果", binary);
waitKey(0);
destroyAllWindows();
return 0;
}
三、threshold 函数参数说明
threshold() 的函数形式可以简化理解为:
double threshold(
InputArray src,
OutputArray dst,
double thresh,
double maxValue,
int type
);
各参数含义如下。
1. src
输入图像。
固定阈值分割通常输入单通道灰度图:
CV_8UC1
例如:
threshold(gray, binary, 100, 255, THRESH_BINARY);
2. dst
输出图像。
输出图像的尺寸通常与输入图像一致。
3. thresh
设定的分割阈值。
例如:
100
表示使用灰度值 100 作为目标与背景的分界线。
4. maxValue
当像素满足阈值条件时,赋予的像素值。
二值图中通常使用:
255
因此结果中的目标区域通常显示为白色。
5. type
阈值处理方式。
常见类型包括:
THRESH_BINARY
THRESH_BINARY_INV
THRESH_TRUNC
THRESH_TOZERO
THRESH_TOZERO_INV
还可以与自动阈值方法组合:
THRESH_OTSU
THRESH_TRIANGLE
6. 返回值
threshold() 不仅会生成输出图像,还会返回实际使用的阈值。
double usedThreshold = threshold(
gray,
binary,
100,
255,
THRESH_BINARY
);
在普通固定阈值模式下,返回值通常就是传入的阈值。
在 OTSU 或 Triangle 模式下,返回的是算法自动计算出的阈值。
四、THRESH_BINARY
THRESH_BINARY 是最常用的阈值类型。
它的规则可以表示为:
如果像素值大于阈值,输出 maxValue;
否则输出 0。
代码:
threshold(gray, binary, 100, 255, THRESH_BINARY);
假设输入像素值为:
50、100、150、200
阈值为:
100
输出结果为:
0、0、255、255
需要注意,OpenCV 的判断条件是:
像素值 > 阈值
而不是大于等于。
五、THRESH_BINARY_INV
THRESH_BINARY_INV 是反向二值化。
规则为:
如果像素值大于阈值,输出 0;
否则输出 maxValue。
代码:
threshold(
gray,
binaryInverse,
100,
255,
THRESH_BINARY_INV
);
假设输入像素值为:
50、100、150、200
输出结果为:
255、255、0、0
THRESH_BINARY 和 THRESH_BINARY_INV 的选择,主要取决于希望目标区域显示为白色还是黑色。
在后续进行轮廓提取、连通域分析或者形态学处理时,通常更习惯让目标区域为白色,也就是非零区域。
例如:
- 黑色缺陷位于白色背景上;
- 希望把黑色缺陷提取为白色;
- 可以使用
THRESH_BINARY_INV。
六、THRESH_TRUNC
THRESH_TRUNC 表示截断阈值。
规则为:
如果像素值大于阈值,就将其设置为阈值;
否则保留原值。
代码:
Mat result;
threshold(gray, result, 100, 255, THRESH_TRUNC);
假设输入像素值为:
50、80、120、200
阈值为:
100
输出结果为:
50、80、100、100
这里的 maxValue 参数不会像二值化那样直接作为输出值。
这种方法不会生成真正的二值图,而是将较亮的区域统一压缩到阈值位置。
七、THRESH_TOZERO
THRESH_TOZERO 的规则为:
如果像素值大于阈值,保留原值;
否则设置为 0。
代码:
threshold(gray, result, 100, 255, THRESH_TOZERO);
假设输入像素值为:
50、80、120、200
输出结果为:
0、0、120、200
这种方法会保留较亮区域中的原始灰度信息。
八、THRESH_TOZERO_INV
THRESH_TOZERO_INV 与 THRESH_TOZERO 相反。
规则为:
如果像素值大于阈值,设置为 0;
否则保留原值。
代码:
threshold(
gray,
result,
100,
255,
THRESH_TOZERO_INV
);
假设输入像素值为:
50、80、120、200
输出结果为:
50、80、0、0
这种方法会保留较暗区域中的原始灰度信息。
九、几种阈值类型对比
下面使用同一张灰度图,观察不同阈值类型的效果。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread(
"D:/images/test.jpg",
IMREAD_GRAYSCALE
);
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat binary;
Mat binaryInv;
Mat truncResult;
Mat toZero;
Mat toZeroInv;
threshold(
source,
binary,
100,
255,
THRESH_BINARY
);
threshold(
source,
binaryInv,
100,
255,
THRESH_BINARY_INV
);
threshold(
source,
truncResult,
100,
255,
THRESH_TRUNC
);
threshold(
source,
toZero,
100,
255,
THRESH_TOZERO
);
threshold(
source,
toZeroInv,
100,
255,
THRESH_TOZERO_INV
);
imshow("Gray", source);
imshow("THRESH_BINARY", binary);
imshow("THRESH_BINARY_INV", binaryInv);
imshow("THRESH_TRUNC", truncResult);
imshow("THRESH_TOZERO", toZero);
imshow("THRESH_TOZERO_INV", toZeroInv);
waitKey(0);
destroyAllWindows();
return 0;
}
在工业视觉项目中,最常用的通常还是:
THRESH_BINARY
和:
THRESH_BINARY_INV
因为大部分区域分析算法都需要清晰的二值区域。
十、固定阈值的问题
固定阈值的优点是:
- 逻辑简单;
- 速度快;
- 结果稳定;
- 参数容易理解;
- 适合光照稳定的工业环境。
但是固定阈值也存在明显问题。
假设设置:
threshold(gray, binary, 120, 255, THRESH_BINARY);
如果图像的亮度发生变化:
- 第一张图像整体较亮;
- 第二张图像整体较暗;
- 第三张图像局部存在阴影。
固定阈值 120 可能只适合其中一张图像。
这时可能出现:
- 目标区域缺失;
- 背景被误分为目标;
- 同一个目标被切成多个区域;
- 阴影区域被识别为缺陷;
- 高光区域被错误排除。
因此,固定阈值更适合以下情况:
- 相机位置固定;
- 曝光时间固定;
- 光源稳定;
- 产品表面差异较小;
- 背景灰度稳定;
- 目标和背景对比明显。
这也是工业视觉系统强调光源设计和遮光结构的重要原因。
很多时候,一个稳定的光学环境比复杂的算法更加有效。
十一、OTSU 自动阈值
当无法确定固定阈值时,可以使用 OTSU 自动阈值。
OTSU 也称为大津法。
它会根据图像的灰度直方图,自动寻找一个合适的分割阈值。
代码如下:
Mat binary;
double otsuThreshold = threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
需要注意:
THRESH_BINARY | THRESH_OTSU
表示将普通二值化方式和 OTSU 自动阈值组合使用。
此时传入的阈值参数:
0
不会作为最终阈值使用。
最终阈值由 OTSU 算法自动计算。
可以通过返回值获取自动计算出的阈值:
cout << "OTSU计算出的阈值:"
<< otsuThreshold
<< endl;
十二、OTSU 的基本原理
一张适合使用 OTSU 的图像,其灰度直方图通常具有两个比较明显的峰值:
- 一个峰值对应背景;
- 一个峰值对应目标。
OTSU 会尝试找到一个阈值,将图像分成两类:
- 前景;
- 背景。
它的目标是:
让同一类别内部的灰度差异尽量小,让不同类别之间的灰度差异尽量大。
可以简单理解为:
- 目标内部尽量相似;
- 背景内部尽量相似;
- 目标与背景之间尽量不同。
OTSU 会遍历可能的阈值,并选择分类效果最好的一个。
十三、OTSU 完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat binary;
double otsuValue = threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
cout << "OTSU自动阈值:"
<< otsuValue
<< endl;
imshow("原始图像", source);
imshow("灰度图", gray);
imshow("OTSU结果", binary);
waitKey(0);
destroyAllWindows();
return 0;
}
十四、OTSU 反向二值化
如果目标比背景更暗,希望将目标提取为白色,可以使用:
threshold(
gray,
binary,
0,
255,
THRESH_BINARY_INV | THRESH_OTSU
);
这里 OTSU 负责自动计算阈值,而 THRESH_BINARY_INV 负责决定黑白方向。
因此需要区分两个概念:
- OTSU 决定阈值是多少;
THRESH_BINARY或THRESH_BINARY_INV决定输出方向。
十五、OTSU 前为什么经常进行滤波
如果图像中存在大量噪声,灰度直方图可能会变得不稳定,从而影响 OTSU 的阈值选择。
因此,在执行 OTSU 前,经常先进行高斯滤波。
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat binary;
double otsuValue = threshold(
blurred,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
完整代码:
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat binary;
double value = threshold(
blurred,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
cout << "OTSU阈值:" << value << endl;
高斯滤波可以减少细小噪点对灰度分布的干扰。
但是滤波核也不能设置得过大,否则可能导致:
- 小缺陷被模糊掉;
- 边缘位置发生偏移;
- 相邻区域发生粘连;
- 细线结构消失。
十六、OTSU 的适用条件
OTSU 比较适合:
- 目标与背景灰度差异明显;
- 图像灰度直方图具有双峰特征;
- 图像整体光照较均匀;
- 前景和背景可以近似分成两类。
OTSU 不太适合:
- 图像存在明显亮度渐变;
- 局部阴影严重;
- 背景灰度变化很大;
- 图像包含多个不同灰度区域;
- 目标面积特别小;
- 目标灰度与背景高度重叠。
如果一张图像中,背景占据绝大部分,而缺陷只占很少像素,OTSU 可能更倾向于按照背景内部的灰度变化进行分割,而不是准确提取小缺陷。
因此,OTSU 是自动计算阈值,但并不代表它适合所有图像。
十七、Triangle 三角阈值
OpenCV 还提供了另一种自动阈值方法:
THRESH_TRIANGLE
使用方式如下:
Mat binary;
double triangleValue = threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_TRIANGLE
);
Triangle 方法同样会根据灰度直方图自动计算阈值。
它通常更适合:
- 灰度直方图只有一个明显主峰;
- 目标只占图像的一小部分;
- 直方图呈现长尾分布;
- 前景和背景比例差异较大。
例如,图像中大部分区域都是背景,只有少量较亮或较暗的缺陷时,Triangle 有时会比 OTSU 更有效。
不过,具体效果仍然需要结合实际图像进行测试。
十八、OTSU 与 Triangle 不能同时使用
下面这种写法是不合理的:
THRESH_OTSU | THRESH_TRIANGLE
因为两者都是自动计算阈值的方法,只能选择其中一种。
正确写法:
THRESH_BINARY | THRESH_OTSU
或者:
THRESH_BINARY | THRESH_TRIANGLE
十九、什么是自适应阈值
固定阈值和 OTSU 都属于全局阈值。
全局阈值的特点是:
整张图像使用同一个阈值。
例如:
整张图像的阈值全部为 120
但是如果图像存在亮度不均匀:
- 左侧较亮;
- 右侧较暗;
- 中间有阴影;
- 边缘存在光照衰减。
整张图像使用同一个阈值,就可能无法获得理想结果。
自适应阈值的核心思想是:
根据像素周围的局部区域,为不同位置计算不同的阈值。
因此:
- 图像左侧可能使用阈值 130;
- 图像右侧可能使用阈值 90;
- 阴影区域可能使用阈值 70。
这种方式更适合光照不均匀的图像。
二十、adaptiveThreshold 函数
OpenCV 使用 adaptiveThreshold() 进行自适应阈值分割。
基本写法:
Mat binary;
adaptiveThreshold(
gray,
binary,
255,
ADAPTIVE_THRESH_MEAN_C,
THRESH_BINARY,
11,
2
);
函数可以简化理解为:
adaptiveThreshold(
输入图像,
输出图像,
最大值,
局部阈值计算方法,
二值化类型,
邻域大小,
常数C
);
二十一、adaptiveThreshold 参数说明
1. 输入图像
输入图像通常要求是:
CV_8UC1
也就是 8 位单通道灰度图。
如果输入的是彩色图,需要先灰度化:
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
2. 输出图像
输出为二值图像。
3. maxValue
目标像素最终赋予的最大值。
通常设置为:
255
4. adaptiveMethod
局部阈值的计算方法。
有两种常见方式:
ADAPTIVE_THRESH_MEAN_C
和:
ADAPTIVE_THRESH_GAUSSIAN_C
5. thresholdType
自适应阈值只支持常用二值化方向:
THRESH_BINARY
或者:
THRESH_BINARY_INV
6. blockSize
计算局部阈值时使用的邻域大小。
例如:
11
表示使用当前像素周围的 11 × 11 区域计算局部阈值。
blockSize 必须满足:
- 大于 1;
- 必须是奇数。
例如:
3、5、7、9、11、15、21
都可以。
下面这些值不可以:
0、1、2、4、10
7. C
从局部计算结果中减去的常数。
例如:
C = 2
局部阈值可以简单理解为:
局部统计值 - C
C 会影响最终分割区域的大小和敏感程度。
二十二、局部均值自适应阈值
使用:
ADAPTIVE_THRESH_MEAN_C
表示局部阈值由邻域内像素的平均值计算得到。
代码:
adaptiveThreshold(
gray,
binaryMean,
255,
ADAPTIVE_THRESH_MEAN_C,
THRESH_BINARY,
11,
2
);
可以简单理解为:
当前像素阈值 = 邻域平均灰度值 - C
二十三、局部高斯自适应阈值
使用:
ADAPTIVE_THRESH_GAUSSIAN_C
表示根据高斯加权平均值计算局部阈值。
代码:
adaptiveThreshold(
gray,
binaryGaussian,
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY,
11,
2
);
高斯方式会让距离当前像素较近的像素具有更大的权重。
相比普通均值方式,它通常对局部亮度变化的处理更加平滑。
二十四、自适应阈值完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat binaryMean;
Mat binaryGaussian;
adaptiveThreshold(
gray,
binaryMean,
255,
ADAPTIVE_THRESH_MEAN_C,
THRESH_BINARY,
11,
2
);
adaptiveThreshold(
gray,
binaryGaussian,
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY,
11,
2
);
imshow("原始图像", source);
imshow("灰度图", gray);
imshow("局部均值阈值", binaryMean);
imshow("局部高斯阈值", binaryGaussian);
waitKey(0);
destroyAllWindows();
return 0;
}
二十五、blockSize 的影响
blockSize 决定计算局部阈值时参考多大的区域。
blockSize 较小
例如:
blockSize = 3
或者:
blockSize = 5
特点:
- 对局部变化非常敏感;
- 可以保留小结构;
- 容易把噪点提取出来;
- 二值结果可能比较碎。
blockSize 较大
例如:
blockSize = 31
或者:
blockSize = 51
特点:
- 局部阈值变化更加平滑;
- 对噪点不那么敏感;
- 可能忽略较小目标;
- 可能导致细节丢失。
因此,blockSize 应根据目标尺寸选择。
一个常见思路是:
邻域尺寸应该大于需要检测的局部纹理或缺陷尺寸,但又不能大到失去局部意义。
例如,一个缺陷宽度大约为 5 个像素,可以尝试:
11、15、21
而不是直接使用:
3
二十六、C 参数的影响
自适应阈值中的 C 会从局部均值或高斯加权均值中减去。
对于:
THRESH_BINARY
可以简单理解为:
像素值 > 局部均值 - C
则输出白色。
当 C 增大时,局部阈值会降低,因此更容易满足:
像素值 > 阈值
最终白色区域可能会增多。
当 C 减小时,局部阈值会提高,白色区域可能会减少。
但是实际图像的黑白变化还与:
THRESH_BINARY;THRESH_BINARY_INV;- 图像本身的亮暗关系;
有关。
因此不能只靠背结论,最好通过参数实验观察结果。
二十七、使用滑动条调节固定阈值
为了直观观察不同阈值的效果,可以使用滑动条动态调节。
#include <opencv2/opencv.hpp>
using namespace cv;
Mat grayImage;
Mat binaryImage;
int thresholdValue = 100;
void OnThresholdChanged(int, void*)
{
threshold(
grayImage,
binaryImage,
thresholdValue,
255,
THRESH_BINARY
);
imshow("二值图", binaryImage);
}
int main()
{
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
return -1;
}
cvtColor(source, grayImage, COLOR_BGR2GRAY);
namedWindow("二值图", WINDOW_NORMAL);
createTrackbar(
"阈值",
"二值图",
&thresholdValue,
255,
OnThresholdChanged
);
OnThresholdChanged(0, nullptr);
waitKey(0);
destroyAllWindows();
return 0;
}
通过拖动滑动条,可以观察:
- 阈值较小时哪些区域被提取;
- 阈值较大时哪些区域被合并;
- 目标和背景在哪个阈值附近分离得最好。
这种方式很适合前期进行参数验证。
二十八、固定阈值、OTSU 和自适应阈值对比
| 方法 | 阈值数量 | 优点 | 缺点 |
|---|---|---|---|
| 固定阈值 | 一个手动阈值 | 快、稳定、可控 | 对光照变化敏感 |
| OTSU | 一个自动阈值 | 不需要手动设置阈值 | 不适合明显光照不均 |
| Triangle | 一个自动阈值 | 适合单峰长尾分布 | 对部分图像效果不稳定 |
| 自适应阈值 | 每个局部区域一个阈值 | 适合亮度不均匀 | 容易产生噪点,参数较多 |
二十九、如何选择阈值方法
情况一:光照稳定,产品一致
优先考虑固定阈值:
threshold(
gray,
binary,
120,
255,
THRESH_BINARY
);
原因是固定阈值:
- 速度快;
- 结果可预测;
- 参数容易管理;
- 不会因为不同图片的灰度分布而自动变化。
工业视觉系统追求的是稳定,而不是算法看起来更加智能。
情况二:整体亮度变化,但目标和背景仍然明显分离
可以考虑 OTSU:
threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
例如:
- 曝光存在轻微波动;
- 不同批次产品整体亮度略有不同;
- 图像仍然具有明显的双峰灰度分布。
情况三:背景占绝大多数,目标很少
可以测试 Triangle:
threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_TRIANGLE
);
情况四:图像存在局部阴影或亮度渐变
可以考虑自适应阈值:
adaptiveThreshold(
gray,
binary,
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY,
21,
5
);
情况五:背景变化复杂
如果自适应阈值仍然无法稳定分割,可能需要先进行背景校正,例如:
- 大尺度模糊估计背景;
- 原图减去背景图;
- 光照补偿;
- 顶帽变换;
- 黑帽变换;
- 颜色空间分割;
- 局部对比度增强。
因此,阈值算法并不是解决所有图像分割问题的唯一方法。
三十、工业视觉中的实际思路
在工业检测中,通常不会直接读取图像后立刻二值化,而是先完成一系列预处理。
一个常见流程如下:
读取图像
↓
截取 ROI
↓
灰度化
↓
滤波降噪
↓
亮度校正或对比度增强
↓
阈值分割
↓
形态学处理
↓
连通域或轮廓分析
↓
面积、宽高、位置筛选
对应代码结构可能如下:
Mat source = imread("D:/images/test.jpg");
Mat roi = source(
Rect(100, 100, 500, 300)
);
Mat gray;
cvtColor(roi, gray, COLOR_BGR2GRAY);
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat binary;
threshold(
blurred,
binary,
120,
255,
THRESH_BINARY_INV
);
阈值分割只是整个检测流程中的一个步骤。
最终检测是否稳定,还取决于:
- 光源;
- 相机曝光;
- 镜头;
- 产品定位;
- ROI;
- 滤波方式;
- 阈值方式;
- 后处理逻辑;
- 特征筛选条件。
三十一、一个简单的暗缺陷提取示例
假设:
- 产品表面较亮;
- 缺陷区域较暗;
- 希望将缺陷提取为白色。
可以使用反向二值化:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread("D:/images/defect.jpg");
if (source.empty())
{
cerr << "读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat binary;
double thresholdValue = threshold(
blurred,
binary,
0,
255,
THRESH_BINARY_INV | THRESH_OTSU
);
cout << "自动阈值:"
<< thresholdValue
<< endl;
imshow("原图", source);
imshow("灰度图", gray);
imshow("暗缺陷区域", binary);
waitKey(0);
destroyAllWindows();
return 0;
}
此时:
- 较暗区域会变成白色;
- 较亮背景会变成黑色;
- 后续可以对白色区域进行面积和形状分析。
三十二、阈值分割后为什么经常有噪点
二值化后出现很多白色小点,常见原因包括:
- 原图本身存在噪声;
- 产品表面纹理较多;
- 阈值设置不合理;
- 自适应阈值邻域太小;
- 自适应阈值的
C不合适; - 光照不均匀;
- 图像压缩造成伪影;
- 表面存在反光。
可以尝试:
GaussianBlur()
或者:
medianBlur()
进行滤波。
二值化后还可以使用形态学开运算去除小噪点:
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(3, 3)
);
Mat opened;
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
不过形态学操作会在后续文章中单独学习。
三十三、常见问题
1. threshold 可以直接处理彩色图吗
普通阈值处理应先转换成单通道灰度图:
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
然后再执行:
threshold(gray, binary, 100, 255, THRESH_BINARY);
2. 为什么 OTSU 中阈值参数写 0
因为启用:
THRESH_OTSU
后,阈值由算法自动计算。
因此下面的参数:
0
只是占位,并不是实际阈值。
3. OTSU 自动计算的阈值怎么获取
通过 threshold() 的返回值获取:
double value = threshold(
gray,
binary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
4. 为什么 OTSU 的效果反而不如固定阈值
因为 OTSU 只根据当前图像的灰度分布自动寻找阈值。
如果图像:
- 没有明显双峰;
- 目标区域太小;
- 光照不均匀;
- 背景纹理复杂;
OTSU 可能无法获得理想结果。
固定阈值虽然简单,但在稳定工业环境中往往更加可靠。
5. 自适应阈值为什么产生很多纹理
因为自适应阈值会比较每个像素和局部背景之间的差异。
产品表面的轻微纹理,也可能被认为是局部目标。
可以尝试:
- 增大
blockSize; - 调整
C; - 先进行高斯滤波;
- 先进行背景校正;
- 使用连通域面积筛选。
6. blockSize 为什么必须是奇数
因为需要以当前像素为中心,向四周取相同范围的邻域。
例如:
3 × 3
5 × 5
11 × 11
都可以明确确定中心像素。
而偶数尺寸没有唯一的中心位置。
7. 应该使用 THRESH_BINARY 还是 THRESH_BINARY_INV
通常希望后续需要处理的目标区域为白色。
因此:
- 目标比背景亮,通常使用
THRESH_BINARY; - 目标比背景暗,通常使用
THRESH_BINARY_INV。
不过最终还是需要根据实际黑白方向进行判断。
三十四、完整对比代码
下面使用同一张图像对比固定阈值、OTSU 和自适应阈值。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread("D:/images/test.jpg");
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(source, gray, COLOR_BGR2GRAY);
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat fixedBinary;
threshold(
blurred,
fixedBinary,
120,
255,
THRESH_BINARY
);
Mat otsuBinary;
double otsuValue = threshold(
blurred,
otsuBinary,
0,
255,
THRESH_BINARY | THRESH_OTSU
);
Mat meanBinary;
adaptiveThreshold(
blurred,
meanBinary,
255,
ADAPTIVE_THRESH_MEAN_C,
THRESH_BINARY,
21,
5
);
Mat gaussianBinary;
adaptiveThreshold(
blurred,
gaussianBinary,
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY,
21,
5
);
cout << "OTSU阈值:"
<< otsuValue
<< endl;
imshow("原始图像", source);
imshow("灰度图", gray);
imshow("固定阈值", fixedBinary);
imshow("OTSU", otsuBinary);
imshow("自适应均值", meanBinary);
imshow("自适应高斯", gaussianBinary);
waitKey(0);
destroyAllWindows();
return 0;
}
运行后,需要重点观察:
- 哪种方法能够完整保留目标;
- 哪种方法产生的噪点较少;
- 目标边缘是否准确;
- 光照较暗区域是否仍能被提取;
- 相同参数能否适用于多张图像。
不能只根据单张图像判断算法是否有效。
工业视觉算法需要使用多张不同情况的图像进行验证,例如:
- 正常产品;
- 不同缺陷;
- 亮度波动;
- 位置偏差;
- 产品批次变化;
- 边缘和中心区域;
- 临界样本。
三十五、本篇总结
本篇主要学习了 OpenCV 中的阈值分割。
需要重点掌握以下内容:
- 二值图像通常只包含 0 和 255;
threshold()用于全局阈值分割;THRESH_BINARY用于正向二值化;THRESH_BINARY_INV用于反向二值化;- 固定阈值适合光照稳定的工业环境;
- OTSU 可以自动计算一个全局阈值;
- OTSU 更适合目标和背景灰度差异明显的图像;
- Triangle 适合部分单峰、长尾灰度分布;
- 自适应阈值会根据局部区域计算不同阈值;
blockSize必须是大于 1 的奇数;C会影响局部阈值和最终白色区域;- 自适应阈值适合光照不均匀的图像;
- 自动阈值并不一定比固定阈值更加稳定;
- 阈值分割结果还需要结合滤波、形态学和区域筛选进行处理。
在工业视觉项目中,算法的目标不是在某一张图像上获得最好看的结果,而是在不同批次、不同时间和轻微环境变化下,仍然能够保持稳定。
浙公网安备 33010602011771号