图像形态学操作
微信公众号:幼儿园的学霸
个人的学习笔记,关于OpenCV,关于机器学习, … 问题或建议,请公众号留言;
看到一段话,深有感触
作为软件开发者,我们曾经写过的或者正在写的每一行代码都对我们的渐进式互联世界负有重要的责任。它在很大程度上改变了人类的命运。我们应该为自己所做的事情感到自豪,并且必须将责任进行到底,因为整个世界都在期待我们创造更多的魔力,实现更多的创新。
目录
前言结构元素腐蚀与膨胀操作开闭运算形态学梯度顶帽、黑帽morphologyEx函数源码:
前言
形态学图像处理的基本运算有:膨胀、腐蚀、开操作和闭操作,击中与击不中变换,TOP-HAT变换,黑帽变换等。
形态学的应用:消除噪声、边界提取、区域填充、连通分量提取、凸壳、细化、粗化等;分割出独立的图像元素,或者图像中相邻的元素;求取图像中明显的极大值区域和极小值区域;求取图像梯度
开操作- open open() = dilate(erode())
先腐蚀后膨胀,可以去掉小的对象,假设对象是前景色,背景是黑色
闭操作- close close() = erode(dilate())
先膨胀后腐蚀(bin2),可以填充前景上小的洞(fill hole),假设对象是前景色,背景是黑色
形态学梯度- Morphological Gradient dst = dilate() - erode() 可以让像素间的像素值形成梯度
膨胀减去腐蚀,又称为基本梯度(其它还包括-内部梯度(腐蚀减原图还是原图减腐蚀?)、方向梯度(在xy方向进行计算得出的结果))
顶帽 – top hat
顶帽 是原图像与开操作之间的差值图像,结果类似于 !开操作 的图像结果
黑帽 – black hat
黑帽是闭操作图像与源图像的差值图像,可以检测出原图前景色中的黑点
上列的操作基本都是基于膨胀与腐蚀操作的,这些操作在二值图像(位图)、灰度图像中用的特别多,在彩色图像上效果不是很明显,可以将彩色图像转成二值图像
结构元素
在讲各种形态学操作之前,先来看看结构元素:
膨胀和腐蚀操作的核心内容是结构元素。(后面的开闭运算等重要的也是结构元素的设计,一个合适的结构元素的设计可以带来很好的处理效果)一般来说结构元素是由元素为1或者0的矩阵组成。结构元素为1的区域定义了图像的领域,领域内的像素在进行膨胀和腐蚀等形态学操作时要进行考虑。
一般来说,二维或者平面结构的结构元素要比处理的图像小得多。结构元素的中心像素,即结构元素的原点,与输入图像中感兴趣的像素值(即要处理的像素值)相对应。三维的结构元素使用0和1来定义x-y平面中结构元素的范围,使用高度值定义第三维。
OpenCV中获取结构元素的函数为:
//返回值:返回指定形状和尺寸的结构元素
cv::Mat kernel = getStructuringElement(int shape,Size ksize,Point anchor);
//结构元素的定义:形状 (MORPH_RECT(矩形核)
\MORPH_CROSS(十字交叉形核) \MORPH_ELLIPSE(椭圆形核));结构元素大小;锚点 默认是Point(-1, -1)意思就是中心像素,也可以自己指定
如下所示为3中结构元素示例:
代码:
cv::Mat elementRect,elementCross,elementEllipse;
elementRect = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(3,3),cv::Point(-1,-1));
elementCross = cv::getStructuringElement(cv::MORPH_CROSS,cv::Size(3,3),cv::Point(-1,-1));
elementEllipse = cv::getStructuringElement(cv::MORPH_ELLIPSE,cv::Size(5,5),cv::Point(-1,-1));
std::cout<<"3X3矩形核:"<<std::endl<<elementRect<<std::endl;
std::cout<<"3X3十字交叉形核:"<<std::endl<<elementCross<<std::endl;
std::cout<<"5X5椭圆形核:"<<std::endl<<elementEllipse<<std::endl;
输出结果:
3X3矩形核:
[ 1, 1, 1;
1, 1, 1;
1, 1, 1]
3X3十字交叉形核:
[ 0, 1, 0;
1, 1, 1;
0, 1, 0]
5X5椭圆形核:
[ 0, 0, 1, 0, 0;
1, 1, 1, 1, 1;
1, 1, 1, 1, 1;
1, 1, 1, 1, 1;
0, 0, 1, 0, 0]
此外,我们也可以自定义结构元素,如下:
使用Mat_模板类自定义5×5大小十字形、菱形、方形、x形结构元素:
//自定义核(结构元素)
cv::Mat_<uchar> cross(5,5);
cv::Mat_<uchar> diamond(5,5);
cv::Mat_<uchar> x(5,5);
cv::Mat_<uchar> square(5,5);
// Creating the cross-shaped structuring element
cross <<
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;
// Creating the diamond-shaped structuring element
diamond <<
0, 0, 1, 0, 0,
0, 1, 1, 1, 0,
1, 1, 1, 1, 1,
0, 1, 1, 1, 0,
0, 0, 1, 0, 0;
// Creating the x-shaped structuring element
x <<
1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1;
// Creating the square-shaped structuring element
square <<
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1;
int xnr = x.rows;
int xnl = x.cols;
for(int j = 0;j<xnr;j++)
{
char *data = x.ptr<char>(j);
for(int i = 0; i<xnl; i++)
{
int value = data[i];
std::cout<<value<<" ";
}
std::cout<<std::endl;
}
腐蚀与膨胀操作
腐蚀和膨胀是形态学的基本操作,在灰度图像中也是如此,在二值图像中腐蚀和膨胀定义为对图像进行translation以后的“与”和“或”的逻辑操作结果。 也可以这样理解:
腐蚀的具体操作是:用一个结构元素(一般是3×3的大小)扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为1,则该像素为1,否则为0。
膨胀的具体操作是:用一个结构元素(一般是3×3的大小)扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为0,则该像素为0,否则为1。
在灰度图像中,为了保存灰度信息,“与”和“或”操作被对应的替换成了“最大值”和“最小值”操作这样就给出了灰度图像中腐蚀和膨胀的操作定义。
以矩阵中的操作为例,腐蚀和膨胀操作过程如图1和图2所示(图片来源于网络):
腐蚀操作:
图1 腐蚀操作
膨胀操作:
图2 膨胀操作
腐蚀的作用是消除物体边界点,使目标缩小,可以消除小于结构元素的噪声点;腐蚀了就意味着图像中物体的边界被侵蚀了,轮廓向内收缩,体积变小了。
膨胀的作用是将与物体接触的所有背景点合并到物体中,使目标增大,可添补目标中的空洞,通过对图像的胀大,使图像中的物体的轮廓向外发散,体积变大。
腐蚀就是把当前像素替换成所定义的像素集中的最小像素值。由于输入的二值图像只包含黑色(0)和白色(255)像素,因此如果结构元素覆盖的图像区域中有黑色像素,则锚点所在像素(x,y)将会被替换成黑色0,否则替换成白色255。而物体的边界通常会有黑色像素,所以腐蚀相当于收缩边界。
膨胀是腐蚀的反运算,它把当前像素(原点所在位置(x,y))替换成所定义的像素集中的最大像素值。由于输入的二值图像只包含黑色(0)和白色(255)像素,因此当结构元素覆盖的图像中有白色(物体),则该结构元素原点所在位置(x,y)的值将会被替换成白色255。
要注意的是这里的膨胀和腐蚀的概念是针对图像中高亮区域而言的(二值图像中对应像素值255,白色),变大和缩小也是针对图像中高亮的部分。
所以对图像执行膨胀,对高亮区域是胀大,面积增大,对暗区域相当于是腐蚀,面积缩小。
在OpenCV中进行腐蚀与膨胀操作可以采用函数cv::erode()
和cv::dilate()
,或者采用cv::morphologyEx()
函数并输出需要进行的形态学操作类型。如下为cv::dilate()
的函数原型,cv::erode()
函数参数与此类似:
void dilate(
InputArray src,//输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一
OutputArray dst,//目标图像,需要和源图片有一样的尺寸和类型
InputArray kernel,//操作的核。若为NULL时,表示的是使用参考点位于中心3x3的核,可以使用getStructuringElement来创建结构元素
Point anchor=Point(-1,-1),//锚点的位置,其有默认值(-1,-1),表示锚位于中心
int iterations=1,//迭代使用该函数的次数,默认值为1
int borderType=BORDER_CONSTANT,//用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT
const Scalar& borderValue=morphologyDefaultBorderValue() //当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般我们不用去管他。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释
);
//使用dilate函数时,一般我们只需要填前面的三个参数,后面的四个参数都有默认值。而且往往结合getStructuringElement一起使用。
以下为代码示例及腐蚀和膨胀操作结果:
代码:
int main()
{
//首先读入图像,并二值化
cv::Mat srcImage = cv::imread("../pictures/000177.png",cv::IMREAD_GRAYSCALE);
cv::threshold(srcImage,srcImage,125,255,cv::THRESH_BINARY);
cv::imshow("srcImage",srcImage);
//获取进行形态学操作的核
cv::Mat elementRect;
elementRect = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(3,3),cv::Point(-1,-1));
//膨胀
cv::Mat dilateImage;
cv::dilate(srcImage,dilateImage,elementRect);
//cv::morphologyEx(srcImage,dilateImage,cv::MORPH_DILATE,elementRect);
cv::imshow("dilateImage",dilateImage);
//腐蚀
cv::Mat erodeImage;
cv::erode(srcImage,erodeImage,elementRect);
//cv::morphologyEx(srcImage,dilateImage,cv::MORPH_ERODE,elementRect);
cv::imshow("erodeImage",erodeImage);
cv::waitKey(0);
retrun 0;
}
操作结果:
原始二值化图像:
图3 二值化图像
腐蚀和膨胀后图像:
图4 腐蚀和膨胀后图像
开闭运算
开运算:先腐蚀后膨胀,作用:用来消除图像中细小对象,在纤细点处分离物体和平滑较大物体的边界而有不明显改变其面积和形状,所有小到不能容纳结构元素的物体都会被移除。
闭运算:先膨胀后腐蚀,作用:用来填充目标内部的细小孔洞(fill hole),将断开的邻近目标连接,在不明显改变物体面积和形状的情况下平滑其边界,基本上所有小到不能完整容纳结构元素的空隙或间隙,都会被闭运算消除(即连起来)
OpenCV代码:
int main()
{
//首先读入图像,并二值化
cv::Mat srcImage = cv::imread("../pictures/000177.png",cv::IMREAD_GRAYSCALE);
cv::threshold(srcImage,srcImage,125,255,cv::THRESH_BINARY);
cv::imshow("srcImage",srcImage);
//获取进行形态学操作的核
cv::Mat elementRect;
elementRect = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(3,3),cv::Point(-1,-1));
//开运算
cv::Mat opendImage;
cv::morphologyEx(srcImage,opendImage,cv::MORPH_OPEN,elementRect);
cv::cvtColor(opendImage,opendImage,cv::COLOR_GRAY2BGR);
cv::putText(opendImage,"opendImage",cv::Point(0,30),cv::FONT_HERSHEY_SIMPLEX,1.2,cv::Scalar(0,255,0),2);
//闭运算
cv::Mat closedImage;
cv::morphologyEx(srcImage,closedImage,cv::MORPH_CLOSE,elementRect);
cv::cvtColor(closedImage,closedImage,cv::COLOR_GRAY2BGR);
cv::putText(closedImage,"closedImage",cv::Point(0,30),cv::FONT_HERSHEY_SIMPLEX,1.2,cv::Scalar(0,255,0),2);
cv::hconcat(opendImage,closedImage,opendImage);
cv::imshow("res",opendImage);
cv::resize(srcImage,srcImage,cv::Size(),0.5,0.5);
cv::resize(opendImage,opendImage,cv::Size(),0.5,0.5);
cv::imwrite("srcImage.jpg",srcImage);
cv::imwrite("opendImage.jpg",opendImage);
cv::waitKey(0);
return 0;
}
图像开闭运算操作后结果:
形态学梯度
通常所说形态学梯度(Morphological Gradient)是膨胀图像与腐蚀图像的之差得到的图像,也是基本梯度。数学表达式如下:
dst = morph_grad(src,element) = dilate(src,element) - erode(src,element)
梯度用于刻画目标边界或边缘位于图像灰度级剧烈变化的区域,形态学梯度根据膨胀或者腐蚀与原图作差组合来实现增强结构元素领域中像素的强度,突出高亮区域的外围。计算图像的形态学梯度是形态学重要操作,常常将膨胀和腐蚀基础操作组合起来一起使用实现一些复杂的图像形态学梯度。可以计算的梯度常见如下四种:
- 基本梯度:
基本梯度是用膨胀后的图像减去腐蚀后的图像得到差值图像,称为梯度图像也是OpenCV中支持的计算形态学梯度的方法,而此方法得到梯度有被称为基本梯度。- 内部梯度:
是用原图像减去腐蚀之后的图像得到差值图像,称为图像的内部梯度。- 外部梯度:
是用图像膨胀之后再减去原来的图像得到的差值图像,称为图像的外部梯度。- 方向梯度:
方向梯度是使用X方向与Y方向的直线作为结构元素之后得到图像梯度,用X方向直线做结构元素分别膨胀与腐蚀之后得到图像求差值之后称为X方向梯度,用Y方向直线做结构元素分别膨胀与腐蚀之后得到图像求差值之后称为Y方向梯度。
- 特点
形态学梯度操作的输出图像像素值是在对应结构元素而非局部过渡区域所定义的领域中灰度级强度变化的最大值。
对二值图像进行形态学操作可以将团块(blob)的边缘突出出来,可以用形态学梯度来保留物体的边缘轮廓。
代码:
int main()
{
//首先读入图像,并二值化
cv::Mat srcImage = cv::imread("../pictures/000177.png",cv::IMREAD_GRAYSCALE);
cv::threshold(srcImage,srcImage,125,255,cv::THRESH_BINARY);
cv::imshow("srcImage",srcImage);
//获取进行形态学操作的核
cv::Mat elementRect;
elementRect = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(3,3),cv::Point(-1,-1));
//膨胀
cv::Mat dilateImage;
cv::dilate(srcImage,dilateImage,elementRect);
//cv::morphologyEx(srcImage,dilateImage,cv::MORPH_DILATE,elementRect);
//腐蚀
cv::Mat erodeImage;
cv::erode(srcImage,erodeImage,elementRect);
//cv::morphologyEx(srcImage,dilateImage,cv::MORPH_ERODE,elementRect);
//1.计算基本梯度:膨胀后的图像减去腐蚀后的图像
cv::Mat basicGradient;
cv::subtract(dilateImage, erodeImage, basicGradient, cv::Mat());
//cv::morphologyEx(srcImage,basicGradient,cv::MORPH_GRADIENT,elementRect);//两者等效
cv::imshow("basicGradient", basicGradient);
//2.计算内部梯度:原图像减去腐蚀之后的图像
cv::Mat internalGradientImg;
cv::subtract(srcImage, erodeImage, internalGradientImg, cv::Mat());
cv::imshow("internalGradientImg", internalGradientImg);
//3.计算外部梯度:膨胀后的图像减去原图像
cv::Mat externalGradientImg;
cv::subtract(dilateImage, srcImage, externalGradientImg, cv::Mat());
cv::imshow("externalGradientImg", externalGradientImg);
//4.方向梯度:使用X方向与Y方向的直线作为结构元素---------
//cv::Mat hse = getStructuringElement(cv::MORPH_RECT, cv::Size(srcImage.cols / 16, 1),cv::Point(-1,-1));
//cv::Mat vse = getStructuringElement(cv::MORPH_RECT, cv::Size(1, srcImage.rows / 16),cv::Point(-1,-1));
cv::Mat hse = getStructuringElement(cv::MORPH_RECT, cv::Size(3, 1),cv::Point(-1,-1));
cv::Mat vse = getStructuringElement(cv::MORPH_RECT, cv::Size(1, 3),cv::Point(-1,-1));
cv::Mat xDirectImg, yDirectImg;
//4.1 X 方向梯度:膨胀与腐蚀之后得到图像求差值
cv::erode(srcImage, erodeImage, hse);
cv::dilate(srcImage, dilateImage, hse);
cv::subtract(dilateImage, erodeImage, xDirectImg, cv::Mat());
cv::imshow("xDirectImg", xDirectImg);
//cv::imshow("dilateImage",dilateImage);
//cv::imshow("erodeImage",erodeImage);
//4.2 Y 方向梯度:膨胀与腐蚀之后得到图像求差值
cv::erode(srcImage, erodeImage, vse);
cv::dilate(srcImage, dilateImage, vse);
cv::subtract(dilateImage, erodeImage, yDirectImg, cv::Mat());
cv::imshow("yDirectImg", yDirectImg);
cv::waitKey(0);
return 0;
}
为了显示效果,在进行方向梯度操作时,采用的是二值化图像。其实也可用RGB图像进行操作,(PS.知道4种梯度是怎么用腐蚀,膨胀组合的就好)
操作结果:
基本梯度:
图6 基本梯度
内部、外部梯度:
方向梯度:
顶帽、黑帽
当我们在处理一张图片的时候(例如提取眼底彩照的血管等信息),很多情况需要对这张图片进行一定的预处理,而通常我们就会用顶帽或者底帽变换。
顶帽变换:原图像与开操作对象的差。公式如下:
dst = tophat(src,element) = src - open(src,element)
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。因此,顶帽变换用于凸显暗背景上的亮物体。对二值图来说,进行顶帽变换或之后底帽变换看起来就像是加一个阴影,有一种立体的效果。
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。
黑帽变换:闭操作与原图像的差值。,数学表达式为:
dst = blackhat(src,element) = close(src,element) - src;
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。所以,黑帽运算用来分离比邻近点暗一些的斑块。黑帽变换可以用于凸显亮背景上的暗物体。二值图效果与顶帽变换相比,就是一个方向相反的阴影。
代码如下:
int main()
{
//首先读入图像
cv::Mat srcImage = cv::imread("../pictures/000177.png",cv::IMREAD_GRAYSCALE);
//cv::threshold(srcImage,srcImage,125,255,cv::THRESH_BINARY);
cv::imshow("srcImage",srcImage);
//获取进行形态学操作的核
cv::Mat elementRect;
elementRect = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(3,3),cv::Point(-1,-1));
//顶帽、黑帽
cv::Mat topHatImage;
cv::morphologyEx(srcImage,topHatImage,cv::MORPH_TOPHAT,elementRect,cv::Point(-1,-1));
cv::imshow("topHatImage",topHatImage);
再进一步,既然已经可以提取这个图的背景,那么通过背景与二值化图像异或,即可提取前景。
//cv::Mat bit_xor;
//cv::threshold(srcImage,srcImage,125,255,cv::THRESH_BINARY);
//bitwise_xor(srcImage, topHatImage, bit_xor);
//imshow("异或", bit_xor);
cv::Mat blackHatImage;
cv::morphologyEx(srcImage,blackHatImage,cv::MORPH_TOPHAT,elementRect,cv::Point(-1,-1));
cv::imshow("blackHatImage",blackHatImage);
cv::waitKey(0);
return 0;
}
操作结果:
原始灰度图像:
顶帽、黑帽操作结果:
[
图10 顶帽、黑帽操作结果
morphologyEx函数源码:
从上面代码中可以看到,OpenCV中对图像的形态学操作都是通过morphologyEx()函数完成的,在OpenCV中其源码如下:
void cv::morphologyEx( InputArray _src,OutputArray _dst, int op,
InputArray kernel, Pointanchor, int iterations,
int borderType, constScalar& borderValue )
{
//拷贝Mat数据到临时变量
Mat src = _src.getMat(), temp;
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat();
//一个大switch,根据不同的标识符取不同的操作
switch( op )
{
case MORPH_ERODE:
erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
break;
case MORPH_DILATE:
dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
break;
case MORPH_OPEN:
erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
dilate( dst, dst, kernel, anchor, iterations, borderType, borderValue );
break;
case CV_MOP_CLOSE:
dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
break;
case CV_MOP_GRADIENT:
erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
dst -= temp;
break;
case CV_MOP_TOPHAT:
if( src.data != dst.data )
temp = dst;
erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
dilate( temp, temp, kernel, anchor,iterations, borderType, borderValue );
dst = src - temp;
break;
case CV_MOP_BLACKHAT:
if( src.data != dst.data )
temp = dst;
dilate( src, temp, kernel, anchor, iterations, borderType, borderValue);
erode( temp, temp, kernel, anchor, iterations, borderType, borderValue);
dst = temp - src;
break;
default:
CV_Error( CV_StsBadArg, "unknown morphological operation" );
}
}
看上面的源码可以发现,其实morphologyEx函数其实就是内部一个大switch而已。根据不同的标识符取不同的操作。比如开运算MORPH_OPEN,按我们上文中讲解的数学表达式,就是先腐蚀后膨胀,即依次调用erode和dilate函数,为非常简明干净的代码。
图注:幼儿园的学霸