OpenCV 工业视觉学习笔记(三):腐蚀、膨胀与形态学操作

前言

在上一篇文章中,学习了固定阈值、OTSU 自动阈值以及自适应阈值。

经过阈值分割后,通常会得到一张二值图像。但是二值化结果往往并不完美,可能存在以下问题:

  • 目标内部存在小孔洞;
  • 目标边缘不连续;
  • 图像中存在孤立噪点;
  • 相邻区域没有连接;
  • 多个目标发生轻微粘连;
  • 细小缺陷被断开;
  • 目标轮廓不够平滑。

这时就需要使用形态学操作对二值区域进行处理。

形态学操作的核心思想是:

使用一个指定形状和大小的结构元素,在图像上进行滑动,并根据结构元素覆盖范围内的像素情况修改当前像素。

本篇主要学习以下内容:

  1. 什么是形态学操作;
  2. 什么是结构元素;
  3. 腐蚀操作;
  4. 膨胀操作;
  5. 开运算;
  6. 闭运算;
  7. 形态学梯度;
  8. 顶帽运算;
  9. 黑帽运算;
  10. 结构元素形状和尺寸的影响;
  11. 形态学操作在工业视觉中的使用方法。

一、什么是形态学操作

形态学操作最初是针对二值图像提出的一类图像处理方法。

在二值图中,通常认为:

  • 白色像素,也就是非零像素,是前景或目标;
  • 黑色像素,也就是零值像素,是背景。

形态学操作主要研究目标区域的形状变化,例如:

  • 让白色区域变大;
  • 让白色区域变小;
  • 去除小白点;
  • 填补小黑洞;
  • 连接断开的区域;
  • 分离轻微粘连的区域;
  • 提取目标边界;
  • 提取局部亮区域或暗区域。

需要特别注意:

腐蚀和膨胀描述的是白色前景区域的变化。

因此,在使用形态学操作前,最好先确定目标区域是白色还是黑色。

如果目标是黑色,而背景是白色,可以先通过反向二值化让目标变成白色:

threshold(
    gray,
    binary,
    100,
    255,
    THRESH_BINARY_INV
);

二、什么是结构元素

形态学操作不是单独观察一个像素,而是使用一个小矩阵在图像上滑动。

这个小矩阵称为:

结构元素

英文为:

Structuring Element

也经常称为:

形态学核

例如,一个 3×3 的矩形结构元素可以表示为:

1 1 1
1 1 1
1 1 1

结构元素移动到某个像素位置后,会观察该像素周围 3×3 范围内的像素。

OpenCV 使用 getStructuringElement() 创建结构元素:

Mat kernel = getStructuringElement(
    MORPH_RECT,
    Size(3, 3)
);

这里:

  • MORPH_RECT 表示矩形结构元素;
  • Size(3, 3) 表示结构元素大小为 3×3

三、常见结构元素形状

OpenCV 提供了三种常见结构元素。

1. 矩形结构元素

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

对应:

MORPH_RECT

矩形结构元素中的所有位置都会参与计算。

它的作用范围比较完整,膨胀和腐蚀效果通常比较明显。

矩形核适合:

  • 填补矩形缺口;
  • 连接水平或垂直区域;
  • 处理规则形状目标;
  • 强度较大的形态学处理。

2. 十字形结构元素

Mat kernel = getStructuringElement(
    MORPH_CROSS,
    Size(5, 5)
);

对应:

MORPH_CROSS

一个 5×5 的十字形结构元素大致可以表示为:

0 0 1 0 0
0 0 1 0 0
1 1 1 1 1
0 0 1 0 0
0 0 1 0 0

十字形主要作用于水平和垂直方向,对斜方向的影响相对较小。

适合处理:

  • 水平线;
  • 垂直线;
  • 十字形连接关系;
  • 不希望过度扩张对角线方向的情况。

3. 椭圆形结构元素

Mat kernel = getStructuringElement(
    MORPH_ELLIPSE,
    Size(5, 5)
);

对应:

MORPH_ELLIPSE

椭圆形结构元素更接近圆形。

它对目标边缘的处理通常比矩形核更加平滑,适合:

  • 圆形目标;
  • 孔洞;
  • 焊盘;
  • 颗粒;
  • 圆点;
  • 希望减少矩形棱角的区域。

四、打印结构元素

可以直接输出结构元素,观察不同形状之间的区别:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat rectKernel = getStructuringElement(
        MORPH_RECT,
        Size(5, 5)
    );

    Mat crossKernel = getStructuringElement(
        MORPH_CROSS,
        Size(5, 5)
    );

    Mat ellipseKernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(5, 5)
    );

    cout << "矩形核:" << endl;
    cout << rectKernel << endl;

    cout << "十字核:" << endl;
    cout << crossKernel << endl;

    cout << "椭圆核:" << endl;
    cout << ellipseKernel << endl;

    return 0;
}

输出结构元素有助于理解:

  • 哪些位置会参与计算;
  • 为什么不同形状会产生不同效果;
  • 为什么矩形核对角线方向的影响更明显。

五、腐蚀操作

OpenCV 使用 erode() 进行腐蚀。

基本代码:

Mat eroded;

erode(
    binary,
    eroded,
    kernel
);

腐蚀对白色前景区域的主要作用是:

使白色区域缩小。

腐蚀后可能出现以下效果:

  • 白色区域边缘向内收缩;
  • 细小白色噪点消失;
  • 细线被削弱或断开;
  • 相邻目标可能被分离;
  • 小目标可能完全消失;
  • 白色区域中的黑色孔洞变大。

六、如何理解腐蚀

对于二值图像,可以将腐蚀简单理解为:

结构元素覆盖范围内的像素必须全部满足条件,中心像素才能保留为白色。

3×3 矩形结构元素为例。

如果当前白色像素位于目标内部,并且周围都是白色,那么腐蚀后它仍然是白色。

如果当前像素位于目标边缘,周围包含黑色背景,那么腐蚀后它可能会变成黑色。

因此,白色目标的边缘会向内部收缩。


