OpenCV17-图像形态学操作
1.形态学操作
1.1腐蚀
图像腐蚀(Image erosion)可用于减小图像中物体的大小、填充孔洞或者分离邻近的物体。腐蚀操作通过对图像中的每个像素应用结构元素(也称为腐蚀内核)来实现。
腐蚀操作的原理是将结构元素与图像进行逐像素的比较。如果结构元素的所有像素与图像中对应位置的像素都匹配,那么该像素保持不变。否则,该像素被腐蚀为背景像素。这样,腐蚀操作将使物体边界向内收缩,并且可以消除小的物体或连接物体之间的细小连接。
下面是一种常见的图像腐蚀算法的伪代码表示:
对于图像中的每个像素(x, y):
对于结构元素中的每个像素(i, j):
如果结构元素的中心与图像中的像素(x, y)对齐:
如果结构元素的所有像素与图像中对应位置的像素都匹配:
继续下一步
否则:
将图像中的像素(x, y)设置为背景像素
中断内层循环
腐蚀就是寻找图像中能够将结构元素全部包含的像素点。对于输出图像中的每个像素位置(x, y),将结构元素B的中心与输入图像I的对应位置(x, y)对齐,然后在结构元素B覆盖的区域内选择输入图像中的最小像素值作为输出图像中的像素值。
OpenCV提供了 getStructuringElement()
函数用于生成常用的矩形结构元素、十字结构元素和椭圆结构元素。
cpp
Mat getStructuringElement(
int shape, // 生成结构元素的形状
Size ksize, // 结构元素的尺寸
Point anchor = Point(-1,-1)
);
//! shape of the structuring element
enum MorphShapes {
MORPH_RECT = 0, //矩形结构元素,所有元素都为1
MORPH_CROSS = 1, //十字结构元素,中间的列和行元素为1
MORPH_ELLIPSE = 2 //椭圆结构元素,矩形的内接椭圆元素为1
};
OpenCV提供了 erode()
函数用于图像腐蚀:
cpp
void erode(
InputArray src,
OutputArray dst,
InputArray kernel, // 结构元素
Point anchor = Point(-1,-1),
int iterations = 1, // 腐蚀的次数,默认为1
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() // 使用边界不变外推法时的边界值
);
该函数根据结构元素对输入图像进行腐蚀,在腐蚀多通道图像时,每个通道独立进行腐蚀运算。
需要注意的是,该函数的腐蚀过程只针对图像中的非零像素,因此如果图像是以0像素为背景(黑底),那么腐蚀操作后会看到图像中的内容变得更瘦更小;如果图像是以255像素为背景(白底),那么腐蚀操作后看到的图像中的内容变得更粗更大。(与下面的膨胀相反)
下面代码中演示了腐蚀的效果,实现对原图中米粒进行计数,通过结果可以发现,腐蚀操作可以去除由噪声引起的较小的连通域,得到了正确的米粒数:
cpp
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
//绘制包含区域函数
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);
// 中心位置绘制
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);
}
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//生成用于腐蚀的原图像
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);
Mat LearnCV_black = imread("black_white_handwritten.jpg", IMREAD_ANYCOLOR);
Mat LearnCV_write = imread("white_black_handwritten.jpg", IMREAD_ANYCOLOR);
Mat erode_black1, erode_black2, erode_write1, erode_write2;
//黑背景图像腐蚀
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);
//白背景腐蚀
erode(LearnCV_write, erode_write1, struct1);
erode(LearnCV_write, erode_write2, struct2);
imshow("LearnCV_write", LearnCV_write);
imshow("erode_write1", erode_write1);
imshow("erode_write2", erode_write2);
//验证腐蚀对小连通域的去除
Mat img = imread("rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
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);
cout << "未腐蚀:" << number << endl; // 27
drawState(img, number, centroids, stats, "未腐蚀时统计连通域"); //绘制图像
erode(riceBW, riceBW, struct1); //对图像进行腐蚀
number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
cout << "腐蚀后:" << number << endl; // 26
drawState(img2, number, centroids, stats, "腐蚀后统计连通域"); //绘制图像
waitKey(0);
return 0;
}
1.2膨胀
图像膨胀(Image dilation)是一种图像处理操作,通常用于增加图像中物体的大小或填充物体的空隙,其目标是扩展和增强图像中的图形特征。
在定义结构元素之后,将结构元素的中心点依次放到图像中每一个非零元素处,如果原图中某个元素被结构元素覆盖,但是该像素的像素值不与结构元素中心点对应的像素点的像素值相同,那么将原图中该像素的像素值修改为结构元素中心点对应点的像素值。(将结构元素覆盖的所有区域,修改为1)
OpenCV提供了 dilate()
函数用于图像膨胀。
cpp
void dilate(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor = Point(-1,-1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue()
);
该函数用法同 erode()
。第三个参数是结构元素尺寸,在图像膨胀操作中,结构元素的形状和大小起着关键作用。较大的结构元素将导致更强的膨胀效果,而较小的结构元素则会产生细微的膨胀效果。第五个参数是使用结构元素膨胀的次数,膨胀次数越多效果越明显。
需要注意的是,该函数的膨胀过程只针对图像中的非零像素,因此如果图像是以0像素为背景(黑底),那么膨胀操作后会看到图像中的内容变得更粗和更大;如果图像是以255像素为背景(白底),那么膨胀操作后看到的图像中的内容变得更细和更小。(与上面的腐蚀相反)
下面代码示例,代码最后为了验证膨胀与腐蚀效果之间的关系,求取黑色背景图像的复试结果与白色背景图像的膨胀结果进行逻辑"与"、"异或"运算,证明两个过程的相反性。
cpp
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//生成用于腐蚀的原图像
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(0, Size(3, 3)); //矩形结构元素
struct2 = getStructuringElement(1, Size(3, 3)); //十字结构元素
Mat erodeSrc; //存放膨胀后的图像
dilate(src, erodeSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("dilateSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("dilateSrc", erodeSrc);
Mat LearnCV_black = imread("black_white_handwritten.jpg", IMREAD_ANYCOLOR);
Mat LearnCV_write = imread("white_black_handwritten.jpg", IMREAD_ANYCOLOR);
if (LearnCV_black.empty() || LearnCV_write.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat dilate_black1, dilate_black2, dilate_write1, dilate_write2;
//黑背景图像膨胀
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);
//白背景图像膨胀
dilate(LearnCV_write, dilate_write1, struct1);
dilate(LearnCV_write, dilate_write2, struct2);
imshow("LearnCV_write", LearnCV_write);
imshow("dilate_write1", dilate_write1);
imshow("dilate_write2", dilate_write2);
//比较膨胀和腐蚀的结果
Mat erode_black1, resultXor, resultAnd;
erode(LearnCV_black, erode_black1, struct1);
bitwise_xor(erode_black1, dilate_write1, resultXor);
bitwise_and(erode_black1, dilate_write1, resultAnd);
imshow("resultXor", resultXor);
imshow("resultAnd", resultAnd);
waitKey(0);
return 0;
}
2.形态学应用
图像形态学腐蚀可以将细小的噪声区域去除,但是会将图像主要区域的面积缩小,造成主要区域的形状发生改变。图像形态学膨胀可以扩充每一个区域的面积,填充较小的空洞,但是会增加噪声的面积。根据两者的特性,将图像腐蚀和膨胀适当结合,便可以既去除图像中的噪声,又不缩小图像中主要区域的面积;既填充较小的空洞,又不增加噪声的面积。
2.1开运算
图像开运算可以去除图像中的噪声,消除较小连通域,保留较大连通域,同时能够在两个物体纤细的连接处将两个物体分离,并且在不明显改变较大连通区域面积的同时能够平滑连通域的边界。
开运算:先腐蚀,消除图像中的噪声和较小的连通域;后膨胀,弥补较大的连通域因腐蚀而造成的面积减小。
开运算是对图像腐蚀和膨胀的结合,OpenCV中没有通过只用于图像开运算的函数,而是提供了图像腐蚀和膨胀运算不同组合形式的 morphologyEx()
函数,以实现开运算、闭运算、形态学梯度、顶帽运算、黑帽运算以及击中击不中变换。
cpp
void 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()
);
该函数根据结构元素对输入图像进行多种形态学操作,在处理多通道图像时,每个通道独立进行处理。
第三个参数是形态学操作类型的标志:
cpp
//! type of morphological operation
enum MorphTypes{
MORPH_ERODE = 0, //腐蚀
MORPH_DILATE = 1, //膨胀
MORPH_OPEN = 2, //开运算
MORPH_CLOSE = 3, //闭运算
MORPH_GRADIENT = 4, //形态学梯度
MORPH_TOPHAT = 5, //顶帽运算
MORPH_BLACKHAT = 6, //黑帽运算
MORPH_HITMISS = 7 //击中击不中bian
};
2.2闭运算
图像闭运算可以去除连通域内的小型空洞,平滑物体轮廓,连接两个临近的连通域。闭运算,先膨胀,填充连通域内小型空洞,扩大连通域边界,将临近的两个连通域连接;后腐蚀,减少由膨胀运算引起的连通域边界的扩大以及面积的增加。
闭运算是对图像膨胀和腐蚀的结合,可以使用 morphologyEx()
函数选择闭运算参数 MORPH_CLOSE
实现闭运算。
2.3形态学梯度
形态学梯度能够描述目标的边界,根据图象腐蚀和膨胀与原图之间的关系计算得到,形态学梯度可以分为基本梯度、内部梯度和外部梯度。
基本梯度:膨胀后图像与腐蚀后图像差值。
内部梯度:原图像与腐蚀后图像差值。
外部梯度:膨胀后图像与原图像差值。
OpenCV提供的 morphologyEx()
函数可以选择形态学梯度参数 MORPH_GRADIENT
实现图像的基本梯度。如果需要计算图像的内部梯度和外部梯度,需要自己通过程序实现。
2.4顶帽运算
顶帽运算是原图像与开运算结果之间的差值,往往用来分离比临近点亮一些的斑块。因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域。
顶帽先对图像进行开运算,之后用原图减去开运算计算的结果。使用 morphologyEx()
函数选择顶帽运算的参数 MORPH_TOPHAT
。
2.5黑帽运算
与顶帽运算不同,黑帽运算是原图像与闭运算结果之间的差值,往往用来分离比邻近点暗一些的斑块。黑帽运算先对图像进行闭运算,之后从闭运算结果减去原图。使用 morphologyEx()
函数选择黑帽运算的参数 MORPH_BLACKHAT
。
2.6击中击不中变换
击中击不中变换(Hit-or-Miss Transform)利用两个结构元素,一个称为"击中"元素(Hit element),另一个称为"不中"元素(Miss element)。
击中击不中变换的目的是通过将击中元素与图像进行腐蚀(erosion)操作和将不中元素与图像进行膨胀(dilation)操作,来检测指定形状在图像中的位置。
输出图像中的白色像素表示原始图像中存在与击中元素形状相匹配的区域,黑色像素表示不匹配的区域。
使用 morphologyEx()
函数选择击中击不中变换的参数 MORPH_HITMISS
。
2.7形态学应用示例
cpp
//#include <opencv2/core.hpp>
//#include <opencv2/highgui.hpp>
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//用于验证形态学应用的二值化矩阵
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);
namedWindow("src", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("src", src);
//3×3矩形结构元素
Mat kernel = getStructuringElement(0, 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);
//用图像验证形态学操作效果
Mat keys = imread("keys.jpg", IMREAD_GRAYSCALE);
imshow("原图像", keys);
threshold(keys, keys, 80, 255, THRESH_BINARY);
imshow("二值化后的keys", keys);
//5×5矩形结构元素
Mat kernel_keys = getStructuringElement(0, 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);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}