2023.4.16日更新
1.利用一阶矩增加了草莓等水果的质心绘制。
2.绘制出了生长方向。
原为本人机器人视觉作业。参考文章(目测是上一届的学长)
要求:在网络上寻找水果重叠在一起的图片、经过一系列图像处理,完成每个水果的分割,并单独标记出来。
- 导入图片
在网上找到了一些水果叠在一起的图片,选一个作为本次调试的样图,导入图片如下。
为显示方便,将图像缩小两倍,缩小同前几次作业相同,代码如下
2.颜色通道选择
首先尝试了不同通道单独二值化。发现效果不如人意。
以红色通道为例。
可以看到四个阈值大范围都不能完成较好的分割(也可能是我哪里出错了)。
为解决不同光照情况对图像的影响,对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;
}
归一化后图像与之前的对比如下所示。
要想看到归一化后消除亮度影响效果,可以绘制其直方图,绘制代码及绘制后的直方图如下所示。
直方图部分最终代码中没有,这边做测试用。
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;
}
可以发现是消除了亮度的影响
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;
}
处理后效果如下
得到灰度图就可以进行阈值分割.使用代码如下
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效果不错),处理结果如下
可以看到右下方有噪点,中部由于梗的存在也有。
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;
}
逐步显示出的结果如下:
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;
}
得到图像如下
与原图像对比可以看到,分割效果明显。
6.其他图片测试
- 测试图片1如下(来源ppt)
2.测试图片2如下(来源百度)
3.测试图片3如下(来源百度)
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);//绘制生长方向线段(带箭头)
}
}
}
}
绘制效果如下
这种方法存在不足之处,仍没有趋于完美,取决于自己分水岭的分割效果,可以看以下“失败”范例。
可以看到橙子由于分水岭分割出来存在一些空洞,故无法较为完美的拟合出唯一的质心
下两组表现了当水果数目增多,这个差异会越来越大
且实在无法规避的一件事情为,这种识别方法终归是不太完美的,尤其需要对重心进行一定的筛选,才能选出没有干扰的重心。时间问题这边也没法开展进一步的修改。
比较有趣的一件事情是,发现了分水岭图像分割还受图像大小的影响,当我缩放过于小时,分水岭往往会把多个水果分割成一个,而重新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.总结
- 由于灰度化是使用红绿分割,导致绿色水果+绿色背景或红色水果+红色背景会严重失真甚至分割不出来。
- 基本完成了分割,而参考的博客中(目测是上一届学长写的)没有使用rgb归一化完成最后的处理,其分水岭第一个参数的格式问题这边优化解决了。