七、腐蚀完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat source = imread(
        "D:/images/binary.png",
        IMREAD_GRAYSCALE
    );

    if (source.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat binary;
    threshold(
        source,
        binary,
        127,
        255,
        THRESH_BINARY
    );

    Mat kernel = getStructuringElement(
        MORPH_RECT,
        Size(3, 3)
    );

    Mat eroded;
    erode(
        binary,
        eroded,
        kernel
    );

    imshow("原始二值图", binary);
    imshow("腐蚀结果", eroded);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

八、腐蚀的常见用途

1. 去除小白点

如果二值图中存在一些很小的白色噪点,腐蚀可以使这些小区域缩小甚至消失。

erode(
    binary,
    result,
    kernel
);

但是腐蚀也会让正常目标缩小,因此不能只看噪点是否被去除,还需要观察目标尺寸是否受到过大影响。


2. 分离轻微粘连的目标

如果两个白色目标之间只有一条很窄的连接区域,腐蚀可能会将连接区域去掉,从而把两个目标分开。

例如:

目标A──细连接──目标B

腐蚀后可能变成:

目标A        目标B

但是,如果连接区域较粗,仅使用小尺寸腐蚀可能无法分开。


3. 缩小检测区域

有时为了排除目标边缘的不稳定区域,可以先腐蚀 ROI,只分析目标内部。

例如:

  • 排除产品边缘反光;
  • 排除焊盘边缘;
  • 排除字符轮廓附近的干扰;
  • 只检测中心区域缺陷。

九、膨胀操作

OpenCV 使用 dilate() 进行膨胀。

基本代码:

Mat dilated;

dilate(
    binary,
    dilated,
    kernel
);

膨胀对白色前景区域的主要作用是:

使白色区域扩大。

膨胀后可能出现以下效果:

  • 白色目标边缘向外扩张;
  • 细小裂缝被连接;
  • 断开的线段被连通;
  • 白色区域中的小黑孔被缩小;
  • 相邻目标可能粘连;
  • 小白点会变大。

十、如何理解膨胀

对于二值图像,可以简单理解为:

只要结构元素覆盖范围内存在白色像素,中心位置就可能变成白色。

当结构元素移动到白色区域边缘时,原本位于目标外部的黑色像素也可能被设置为白色。

因此,白色区域会向外扩张。


十一、膨胀完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat source = imread(
        "D:/images/binary.png",
        IMREAD_GRAYSCALE
    );

    if (source.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat binary;
    threshold(
        source,
        binary,
        127,
        255,
        THRESH_BINARY
    );

    Mat kernel = getStructuringElement(
        MORPH_RECT,
        Size(3, 3)
    );

    Mat dilated;
    dilate(
        binary,
        dilated,
        kernel
    );

    imshow("原始二值图", binary);
    imshow("膨胀结果", dilated);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

十二、膨胀的常见用途

1. 连接断开的区域

如果缺陷区域或线条中存在较小间隙,可以通过膨胀将其连接。

例如,一条线段中间断开两个像素:

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

经过适当膨胀后可能变成:

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

2. 填补小孔洞

白色目标内部如果存在很小的黑色孔洞,膨胀会让周围白色区域扩张,从而缩小孔洞。

但是膨胀不能保证完全填满所有孔洞。

如果需要完整填充封闭区域中的孔洞,还可能需要:

  • 闭运算;
  • 漫水填充;
  • 轮廓填充;
  • 连通域处理。

3. 扩大检测区域

在工业检测中,有时需要将某个区域向外扩展一定范围。

例如:

  • 焊盘区域向外扩展;
  • 孔区域向外扩展;
  • 缺陷附近设置屏蔽区;
  • 对定位结果增加容差;
  • 将断开的轮廓连接起来。

十三、腐蚀与膨胀的方向

对于白色前景区域:

腐蚀:白色区域缩小
膨胀:白色区域扩大

对于黑色背景区域,表现正好相反:

腐蚀白色区域,相当于扩大黑色区域
膨胀白色区域,相当于缩小黑色区域

因此,不能只记住“腐蚀就是缩小、膨胀就是扩大”,还需要明确缩小和扩大的是哪一种区域。

OpenCV 默认是对较亮区域,也就是局部最大值进行膨胀,对局部最小值进行腐蚀。

在常见二值图中,就表现为对白色区域进行扩张或收缩。


十四、腐蚀和膨胀的函数参数

erode()dilate() 的参数形式基本一致。

erode() 为例:

erode(
    src,
    dst,
    kernel,
    anchor,
    iterations,
    borderType,
    borderValue
);

常用参数如下。

1. src

输入图像。

可以是二值图,也可以是灰度图或彩色图。

不过最容易理解和最常用的情况是二值图。


2. dst

输出图像。


3. kernel

结构元素。

例如:

Mat kernel = getStructuringElement(
    MORPH_ELLIPSE,
    Size(5, 5)
);

如果传入空矩阵:

Mat()

OpenCV 会使用默认的 3×3 矩形结构元素。

例如:

erode(
    binary,
    result,
    Mat()
);

4. anchor

锚点表示结构元素的参考位置。

通常使用默认值:

Point(-1, -1)

表示锚点位于结构元素中心。

例如:

erode(
    binary,
    result,
    kernel,
    Point(-1, -1)
);

大部分项目不需要手动修改锚点。


5. iterations

表示重复执行次数。

例如:

erode(
    binary,
    result,
    kernel,
    Point(-1, -1),
    2
);

表示连续腐蚀两次。

次数越多:

  • 腐蚀越强;
  • 膨胀越强;
  • 对目标尺寸影响越大。

6. borderType

用于设置图像边界外部像素的处理方式。

通常可以使用默认值:

BORDER_CONSTANT

一般学习和常规工业处理时,不需要单独设置。


十五、结构元素尺寸的影响

结构元素尺寸越大,形态学处理效果通常越强。

例如:

Size(3, 3)
Size(5, 5)
Size(9, 9)
Size(21, 21)

产生的结果会明显不同。

小尺寸结构元素

例如:

Size(3, 3)

特点:

  • 对图像修改较小;
  • 能够保留更多细节;
  • 适合去除极小噪点;
  • 适合连接很小的断口;
  • 不容易严重改变目标尺寸。

大尺寸结构元素

例如:

Size(15, 15)

特点:

  • 处理效果明显;
  • 可以去除较大噪声;
  • 可以连接较大间隙;
  • 容易让相邻目标粘连;
  • 容易使小目标消失;
  • 容易改变目标真实尺寸。

在工业测量中需要特别注意:

形态学操作会改变目标边界,因此不能在经过大幅腐蚀或膨胀的区域上直接进行高精度尺寸测量。


十六、重复次数与大尺寸结构元素是否相同

下面两种操作可能产生类似效果:

erode(
    binary,
    result,
    smallKernel,
    Point(-1, -1),
    3
);

和:

erode(
    binary,
    result,
    largeKernel
);

但是它们并不一定完全等价。

原因包括:

  • 结构元素形状不同;
  • 连续多次操作会逐步改变目标形状;
  • 图像边缘和细节处理方式可能不同;
  • 非矩形结构元素重复操作后的实际作用形状会发生变化。

因此,不应该简单认为:

3次3×3腐蚀 = 1次9×9腐蚀

实际效果需要通过图像验证。


十七、开运算

开运算的执行顺序是:

先腐蚀,再膨胀

OpenCV 中使用:

morphologyEx(
    binary,
    opened,
    MORPH_OPEN,
    kernel
);

也可以手动写成:

Mat temp;
Mat opened;

erode(binary, temp, kernel);
dilate(temp, opened, kernel);

开运算的主要作用是:

  • 去除小白点;
  • 去除细小突出部分;
  • 断开狭窄连接;
  • 分离轻微粘连目标;
  • 在一定程度上保持较大目标的整体尺寸。

十八、如何理解开运算

第一步腐蚀:

  • 小白点被去除;
  • 大目标缩小;
  • 细连接可能被断开。

第二步膨胀:

  • 剩余的大目标重新扩大;
  • 已经被完全腐蚀掉的小噪点不会恢复;
  • 已经断开的细连接可能不会重新连接。

因此,开运算可以理解为:

在尽量保留主要目标的同时,去除比结构元素更小的白色结构。


十九、开运算完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat binary = imread(
        "D:/images/binary_noise.png",
        IMREAD_GRAYSCALE
    );

    if (binary.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat kernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(5, 5)
    );

    Mat opened;
    morphologyEx(
        binary,
        opened,
        MORPH_OPEN,
        kernel
    );

    imshow("原图", binary);
    imshow("开运算", opened);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

二十、闭运算

闭运算的执行顺序是:

先膨胀,再腐蚀

OpenCV 中使用:

morphologyEx(
    binary,
    closed,
    MORPH_CLOSE,
    kernel
);

也可以手动写成:

Mat temp;
Mat closed;

dilate(binary, temp, kernel);
erode(temp, closed, kernel);

闭运算的主要作用是:

  • 填补小黑洞;
  • 连接相邻白色区域;
  • 补全断裂轮廓;
  • 填补狭窄裂缝;
  • 平滑目标内部的小缺口。

二十一、如何理解闭运算

第一步膨胀:

  • 白色目标扩大;
  • 小黑孔变小;
  • 相邻区域可能连接;
  • 小断口被填补。

第二步腐蚀:

  • 扩大的目标重新收缩;
  • 已经连接的区域可能保持连接;
  • 已经填掉的小孔可能不会重新出现。

因此,闭运算可以理解为:

在尽量保持主要目标尺寸的同时,填补比结构元素更小的黑色间隙。


二十二、闭运算完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat binary = imread(
        "D:/images/binary_hole.png",
        IMREAD_GRAYSCALE
    );

    if (binary.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat kernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(7, 7)
    );

    Mat closed;
    morphologyEx(
        binary,
        closed,
        MORPH_CLOSE,
        kernel
    );

    imshow("原图", binary);
    imshow("闭运算", closed);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

二十三、开运算与闭运算的区别

可以简单记忆为:

开运算:先腐蚀后膨胀,主要去除小白点
闭运算:先膨胀后腐蚀,主要填补小黑洞

对比如下:

操作 执行顺序 主要作用
开运算 腐蚀后膨胀 去除小白点、断开细连接
闭运算 膨胀后腐蚀 填补小黑洞、连接小间隙

需要注意,白点和黑洞是相对于白色前景而言的。

如果目标区域是黑色,则需要重新考虑黑白方向,或者先将二值图反转。


二十四、先开后闭与先闭后开的区别

在实际项目中,有时会连续使用开运算和闭运算。

先开后闭

Mat opened;
Mat result;

morphologyEx(
    binary,
    opened,
    MORPH_OPEN,
    kernel
);

morphologyEx(
    opened,
    result,
    MORPH_CLOSE,
    kernel
);

适合:

  • 先去除白色噪点;
  • 再填补目标内部小孔;
  • 图像中同时存在小白点和小黑洞。

先闭后开

Mat closed;
Mat result;

morphologyEx(
    binary,
    closed,
    MORPH_CLOSE,
    kernel
);

morphologyEx(
    closed,
    result,
    MORPH_OPEN,
    kernel
);

适合:

  • 先连接断开的目标;
  • 再去除连接后残留的小突出部分;
  • 优先保证目标连通性。

两种顺序的结果通常不同。

因此,不能把开运算和闭运算当成可以随意交换顺序的操作。


二十五、形态学梯度

形态学梯度可以理解为:

膨胀图 - 腐蚀图

OpenCV 使用:

morphologyEx(
    binary,
    gradient,
    MORPH_GRADIENT,
    kernel
);

它会提取目标边缘附近的区域。

对于二值图像:

  • 膨胀图比原目标大;
  • 腐蚀图比原目标小;
  • 两者相减后,剩下的主要是目标边缘。

二十六、形态学梯度完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat binary = imread(
        "D:/images/binary.png",
        IMREAD_GRAYSCALE
    );

    if (binary.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat kernel = getStructuringElement(
        MORPH_RECT,
        Size(3, 3)
    );

    Mat gradient;
    morphologyEx(
        binary,
        gradient,
        MORPH_GRADIENT,
        kernel
    );

    imshow("原始图像", binary);
    imshow("形态学梯度", gradient);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

形态学梯度可以用于:

  • 提取目标轮廓;
  • 检测区域边界;
  • 观察孔洞边缘;
  • 获取字符外轮廓;
  • 分析目标边缘宽度;
  • 生成后续轮廓检测的输入图。

不过形态学梯度得到的边缘宽度与结构元素尺寸有关。

结构元素越大,边缘区域通常越宽。


二十七、顶帽运算

顶帽运算也称为白帽运算。

其计算方式可以理解为:

原图 - 开运算结果

OpenCV 使用:

morphologyEx(
    gray,
    topHat,
    MORPH_TOPHAT,
    kernel
);

开运算会去除比结构元素小的亮区域。

原图减去开运算结果后,保留下来的主要是:

比周围背景更亮,并且尺寸小于结构元素的区域。

因此,顶帽运算常用于提取:

  • 局部亮点;
  • 亮色小缺陷;
  • 反光点;
  • 亮颗粒;
  • 亮线;
  • 光照不均匀背景上的亮目标。

二十八、顶帽运算完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat gray = imread(
        "D:/images/bright_defect.png",
        IMREAD_GRAYSCALE
    );

    if (gray.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat kernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(21, 21)
    );

    Mat topHat;
    morphologyEx(
        gray,
        topHat,
        MORPH_TOPHAT,
        kernel
    );

    Mat binary;
    threshold(
        topHat,
        binary,
        30,
        255,
        THRESH_BINARY
    );

    imshow("原始灰度图", gray);
    imshow("顶帽结果", topHat);
    imshow("亮缺陷二值图", binary);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

在这个流程中:

灰度图
  ↓
顶帽运算
  ↓
突出局部亮区域
  ↓
阈值分割
  ↓
得到亮缺陷区域

二十九、黑帽运算

黑帽运算的计算方式可以理解为:

闭运算结果 - 原图

OpenCV 使用:

morphologyEx(
    gray,
    blackHat,
    MORPH_BLACKHAT,
    kernel
);

闭运算会填补比结构元素小的暗区域。

闭运算结果减去原图后,保留下来的主要是:

比周围背景更暗,并且尺寸小于结构元素的区域。

因此,黑帽运算常用于提取:

  • 局部黑点;
  • 暗色缺陷;
  • 小孔;
  • 裂纹;
  • 划痕;
  • 暗线;
  • 光照不均匀背景上的暗目标。

三十、黑帽运算完整示例

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat gray = imread(
        "D:/images/dark_defect.png",
        IMREAD_GRAYSCALE
    );

    if (gray.empty())
    {
        cerr << "图像读取失败" << endl;
        return -1;
    }

    Mat kernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(21, 21)
    );

    Mat blackHat;
    morphologyEx(
        gray,
        blackHat,
        MORPH_BLACKHAT,
        kernel
    );

    Mat binary;
    threshold(
        blackHat,
        binary,
        30,
        255,
        THRESH_BINARY
    );

    imshow("原始灰度图", gray);
    imshow("黑帽结果", blackHat);
    imshow("暗缺陷二值图", binary);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

三十一、顶帽和黑帽中的结构元素尺寸

顶帽和黑帽对结构元素尺寸非常敏感。

假设需要检测的缺陷宽度约为 5 个像素。

可以尝试:

Size(11, 11)
Size(15, 15)
Size(21, 21)

结构元素通常要比目标尺寸更大,才能在开运算或闭运算时把目标作为局部小结构去除。

如果结构元素太小:

  • 缺陷可能没有被去除;
  • 顶帽或黑帽结果不明显;
  • 只能提取缺陷中的一部分。

如果结构元素太大:

  • 会带入较大范围的背景变化;
  • 可能提取出产品整体结构;
  • 计算量增加;
  • 边缘区域可能产生较强响应。

因此,可以把结构元素尺寸理解为:

需要关注的局部尺度。


三十二、morphologyEx 函数

OpenCV 使用 morphologyEx() 完成多种组合形态学操作。

基本形式:

morphologyEx(
    src,
    dst,
    operation,
    kernel
);

常见操作类型包括:

MORPH_OPEN
MORPH_CLOSE
MORPH_GRADIENT
MORPH_TOPHAT
MORPH_BLACKHAT

对应关系如下:

操作类型 含义
MORPH_OPEN 开运算
MORPH_CLOSE 闭运算
MORPH_GRADIENT 形态学梯度
MORPH_TOPHAT 顶帽运算
MORPH_BLACKHAT 黑帽运算

三十三、腐蚀和膨胀也可以处理灰度图

腐蚀和膨胀并不只能处理二值图像。

对于灰度图像:

灰度膨胀

膨胀会在结构元素范围内选择较大的灰度值。

可以简单理解为:

局部最大值滤波

效果通常表现为:

  • 亮区域扩张;
  • 暗区域缩小;
  • 小暗点被压缩;
  • 图像整体局部变亮。

灰度腐蚀

腐蚀会在结构元素范围内选择较小的灰度值。

可以简单理解为:

局部最小值滤波

效果通常表现为:

  • 暗区域扩张;
  • 亮区域缩小;
  • 小亮点被压缩;
  • 图像整体局部变暗。

示例:

Mat grayErode;
Mat grayDilate;

erode(
    gray,
    grayErode,
    kernel
);

dilate(
    gray,
    grayDilate,
    kernel
);

顶帽和黑帽正是基于灰度形态学操作实现的。


三十四、使用不同方向的结构元素

结构元素不一定必须是正方形。

例如,可以创建水平结构元素:

Mat horizontalKernel = getStructuringElement(
    MORPH_RECT,
    Size(15, 1)
);

也可以创建垂直结构元素:

Mat verticalKernel = getStructuringElement(
    MORPH_RECT,
    Size(1, 15)
);

水平结构元素

Size(15, 1)

主要作用于水平方向,可以用于:

  • 连接水平断线;
  • 提取水平线;
  • 去除较短的垂直干扰;
  • 修复水平裂缝。

垂直结构元素

Size(1, 15)

主要作用于垂直方向,可以用于:

  • 连接垂直断线;
  • 提取垂直线;
  • 去除较短的水平干扰;
  • 修复垂直裂缝。

这种具有方向性的结构元素在工业视觉中非常实用。


三十五、提取水平线示例

可以使用水平结构元素进行开运算,保留较长的水平结构。

Mat horizontalKernel = getStructuringElement(
    MORPH_RECT,
    Size(30, 1)
);

Mat horizontalLines;
morphologyEx(
    binary,
    horizontalLines,
    MORPH_OPEN,
    horizontalKernel
);

其原理是:

  1. 水平长线能够在水平腐蚀后保留下来;
  2. 较短区域或其他方向结构可能被去除;
  3. 再经过膨胀恢复水平线的宽度。

三十六、提取垂直线示例

Mat verticalKernel = getStructuringElement(
    MORPH_RECT,
    Size(1, 30)
);

Mat verticalLines;
morphologyEx(
    binary,
    verticalLines,
    MORPH_OPEN,
    verticalKernel
);

这种方法可以用于:

  • 表格线提取;
  • 网格检测;
  • 水平和垂直边框提取;
  • 工件规则线条分析;
  • 字符区域预处理。

三十七、形态学操作的组合示例

假设阈值分割后:

  • 存在较小白色噪点;
  • 目标内部存在小黑孔;
  • 目标边缘存在小间隙。

可以先进行开运算,再进行闭运算:

Mat kernel = getStructuringElement(
    MORPH_ELLIPSE,
    Size(5, 5)
);

Mat opened;
morphologyEx(
    binary,
    opened,
    MORPH_OPEN,
    kernel
);

Mat result;
morphologyEx(
    opened,
    result,
    MORPH_CLOSE,
    kernel
);

处理流程为:

原始二值图
    ↓
开运算去除小白点
    ↓
闭运算填补小孔和断口
    ↓
获得较完整目标区域

但是,每增加一次形态学操作,都会对目标边界产生一定影响。

因此,形态学操作并不是越多越好。


三十八、工业视觉中的参数选择思路

形态学参数不能只根据图像分辨率随意设置,而应该结合实际目标尺寸。

例如,一个噪点直径大约为 3 个像素,而正常缺陷最小直径为 10 个像素。

可以尝试使用:

Size(5, 5)

的开运算:

  • 3 像素噪点可能被去除;
  • 10 像素缺陷有机会保留下来。

如果直接使用:

Size(15, 15)

可能连正常缺陷也会被去除。

因此,结构元素尺寸应该根据以下因素确定:

  • 噪点尺寸;
  • 缺陷尺寸;
  • 目标之间的距离;
  • 断口宽度;
  • 孔洞尺寸;
  • 图像分辨率;
  • 实际物理尺寸;
  • 相机标定比例。

如果已知图像比例是:

每像素 0.02 mm

希望连接宽度小于:

0.1 mm

那么对应像素数量约为:

0.1 ÷ 0.02 = 5 像素

此时可以围绕 5 像素设计结构元素,而不是完全凭感觉选择参数。


三十九、一个暗缺陷检测流程示例

假设产品表面较亮,缺陷较暗,并且缺陷中存在断裂和噪点。

可以使用以下流程:

灰度化
  ↓
高斯滤波
  ↓
反向阈值分割
  ↓
开运算去除小白点
  ↓
闭运算连接缺陷
  ↓
连通域或轮廓分析

代码如下:

#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;
    threshold(
        blurred,
        binary,
        0,
        255,
        THRESH_BINARY_INV | THRESH_OTSU
    );

    Mat openKernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(3, 3)
    );

    Mat opened;
    morphologyEx(
        binary,
        opened,
        MORPH_OPEN,
        openKernel
    );

    Mat closeKernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(7, 7)
    );

    Mat result;
    morphologyEx(
        opened,
        result,
        MORPH_CLOSE,
        closeKernel
    );

    imshow("原图", source);
    imshow("灰度图", gray);
    imshow("二值图", binary);
    imshow("开运算", opened);
    imshow("最终结果", result);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

