学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

时间:2023-03-09 04:32:04
学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

上篇文章中,我们重点了解了腐蚀和膨胀这两种最基本的形态学操作,而运用这两个基本操作,我们可以实现更高级的形态学变换。

所以,本文的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等等。

第二件事,是浅墨想跟大家做一个关于OpenCV系列文章的书写内容和风格的思想汇报。

是这样的,浅墨发现最近几期写出来的文章有些偏离自己开始开这个专栏的最初的愿望——原理和概念部分占的比重有些大,有些弱化OpenCV实际的使用。

写这些博文的初心是教大家如何使用OpenCV来写代码,原理部分我想很多朋友应该多少都懂,就算某些同学对某些概念有些模糊,大家也完全可以带着关键词句去google或者百度。

浅墨的想法是,以后的专栏文章原理部分尽量从简,“深入”的源码剖析部分也是从简,重点突出“浅出”部分,让大家快速上手OpenCV函数的使用,这样浅墨的工作量也会小很多,更新也会更勤。

PS:浅墨其实每次在写图像处理原理部分的时候都特纠结,因为浅墨其实感兴趣的和大家一样,也是如何写代码,而不是那些多多少少让人提不起兴趣来的图像处理公式和概念。这往往就照成了博文更新的拖延症。

所以呢,在浅墨以后写的OpenCV文章中,原理和深入部分我们就点到为止,文章的拳头内容是“浅出”部分,重点教大家如何快速上手OpenCV API。我想这也是大家一直期待和想要看到的浅墨出品的文章的样子吧。:)

OK,大概就是这些。我们开始今天的正题。

一、理论与概念讲解——从现象到本质

首先呢,要知道形态学的高级形态,往往都是建立在腐蚀和膨胀这两个基本操作之上的。而关于腐蚀和膨胀,概念和细节以及相关代码可以看浅墨之前写的这篇文章:【OpenCV入门教程之十】 形态学图像处理(一):膨胀与腐蚀

对膨胀和腐蚀心中有数了,接下来的高级形态学操作,应该就不难理解。

另外,为了下面对比和演示以及理解的方便,浅墨自己制作了一张毛笔字图,这里先上原图:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

1.1 开运算(Opening Operation)

开运算(opening operation),其实就是先腐蚀后膨胀过得过程,其数学表达式如下:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

****  括号的优先级大,所以先运算erode

开运算可以用来消除小物体,在纤细点出分离物体,平滑较大物体的边界同时并不明显改变其面积,效果图是这样的:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

1.2 闭运算(Closing Operation)

先膨胀后腐蚀的过程称为闭运算(closing operation),数学表达式如下:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

闭运算能够排除小型黑洞(黑色区域),如下所示:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

1.3 形态学梯度(MorphologicalGradient)

形态学梯度(morphological gradient)为膨胀图与腐蚀图之差,表达式如下:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

对二值图像进行这一操作可以将团块(blob)的边缘突出来。我们可以用形态学梯度来保留物体的边缘轮廓,如下所示:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

1.4 顶帽(Top Hat)

顶帽运算(top hat) 又常常被译为“礼帽”运算,为原图像与上文刚介绍的“开运算”的结果图之差,表达式如下:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原型轮廓周围的区域更明亮的区域。且这一操作和选择的核的大小有关

顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的时候,可以用顶帽运算进行背景提取。

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

1.5 黑帽(Black Hat)

黑帽(Black Hat)运算为”闭运算“的结果图与原图像之差。数学表达式为:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。

所以,黑帽运算用来分离比邻近点暗一些的斑块。非常完美的轮廓效果图:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

二、深入——OpenCV源码分析溯源

本文的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,

如开闭运算,形态学梯度,“顶帽”、“黑帽”等等。这一节我们来一起看一下morphologyEx函数的源代码。

 void cv::morphologyEx( InputArray _src, OutputArray _dst, int op,
InputArray _kernel, Point anchor, int iterations,
int borderType, const Scalar& borderValue )
{
Mat kernel = _kernel.getMat();
if (kernel.empty())
{
kernel = getStructuringElement(MORPH_RECT, Size(,), Point(,));
}
#ifdef HAVE_OPENCL
Size ksize = kernel.size();
anchor = normalizeAnchor(anchor, ksize); CV_OCL_RUN(_dst.isUMat() && _src.dims() <= && _src.channels() <= &&
anchor.x == ksize.width >> && anchor.y == ksize.height >> &&
borderType == cv::BORDER_CONSTANT && borderValue == morphologyDefaultBorderValue(),
ocl_morphologyEx(_src, _dst, op, kernel, anchor, iterations, borderType, borderValue))
#endif //拷贝Mat 数据到临时变量
Mat src = _src.getMat(), temp;
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat(); Mat k1, k2, e1, e2; //only for hit and miss op //一个大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;
case MORPH_HITMISS:
CV_Assert(src.type() == CV_8UC1);
k1 = (kernel == );
k2 = (kernel == -);
if (countNonZero(k1) <= )
e1 = src;
else
erode(src, e1, k1, anchor, iterations, borderType, borderValue);
if (countNonZero(k2) <= )
e2 = src;
else
{
Mat src_complement;
bitwise_not(src, src_complement);
erode(src_complement, e2, k2, anchor, iterations, borderType, borderValue);
}
dst = e1 & e2;
break;
default:
CV_Error( CV_StsBadArg, "unknown morphological operation" );
}
}

