基于OPENCV分水岭的球团分割

本文介绍了一种在高速铁矿球团粒径检测中的应用,通过高像素相机和快速曝光处理原始图像,然后采用顶帽变换和中值滤波增强。使用分水岭算法进行粒度分割,重点讲解了如何构建前景和背景marks,优化处理过程以减少噪声并提高精度。最终通过统计分析,实现了颗粒尺寸的准确测量。
摘要由CSDN通过智能技术生成

       因为项目需求,需要对某铁矿厂的球团进行粒径检测。采集系统就不详细说了,主要是颗粒运动很快,粒径在8-12mm,范围1米左右,所以既要高像素的相机,又要曝光时间很短的相机,前期拍出来的光照不足,图像很暗。

   

图1 现场图                                                  图2. 球团图

        查了下资料,相似的文献比如水果分拣啊,硬币分拣啊,都是用的分水岭。我也尝试这分割了下。刚开始效果不行,对光滑的表面效果挺好,但是对这种小颗粒,表面不是特光滑的,效果不理想。主要是噪声多,精确度差。后来通过对背景和前景的调整,达到如图4的效果,基本上满足了生产需求。

 

图3. 改进前                                                                                     图4. 改进后

下面简单介绍下过程。

 一. 图像预处理

        因为原图是短曝光+提高增益的来的,比较黑,噪声大。需要对图像进行增强处理,本文用了顶帽变换+中值滤波的方式。

cv::Mat kernel2 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(12, 12), cv::Point(-1, -1));
cv::morphologyEx(src, src, CV_MOP_TOPHAT, kernel2, cv::Point(-1, -1), 1);//顶帽变换
cv::medianBlur(src, src, 3);

图5. 增强后的球团图 

二. 分水岭算法

      分水岭的基本理论主要是漫水筑坝的过程,可以参考文章1文章2。这里主要谈谈实现过程。opencv的分水岭算法主要依靠marks来标记待分割的边界。一方面减少噪声,一方面提高巡边速度。因此,marks的设置就很重要。

2.1. 构建marks

2.1.1  构造前景

      这部分主要是对球团进行标记。因为球团是层叠的,我们主要把完整的球团标记出来作为前景。这部分我借鉴了网络上其他人的,然后调整了下参数。主要的过程:

(1)二值化,分割目标

(2)开运算,减少噪声

(3)距离变换+二值化

(4)膨胀运算,精简前景

cv::threshold(src, src, 0, 255, cv::THRESH_BINARY|cv::THRESH_OTSU);

cv::Mat old_src = src.clone(); //保存处理前的图,以方便求背景
// 开操作
cv::morphologyEx(src, src, cv::MORPH_OPEN, kernel, cv::Point(-1, -1), 2);
//---------------距离变换-------------------
cv::Mat dist_img;
cv::distanceTransform(src, dist_img, CV_DIST_L2, 3/*distance type*/);
cv::normalize(dist_img, dist_img, 0, 1., cv::NORM_MINMAX);
cv::threshold(dist_img, dist_img, 0.1, 1., CV_THRESH_BINARY);
// 膨胀
cv::Mat kernel1 = cv::Mat::ones(3, 3, CV_8UC1);
cv::morphologyEx(dist_img, dist_img, cv::MORPH_DILATE, kernel1, cv::Point(-1, -1), 2);//
dist_img.convertTo(src, CV_8U);

这部分的可操作性蛮大,主要表现在:

(1)是用开运算还是其他的,我也尝试过先两次膨胀,再1次腐蚀,效果也有差别,在某些图像上效果更好。还有,形态学运算的次数,也会影响结果;

(2)距离变换后的二值化阈值范围:我这里选的是0.1,起点越小,颗粒越小。所以噪声也会多。所以,这里要结合先验条件设置。

图6 二值化                                 图7 距离变换                                   图8 前景

2.1.2  构造背景

        这里的背景,是指暗区无法分割出来的球团。如果不指定背景,在分水岭过程中,算法认为每个前景是挨着的,会把暗区也分到前景去,使边缘过分割。也就是说,设计marks时,把背景作为标识0(也就是待分区),最后的结果不会分割出背景来。因此,有必要把背景指定。

      这里,只需要把二值化后的前景进行膨胀,剩余部分作为背景。操作如下:

cv::Mat bk;//找背景,扩大目标区,压缩背景,使背景更精确(背景为黑色)
cv::morphologyEx(src_old, bk, CV_MOP_DILATE, kernel, cv::Point(-1, -1), 6);

// 对我们已经确定分类的区域(无论是前景还是背景)使用不同的正整数标记,
// 对我们不确定的区域(要进行分水岭查找边界的区域)使用 0 标记。
cv::Mat marks(src_old.rows, src_old.cols, CV_32S, cv::Scalar(0));

for (int i = 0; i < src.rows; i++)
{
	LPBYTE lpBk = bk.ptr<BYTE>(i);
	INT32 *lpMask = marks.ptr<INT32>(i);
	for (int j = 0; j < src.cols; j++)
	{
		if (*(lpBk + j) == 0)//标记背景为1
			*(lpMask + j) = 1;
	}
}

 2.1.3  构造完整的marks

         marks里面已经指定了背景,再融合进前景就行了。这里用的是drawContours函数,把前景轮廓画到marks里。

vector<vector<cv::Point>> contours;
//此时,src为二值图,白色区域为前景
cv::findContours(src, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size() && i<1000; i++)
{
	double area = cv::contourArea(contours[i]);
	if (area < 200)//剔除小区域
		continue;
    // 标记从2开始,1已经画了背景
	cv::drawContours(marks, contours, static_cast<int>(i), cv::Scalar::all(static_cast<int>(i) + 2), 1, 8);
}

  

图9 背景marks                                                                                             图10 最终的marks


       其中,灰色部分标记为1的背景,白色部分为前景(这里只是示意,实际上灰色为1,每个白色封闭区实际上标号大于1,各自也不一样),这两者为注水区,中间黑色部分(标记0)为未知区,也就是待搜索区。

 2.2. 分水岭分割

     下一步很简单了,就是直调用函数了。但是,操作对象可以是前面增强后的图,也可以是原图。但是,增强后的图对比度大了,但是噪点也多了。所以我用的还是原图。

cv::watershed(input_src, marks);

       

       图11 处理后的marks

三. 统计显示  

         这一步marks里就已经把边界用-1,标识出来了。所以marks一定是一个32位整型矩阵。再下一步就是遍历统计了。我这里是遍历每个标识面积(不要遍历标识为1的背景)和区域中心,然后简单的换算一下圆的半径:

                    r =  2 * sqrt( nArea / PI)

typedef struct _ContourInfo
{
	float x;
	float y;
	int nArea;
	_ContourInfo()
	{
		x = y = -1;
		nArea = 0;
	}
}ContourInfo, *pContourInfo;

std::map<int, ContourInfo> indexContour;
for (int i = 0; i < marks.rows; i++)
{
	for (int j = 0; j < marks.cols; j++)
	{
		int index = marks.at<int>(i, j);//区域标记
		if (index != -1 && index <= static_cast<int>(contours.size()))//-1为边界像素,1为背景
		{
			indexContour[index].nArea++;
			indexContour[index].x += j;
			indexContour[index].y += i;
		}
	}
}

std::map<int, ContourInfo>::iterator itr;
for (itr = indexContour.begin(); itr != indexContour.end(); itr++)
{
	if (itr->second.nArea > 0)
	{
		itr->second.x /= itr->second.nArea;
		itr->second.y /= itr->second.nArea;
	}
}
float fRadio = 0;

//----------show--------------
if (bManuInput)
{
	std::map<int, ContourInfo>::iterator itr;
	for (itr = indexContour.begin(); itr != indexContour.end(); itr++)
	{
		if (itr->second.nArea > 100 && itr->first != 1/*1为背景*/)
		{
			fRadio = sqrt(double(itr->second.nArea) / PI) + 2;
			cv::circle(oImage, cv::Point(itr->second.x, itr->second.y), int(fRadio), cv::Vec3b(0, 0, 255), 1);
		}
	}
	cv::imshow("result", oImage);
}

效果:

四.参考文献

轮廓提取试验_元气少女缘结神的博客-CSDN博客

OpenCV---分水岭算法 - 山上有风景 - 博客园

标记控制的分水岭分割- MATLAB & Simulink Example- MathWorks 中国

分水岭算法的详细介绍(附c代码)_一步一个脚印的屌丝的博客-CSDN博客

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值