2023.4.16日更新

1.利用一阶矩增加了草莓等水果的质心绘制。

2.绘制出了生长方向。

原为本人机器人视觉作业。参考文章(目测是上一届的学长)

要求:在网络上寻找水果重叠在一起的图片、经过一系列图像处理,完成每个水果的分割,并单独标记出来。

  1. 导入图片

在网上找到了一些水果叠在一起的图片,选一个作为本次调试的样图,导入图片如下。

opencv 通道拼接 opencv 连通区域分割_opencv

为显示方便,将图像缩小两倍,缩小同前几次作业相同,代码如下

2.颜色通道选择

首先尝试了不同通道单独二值化。发现效果不如人意。

以红色通道为例。

opencv 通道拼接 opencv 连通区域分割_c++_02

opencv 通道拼接 opencv 连通区域分割_opencv_03

可以看到四个阈值大范围都不能完成较好的分割(也可能是我哪里出错了)。

为解决不同光照情况对图像的影响,对rgb图像进行归一化,代码如下。

Mat version_lesson::normalize_rgb(Mat &image)
{
	Mat normalize(image.rows, image.cols, CV_32FC3);
	for (int i = 0; i < image.rows; i++){
		for (int j = 0; j < image.cols; j++){
			double epslon = 0.000001;//防止rgb均为0
			int b = image.at<Vec3b>(i, j)[0];
			int g = image.at<Vec3b>(i, j)[1];
			int r = image.at<Vec3b>(i, j)[2];
			double sum = b + g + r + epslon;
			normalize.at<Vec3f>(i, j)[2] = r / sum;
			normalize.at<Vec3f>(i, j)[1] = g / sum;
			normalize.at<Vec3f>(i, j)[0] = b / sum;
		}
	}
	return normalize;
}

归一化后图像与之前的对比如下所示。

opencv 通道拼接 opencv 连通区域分割_c++_04

要想看到归一化后消除亮度影响效果,可以绘制其直方图,绘制代码及绘制后的直方图如下所示。

 直方图部分最终代码中没有,这边做测试用。

Mat version_lesson::print_hist_demo(Mat &img) {
	int bins = 256;
	int hist_size[] = { bins };
	float range[] = { 0,256 };
	const float *ranges[] = { range };
	MatND hist;
	int channels[] = { 0 };
	//计算出灰度直方图
	calcHist(&img, 1, channels, Mat(), hist, 1, hist_size, ranges);
	//画出直方图
	double max_val;
	minMaxLoc(hist, 0, &max_val, 0, 0);//定位矩阵中最小值、最大值的位置
	int scale = 2;
	int hist_height = 256;
	Mat hist_img = Mat::zeros(hist_height, bins*scale, CV_8UC3);//创建一个全0的特殊矩阵
	for (int i = 0; i < bins; i++)
	{
		float bin_val = hist.at<float>(i);
		int inten = cvRound(bin_val*hist_height / max_val);//要绘制高度
		//画矩形
		rectangle(hist_img, Point(scale*i, hist_height - 1), Point((i + 1)*scale - 1, hist_height - inten), CV_RGB(255, 255, 255));
	}
	return hist_img;
}

opencv 通道拼接 opencv 连通区域分割_计算机视觉_05

可以发现是消除了亮度的影响


3.二值化处理

由于图像主要分布在红色、绿色通道空间内,因此将归一化后的图像进行红绿分割,即灰度化处理。

思想于平常灰度化亮度值不同,将比较方式改为红色绿色通道内的值大小,详细见

代码如下:

Mat version_lesson::normalize_gray(Mat &image) {
	Mat rg_gray(image.rows, image.cols, CV_8UC1);
		for (int i = 0; i < image.rows; i++){
			for (int j = 0; j < image.cols; j++){
				//读取rg值
				double g = image.at<Vec3f>(i, j)[1];
				double r = image.at<Vec3f>(i, j)[2];
				if (r > g)
					rg_gray.at<uchar>(i, j) = (r - g) * 255;
				else
					rg_gray.at<uchar>(i, j) = 0;
			}
		}
	return rg_gray;
}

