1. 分水岭分割方法
它是依赖于形态学的,图像的灰度等级不一样,如果图像的灰度等级一样的情况下怎么人为的把它造成不一样?可以通过距离变换实现,这样它们的灰度值就有了阶梯状的变换。风水岭算法常见的有三种方法:(1)基于浸泡理论的分水岭分割方法;(2)基于连通图方法;(3)基于距离变换的方法。OpenCV 中是基于距离变换的分割方法,就相当于我们的小山头(认为造成的)。
基本的步骤:
例子1 粘连对象分离和计数。
例子代码:
#include<opencv2/opencv.hpp>
#include<iostream>
usingnamespacestd;
usingnamespacecv;
voidtest(){
MatsrcImg;
srcImg=imread("pill_002.png");
if(srcImg.empty())
{
cout<<"couldnotloadimage...\n"<<endl;
}
namedWindow("Originalimage",CV_WINDOW_AUTOSIZE);
imshow("Originalimage",srcImg);
MatgrayImg,binaryImg,shiftedImg;
//做滤波,使图像更加平滑,保留边缘,类似于双边滤波
pyrMeanShiftFiltering(srcImg,shiftedImg,21,51);
namedWindow("shifted",CV_WINDOW_AUTOSIZE);
imshow("shifted",shiftedImg);
cvtColor(shiftedImg,grayImg,COLOR_BGR2GRAY);//转为灰度图像
//二值化
threshold(grayImg,binaryImg,0,255,THRESH_BINARY|THRESH_OTSU);
namedWindow("binary",CV_WINDOW_AUTOSIZE);
imshow("binary",binaryImg);
//距离变换
MatdistImg;
distanceTransform(binaryImg,distImg,DistanceTypes::DIST_L2,3,CV_32F);
//归一化,因为距离变换后得出来的值都比较小。
normalize(distImg,distImg,0,1,NORM_MINMAX);
namedWindow("distance",CV_WINDOW_AUTOSIZE);
imshow("distance",distImg);
//这个二值化的作用是寻找局部最大。
threshold(distImg,distImg,0.4,1,THRESH_BINARY);
namedWindow("distance_binary",CV_WINDOW_AUTOSIZE);
imshow("distance_binary",distImg);
//生成marker
MatdistMaskImg;
//distImg得到的是0-1之间的数,转化成8位单通道的。
distImg.convertTo(distMaskImg,CV_8U);
vector<vector<Point>>contours;
//找到marker的轮廓
findContours(distMaskImg,contours,RETR_EXTERNAL,
CHAIN_APPROX_SIMPLE,Point(0,0));
//createmarker填充marker
MatmarkersImg=Mat::zeros(srcImg.size(),CV_32SC1);
for(inti=0;i<contours.size();i )
{
drawContours(markersImg,contours,static_cast<int>(i),
Scalar::all(static_cast<int>(i) 1),-1);
}
circle(markersImg,Point(5,5),3,Scalar(255),-1);
//形态学操作-彩色图像,目的是去掉干扰,让结果更好。
Matkernel=getStructuringElement(MORPH_rect,Size(3,3),Point(-1,-1));
morphologyEx(srcImg,srcImg,MORPH_ERODE,kernel);
//完成分水岭变换
watershed(srcImg,markersImg);
Matmark=Mat::zeros(markersImg.size(),CV_8UC1);
markersImg.convertTo(mark,CV_8UC1);
bitwise_not(mark,mark,Mat());
namedWindow("watershed",CV_WINDOW_AUTOSIZE);
imshow("watershed",mark);
//下面的步骤可以不做,最好做出来让结果显示更美观。
//生成随机颜色
vector<Vec3b>colors;
for(inti=0;i<contours.size();i )
{
intr=theRNG().uniform(0,255);
intg=theRNG().uniform(0,255);
intb=theRNG().uniform(0,255);
colors.push_back(Vec3b((uchar)b,(uchar)g,(uchar)r));
}
//颜色填充和最终显示
MatdstImg=Mat::zeros(markersImg.size(),CV_8UC3);
intindex=0;
for(inti=0;i<markersImg.rows;i )
{
for(intj=0;j<markersImg.cols;j )
{
index=markersImg.at<int>(i,j);
if(index>0&&index<=contours.size())
{
dstImg.at<Vec3b>(i,j)=colors[index-1];
}
else
{
dstImg.at<Vec3b>(i,j)=Vec3b(0,0,0);
}
}
}
cout<<"numberofobjects:"<<contours.size()<<endl;
namedWindow("FinalResult",CV_WINDOW_AUTOSIZE);
imshow("FinalResult",dstImg);
}
intmain(){
test();
waitKey(0);
return0;
}
总结:有时候会导致碎片化,过度分割,因为二值化中如果有很多小的黑点或碎片,在分割的时候导致很多 mask ,即小山头太多了,这个时候我们要考虑怎么去合并它,可以通过联通区域的直方图,或者像素值均值相似程度等。
例子2:图像分割
#include<opencv2/opencv.hpp>
#include<iostream>
usingnamespacestd;
usingnamespacecv;
//执行分水岭算法函数
MatwatershedCluster(Mat&srcImg,int&numSegments);
//结果显示函数
voidDisplaySegments(Mat&markersImg,intnumSegments);
voidtest(){
MatsrcImg;
srcImg=imread("toux.jpg");
if(srcImg.empty())
{
cout<<"couldnotloadimage...\n"<<endl;
}
namedWindow("Originalimage",CV_WINDOW_AUTOSIZE);
imshow("Originalimage",srcImg);
intnumSegments;
Matmarkers=watershedCluster(srcImg,numSegments);
DisplaySegments(markers,numSegments);
}
MatwatershedCluster(Mat&srcImg,int&numSegments){
//二值化
MatgrayImg,binaryImg;
cvtColor(srcImg,grayImg,COLOR_BGR2GRAY);
threshold(grayImg,binaryImg,0,255,THRESH_BINARY|THRESH_OTSU);
//形态学和距离变换
Matkernel=getStructuringElement(MORPH_RECT,Size(3,3),Point(-1,-1));
morphologyEx(binaryImg,binaryImg,MORPH_OPEN,kernel,Point(-1,-1));
MatdistImg;
distanceTransform(binaryImg,distImg,DistanceTypes::DIST_L2,3,CV_32F);
normalize(distImg,distImg,0.0,1.0,NORM_MINMAX);
//开始生成标记
threshold(distImg,distImg,0.1,1.0,THRESH_BINARY);
normalize(distImg,distImg,0,255,NORM_MINMAX);
distImg.convertTo(distImg,CV_8UC1);//CV_32F转成CV_8UC1
//标记开始
vector<vector<Point>>contours;
vector<Vec4i>hireachy;
findContours(distImg,contours,hireachy,RETR_CCOMP,CHAIN_APPROX_SIMPLE);
if(contours.empty())
{
returnMat();
}
MatmarkersImg(distImg.size(),CV_32S);
markersImg=Scalar::all(0);
for(inti=0;i<contours.size();i )
{
drawContours(markersImg,contours,i,Scalar(i 1),-1,8,hireachy,INT_MAX);
}
circle(markersImg,Point(5,5),3,Scalar(255),-1);
//分水岭变换
watershed(srcImg,markersImg);
numSegments=contours.size();
returnmarkersImg;
}
voidDisplaySegments(Mat&markersImg,intnumSegments){
//生成随机颜色
vector<Vec3b>colors;
for(inti=0;i<numSegments;i )
{
intr=theRNG().uniform(0,255);
intg=theRNG().uniform(0,255);
intb=theRNG().uniform(0,255);
colors.push_back(Vec3b((uchar)b,(uchar)g,(uchar)r));
}
//颜色填充和最终显示
MatdstImg=Mat::zeros(markersImg.size(),CV_8UC3);
intindex=0;
for(inti=0;i<markersImg.rows;i )
{
for(intj=0;j<markersImg.cols;j )
{
index=markersImg.at<int>(i,j);
if(index>0&&index<=numSegments)
{
dstImg.at<Vec3b>(i,j)=colors[index-1];
}
else
{
dstImg.at<Vec3b>(i,j)=Vec3b(255,255,255);
}
}
}
cout<<"numberofobjects:"<<numSegments<<endl;
namedWindow("FinalResult",CV_WINDOW_AUTOSIZE);
imshow("FinalResult",dstImg);
}
intmain(){
test();
waitKey(0);
return0;
}
效果图:
2. GrabCut 算法分割图像
GrabCut 算法的原理前面有介绍过,这里就不在介绍了,具体可以看下文章末尾往期推荐中阅读。下面例子实现图像中对象的抠图。
基本步骤:
例子代码:
#include<opencv2/opencv.hpp>
#include<iostream>
usingnamespacestd;
usingnamespacecv;
intnumRun=0;//算法迭代次数
boolinit=false;
Rectrect;
MatsrcImg,MaskImg,bgModel,fgModel;
//鼠标回调函数
voidonMouse(intevent,intx,inty,intflags,void*param);
voidshowImg();//显示画的图片
voidsetRoiMask();//选择ROI的函数
voidrunGrabCut();//执行算法函数
staticvoidShowHelpText();//提示用户操作函数
voidtest(){
srcImg=imread("toux.jpg");
if(srcImg.empty())
{
cout<<"couldnotloadimage...\n"<<endl;
}
namedWindow("Originalimage",CV_WINDOW_AUTOSIZE);
imshow("Originalimage",srcImg);
//初始化mask,单通道8位
MaskImg.create(srcImg.size(),CV_8UC1);
//在不知道它是前景还是背景的情况下,把它全部设为背景。
MaskImg.setTo(Scalar::all(GC_BGD));//结果不是0就是1GC_BGD为0
setMouseCallback("Originalimage",onMouse,0);
while(true)
{
charc=(char)waitKey(0);
if(c=='n')//按下n建开始执行算法
{
runGrabCut();
numRun ;
showImg();
cout<<"currentiteativetimes:"<<numRun<<endl;
}
if(c==27)
{
break;
}
}
}
voidonMouse(intevent,intx,inty,intflags,void*param){
switch(event)
{
caseEVENT_LBUTTONDOWN:
rect.x=x;
rect.y=y;
rect.width=1;
rect.height=1;
break;
caseEVENT_MOUSEMOVE:
if(flags&EVENT_FLAG_LBUTTON)
{
rect=Rect(Point(rect.x,rect.y),Point(x,y));
showImg();
}
break;
caseEVENT_LBUTTONUP:
if(rect.width>1&&rect.height>1)
{
showImg();
}
break;
default:
break;
}
}
voidshowImg(){
Matresult,binMask;
binMask.create(MaskImg.size(),CV_8UC1);
binMask=MaskImg&1;
if(init)
{
srcImg.copyTo(result,binMask);
}
else
{
srcImg.copyTo(result);
}
rectangle(result,rect,Scalar(0,0,255),2,8);
namedWindow("Originalimage",CV_WINDOW_AUTOSIZE);
imshow("Originalimage",result);
}
voidsetRoiMask(){
//GC_BGD=0明确属于背景的像素
//GC_FGD=1明确属于前景的像素
//GC_PR_BGD=2可能属于背景的像素
//GC_PR_FGD=3可能属于前景的像素
MaskImg.setTo(GC_BGD);
//为了避免选择越界
rect.x=max(0,rect.x);
rect.y=max(0,rect.y);
rect.width=min(rect.width,srcImg.cols-rect.x);
rect.height=min(rect.height,srcImg.rows-rect.y);
//把我们选取的那一块设为前景
MaskImg(rect).setTo(Scalar(GC_PR_FGD));
}
voidrunGrabCut(){
if(rect.width<2||rect.height<2)
{
return;
}
if(init)
{
grabCut(srcImg,MaskImg,rect,bgModel,fgModel,1);
}
else
{
grabCut(srcImg,MaskImg,rect,bgModel,fgModel,1,GC_INIT_WITH_RECT);
init=true;
}
}
staticvoidShowHelpText(){
cout<<"请先用鼠标在图片窗口中标记出属于前景的区域"<<endl;
cout<<"然后再按按键【n】启动算法"<<endl;
cout<<"按键【ESC】-退出程序"<<endl;
}
intmain(){
ShowHelpText();
test();
waitKey(0);
return0;
}
效果图: