一、形态学操作
1、图像腐蚀
1.1.1 图像腐蚀目的
- **去除图像中微小物体:**在二值图像中,一些由于噪声产生的小亮点、小区域通常面积较小。经过腐蚀处理后,这些小区域会被消除,从而达到去噪的效果,提高图像质量。
- **分离较近的两个物体:**当两个目标之间连接较细或者距离非常接近时,腐蚀会使目标边界向内收缩,从而断开细小连接部分,使两个目标彼此分离,便于后续的目标检测、轮廓提取与连通域分析。
- **细化目标边界:**腐蚀会减小目标区域面积,使目标轮廓更加紧凑,有利于后续形状分析与特征提取。
- **配合膨胀完成开运算:**腐蚀通常与膨胀结合使用,先腐蚀后膨胀形成开运算,能够有效去除小噪声并保持主体形状。
1.1.2 图像腐蚀原理

中间的黄色十字区域为结构元素,红色部分为当前中心像素。
当结构元素覆盖区域内所有对应位置都为前景像素(值为1)时,中心像素才保留;只要有一个位置为背景(0),中心像素就被删除。
1.1.3 结构元素生成函数
cpp
/* 用途:用于创建形态学操作所需的结构元素(卷积模板),
生成指定形状和大小的核矩阵,供腐蚀、膨胀、开运算、
闭运算等形态学处理函数使用 */
Mat cv::getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));
/*
shape:结构元素的种类
ksize:结构元素的尺寸大小
anchor:中心点的位置,默认参数为结构元素的几何中心点
*/
| 标志参数 | 简记 | 作用 |
|---|---|---|
| MORPH_RECT | 0 | 矩形结构元素,所有元素都为1 |
| MORPH_CROSS | 1 | 十字结构元素,中间的列和行元素为1 |
| MORPH_ELLIPSE | 2 | 椭圆结构元素,矩形的椭圆内接元素为1 |
| [矩阵结构元素] |
1.1.4 图像腐蚀操作函数
cpp
/* 用途:用于对图像进行腐蚀操作,通过结构元素扫描图像,
将目标区域边界向内收缩,使亮区域(白色前景)变小 */
void cv::erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
/*
src:输入的待腐蚀图像,图像的通道数可以是任意的,但是图像的数据类型必须是CV_8U、CV_16U、
CV_16S、CV_32F或CV_64F之一
dst:腐蚀后的输出图像,与输入图像src具有相同的尺寸和数据类型
kernel:用于腐蚀操作的结构元素,可以自己输入,也可以用getStructuringElement()函数生成
anchor:中心点再结构元素中的位置,默认参数为结构元素的几何中心点
iterations:腐蚀的次数
borderType:像素外推法选择标志
borderValue:边界不变的边界值
*/
1.1.5 示例代码
cpp
void drawState(Mat& img, int number, Mat centroids, Mat stats, String str)
{
/*生成随机颜色,用于区分不同连通域*/
RNG rng(10086);
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
/*使用均匀分布的随机数确定颜色*/
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
for (int i = 1; i < number; i++)
{
/*中心位置*/
int center_x = centroids.at<double>(i, 0);
int center_y = centroids.at<double>(i, 1);
/*矩形边框*/
int x = stats.at<int>(i, CC_STAT_LEFT);
int y = stats.at<int>(i, CC_STAT_TOP);
int w = stats.at<int>(i, CC_STAT_WIDTH);
int h = stats.at<int>(i, CC_STAT_HEIGHT);
int area = stats.at<int>(i, CC_STAT_AREA);
/*中心位置绘制*/
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, 8, 0);
/*外接矩形*/
Rect rect(x, y, w, h);
rectangle(img, rect, colors[i], 1, 8, 0);
putText(img, format("%d", i), Point(center_x, center_y),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1);
}
imshow(str, img);
}
void erodeStudy()
{
/*生成用于腐蚀的原图像*/
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2;
struct1 = getStructuringElement(MORPH_RECT, Size(3, 3));/*矩形结构元素*/
struct2 = getStructuringElement(MORPH_CROSS, Size(3, 3));/*十字结构元素*/
Mat erodeSrc;/*存放腐蚀后的图像*/
erode(src, erodeSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("erodeSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("erodeSrc", erodeSrc);
waitKey(0);
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat LearnCV_black = imread(s_imgPath + "/LearnCV_black.png", IMREAD_ANYCOLOR);
if (LearnCV_black.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
Mat erode_black1, erode_black2;
/*黑背景图像腐蚀*/
erode(LearnCV_black, erode_black1, struct1);
erode(LearnCV_black, erode_black2, struct2);
imshow("LearnCV_black", LearnCV_black);
imshow("erode_black1", erode_black1);
imshow("erode_black2", erode_black2);
waitKey(0);
Mat img = imread(s_imgPath + "/rice.png");
if (img.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
Mat img2;
copyTo(img, img2, img);/*拷贝一个单独的图像,用于后期图像绘制*/
Mat rice, riceBW;
/*将图像转成二值化图像,用于统计连通域*/
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
Mat out, stats, centroids;
/*统计图像中连通域的个数*/
int number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img, number, centroids, stats, "Blob Statistics Before Erosion");/*绘制图像*/
erode(riceBW, riceBW, struct1);/*对图像进行腐蚀*/
number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img2, number, centroids, stats, "Blobs After Erosion");/*绘制图像*/
waitKey(0);
destroyAllWindows();
}
2、图像膨胀
1.2.1 图像膨胀目的
- **扩大目标区域:**膨胀能够使前景目标变大,填补目标边界附近的空隙,使目标更加明显。
- **填补目标内部的小孔洞:**当目标内部存在小黑洞或缺口时,膨胀可以填充这些小区域,使目标更加完整。
- **连接相邻目标:**当两个目标距离较近时,膨胀会使边界向外扩张,从而将两个区域连接在一起。
- **增强目标特征:**膨胀后目标区域更加明显。
- 配合腐蚀完成闭运算: 膨胀通常与腐蚀结合使用,先膨胀再腐蚀形成闭运算,能够填补小孔洞、连接邻近区域、平滑边界。
1.2.2 图像膨胀原理

中间的黄色十字区域为结构元素,红色部分为当前中心像素。
只要结构元素覆盖区域中存在一个前景像素(值为1),当前中心位置就会被置为前景。
1.2.3 图像膨胀操作函数
cpp
/* 用途:用于对图像进行膨胀操作,通过结构元素扫描图像,
将目标区域边界向外扩张,使亮区域(白色前景)变大。
膨胀能够填补小黑洞、连接邻近目标、增强前景区域 */
void cv::dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
/*
src:输入的待膨胀图像,图像的通道数可以是任意的,但是图像的数据类型必须是CV_8U、CV_16U、
CV_16S、CV_32F或CV_64F之一
dst:膨胀后的输出图像,与输入图像src具有相同的尺寸和数据类型
kernel:用于膨胀操作的结构元素,可以自己输入,也可以用getStructuringElement()函数生成
anchor:中心点再结构元素中的位置,默认参数为结构元素的几何中心点
iterations:膨胀的次数
borderType:像素外推法选择标志
borderValue:边界不变的边界值
*/
1.2.4 示例代码
cpp
/*生成用于膨胀的原图像*/
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2;
struct1 = getStructuringElement(MORPH_RECT, Size(3, 3));/*矩形结构元素*/
struct2 = getStructuringElement(MORPH_CROSS, Size(3, 3));/*十字结构元素*/
Mat dilateSrc;/*存放膨胀后的图像*/
dilate(src, dilateSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("dilateSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("dilateSrc", dilateSrc);
waitKey(0);
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat LearnCV_black = imread(s_imgPath + "/LearnCV_black.png", IMREAD_ANYCOLOR);
if (LearnCV_black.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
Mat dilate_black1, dilate_black2;
/*黑背景图像腐蚀*/
dilate(LearnCV_black, dilate_black1, struct1);
dilate(LearnCV_black, dilate_black2, struct2);
imshow("LearnCV_black", LearnCV_black);
imshow("dilate_black1", dilate_black1);
imshow("dilate_black2", dilate_black2);
waitKey(0);
destroyAllWindows();
二、形态学应用
1、开运算、闭运算、形态学梯度等原理
2.1.1 开运算
**作用:**去除图像中的噪声,消除较小连通域,保留较大连通域
步骤:
- 对原图像腐蚀
- 腐蚀结果再次进行膨胀

2.1.2 闭运算
**作用:**去除连通域内的小型空洞,平滑物体轮廓,连接两个临近的连通域
步骤:
- 对原图像膨胀
- 膨胀结果再次进行腐蚀

2.1.3 形态学梯度
**作用:**得到轮廓的梯度
步骤:
- 对原图像膨胀
- 对原图像腐蚀
- 膨胀结果减去腐蚀结果

2.1.4 顶帽运算
**作用:**分离比邻近点亮一些的斑点
步骤:
- 对原图像开运算
- 原图像减去开运算

2.1.5 黑帽运算
**作用:**分离比邻近点暗一些的斑点
步骤:
- 对原图像闭运算
- 闭运算减去原图像

2.1.6 击中击不中变换
**作用:**比图像腐蚀要求更加苛刻的一种形态学操作
步骤:
- 与腐蚀操作类似

总结:用一个模板,同时检查前景和背景,找到完全符合条件的目标形状
2、形态学操作对比

3、相关函数
cpp
/* 用途:用于执行高级形态学操作,是腐蚀(erode)和膨胀(dilate)
的综合扩展函数。通过不同的op参数,可以实现开运算、
闭运算、形态学梯度、顶帽运算、黑帽运算等多种图像处理效果。
常用于图像去噪、目标增强、边缘提取、孔洞填充、
背景校正以及轮廓分析等场景 */
void cv::morphologyEx( InputArray src, OutputArray dst,
int op, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
/*
src:输入图像,可为灰度图像或二值图像
dst:形态学处理后的输出图像,与src具有相同尺寸和数据类型
op:形态学操作类型标志
kernel:形态学操作使用的结构元素
anchor:结构元素中心位置,默认(-1,-1)表示几何中心
iterations:处理执行次数
borderType:像素边界外推方式
borderValue:边界填充值
*/
| 标志参数 | 简记 | 作用 |
|---|---|---|
| MORPH_ERODE | 0 | 图像腐蚀 |
| MORPH_DILATE | 1 | 图像膨胀 |
| MORPH_OPEN | 2 | 开运算 |
| MORPH_CLOSE | 3 | 闭运算 |
| MORPH_GRADIENT | 4 | 形态学梯度 |
| MORPH_TOPHAT | 5 | 顶帽运算 |
| MORPH_BLACKHAT | 6 | 黑帽运算 |
| MORPH_HITMISS | 7 | 击中击不中运算 |
| [形态学应用对应的参数] |
4、示例代码
cpp
/*用于验证形态学应用的二值化矩阵*/
Mat src = (Mat_<uchar>(9, 12) << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
);
namedWindow("src", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("src", src);
/*3 x 3矩阵结构元素*/
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
/*对二值化矩阵进行形态学操作*/
Mat open, close, gradient, tophat, blackhat, hitmiss;
/*对二值化矩阵进行开运算*/
morphologyEx(src, open, MORPH_OPEN, kernel);
namedWindow("open", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("open", open);
/*对二值化矩阵进行闭运算*/
morphologyEx(src, close, MORPH_CLOSE, kernel);
namedWindow("close", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("close", close);
/*对二值化矩阵进行梯度运算*/
morphologyEx(src, gradient, MORPH_GRADIENT, kernel);
namedWindow("gradient", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("gradient", gradient);
/*对二值化矩阵进行顶帽运算*/
morphologyEx(src, tophat, MORPH_TOPHAT, kernel);
namedWindow("tophat", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("tophat", tophat);
/*对二值化矩阵进行黑帽运算*/
morphologyEx(src, blackhat, MORPH_BLACKHAT, kernel);
namedWindow("blackhat", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("blackhat", blackhat);
/*对二值化矩阵进行击中击不中运算*/
morphologyEx(src, hitmiss, MORPH_HITMISS, kernel);
namedWindow("hitmiss", WINDOW_NORMAL);/*可以自由调节显示图像的尺寸*/
imshow("hitmiss", hitmiss);
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat keys = imread(s_imgPath + "/keys.png", IMREAD_GRAYSCALE);
if (keys.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
imshow("Original keys", keys);
threshold(keys, keys, 80, 255, THRESH_BINARY);
imshow("Binary keys", keys);
/*5 x 5矩阵结构元素*/
Mat kernel_keys = getStructuringElement(MORPH_RECT, Size(5, 5));
Mat open_keys, close_keys, gradient_keys, tophat_keys, blackhat_keys, hitmiss_keys;
/*对图像进行开运算*/
morphologyEx(keys, open_keys, MORPH_OPEN, kernel_keys);
imshow("open_keys", open_keys);
/*对图像进行闭运算*/
morphologyEx(keys, close_keys, MORPH_CLOSE, kernel_keys);
imshow("close_keys", close_keys);
/*对图像进行梯度运算*/
morphologyEx(keys, gradient_keys, MORPH_GRADIENT, kernel_keys);
imshow("gradient_keys", gradient_keys);
/*对图像进行顶帽运算*/
morphologyEx(keys, tophat_keys, MORPH_TOPHAT, kernel_keys);
imshow("tophat_keys", tophat_keys);
/*对图像进行黑帽运算*/
morphologyEx(keys, blackhat_keys, MORPH_BLACKHAT, kernel_keys);
imshow("blackhat_keys", blackhat_keys);
/*对图像进行击中击不中运算*/
morphologyEx(keys, hitmiss_keys, MORPH_HITMISS, kernel_keys);
imshow("hitmiss_keys", hitmiss_keys);
waitKey(0);
destroyAllWindows();