OpenCV识别草莓轮廓、质心、生长方向

机器人视觉系统 作业记录

识别下图中草莓轮廓,计算质心、生长方向。
*2023-04-09 修正BGR2HSI函数,草莓HSI通道图像。
*2023-04-11 删除RGB通道下的相关计算和椭圆拟合计算生长方向内容;增加H,S双通道计算。

0 调试环境

① OpenCV 4.2.0
② Visual Studio 2017
*2023-04-11:  OpenCV 4.5.5 + Visual Studio 2022

1 提取草莓ROI区域

  相比于RGB颜色空间,HSI颜色空间能较好地区分草莓与背景部分,故选择在HSI颜色空间下提取草莓ROI区域。

1.1 RGB转HSI

  遍历原图像的RGB值,逐像素归一化后按下述公式将其转到HSI颜色空间。计算得到的HSI三通道数据取值范围分别为: H [ 0 , 2 π ] H[0,2\pi] H[0,2π] S [ 0 , 1 ] S[0,1] S[0,1] I [ 0 , 1 ] I[0,1] I[0,1],需要将H归一化( H = H / 2 π H = H/2\pi H=H/2π)。
{ H = { θ , B ≤ G π − θ , B > G , θ = arccos ⁡ { ( R − G ) + ( R − B ) 2 ( R − G ) 2 + ( R − B ) ( G − B ) } S = 1 − 3 R + G + B min ⁡ ( R , G , B ) I = 1 − R + G + B 3 \begin{align} \left \{ \begin{array}{ll} H=\begin{cases} \theta, B \le G \\ \pi-\theta, B>G \end{cases},\theta=\arccos\left\{ \frac{(R-G)+(R-B)}{2\sqrt{ (R-G)^2+(R-B)(G-B)}} \right\}\\ S=1-\large\frac{3}{R+G+B}\min(R,G,B)\\ I=1-\large\frac{R+G+B}{3} \end{array} \right. \end{align} H={θ,BGπθ,B>Gθ=arccos{2(RG)2+(RB)(GB) (RG)+(RB)}S=1R+G+B3min(R,G,B)I=13R+G+B
  草莓图像在HSI颜色空间的效果如图1-1所示。

(a) HSI
(b) H通道
(c) S通道
(d) I通道
图1-1 HSI颜色空间下的草莓

1.2 二值化

  为了获取草莓ROI区域,需要将归一化状态的HSI数据( [ 0 , 1 ] [0,1] [0,1])扩展到 [ 0 , 255 ] [0,255] [0,255]区间,便于选取阈值。在H、S通道下的草莓与背景区分较为明显,故选择在这两个通道下通过阈值分割、位运算提取草莓ROI区域。主要步骤如下:
[1] S通道提取草莓主体+叶+茎;
[2] H通道提取绿色及相近色调区域;
[3] 根据[2]中结果在[1]结果中删除绿色相关像素
[4] H通道提取红色及相近色调区域;
[5] 对[3]结果与[4]结果做或运算,叠加得到草莓ROI区域。
  各步骤的二值图如图1-2所示。

(a) 步骤[1]
(b) 步骤[2]
(c) 步骤[3]
(d) 步骤[4]
(e) 步骤[5]
图1-2 提取草莓ROI区域

2 填充空洞

  上述方法得到的草莓二值图包含许多干扰和空洞,为了方便识别,需要尽可能地将干扰去除,将空洞补全。补空洞的步骤为:
(1)用闭操作连通区域;
(2)使用findContours()函数,选择RETR_CCOMP模式,找出草莓轮廓中的内轮廓(即空洞的轮廓);
(3)判断每个轮廓的大小,只有当其小于90像素时才进行填充。
  填充后的草莓二值图如图2-1所示。
  对补全空洞后的图像检测连通域,用最小面积来消除干扰区域。填充空洞且去除干扰的结果如图2-2所示。

图2-1 草莓ROI区域二值图

3 分离草莓

  对上述得到的二值图(方法1)做分水岭分割(结果如图3-1所示),步骤如下:[2]
(1)对填充空洞后的图像使用distanceTransform()函数作距离变换,并归一化;
(2)对归一化后的距离图使用的threshold()函数作阈值处理,得到种子区域;(这种方法其实很难确定种子区域的阈值范围,或许可以考虑k-mean聚类?)
(3)使用watershed()函数执行分水岭分割;
(4)对每个草莓区域上色。

图3-1 分离草莓

4 计算质心、下极值法获取草莓生长方向

  由草莓轮廓区域的一阶矩可以获得草莓的中心点,作为质心。计算生长方向的步骤如下:
(1)遍历各草莓的轮廓数据,找出y值最小值点作为草莓的下极值点;
(2)连接下极值点和中心点(质心),得到草莓的生长方向(姿态)。

图4-1 质心和生长方向

5 参考文献

[1] 纪超. 温室果蔬采摘机器人视觉信息获取方法及样机系统研究[D].中国农业大学,2014.
[2] OpenCV官方文档示例

附录

主函数

#include <iostream>
#include <opencv2/opencv.hpp>
#include "findStrawberry.hpp"
using namespace std;
using namespace cv;
int main()
{
	Mat src = imread("C:/Users/12421/Desktop/草莓1.jpg");
	GaussianBlur(src, src, Size(5, 5), 0, 0);   //滤波
	Mat HSI; bgr2hsi(src, HSI);                 //BGR->HSI
	vector<Mat> channels; split(HSI, channels); //分离HSI
	Mat H = channels.at(0);
	Mat S = channels.at(1);
	H *= 255; S *= 255;      //[0,1] -> [0,255]
	H.convertTo(H, CV_8UC1); //float->uchar
	S.convertTo(S, CV_8UC1); //float->uchar
	Mat binaryS(src.size(), CV_8UC1);
	threshold(S, binaryS, 95, 255, THRESH_BINARY); //S通道草莓主体+叶+茎
	Mat thresh1, thresh2;
	int greenHL = 20; int greenHU = 90;   //绿色H通道阈值
	threshold(H, thresh1, greenHL, 255, THRESH_BINARY);     //双阈值二值化-1
	threshold(H, thresh2, greenHU, 255, THRESH_BINARY_INV); //双阈值二值化-2
	Mat green; //H通道草莓叶+茎+其他绿色
	bitwise_and(thresh1, thresh2, green); //与运算,双阈值二值化
	Mat roiS = binaryS.clone();
	for (int i = 0; i < src.rows; i++)    //获得S通道草莓主体
	{
		for (int j = 0; j < src.cols; j++)
		{
			if (binaryS.ptr<uchar>(i)[j] != 255)
				continue;
			if (green.ptr<uchar>(i)[j] == 255)
				roiS.ptr<uchar>(i)[j] = 0;
		}
	}
	int redHL = 250; int redHU = 5; //红色H通道阈值
	threshold(H, thresh1, redHL, 255, THRESH_BINARY);     //跨越255(0)的二值化-1
	threshold(H, thresh2, redHU, 255, THRESH_BINARY_INV); //跨越255(0)的二值化-2
	Mat roiH; //H通道草莓主体+相近色调物体
	bitwise_or(thresh1, thresh2, roiH);//或运算,二值化
	Mat binary; //H+S通道的ROI区域
	bitwise_or(roiH, roiS, binary); //或运算,H+S通道的ROI区域
	Mat element(5, 5, CV_8U, Scalar(1));	
	morphologyEx(binary, binary, MORPH_CLOSE, element); //闭运算,减少空洞
	fillcavity(binary, binary); //填充空洞
	clearMicroConnectedAreas(binary, binary, 2000); //去除干扰
	Mat ref(src.size(), CV_8UC3); //分水岭分割ref
	for (int i = 0; i < src.rows; i++)
	{
		for (int j = 0; j < src.cols; j++)
		{
			ref.at<Vec3b>(i, j) = binary.at<uchar>(i, j);
		}
	}
	Mat strawberry;
	findstrawberry(binary, strawberry, ref); //找草莓
	orientation(src, strawberry); //绘制重心+生长方向
	imshow("orientation", src);
	waitKey(0);
	return 0;
}

子函数声明

#pragma once
#include <opencv2/core.hpp>

/**
* @brief BGR转HSI
* @param [in]  src RGB图像
* @param [out] dst HSI图像
*/
void bgr2hsi(cv::Mat src, cv::Mat& dst);

/**
* @brief 填充空洞
* @param [in]  src 原图
* @param [out] dst 填充空洞后的图像
*/
void fillcavity(cv::Mat src, cv::Mat& dst);

/**
* @brief  清除小连通区域
* @param  [in]  src     原图
* @param  [out] dst     清除小连通区域后的图像
* @param  [in]  minArea 清除小连通区域的面积阈值
*/
void clearMicroConnectedAreas(cv::Mat src, cv::Mat& dst, float minArea);

/**
* @brief  找草莓
* @param  [in]  src 原图
* @param  [out] dst 找到的草莓
* @param  [in]  ref 分水岭分割图
*/
void findstrawberry(cv::Mat src, cv::Mat& dst, cv::Mat ref);

/**
* @brief 绘制草莓重心;下极值点法绘制草莓生长方向
* @param [in/out] src 输入初始图像,输出位姿图像
* @param [in]     ref 草莓区域
*/
void orientation(cv::Mat& src, cv::Mat ref);

子函数定义

#include "findStrawberry.hpp"
#include <opencv2/opencv.hpp>

void bgr2hsi(cv::Mat src, cv::Mat& dst)
{
	cv::Mat srcc = src.clone();
	cv::Mat dstc(src.size(), CV_32FC3);
	//cv::Mat I(src.size(), CV_32FC1);
	//cv::Mat S(src.size(), CV_32FC1);
	//cv::Mat H(src.size(), CV_32FC1);
	float hValue, sValue, iValue;
	for (int i = 0; i < srcc.rows; i++)
	{
		for (int j = 0; j < srcc.cols; j++)
		{
			float blueValue = srcc.at<cv::Vec3b>(i, j)[0] / 255.f;
			float greenValue = srcc.at<cv::Vec3b>(i, j)[1] / 255.f;
			float redValue = srcc.at<cv::Vec3b>(i, j)[2] / 255.f;
			float sum = blueValue + greenValue + redValue;
			float iValue = sum / 3.0f;
			dstc.ptr<cv::Vec3f>(i)[j][2] = iValue;
			//I.ptr<float>(i)[j] = iValue;
			if (sum == 0)
				sValue = 0;
			else
				sValue = 1 - 3 * cv::min(cv::min(blueValue, greenValue), redValue) / sum;
			dstc.ptr<cv::Vec3f>(i)[j][1] = sValue;
			//S.ptr<float>(i)[j] = sValue;			
			float den = sqrtf((redValue - greenValue) * (redValue - greenValue)
				+ (redValue - blueValue) * (greenValue - blueValue));
			if (den == 0)
				hValue = 0;
			else
			{
				float num = (2 * redValue - greenValue - blueValue) / 2.f;
				float theta = acosf(num / den);
				hValue = blueValue <= greenValue ?
					theta / (2 * CV_PI) : 1 - theta / (2 * CV_PI);
			}
			dstc.ptr<cv::Vec3f>(i)[j][0] = hValue;
			//H.ptr<float>(i)[j] = hValue;
		}
	}
	dst = dstc;
}

void fillcavity(cv::Mat src, cv::Mat& dst)
{
	cv::Mat srcc = src.clone();
	std::vector<std::vector<cv::Point> > contours;
	std::vector<cv::Vec4i> hierarchy;
	findContours(srcc, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
	for (size_t i = 0; i < contours.size(); i++)
	{
		if (hierarchy[i][3] >= 0 && contours[i].size() < 90) //小空洞
		{
			drawContours(srcc, contours, i, cv::Scalar(255, 255, 255), cv::FILLED);
		}
	}
	dst = srcc;
}

void clearMicroConnectedAreas(cv::Mat src, cv::Mat& dst, float minArea)
{
	dst = src.clone();
	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> 	hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());
	if (!contours.empty() && !hierarchy.empty())
	{
		std::vector<std::vector<cv::Point> >::const_iterator itc = contours.begin();
		while (itc != contours.end())
		{
			cv::Rect rect = cv::boundingRect(cv::Mat(*itc));
			double area = contourArea(*itc);//当前面积
			if (area < minArea)
			{
				for (int i = rect.y; i < rect.y + rect.height; i++)
				{
					uchar* output_data = dst.ptr<uchar>(i);
					for (int j = rect.x; j < rect.x + rect.width; j++)
					{
						if (output_data[j] == 255)
						{
							output_data[j] = 0;
						}
					}
				}
			}
			itc++;
		}
	}
}

void findstrawberry(cv::Mat src, cv::Mat& dst, cv::Mat ref)
{	
	cv::Mat dist;
	distanceTransform(src, dist, cv::DIST_L2, 3); //距离变换	
	normalize(dist, dist, 0, 1.0, cv::NORM_MINMAX);      //距离图归一化
	threshold(dist, dist, 0.65, 1.0, cv::THRESH_BINARY);
	cv::Mat dist_8u;
	dist.convertTo(dist_8u, CV_8U);
	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	findContours(dist_8u, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);	
	cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32S);//创建标记
	for (size_t i = 0; i < contours.size(); i++)//绘制前景标记
	{
		drawContours(markers, contours, static_cast<int>(i), cv::Scalar(static_cast<int>(i) + 1), 2, 8, hierarchy, 0, cv::Point(0, 0));
	}	
	circle(markers, cv::Point(5, 5), 3, cv::Scalar(255), -1);//绘制背景标记
	//markers.convertTo(markers, CV_8U); //更改类型显示标记(扩大10000倍)
	//namedWindow("Markers", WINDOW_AUTOSIZE);
	//imshow("Markers", markers * 10000);	
	//markers.convertTo(markers, CV_32S);//改回类型进行分水岭操作
	watershed(ref, markers); //执行分水岭分割	
	cv::Mat mark;
	markers.convertTo(mark, CV_8U);//分割后的图形
	//imshow("Markers_v1", mark);
	bitwise_not(mark, mark);//取反,即将背景置为黑色
	//namedWindow("Watershed", WINDOW_AUTOSIZE);
	//imshow("Watershed", mark);	
	std::vector<cv::Vec3b> colors; 
	for (size_t i = 0; i < contours.size(); i++) //生成颜色
	{
		int b = cv::theRNG().uniform(0, 256);
		int g = cv::theRNG().uniform(0, 256);
		int r = cv::theRNG().uniform(0, 256);
		colors.push_back(cv::Vec3b((uchar)b, (uchar)g, (uchar)r));
	}	
	cv::Mat dstc = cv::Mat::zeros(markers.size(), CV_8UC3);//为轮廓填充颜色
	for (int i = 0; i < markers.rows; i++)
	{
		for (int j = 0; j < markers.cols; j++)
		{
			int index = markers.at<int>(i, j);
			if (index > 0 && index <= static_cast<int>(contours.size()))
			{
				dstc.at<cv::Vec3b>(i, j) = colors[index - 1];
			}
		}
	}
	dst = dstc;
}

void orientation(cv::Mat& src, cv::Mat ref)
{
	cv::Mat gray;
	cvtColor(ref, gray, cv::COLOR_BGR2GRAY);
	cv::Mat element1 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
	cv::Mat er;
	erode(gray, er, element1);
	//草莓轮廓
	std::vector<std::vector<cv::Point>> contours_strawberry;
	std::vector<cv::Vec4i> hierarchy_strawberry;
	findContours(er, contours_strawberry, hierarchy_strawberry, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE, cv::Point());
	//重心+生长方向
	for (int i = 0; i < contours_strawberry.size(); i++)
	{
		drawContours(src, contours_strawberry, i, cv::Scalar(0, 255, 255), 2, 8, hierarchy_strawberry, 0, cv::Point());//在原图上绘制草莓轮廓
		cv::Mat tmp(contours_strawberry.at(i));
		cv::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, cv::Point(x, y), 5, cv::Scalar(235, 191, 0), -1);//绘制实心圆
			std::cout << "-> 第" << i + 1 << "个草莓" << std::endl;
			std::cout << "重心: " << cv::Point(x, y) << std::endl;
			int minyx = contours_strawberry[i][0].x;//当前轮廓上极值点横坐标赋初值
			int minyy = contours_strawberry[i][0].y;//当前轮廓上极值点纵坐标赋初值
			int maxyx = contours_strawberry[i][0].x;//当前轮廓下极值点横坐标赋初值
			int maxyy = contours_strawberry[i][0].y;//当前轮廓下极值点纵坐标赋初值
			for (int j = 0; j < contours_strawberry[i].size(); j++)//遍历轮廓数据
			{
				if (minyy > contours_strawberry[i][j].y)//如果上极值点纵坐标小于当前纵坐标
				{
					minyy = contours_strawberry[i][j].y;//将当前纵坐标赋值给上极值点纵坐标
					minyx = contours_strawberry[i][j].x;//将当前横坐标赋值给上极值点横坐标
				}
				if (maxyy < contours_strawberry[i][j].y)//如果下极值点纵坐标大于当前纵坐标
				{
					maxyy = contours_strawberry[i][j].y;//将当前纵坐标赋值给下极值点纵坐标
					maxyx = contours_strawberry[i][j].x;//将当前横坐标赋值给下极值点横坐标
				}
			}
			circle(src, cv::Point(maxyx, maxyy), 5, cv::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, cv::Point(maxyx, maxyy),
					cv::Point(x1, minyy - 30), cv::Scalar(255, 0, 0), 2, cv::LINE_AA);//绘制生长方向线段(带箭头)
			}
			else//斜率为∞时
			{
				arrowedLine(src, cv::Point(maxyx, maxyy),
					cv::Point(x, minyy - 30), cv::Scalar(255, 0, 0), 2, cv::LINE_AA);//绘制生长方向线段(带箭头)
			}
		}
	}
}
  • 22
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值