看上面的源码可以发现,其实morphologyEx函数其实就是内部一个大switch而已。根据不同的标识符取不同的操作。

比如开运算MORPH_OPEN,按我们上文中讲解的数学表达式,就是先腐蚀后膨胀,即依次调用erode和dilate函数,为非常简明干净的代码。

三、浅出——API函数快速上手

3.1 morphologyEx函数详解

上面我们已经讲到,morphologyEx函数利用基本的膨胀和腐蚀技术,来执行更加高级形态学变换,如开闭运算,形态学梯度,“顶帽”、“黑帽”等等。这一节我们来了解它的参数意义和使用方法。

 void morphologyEx(
InputArray src,
OutputArray dst,
int op,
InputArraykernel,
Pointanchor=Point(-,-),
intiterations=,
intborderType=BORDER_CONSTANT,
constScalar& borderValue=morphologyDefaultBorderValue() );

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像位深应该为以下五种之一:CV_8U, CV_16U,CV_16S, CV_32F 或CV_64F。

  • 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
  • 第三个参数,int类型的op,表示形态学运算的类型,可以是如下之一的标识符:

MORPH_OPEN – 开运算(Opening operation

MORPH_CLOSE – 闭运算(Closing operation)

MORPH_GRADIENT -形态学梯度(Morphological gradient)

MORPH_TOPHAT - “顶帽”(“Top hat”)

MORPH_BLACKHAT - “黑帽”(“Black hat“)

  • 第四个参数,InputArray类型的kernel,形态学运算的内核。若为NULL时,表示的是使用参考点位于中心3x3的核。我们一般使用函数 getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。关于getStructuringElement我们上篇文章中讲过了,这里为了大家参阅方便,再写一遍:

其中,getStructuringElement函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:

矩形: MORPH_RECT

交叉形: MORPH_CROSS

椭圆形: MORPH_ELLIPSE

而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。

我们一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。且需要注意,十字形的element形状唯一依赖于锚点的位置。而在其他情况下,锚点只是影响了形态学运算结果的偏移。

getStructuringElement函数相关的调用示例代码如下:

 int g_nStructElementSize = ; //结构元素(内核矩阵)的尺寸  

 //获取自定义核
Mat element =getStructuringElement(MORPH_RECT,
Size(*g_nStructElementSize+,*g_nStructElementSize+),
Point(g_nStructElementSize, g_nStructElementSize ));

调用这样之后,我们便可以在接下来调用erode、dilate或morphologyEx函数时,kernel参数填保存getStructuringElement返回值的Mat类型变量。对应于我们上面的示例,就是填element变量。

其中的这些操作都可以进行就地(in-place)操作。且对于多通道图像,每一个通道都是单独进行操作。

OK,讲解完毕,下面就是使用的范例。

高能预警!高能预警!高能预警!

  一大波示例代码正在逼近。

   为了方便大家需要的时候随时取用。下面我们依次列举出开运算,闭运算,形态学梯度,顶帽,黑帽,腐蚀,膨胀的效果实现简化版完整代码。

其实说白了,这些代码基本上内容一致,其实就是改一下morphologyEx里面的第三个标识符参数而已。核都是选的MORPH_RECT,矩形元素结构。

另外,通过看源码我们发现,最基本的腐蚀和膨胀操作也可以用morphologyEx函数来实现,他们由morphologyEx函数源码中switch的前两个case来实现(虽然在case体内就是简单地各自调用了一下erode和dilation函数,但还是有写出来的必要)。所以在这里,我们也用morphologyEx再重新来实现一遍他们。

按着顺序来列出吧,就直接列详细注释好的代码和运行结果了。

3.2 开运算示例程序

 #include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream> using namespace cv;
using namespace std; /*------------------------------------------
【1】开运算示例程序
--------------------------------------------*/
/*
int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】开运算");
namedWindow("【效果图】开运算"); //显示原始图
imshow("【原始图】开运算",image); //定义核
Mat element = getStructuringElement(MORPH_RECT,Size(15,15)); //进行形态学操作
morphologyEx(image,image,MORPH_OPEN,element); //显示效果图
imshow("【效果图】开运算",image); waitKey(); return 0;
}
*/

3.3 闭运算示例程序

OpenCV中调用morphologyEx函数进行闭运算操作的示例程序如下:

 /*------------------------------------------
【2】闭运算示例程序
--------------------------------------------*/
int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】闭运算");
namedWindow("【效果图】闭运算"); //显示原始图
imshow("【原始图】闭运算", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image,MORPH_CLOSE, element); //显示效果图
imshow("【效果图】闭运算", image); waitKey(); return ;
}