处理后效果如下

opencv 通道拼接 opencv 连通区域分割_归一化_06

得到灰度图就可以进行阈值分割.使用代码如下

Mat version_lesson::threshold_fenge(Mat &image) {
	Mat binary;
	threshold(image, binary, 100, 255, THRESH_OTSU);
	//imshow("OTSU二值化图像", binary);
	return binary;
}

 其中THRESH_OTSU过滤方法为自适应阈值。(这边借助了imlab调阈值后发现OTSU效果不错),处理结果如下


opencv 通道拼接 opencv 连通区域分割_c++_07

 可以看到右下方有噪点,中部由于梗的存在也有。

4.形态学操作

对处理后的图像进行形态学操作。

填补小空洞:开运算

梗处理:膨胀

为了防止图像严重失真,核不宜过大,故先采用一次开操作将白色噪点填充,后逐渐降低膨胀的核大小,一连四次膨胀,代码如下:

Mat version_lesson::morphology_do(Mat &image) {
	Mat dst2;
	Mat kerne_open = getStructuringElement(MORPH_RECT, Size(7, 7), Point(-1, -1));
	morphologyEx(image, dst2,MORPH_OPEN, kerne_open);
	//imshow("开运算操作", dst2);
	Mat kernel_dilate1 = getStructuringElement(MORPH_RECT, Size(10, 10), Point(-1, -1));
	morphologyEx(dst2, dst2, MORPH_DILATE, kernel_dilate1);
	//imshow("膨胀操作1", dst2);
	Mat kernel_dilate2 = getStructuringElement(MORPH_RECT, Size(8, 8), Point(-1, -1));
	morphologyEx(dst2, dst2, MORPH_DILATE, kernel_dilate2);
	//imshow("膨胀操作2", dst2);
	Mat kernel_dilate3 = getStructuringElement(MORPH_RECT, Size(7, 7), Point(-1, -1));
	morphologyEx(dst2, dst2, MORPH_DILATE, kernel_dilate3);
	//imshow("膨胀操作3", dst2);
	Mat kernel_dilate4 = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(dst2, dst2, MORPH_DILATE, kernel_dilate4);
	//imshow("膨胀操作4", dst2);
	return dst2;
}

逐步显示出的结果如下:

opencv 通道拼接 opencv 连通区域分割_c++_08

opencv 通道拼接 opencv 连通区域分割_c++_09

5.分割操作 

对其使用分水岭分割操作。

由于分水岭操作第一个参数需要使用8bit3通道的图像,而上述归一化后产生的图像时32bit3通道的图像,因此这边重新定义了一个额外的归一化图像,用于生成分水岭操作的第一个参数。(生成的图像与第一个图象基本相同,但rg灰度化后会产生明显差异,故不适用后续操作,这边只用它当分水岭的参数)代码如下。

Mat version_lesson::normalize_rgb2(Mat &image)
{
	Mat normalize(image.rows, image.cols, CV_8UC3);
	for (int i = 0; i < image.rows; i++) {
		for (int j = 0; j < image.cols; j++) {
			double epslon = 0.000001;//防止rgb均为0
			int b = image.at<Vec3b>(i, j)[0];
			int g = image.at<Vec3b>(i, j)[1];
			int r = image.at<Vec3b>(i, j)[2];
			double sum = b + g + r + epslon;
			normalize.at<Vec3b>(i, j)[2] = int((r / sum)*255);
			normalize.at<Vec3b>(i, j)[1] = int((g / sum)*255);
			normalize.at<Vec3b>(i, j)[0] = int((b / sum)*255);
		}
	}
	return normalize;
}

基于距离图的分水岭分割代码操作如下 