这里分别使用了两个不同尺寸的结构元素:

  • 较小的开运算核用于去除小噪点;
  • 较大的闭运算核用于连接缺陷内部断口。

这比所有操作都使用同一个结构元素更加灵活。


四十、常见问题

1. 为什么腐蚀后目标反而变大了

通常是因为目标区域是黑色,而不是白色。

腐蚀会让白色区域缩小,因此黑色区域看起来会扩大。

可以使用:

bitwise_not(
    binary,
    binaryInverse
);

反转图像后再观察。

也可以在阈值分割时使用:

THRESH_BINARY_INV

让目标变成白色。


2. 为什么膨胀后两个目标粘在一起了

因为两个目标之间的距离小于膨胀的作用范围。

可以尝试:

  • 减小结构元素尺寸;
  • 减少膨胀次数;
  • 改用十字形或方向性结构元素;
  • 对不同区域分别处理;
  • 使用腐蚀或距离变换进行分离。

3. 为什么腐蚀后小缺陷消失了

因为缺陷尺寸小于或接近结构元素尺寸。

腐蚀会持续缩小白色目标,小目标可能被完全去除。

可以尝试:

  • 减小结构元素;
  • 减少迭代次数;
  • 改用开运算;
  • 在形态学处理前先保存原始缺陷信息;
  • 使用连通域面积筛选代替强腐蚀。