3.4 形态学梯度示例程序

OpenCV中调用morphologyEx函数进行形态学梯度操作的示例程序如下:

 /*------------------------------------------
【3】形态学梯度示例程序
--------------------------------------------*/ int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】形态学梯度");
namedWindow("【效果图】形态学梯度"); //显示原始图
imshow("【原始图】形态学梯度", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image, MORPH_GRADIENT, element); //显示效果图
imshow("【效果图】形态学梯度", image); waitKey(); return ;
}

3.5 顶帽运算(Top Hat)示例程序

OpenCV中调用morphologyEx函数进行顶帽运算操作的示例程序如下:

 /*------------------------------------------
【4】顶帽运算示例程序
--------------------------------------------*/ int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】顶帽运算");
namedWindow("【效果图】顶帽运算"); //显示原始图
imshow("【原始图】顶帽运算", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image, MORPH_TOPHAT, element); //显示效果图
imshow("【效果图】顶帽运算", image); waitKey(); return ;
}

3.6 黑帽运算(BlackHat)示例程序

OpenCV中调用morphologyEx函数进行黑帽运算操作的示例程序如下:

 /*------------------------------------------
【5】黑帽运算示例程序
--------------------------------------------*/ int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】黑帽运算");
namedWindow("【效果图】黑帽运算"); //显示原始图
imshow("【原始图】黑帽运算", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image, MORPH_BLACKHAT, element); //显示效果图
imshow("【效果图】黑帽运算", image); waitKey(); return ;
}

3.7 腐蚀(morphologyEx调用版)示例程序

OpenCV中调用morphologyEx函数进行腐蚀操作的示例程序如下:

 /*------------------------------------------
【6】腐蚀示例程序
--------------------------------------------*/ int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】腐蚀");
namedWindow("【效果图】腐蚀"); //显示原始图
imshow("【原始图】腐蚀", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image, MORPH_ERODE, element); //显示效果图
imshow("【效果图】腐蚀", image); waitKey(); return ;
}

3.8 膨胀(morphologyEx调用版)示例程序

OpenCV中调用morphologyEx函数进行膨胀操作的示例程序如下:

 /*------------------------------------------
【7】膨胀示例程序
--------------------------------------------*/ int main()
{
//载入原始图
Mat image = imread("1.jpg"); //创建窗口
namedWindow("【原始图】膨胀");
namedWindow("【效果图】膨胀"); //显示原始图
imshow("【原始图】膨胀", image); //定义核
Mat element = getStructuringElement(MORPH_RECT, Size(, )); //进行形态学操作
morphologyEx(image, image, MORPH_DILATE, element); //显示效果图
imshow("【效果图】膨胀", image); waitKey(); return ;
}

四、综合示例——在实战中熟稔

依然是每篇文章都会配给大家的一个详细注释的博文配套示例程序,把这篇文章中介绍的知识点以代码为载体,展现给大家。

这个示例程序中,一共有四个显示图像的窗口。

原始图一个,开/闭运算为一个,腐蚀/膨胀为一个,顶帽/黑帽运算为一个。

分别使用滚动条,来控制得到的形态学效果。且迭代值为10的时候,为中间。

另外,还可以通过键盘按键1,2,3以及空格,来调节成不同的元素结构(矩形、椭圆、十字形)。说明页面如下:

学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑

 //全局变量声明