Mat version_lesson::water_fenge(Mat &image, Mat &src, Mat &gray) {//
	Mat dist;
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
	distanceTransform(image, dist, DIST_L2, 5);
	normalize(dist, dist, 0, 255, NORM_MINMAX);
	double my_minv = 0.0, my_maxv = 0.0;
	minMaxIdx(dist, &my_minv, &my_maxv);
	Mat sure_fg;//注水点
	threshold(dist, sure_fg, 0.8 * my_maxv, 255, THRESH_BINARY);
	sure_fg.convertTo(sure_fg, CV_8U);
	Mat element1 = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
	dilate(sure_fg, sure_fg, element, Point(-1, -1), 3);
	sure_fg.convertTo(sure_fg, CV_8U);
	imshow("sure_fg", sure_fg);
	Mat sure_bg;
	dilate(image, sure_bg, element, Point(-1, -1));
	imshow("sure_bg", sure_bg);
	Mat unkonwn = Mat(image.size(), CV_8U);
	unkonwn = sure_bg - sure_fg;
	imshow("unkonwn", unkonwn);
	Mat label_img = Mat(image.size(), CV_32S);
	int num = connectedComponents(sure_fg, label_img, 8);
	label_img = label_img + 1;
	for (int i = 0; i < unkonwn.rows; i++){
		for (int j = 0; j < unkonwn.cols; j++){
			if (((int)unkonwn.at<uchar>(i, j)) == 255){
				label_img.at<signed int>(i, j) = 0;
			}
		}
	}
	watershed(src, label_img);
	double maxVal = 0;
	double minVal = 0;
	minMaxLoc(label_img, &minVal, &maxVal);
	Mat dst = Mat::zeros(src.size(), CV_8U);
	label_img.convertTo(dst, CV_8U, 255.0 / (maxVal - minVal), -255.0 * minVal / (maxVal - minVal));
	imshow("marks", dst);
	waitKey(0);
	return dst;
}

得到图像如下

opencv 通道拼接 opencv 连通区域分割_归一化_10

opencv 通道拼接 opencv 连通区域分割_归一化_11

              与原图像对比可以看到,分割效果明显。

6.其他图片测试

  1. 测试图片1如下(来源ppt)

opencv 通道拼接 opencv 连通区域分割_opencv_12

opencv 通道拼接 opencv 连通区域分割_c++_13

2.测试图片2如下(来源百度)

opencv 通道拼接 opencv 连通区域分割_c++_14

opencv 通道拼接 opencv 连通区域分割_opencv 通道拼接_15

3.测试图片3如下(来源百度)

 

opencv 通道拼接 opencv 连通区域分割_c++_16

opencv 通道拼接 opencv 连通区域分割_opencv 通道拼接_17

2023.4.16增设内容:

质心可以使用一阶矩进行计算,生长方向可以用绘制下极值点来拟合、绘制

大部分是参考了上文中博客的绘制方法,但由于分水岭效果较差,导致最后绘制出的图像十分杂乱,质心、极值点多的一批。

为解决问题,我在前面又加了一个自适应阈值的分割,以便将图片转成适合求解距的黑白图像。

代码如下:

