机器人视觉系统 作业记录
识别下图中草莓轮廓,计算质心、生长方向。
*2023-04-09 修正BGR2HSI函数,草莓HSI通道图像。
*2023-04-11 删除RGB通道下的相关计算和椭圆拟合计算生长方向内容;增加H,S双通道计算。
|
OpenCV识别草莓轮廓、质心、生长方向
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={θ,B≤Gπ−θ,B>G,θ=arccos{2(R−G)2+(R−B)(G−B)(R−G)+(R−B)}S=1−R+G+B3min(R,G,B)I=1−3R+G+B
草莓图像在HSI颜色空间的效果如图1-1所示。
|
|
|
|
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所示。
|
|
|
|
|
2 填充空洞
上述方法得到的草莓二值图包含许多干扰和空洞,为了方便识别,需要尽可能地将干扰去除,将空洞补全。补空洞的步骤为:
(1)用闭操作连通区域;
(2)使用findContours()函数,选择RETR_CCOMP模式,找出草莓轮廓中的内轮廓(即空洞的轮廓);
(3)判断每个轮廓的大小,只有当其小于90像素时才进行填充。
填充后的草莓二值图如图2-1所示。
对补全空洞后的图像检测连通域,用最小面积来消除干扰区域。填充空洞且去除干扰的结果如图2-2所示。
|
3 分离草莓
对上述得到的二值图(方法1)做分水岭分割(结果如图3-1所示),步骤如下:[2]
(1)对填充空洞后的图像使用distanceTransform()函数作距离变换,并归一化;
(2)对归一化后的距离图使用的threshold()函数作阈值处理,得到种子区域;(这种方法其实很难确定种子区域的阈值范围,或许可以考虑k-mean聚类?)
(3)使用watershed()函数执行分水岭分割;
(4)对每个草莓区域上色。
|
4 计算质心、下极值法获取草莓生长方向
由草莓轮廓区域的一阶矩可以获得草莓的中心点,作为质心。计算生长方向的步骤如下:
(1)遍历各草莓的轮廓数据,找出y值最小值点作为草莓的下极值点;
(2)连接下极值点和中心点(质心),得到草莓的生长方向(姿态)。
|
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);//绘制生长方向线段(带箭头)
}
}
}
}