Mat g_srcImage, g_dstImage; //原始图和效果图
int g_nElementShape = MORPH_RECT; //元素结构的形状 //变量接收的TrackBar 位置参数
int g_nMaxIterationNum = ;
int g_nOpenCloseNum = ;
int g_nErodeDilateNum = ;
int g_nTopBlackHatNum = ; //全局函数声明
static void on_OpenClose(int,void*); //回调函数
static void on_ErodeDilate(int,void *); //回调函数
static void on_TopBlackHat(int,void *); //回调函数
static void ShowHelpText(); //终端显示操作信息 /*-------------------------------------------------
on_OpenClose()函数
描述:【开运算/闭运算】窗口的回调函数
---------------------------------------------------*/
static void on_OpenClose(int, void*)
{
//偏移量的定义
int offset = g_nOpenCloseNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > ? offset : -offset; //偏移量绝对值 //自定义核
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * + , Absolute_offset * + ),
Point(Absolute_offset, Absolute_offset)); //进行操作
if (offset < )
morphologyEx(g_srcImage, g_dstImage, CV_MOP_OPEN, element);
else
morphologyEx(g_srcImage,g_dstImage,CV_MOP_CLOSE,element); //显示图像
imshow("【开运算/闭运算】",g_dstImage);
} /*-------------------------------------------------
on_ErodeDilate()函数
描述:【腐蚀/膨胀】窗口的回调函数
---------------------------------------------------*/ static void on_ErodeDilate(int, void *)
{
//偏移量的定义
int offset = g_nErodeDilateNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > ? offset: -offset; //偏移量绝对值 //自定义核
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * + , Absolute_offset * + ), Point(Absolute_offset, Absolute_offset)); //进行操作
if (offset < )
erode(g_srcImage, g_dstImage, element);
else
dilate(g_srcImage, g_dstImage, element); //显示图像
imshow("【腐蚀/膨胀】", g_dstImage); } /*-------------------------------------------------
on_TopBlackHat()函数
描述:【顶帽/黒帽 运算】窗口的回调函数
---------------------------------------------------*/
static void on_TopBlackHat(int, void *)
{
//偏移量的定义
int offset = g_nTopBlackHatNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > ? offset : -offset; //偏移量绝对值 //自定义核
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * + , Absolute_offset * + ), Point(Absolute_offset, Absolute_offset)); //进行操作
if (offset < )
morphologyEx(g_srcImage, g_dstImage, MORPH_TOPHAT, element);
else
morphologyEx(g_srcImage,g_dstImage,MORPH_BLACKHAT,element); //显示图像
imshow("【顶帽/黒帽】", g_dstImage); } /*-----------------------------------------------------------
ShowHelpText()函数
描述:输出一些帮助信息
------------------------------------------------------------*/
static void ShowHelpText()
{
//输出一些帮助信息
printf("\n\n\n\t请调整滚动条观察图像效果~\n\n");
printf("\n\n\t按键操作说明: \n\n"
"\t\t键盘按键【ESC】或者【Q】- 退出程序\n"
"\t\t键盘按键【1】- 使用椭圆(Elliptic)结构元素\n"
"\t\t键盘按键【2】- 使用矩形(Rectangle )结构元素\n"
"\t\t键盘按键【3】- 使用十字型(Cross-shaped)结构元素\n"
"\t\t键盘按键【空格SPACE】- 在矩形、椭圆、十字形结构元素中循环\n"
"\n\n\t\t\t\t\t\t\t\t by hehheheh"
); } /*------------------------------------------------
main()函数
--------------------------------------------------*/ int main()
{
//改变console字体颜色
system("color 2F"); ShowHelpText(); //载入原图
g_srcImage = imread("1.jpg");
if (!g_srcImage.data)
{
printf("Oh,no,读取srcImage错误~! \n");
return false;
} //显示原始图
namedWindow("【原始图】");
imshow("【原始图】",g_srcImage); //创建3个窗口
namedWindow("【开运算/闭运算】", );
namedWindow("【腐蚀/膨胀】",);
namedWindow("【顶帽/黒帽】",); //参数赋值
g_nOpenCloseNum = ;
g_nErodeDilateNum = ;
g_nTopBlackHatNum = ; //分别为3个窗口创建滚动条
createTrackbar("迭代值","【开运算/闭运算】",&g_nOpenCloseNum,g_nMaxIterationNum* +,on_OpenClose);
createTrackbar("迭代值", "【腐蚀/膨胀】", &g_nErodeDilateNum, g_nMaxIterationNum * + , on_ErodeDilate);
createTrackbar("迭代值", "【顶帽/黒帽】", &g_nTopBlackHatNum, g_nMaxIterationNum * + , on_TopBlackHat); //轮询获取按键信息
while ()
{
int c; //执行回调函数
on_OpenClose(g_nOpenCloseNum,);
on_ErodeDilate(g_nErodeDilateNum,);
on_TopBlackHat(g_nTopBlackHatNum,); //获取按键
c = waitKey(); //按下键盘按键Q或者ESC,程序退出
if ((char)c == 'q' || (char)c == )
break; //按下键盘按键1,使用椭圆形(Elliptic)结构元素MORPH_ELLIPSE
if ((char)c == ) //按键1的ASII码为49
g_nElementShape = MORPH_ELLIPSE; //按下键盘按键2,使用矩形(Rectangle)结构元素MORPH_RECT
else if ((char)c == ) //按键1的ASII码为50
g_nElementShape = MORPH_RECT; //按下键盘按键3,使用十字形(Cross-shaped)结构元素MORPH_CROSS
else if ((char)c == ) //按键1的ASII码为51
g_nElementShape = MORPH_CROSS; //按下键盘按键space,在矩形,椭圆,十字形结构元素中循环
else if ((char)c == ' ')
g_nElementShape =(g_nElementShape+) % ;
} return ;
}