4. 为什么闭运算没有完全填满孔洞

闭运算只能填补相对于结构元素较小的孔洞和间隙。

如果孔洞比结构元素大,闭运算可能只能缩小孔洞,而不能完全填满。

可以:

  • 增大结构元素;
  • 增加迭代次数;
  • 使用轮廓填充;
  • 使用漫水填充;
  • 使用专门的孔洞填充方法。

5. 为什么开运算后目标尺寸仍然发生了变化

开运算虽然会在腐蚀后再次膨胀,但不一定能完全恢复原始形状。

尤其是:

  • 尖角;
  • 细线;
  • 狭窄突出区域;
  • 小目标;
  • 不规则轮廓。

这些结构在腐蚀阶段被去除后,膨胀无法恢复。

因此,开运算并不是无损操作。


6. 腐蚀和膨胀应该先用哪一个

取决于需要解决的问题:

  • 去小白点:先腐蚀,也就是开运算;
  • 填小黑洞:先膨胀,也就是闭运算;
  • 连接断口:优先膨胀或闭运算;
  • 分离粘连:优先腐蚀或开运算;
  • 提取边缘:形态学梯度;
  • 提取亮缺陷:顶帽;
  • 提取暗缺陷:黑帽。

7. 结构元素应该使用矩形还是椭圆形

