目录
0x01 腐蚀膨胀操作
0x02 开闭运算操作
0x03 形态学梯度
0x04 形态学Top-Hat
0x05 用在哪?角点提取、车牌提取
数学形态学是基于集合论的图像处理方法,最早出现于生物学的形态与结构中,图像处理中的形态学操作用于图像预处理操作(去噪、形状简化)、图像增强(骨架提取、细化、凸包及物体标记)、物体背景分割及物体形态量化等场景中。数学形态学利用点集的性质、积分几何集及拓扑学理论对物体像素集进行变换。
那么操作有如下:
腐蚀与膨胀操作
开闭运算操作及实现
形态学梯度操作
形态学TOP-Hat
膨胀操作时形态学的基本操作,实现了对目标像素点进行扩展的目的,其定义如下:
形态学膨胀操作的思路:
运算前A与B分别为两个区域,B区域的黑点表示B的中心点,非B表示B相对于自己中心对称变换后的图形,运算后相当于B对称,沿着区域A的边界遍历一圈,区域B的中心扫过区域加上A本身的区域就是区域A膨胀区域B的结果,其中(B)z表示将B平移,使其中心点位于z位置。
腐蚀操作也是形态学的基本操作,实现了对目标像素点进行缩小的目的,其定义:
运算前A与B分别为两个区域,使用B对A进行腐蚀就是沿着区域A的内部边界遍历一圈,平移区域B形成的集合区。B的中心形成的轨迹即是腐蚀后Z的边界,(B)z表示的是B的平移,使其中心点位于z位置。
腐蚀与膨胀可以看作一种互逆运算,膨胀对原始区域扩大,腐蚀对原始区域缩小。在OpenCV中如何操作?erode()腐蚀,dilate()膨胀。
void erode(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor=Point(-1,-1),
int iterations=1,
int borderType=BORDER_CONSTANT,
const Scalar& borderValue=morphologyDefaultB-orderValue()
)
void dilate(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor = Point(-1,-1),
int iterations = 1,
int borderType=BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefault-BorderValue()
)
函数解释:
Erode()使用一个特定的结构化元素侵蚀一个图像,Dilate()使用图像结构化元素进行膨胀。
src:输入图像(二值化、灰度图像)
dst:输出图像(参数类型与输入图像一致)
kernel:表示定义的结构元素大小
anchor:结构元素的中心,如果默认参数(-1,-1),程序会自动将其设置为结构元素的中心。
iteration:迭代次数,可以选择对图像进行多次形态学运算。
borderType以及borderValue:可选择参数设置,针对边界设置。
那么腐蚀膨胀后的图像到底是什么样子?我们以下面这张图片做实验:
使用腐蚀:
使用膨胀:
代码:
int main()
{
cv::Mat srcImage = cv::imread("./image/CC.png");
if (!srcImage.data)
return 1;
cv::Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
cv::Mat segDst, dilDst, eroDst;
// 分通道二值化
cv::inRange(srcGray, cv::Scalar(100),
cv::Scalar(255), segDst);
// 定义结构元素
cv::Mat element = cv::getStructuringElement(
cv::MORPH_ELLIPSE, cv::Size(5, 5));
// 腐蚀膨胀操作
cv::dilate(segDst, dilDst, element);
cv::erode(segDst, eroDst, element);
cv::imshow(" srcGray ", srcGray);
cv::imshow(" segDst ", segDst);
cv::imshow(" dilDst ", dilDst);
cv::imshow(" eroDst ", eroDst);
cv::waitKey();
return 0;
}
cv::inRange:将两个阈值内的像素值设置为白色,而不在阈值内的像素值设置为黑色。
cv::getStructuringElement:可用于构造一个特定大小和形状的结构元素,用于图像形态学处理。
cv::getStructuringElement(
int shape, //结构元素形状
Size ksize, //结构元素大小
Point anchor = Point(-1,-1) //锚点 默认值Point(-1,-1)表示锚点位于结构元素中心
)
对于结构元素的形状:
从上往下是:方形、交错形状、椭圆形元素,内切于Rect(0, 0, esize.width, 0.esize.height)
定义的矩形。
那么再往下讲叭:
膨胀操作中队输入图像进行特定结构元素操作取决于滑动窗口图像相关邻域的最大值,同时该函数可以多次队原输入图像进行操作。对于多通道图像,应该对每个通道独立地进行膨胀操作,再合并相应的结果。滑动窗口邻域最大值操作如下所示:
腐蚀操作中对输入图像进行特定结构元素操作取决于滑动窗口图像相关邻域的最小值,同膨胀操作一样,也支持多次操作。对多通道数据进行分离处理,滑动窗口邻域最小值的操作如下:
形态学操作通过结构元素及邻域对图像的每个像素进行逻辑变换,经过形态学得到的新图像对应坐标(x',y')是通过对输入图像(x,y)及其邻域根据结构元素形状进行遍历,最终得到像素坐标变换后的图像。
形态学开运算操作能够去除噪声以及平滑目标等功能,其定义为:
这个操作再结合上面的公式来看,这个定义是在于先对图像进行腐蚀操作,然后再进行膨胀操作,结构元素各向同性的开运算操作主要用于消除图像中小于结构元素的细节部分,物体的局部形状不变。物体较背景明亮时能够排除小区域物体,消除高于邻近点的孤立点,达到去噪的作用,可以平滑物体轮廓,断开较窄的狭颈。
形态学闭运算操作能够填充目标区域内的离散小空间和分散部分,其定义为:
这个操作是用结构元素B对A先进行膨胀,然后再进行腐蚀。形态学闭操作能够排除小型黑洞(黑色区域),消除低于邻近点的孤立点,达到去噪的作用,也可以平滑物体轮廓,拟合较窄的间断和细长的沟壑,消除小孔洞,填补轮廓线中的断裂。
OpenCV中提供了morphologyEx函数用于形态学开闭运算操作,比如上一章讲的车牌检测就是运用了这个函数:
cv::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 = morph - ologyDefaultBorderValue()
)
计算复杂的形态学变换:
src:输入图像,二值图像或灰度图像。
dst:输出图像,参数类型于输入图像一致。
op:形态学操作算子类型,可以设置为MORPH_OPEN开操作、MORPH_CLOSE闭操作、MORPH_GRADIENT形态学梯度操作、MORPH_TOPHAT顶帽操作、MORPH_BLACKHAT黑帽操作。
iterations:设置腐蚀与膨胀操作的次数。
borderType与borderValue可选参数设置:针对边界处理。
那么这么操作下来上面那副图像会变成什么:
int main()
{
cv::Mat srcImage = cv::imread("./image/CC.png");
if (!srcImage.data)
return 1;
cv::Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
cv::Mat segDst, dilDst, eroDst;
// 分通道二值化
cv::inRange(srcGray, cv::Scalar(100),
cv::Scalar(255), segDst);
// 定义结构元素
cv::Mat element = cv::getStructuringElement(
cv::MORPH_ELLIPSE, cv::Size(10, 10));
// 腐蚀膨胀操作
cv::dilate(segDst, dilDst, element);
cv::erode(segDst, eroDst, element);
// 形态学闭操作
cv::Mat closeMat;
cv::morphologyEx(segDst,closeMat,cv::MORPH_CLOSE,element);
// 形态学开操作
cv::Mat openMat;
cv::morphologyEx(segDst, openMat,cv::MORPH_OPEN,element);
cv::imshow(" srcGray ", srcGray);
cv::imshow(" segDst ", segDst);
cv::imshow(" dilDst ", dilDst);
cv::imshow(" eroDst ", eroDst);
cv::imshow(" closeMat", closeMat);
cv::imshow(" openMat", openMat);
cv::waitKey();
return 0;
}
当我们把形态学那个元素长与宽都加大5的时候,可以发现断裂处的地方通过膨胀后被放大了,而细小的地方则被忽略:
梯度用于刻画目标边界或边缘位于图像灰度级剧烈变换的区域,形态学梯度根据膨胀或腐蚀与原图作差组合来实现增强结构元素邻域中像素的强度,突出高亮区域的外围。最常用的形态学梯度是计算膨胀与腐蚀间的算术差,另外两种计算形态学梯度分别是膨胀结果与原图的算术差以及原图与腐蚀结果的算术差。对于结构元素E,形态学梯度G操作可以表示为:
形态学操作的输出图像像素值是在对应结构元素而非局部过渡区域所定义的邻域中灰度级强度变化的最大值。形态学梯度操作如下:
// 形态学梯度
cv::Mat gradMat;
cv::morphologyEx(segDst, gradMat, cv::MORPH_GRADIENT, element);
最后生成的图像如下:
形态学Top-Hat变换是指形态学顶帽操作与黑帽操作,前者是计算源图像与开运算结果图之差,后者是计算闭运算结果与源图像之差。
形态学Top-Hat变换是常用的一种滤波手段,具有高通滤波的某部分特性,可实现在图像中检测出周围背景亮结构或周边背景暗结构。
顶帽操作常用于检测图像中的峰结构。
黑帽操作常用于检测图像中的波谷结构。
那么拿下面这张图做对比:
使用如下代码处理:
int main()
{
cv::Mat srcImage = cv::imread("./image/beauty.png");
if (!srcImage.data)
return 1;
cv::Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
// 定义结构元素
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(15,15));
cv::Mat topHatMat, blackHatMat;
// 形态学Top-Hat 顶帽
cv::morphologyEx(srcGray, topHatMat,cv::MORPH_TOPHAT,element);
// 形态学Top-Hat 黑帽
cv::morphologyEx(srcGray, blackHatMat,cv::MORPH_BLACKHAT,element);
cv::imshow("topHatMat", topHatMat);
cv::imshow("blackHatMat", blackHatMat);
cv::waitKey(0);
return 0;
}
运行结果如下:
顶帽处理:
黑帽处理:
如果最后生成的图像的区分度太低的话,可以试着调整一下定义结构元素的大小,将其变大。
(一)形态学滤波角点提取
形态学边缘检测的原理是利用膨胀与腐蚀变化区特征来完成边缘检测,膨胀操作是将目标物体向周围邻域扩展,而腐蚀操作是将目标物体向邻域收缩。按照这么讲找边缘就很好找了。其实边缘恰好反映在了形态学腐蚀与膨胀中变化的区域。那么我们只需要将图像膨胀后以及腐蚀后进行作差运算,则可以得到物体的边缘。形态学的边缘检测可以利用形态学梯度操作函数morphologyEx直接得到,具体是通过计算形态学膨胀结果图与腐蚀结果图之差,再进行相应阈值化操作实现的。
由于结构元素有多种形状,因此我们会考虑对图像进行形态学滤波时,可进行相应结构元素的组合进而实现角点提取,opencv也提供了专门的函数去实现复杂的形状元素构建:
cv::Mat getStructingElement(
int shape, //形状元素
Size ksize, //大小
Point anchor=Point(-1,-1))
这个函数在上面详述了,这就不继续往下讲了。
那么原理是什么:(摘自《OpenCV图像处理编程实例》)
形态学滤波在进行角点检测时闭边缘检测过程略显复杂,但基本原理相似。根据角点特征性质,对原图像先利用十字形的结构元素进行膨胀,这种情况下只会使目标物体在边缘处扩展,而角点并不会发生变化。然后利用菱形的结构元素对上一步得到的图形进行腐蚀操作,这种情况下会使目标物体在边缘处无变化,而在角点处发生收缩。接着使用X形结构元素对源图像进行膨胀操作,这种情况下会使得角点处发生扩展。最后利用矩形结构元素对上一步得到的图像进行腐蚀操作,这种情况会使角点恢复原状,同样边缘将腐蚀得更多。将得到的膨胀图与腐蚀图进行相减操作后,就能得到图像的角点。
那么接下来实现角点检测:
对于要跑的赛道 ,我觉得这个可以发挥很大的作用,它可以找到赛道的拐点,之后对赛道进行数学函数的处理。
#include
#include
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
using namespace cv;
int main()
{
cv::Mat srcImage = cv::imread("./image/circle.jpg");
if (!srcImage.data)
return 1;
cv::Mat srcGray;
cv::cvtColor(srcImage, srcGray, CV_RGB2GRAY);
// 定义结构元素
Mat CrossMat(5, 5, CV_8U, Scalar(0));
Mat diamondMat(5, 5, CV_8U, Scalar(1));
Mat squareMat(5, 5, CV_8U, Scalar(1));
Mat x(5, 5, CV_8U, Scalar(0));
// 十字形形状
for (int i = 0; i < 5; i++)
{
CrossMat.at(2, i) = 1;
CrossMat.at(i, 2) = 1;
}
// 菱形形状
diamondMat.at(0, 0) = 0;
diamondMat.at(0, 1) = 0;
diamondMat.at(1, 0) = 0;
diamondMat.at(4, 4) = 0;
diamondMat.at(3, 4) = 0;
diamondMat.at(4, 3) = 0;
diamondMat.at(4, 0) = 0;
diamondMat.at(4, 1) = 0;
diamondMat.at(3, 0) = 0;
diamondMat.at(0, 4) = 0;
diamondMat.at(0, 3) = 0;
diamondMat.at(1, 4) = 0;
// X形状
for (int i = 0; i < 5; i++) {
x.at(i, i) = 1;
x.at(4 - i, i) = 1;
}
// 第1步:十字形对原图进行膨胀
Mat result;
dilate(srcGray, result, CrossMat);
// 第2步:菱形对上步进行腐蚀
erode(result, result, diamondMat);
Mat result2;
// 第3步:X形对原图进行腐蚀
dilate(srcGray, result2, x);
// 第4步:正方形对上步进行腐蚀
erode(result2, result2, squareMat);
// 第4步:计算差值
absdiff(result2, result, result);
threshold(result, result, 40, 255, THRESH_BINARY);
// 绘图
for (int i = 0; i < result.rows; i++)
{
// 获取行指针
const uchar* data = result.ptr(i);
for (int j = 0; j < result.cols; j++)
{
// 如果是角点 则进行绘制圆圈
if (data[j])
circle(srcImage, Point(j, i), 8,
Scalar(0, 255, 0));
}
}
cv::imshow("result", result);
cv::imshow("srcImage", srcImage);
cv::waitKey(0);
return 0;
}
(二)车牌目标提取
上篇博客:OpenCV入门(七)——车牌区域检测_郑烯烃快去学习的博客-CSDN博客
根据形态学梯度检测原理,可应用垂直分量对车牌区域进行竖直边缘检测,然后利用形态学的水平与竖直方向的闭操作算子得到的竖直边缘进行区域连通得到图像Ic。
我们可以根据检测后车辆的目标尺寸大小,自适应改变闭操作算子矩阵,规则如下:
(1)检测目标尺寸宽度与高度为400-600的像素使用的闭操作单元算子分别为1*25矩阵与8 * 1矩阵。
(2)检测目标尺寸宽度与高度为200-300的像素使用的闭操作单元算子分别为1 * 20矩阵与6 * 1矩阵。
(3)检测目标尺寸宽度与高度大于600的像素使用的闭操作单元算子分别为 1 * 28矩阵与6 * 1矩阵。
(4)其余情况使用闭操作单元算子分别为1 *15矩阵与4 *1矩阵。
接着对Ic求其连通域,并在此基础上求最小外接矩形以得到图像Ir。然后对Ir中得到的最小的外接矩形进行车牌尺寸形态判定操作,去除大小、比例不符合要求的最小外接矩形,最终保留的外接矩形连通区域即为疑似车牌区域。
代码:
#include
#include
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
using namespace cv;
cv::Mat getPlate(int width, int height, cv::Mat srcGray)
{
cv::Mat result;
// 形态学梯度检测边缘
morphologyEx(srcGray, result, MORPH_GRADIENT,
Mat(1, 2, CV_8U, Scalar(1)));
cv::imshow("1result", result);
// 阈值化
threshold(result, result, 255 * (0.1), 255,
THRESH_BINARY);
cv::imshow("2result", result);
// 水平方向闭运算
if (width >= 400 && width < 600)
morphologyEx(result, result, MORPH_CLOSE,
Mat(1, 25, CV_8U, Scalar(1)));
else if (width >= 200 && width < 300)
morphologyEx(result, result, MORPH_CLOSE,
Mat(1, 20, CV_8U, Scalar(1)));
else if (width >= 600)
morphologyEx(result, result, MORPH_CLOSE,
Mat(1, 28, CV_8U, Scalar(1)));
else
morphologyEx(result, result, MORPH_CLOSE,
Mat(1, 15, CV_8U, Scalar(1)));
// 垂直方向闭运算
if (height >= 400 && height < 600)
morphologyEx(result, result, MORPH_CLOSE,
Mat(8, 1, CV_8U, Scalar(1)));
else if (height >= 200 && height < 300)
morphologyEx(result, result, MORPH_CLOSE,
Mat(6, 1, CV_8U, Scalar(1)));
else if (height >= 600)
morphologyEx(result, result, MORPH_CLOSE,
Mat(10, 1, CV_8U, Scalar(1)));
else
morphologyEx(result, result, MORPH_CLOSE,
Mat(4, 1, CV_8U, Scalar(1)));
return result;
}
int main()
{
cv::Mat srcImage = cv::imread("./image/cheche.png");
if (!srcImage.data)
return 1;
cv::Mat srcGray;
cv::cvtColor(srcImage, srcGray, CV_RGB2GRAY);
cv::imshow("srcGray", srcGray);
cv::Mat result = getPlate(400, 300, srcGray);
// 连通域检测
std::vector > blue_contours;
std::vector blue_rect;
//寻找外轮廓
cv::findContours(result.clone(),
blue_contours, CV_RETR_EXTERNAL,
CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
// 连通域遍历 车牌目标提取
for (size_t i = 0; i != blue_contours.size(); ++i)
{
cv::Rect rect = cv::boundingRect(blue_contours[i]);
double wh_ratio = double(rect.width) / rect.height;
int sub = cv::countNonZero(result(rect));
double ratio = double(sub) / rect.area();
if (wh_ratio > 2 && wh_ratio < 8 && rect.height >
12 && rect.width > 60 && ratio > 0.4)
{
cv::imshow("rect", srcGray(rect));
cv::waitKey(0);
}
}
cv::imshow("result", result);
cv::waitKey(0);
return 0;
}
效果:
关于连通区域检测函数:
OpenCV通过使用findContours函数,简单几个步骤就可以检测出物体的轮廓。
函数原型:
cv::findContours( InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset=Point());
image:单通道图像矩阵,可以是灰度图,但更常用的是二值化图像,一般是结果canny、拉普拉斯等边缘检测算子处理过的二值化图像。
contours:定义为std::vector< std::vector< cv::Point> > blue_contours,是一个向量,并且是一个双重向量,向量每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少个元素。
hierarchy:定义为vector< Vec4i> hierarchy,这个定义为向量内每一个元素包含了4个int类型变量。所以它其实也是一个向量,向量内每个元素保存了一个包含4个int整形的数组。向量hierarchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。那么它的每一个元素的4个int类型变量分别为:hierarchy[i] [0]~hierarchy[i] [3],他分别表示的是第i轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个其中一个值的话,那么默认值就是-1。
mode:定义轮廓的检索模式:
(1)CV_RETR_EXTERNAL,只检测最外围轮廓,包含在外围轮廓内的内围轮廓都将被忽略。
(2)CV_RETR_LIST:检测所有轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、4个分量都会被置为-1。
(3)CV_RETR_CCOMP :检测所有轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内所有的轮廓均属于顶层。
(4)CV_RETR_TREE:检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
method:定义轮廓的近似方法。
(1)CV_CHAIN_APPROX_NONE :保存物体边界上所有连续的轮廓点到contours向量内。
(2)CV_CHAIN_APPROX_SIMPLE :仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线上的信息不予保留。
(3)CV_CHAIN_APPROX_TC89_L1、CV_CHAIN_APPROX_TC89_KCOS:使用teth-Chinl chain近似算法。
Point:偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,也可以为负值。
在上面的程序中,使用形态学梯度边缘检测得到的图像,目的是为了突显边缘:
这个时候除了车牌以及其他一些明显特征被留下来了,但是其他干扰元素已经大部分的被滤掉了,确实这个操作可以滤掉很多干扰的地方。
之后他再进行阈值化,图像的变化是这样的:
也就是将我们显示出来的主体再明显化,但是同时也将其他干扰的地方阈值化出来了。
之后要自适应出自己的闭操作算子矩阵,对我们这幅图像进行闭操作,滤掉一些没有用的东西,最后得到的图像如下:
之后对齐检测连通区域。之后再剔除大小不合适的连通区域,最后得到车牌的区域。
到此为止,应该对这个操作有些深刻的理解以及见解了,那么这个方法跟我昨天使用的方法有什么区别?昨天的处理是在RGB图像转HSI图像的基础上进行,当我们使用形态学的时候,我们可以直接操作灰度图像,并且不需要指定车牌的背景颜色,可以适应更多种情况,因为使用形态学检测,可以快速的找到车牌的边缘。上一篇的找边缘是通过sobel算子进行卷积计算的,最后画出蓝色对应的区域的轮廓。之后再进行形态学闭操作,这一步是相同的,只不过现在多了个自适应大小,那么这个形态学闭操作,是将空间内间隙小的地方进行填充,也就是尽量的找到车牌的区域,防止遗漏,所以最后输出的车牌的区域,才会这么的花,但是没关系,他已经找到车牌了。最后的处理就是一样的了,找到符合的条件区域,大小等,将我们的车牌进行打印出来。
那么以上就是我对形态学技术的一些理解以及应用,这个技术确实可以造福很多种图像边缘提取、区域提取等多个领域,是个很好用的东西。