OpenCV 工业视觉学习笔记(二):固定阈值、OTSU 与自适应阈值

前言

在上一篇文章中,学习了 OpenCV 中的 Mat、图像读取、显示、保存以及灰度化。

将彩色图像转换为灰度图之后,下一步通常是把目标区域从背景中分离出来。

在传统图像处理中,最常用、最基础的图像分割方法之一就是阈值分割。

阈值分割的核心思想是:

根据像素灰度值,将图像中的像素划分为目标和背景两部分。

例如,一张灰度图中:

  • 背景灰度较亮;
  • 缺陷区域灰度较暗。

那么可以设置一个阈值,将低于阈值的像素划分为缺陷,高于阈值的像素划分为背景。

本篇主要学习以下内容:

  1. 什么是二值图像;
  2. 固定阈值分割;
  3. threshold() 函数的参数;
  4. 常见阈值类型;
  5. OTSU 自动阈值;
  6. Triangle 三角阈值;
  7. 自适应阈值;
  8. 全局阈值与自适应阈值的区别;
  9. 阈值分割在工业视觉中的应用;
  10. 常见问题和参数选择方法。

一、什么是二值图像

二值图像通常只有两种像素值:

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_BINARYTHRESH_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_INVTHRESH_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_BINARYTHRESH_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 中的阈值分割。

需要重点掌握以下内容:

  1. 二值图像通常只包含 0 和 255;
  2. threshold() 用于全局阈值分割;
  3. THRESH_BINARY 用于正向二值化;
  4. THRESH_BINARY_INV 用于反向二值化;
  5. 固定阈值适合光照稳定的工业环境;
  6. OTSU 可以自动计算一个全局阈值;
  7. OTSU 更适合目标和背景灰度差异明显的图像;
  8. Triangle 适合部分单峰、长尾灰度分布;
  9. 自适应阈值会根据局部区域计算不同阈值;
  10. blockSize 必须是大于 1 的奇数;
  11. C 会影响局部阈值和最终白色区域;
  12. 自适应阈值适合光照不均匀的图像;
  13. 自动阈值并不一定比固定阈值更加稳定;
  14. 阈值分割结果还需要结合滤波、形态学和区域筛选进行处理。

在工业视觉项目中,算法的目标不是在某一张图像上获得最好看的结果,而是在不同批次、不同时间和轻微环境变化下,仍然能够保持稳定。

posted on 2026-08-01 22:56  南小欢  阅读(3)  评论(0)    收藏  举报