需要根据目标形状和处理方向选择。

矩形核:

  • 作用更强;
  • 对水平和垂直方向明显;
  • 容易产生棱角;
  • 适合规则区域。

椭圆核:

  • 处理更加平滑;
  • 更适合圆形目标;
  • 对角线方向过渡更加自然;
  • 常用于颗粒、孔洞和焊盘。

十字核:

  • 主要作用于水平和垂直方向;
  • 对对角线方向影响较小;
  • 适合方向性连接。

8. 可以直接对彩色图像做腐蚀和膨胀吗

可以。

OpenCV 会分别处理各个通道。

但是彩色图像进行形态学处理后,颜色关系可能发生变化,结果不一定容易解释。

工业检测中更常见的方式是:

彩色图像
  ↓
选择合适颜色通道或转换颜色空间
  ↓
得到单通道图
  ↓
阈值分割
  ↓
形态学处理

四十一、完整对比代码

下面将腐蚀、膨胀、开运算、闭运算和形态学梯度放在同一个程序中进行对比。

#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,
        0,
        255,
        THRESH_BINARY_INV | THRESH_OTSU
    );

    Mat kernel = getStructuringElement(
        MORPH_ELLIPSE,
        Size(5, 5)
    );

    Mat eroded;
    erode(
        binary,
        eroded,
        kernel
    );

    Mat dilated;
    dilate(
        binary,
        dilated,
        kernel
    );

    Mat opened;
    morphologyEx(
        binary,
        opened,
        MORPH_OPEN,
        kernel
    );

    Mat closed;
    morphologyEx(
        binary,
        closed,
        MORPH_CLOSE,
        kernel
    );

    Mat gradient;
    morphologyEx(
        binary,
        gradient,
        MORPH_GRADIENT,
        kernel
    );

    imshow("原始图像", source);
    imshow("灰度图", gray);
    imshow("二值图", binary);
    imshow("腐蚀", eroded);
    imshow("膨胀", dilated);
    imshow("开运算", opened);
    imshow("闭运算", closed);
    imshow("形态学梯度", gradient);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