void version_lesson::orientation(Mat& src, Mat ref)
{
	Mat binary;
	threshold(ref, binary, 100, 255, THRESH_OTSU);
	imshow("binary", binary);
	Mat element1 = getStructuringElement(MORPH_RECT, Size(3, 3));
	Mat er;
	erode(binary, er, element1);
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(er, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
	for (int i = 0; i < contours.size(); i++){
		drawContours(src, contours, i, Scalar(0, 255, 255), 2, 8, hierarchy, 0,Point());
		Mat tmp(contours.at(i));
		Moments moment = moments(tmp, false);
		if (moment.m00 != 0){
			int x = cvRound(moment.m10 / moment.m00);//计算重心横坐标
			int y = cvRound(moment.m01 / moment.m00);//计算重心纵坐标
			circle(src, Point(x, y), 5, Scalar(235, 191, 0), -1);//绘制实心圆
			int minyx = contours[i][0].x;//当前轮廓上极值点横坐标赋初值
			int minyy = contours[i][0].y;//当前轮廓上极值点纵坐标赋初值
			int maxyx = contours[i][0].x;//当前轮廓下极值点横坐标赋初值
			int maxyy = contours[i][0].y;//当前轮廓下极值点纵坐标赋初值
			for (int j = 0; j < contours[i].size(); j++){
				if (minyy > contours[i][j].y){
					minyy = contours[i][j].y;
					minyx = contours[i][j].x;
				}
				if (maxyy < contours[i][j].y){
					maxyy = contours[i][j].y;
					maxyx = contours[i][j].x;
				}
			}
			circle(src, Point(maxyx, maxyy), 5, Scalar(0, 255, 0), -1);//绘制当前轮廓下极值点
			if (maxyx != x){
				double k = (maxyy - y) / (maxyx - x);//斜率
				double b = y - k * x;//纵向偏移
				double x1 = (minyy - 30 - b) / k;//上极值点纵坐标对应于直线上的横坐标
				arrowedLine(src, Point(maxyx, maxyy),
					Point(x1, minyy - 30), Scalar(255, 255, 0), 2, LINE_AA);//绘制生长方向线段(带箭头)
			}
			else{
				arrowedLine(src, Point(maxyx, maxyy),
					Point(x, minyy - 30),Scalar(255, 255, 0), 2, LINE_AA);//绘制生长方向线段(带箭头)
			}
		}
	}
}

绘制效果如下

opencv 通道拼接 opencv 连通区域分割_opencv 通道拼接_18

这种方法存在不足之处,仍没有趋于完美,取决于自己分水岭的分割效果,可以看以下“失败”范例。

opencv 通道拼接 opencv 连通区域分割_opencv_19

可以看到橙子由于分水岭分割出来存在一些空洞,故无法较为完美的拟合出唯一的质心

下两组表现了当水果数目增多,这个差异会越来越大

opencv 通道拼接 opencv 连通区域分割_c++_20

opencv 通道拼接 opencv 连通区域分割_归一化_21

且实在无法规避的一件事情为,这种识别方法终归是不太完美的,尤其需要对重心进行一定的筛选,才能选出没有干扰的重心。时间问题这边也没法开展进一步的修改。

比较有趣的一件事情是,发现了分水岭图像分割还受图像大小的影响,当我缩放过于小时,分水岭往往会把多个水果分割成一个,而重新resize大一些的时候,往往会比较准确。

生长方向往往可以使用其他方法绘制,这个方法仍具有局限性

 下附主函数代码:

#include<opencv2\opencv.hpp>
#include <version_lesson.h>
#include<quickopencv.h>
#include<iostream>
using namespace std;
using namespace cv;

int main(int argc, char **argv) {
	version_lesson vl;
	QuickDemo qd;
	Mat dst, gray, red, morphology, water;
	Mat src = imread("D:/Open CV/picture/柿子.jpg");
	src = qd.resize_demo(src);
	imshow("原图", src);
	red = vl.normalize_rgb(src);
	src = vl.normalize_rgb2(src);
	//imshow("rgb归一化后", red);
	//gray=vl.rgb2hsi(src);
	gray = vl.normalize_gray(red);
	//imshow("灰度图像", gray);
	dst = vl.threshold_fenge(gray);
	//imshow("OTSU二值化图像", dst);
	morphology = vl.morphology_do(dst);
	//imshow("形态学操作图像", morphology);
	//dst = vl.threshold_fenge(red);
//src = vl.normalize_rgb(src);
//imshow("rgb归一化后",src);
//cvtColor(src, gray, COLOR_BGR2GRAY);
//dst = vl.threshold_fenge(gray);
imshow("原图", src);
//green=vl.rgb_divide(src);
//erzhi = vl.threshold_fenge(green);
//hist = vl.print_hist_demo(red);
//imshow("绿色通道直方图", hist);
	water = vl.water_fenge(dst, src, gray);
	vl.orientation(src, water);
	imshow("water", water);
	imshow("生长", src);
	waitKey(0);
	destroyAllWindows();
	return 0;
}//

主要函数及引用关系见上。

7.总结

  1. 由于灰度化是使用红绿分割,导致绿色水果+绿色背景或红色水果+红色背景会严重失真甚至分割不出来。
  2. 基本完成了分割,而参考的博客中(目测是上一届学长写的)没有使用rgb归一化完成最后的处理,其分水岭第一个参数的格式问题这边优化解决了。