2023.4.16日更新
1.利用一階矩增加了草莓等水果的質心繪制。
2.繪制出了生長方向。
原為本人機器人視覺作業(yè)。參考文章http://t.csdn.cn/eQ0qp(目測是上一屆的學長)
要求:在網絡上尋找水果重疊在一起的圖片、經過一系列圖像處理,完成每個水果的分割,并單獨標記出來。
- 導入圖片
在網上找到了一些水果疊在一起的圖片,選一個作為本次調試的樣圖,導入圖片如下。
為顯示方便,將圖像縮小兩倍,縮小同前幾次作業(yè)相同,代碼如下
2.顏色通道選擇
首先嘗試了不同通道單獨二值化。發(fā)現效果不如人意。
以紅色通道為例。
可以看到四個閾值大范圍都不能完成較好的分割(也可能是我哪里出錯了)。
為解決不同光照情況對圖像的影響,對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);//創(chuàng)建一個全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;
}
可以發(fā)現是消除了亮度的影響
3.二值化處理
由于圖像主要分布在紅色、綠色通道空間內,因此將歸一化后的圖像進行紅綠分割,即灰度化處理。
思想于平?;叶然炼戎挡煌瑢⒈容^方式改為紅色綠色通道內的值大小,詳細見http://t.csdn.cn/eQ0qp
代碼如下:
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調閾值后發(fā)現OTSU效果不錯),處理結果如下
?可以看到右下方有噪點,中部由于梗的存在也有。
4.形態(tài)學操作
對處理后的圖像進行形態(tài)學操作。
填補小空洞:開運算
梗處理:膨脹
為了防止圖像嚴重失真,核不宜過大,故先采用一次開操作將白色噪點填充,后逐漸降低膨脹的核大小,一連四次膨脹,代碼如下:
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灰度化后會產生明顯差異,故不適用后續(xù)操作,這邊只用它當分水嶺的參數)代碼如下。
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);//繪制生長方向線段(帶箭頭)
}
}
}
}
繪制效果如下
這種方法存在不足之處,仍沒有趨于完美,取決于自己分水嶺的分割效果,可以看以下“失敗”范例。
可以看到橙子由于分水嶺分割出來存在一些空洞,故無法較為完美的擬合出唯一的質心
下兩組表現了當水果數目增多,這個差異會越來越大
且實在無法規(guī)避的一件事情為,這種識別方法終歸是不太完美的,尤其需要對重心進行一定的篩選,才能選出沒有干擾的重心。時間問題這邊也沒法開展進一步的修改。
比較有趣的一件事情是,發(fā)現了分水嶺圖像分割還受圖像大小的影響,當我縮放過于小時,分水嶺往往會把多個水果分割成一個,而重新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("形態(tài)學操作圖像", 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;
}//
主要函數及引用關系見上。文章來源:http://www.zghlxwxcb.cn/news/detail-757754.html
7.總結文章來源地址http://www.zghlxwxcb.cn/news/detail-757754.html
- 由于灰度化是使用紅綠分割,導致綠色水果+綠色背景或紅色水果+紅色背景會嚴重失真甚至分割不出來。
- 基本完成了分割,而參考的博客中(目測是上一屆學長寫的)沒有使用rgb歸一化完成最后的處理,其分水嶺第一個參數的格式問題這邊優(yōu)化解決了。
到了這里,關于使用opencv c++完成圖像中水果分割(分水嶺、形態(tài)學操作、通道處理)單獨標記每個水果的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!