运行代码后,可以重点观察:

  • 腐蚀后白色目标缩小了多少;
  • 膨胀后白色目标扩大了多少;
  • 开运算去除了哪些小区域;
  • 闭运算连接了哪些断口;
  • 形态学梯度提取出的边缘有多宽;
  • 不同大小和形状的结构元素对结果有什么影响。

四十二、形态学操作在工业视觉中的使用原则

1. 先明确前景颜色

在进行操作前,要先明确目标是白色还是黑色。

最好统一让待分析目标为白色。


2. 结构元素尺寸要有实际依据

不要只尝试:

3×3、5×5、7×7

而不知道为什么。

应结合:

  • 缺陷像素尺寸;
  • 噪点尺寸;
  • 目标间距;
  • 孔洞尺寸;
  • 标定比例。

3. 尽量减少不必要的形态学处理

每次腐蚀、膨胀、开运算和闭运算都会改变目标形状。

如果可以通过连通域面积筛选去除噪点,就不一定需要使用很强的腐蚀。


4. 不要使用处理后的区域直接做精密测量

经过腐蚀或膨胀后,目标边界已经发生变化。

形态学结果可以用于:

  • 目标定位;
  • 区域筛选;
  • 噪点去除;
  • 生成掩膜。

真正测量尺寸时,可以回到原始灰度图或原始二值图,在定位到的区域中重新提取精确边缘。


5. 应使用多张图像验证

同一组形态学参数需要在以下图像上进行测试:

  • 正常产品;
  • 不同大小缺陷;
  • 不同位置缺陷;
  • 临界缺陷;
  • 不同曝光;
  • 不同批次;
  • 边缘区域;
  • 轻微位置偏差。

在单张图像上效果很好,并不代表可以直接用于生产环境。


四十三、本篇总结

本篇主要学习了 OpenCV 中的腐蚀、膨胀和常见形态学操作。

需要重点掌握以下内容:

  1. 形态学操作通常以白色区域作为前景;
  2. 腐蚀会使白色区域缩小;
  3. 膨胀会使白色区域扩大;
  4. 开运算是先腐蚀后膨胀;
  5. 开运算常用于去除小白点和细小突出区域;
  6. 闭运算是先膨胀后腐蚀;
  7. 闭运算常用于填补小黑洞和连接小间隙;
  8. 形态学梯度可以提取目标边缘;
  9. 顶帽运算适合提取局部亮目标;
  10. 黑帽运算适合提取局部暗目标;
  11. 结构元素可以是矩形、十字形或椭圆形;
  12. 结构元素的大小决定了处理的空间尺度;
  13. 水平和垂直结构元素可以提取特定方向的线条;
  14. 形态学操作会改变目标边界;
  15. 工业检测中需要根据目标实际尺寸选择参数;
  16. 形态学处理结果更适合作为掩膜或定位依据,不一定适合直接进行精密测量。

形态学操作本身并不复杂,但真正困难的是:

根据实际目标、噪声、孔洞和间隙的尺寸,选择合适的结构元素形状和大小。

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