经典图割算法中图的构建及实现:Graph-Cut

讲解目前典型的3种图割算法:graph-cut、grab-but、one-cut。本文主要讲解graph-cut的方法在应用时,准则函数与图构建关系,如何构建图,以及如何代码实现图的构建。图割的原理网上文章和论文已介绍比较详细,不再详细介绍。

一.graph-cut:准则函数

该方法可谓是图割方法的开山鼻祖。该方法的准则函数如下:

E ( A ) = λ R ( A ) + B ( A ) E(A) = \lambda R(A) + B(A) E(A)=λR(A)+B(A)

R(A)是先验惩罚项,B(A)是区域相似度惩罚项, λ \lambda λ是平衡因子。

该准则函数意义:同类间,颜色差别小;异类间,颜色差别大。原则上该准则可解决图像任意类分割,并且一定是有全局最优解得,但在无种子点的超过2分类的问题时,该优化是个NP难问题,需要进行指数级的比较才能获得最优解,无工程价值。

二.Graph-cut:图的建立

1.术语:

在这里插入图片描述

  1. 与S和T链接的边叫t-link(红线与绿线),领域之间的链接边叫n-link(黑线)。其中红线进一步称为s-t-link,绿线进一步称为t-t-link。
  2. 黑线的权值对应的是B(A)项,红线与绿线的权值对应的是R(A)项。
  3. 权值用w表示。
  4. 蓝色节点表示类别标志节点,S表示正类类标节点,T表示负类类标节点,黄色节点是图像中的每一个像素点。

最终通过求最小割之后,与节点S相连的所有黄色节点(图像像素点)属于一类,同理与节点T相连的所有黄色节点属于另一类。两类被最小割割开,割值即是准则函数的值。

2.图的建立

   拿到待分割的图像后,图的节点与边已确定,即图的形状已确定下来。仅仅需要做的就是给图中所有边赋值相应的权值。

在这里插入图片描述

图中的边有3种情况:种子点的t-link;非种子点的t-link;像素领域关系的n-link。接下来将说明每一种边的权值取值。

1).种子点t-link权值:种子点认为是硬约束,其用户预设类别后,类别不会随分割算法而改变。

a.对于正类别种子点,s-t-link必须保留,t-t-link必须割去。工程中,通过将s-t-link权值设置为超级大值,t-t-link设置为0。保证一定仅仅割去t-t-link,否则一定不是最小割,因为当前w(s-t-link)权值是超级大值,割去这条边的代价一定是最大的。
b.反之同理。

2).非种子点的t-link权值:通过正负类种子点,我们能建立2类的颜色直方图。将直方图归一化成概率密度函数,定义为H_F,H_B。其中s-t-link权值为-ln(H_F(x)),t-t-link权值为-ln(H_B(x)),x为该像素点颜色值。

3).n-link权值:n-link用于度量相邻像素点之间颜色的差异性。设一对相邻点Pi,Pj,则n-link(Pi-Pj)的权值w等于:

w = exp ⁡ ( − ( x i − x j ) 2 2 σ 2 ) ⋅ 1 d i s t ( P i , P j ) w = \exp(-\frac{(x_i - x_j)^2}{2\sigma^2}) \cdot \frac{1}{dist(P_i,P_j)} w=exp(2σ2(xixj)2)dist(Pi,Pj)1

其中,dist()是距离函数,表示点之间的图像距离。即4领域下,所以领域点距离均为1,;8领域下,对角像素点距离为 2 \sqrt{2} 2 ;在5*5领域下,对角像素点距离为 2 2 2\sqrt{2} 22

设种子点的超级大值是1000, σ = 1 \sigma = 1 σ=1。图像是3*2的灰度图,数字表示灰度值,红色和蓝色节点表示用户选择的正负种子点。当然种子点过少时,计算的H_F,H_B可能不准,可将种子点附近的像素点也算入先验直方图中,往往可以取得更好效果

在这里插入图片描述
如上图所示,将所有边的权值赋值后,图就建立完毕。剩余则直接运用最小割算法即可求解。最小割算法有很多,包括graph-cut作者提出的快速算法An Experimental Comparison of Min-Cut/Max-Flow Algorithms for Energy Minimization in Vision。Opencv即采用该算法计算最小割。

代码适配了opencv4.6.0

3. 头文件

gcgraphMy.h

#include <vector>  
#include <assert.h>
using namespace std;

#define MIN(a,b) (((a)<(b))?(a):(b))  

typedef unsigned char uchar;

template <class TWeight>
class GCGraphMy
{
public:
	GCGraphMy();
	GCGraphMy(unsigned int vtxCount, unsigned int edgeCount);
	~GCGraphMy();
	void create(unsigned int vtxCount, unsigned int edgeCount); //给图的结点容器和边容器分配内存  
	int addVtx(); //添加空结点  
	void addEdges(int i, int j, TWeight w, TWeight revw); //添加点之间的边n-link  
	void addTermWeights(int i, TWeight sourceW, TWeight sinkW); //添加结点到顶点的边t-link  
	TWeight maxFlow(); //最大流函数  
	bool inSourceSegment(int i); //图对象调用最大流函数后,判断结点属不属于属于源点类(前景)  
private:
	class Vtx  //结点类  
	{
	public:
		Vtx *next; //在maxflow算法中用于构建先进-先出队列  
		int parent;
		int first; //首个相邻边  
		int ts; //时间戳  
		int dist; //到树根的距离  
		TWeight weight;
		uchar t; //图中结点的标签,取值0或1,0为源节点(前景点),1为汇节点(背景点)  
	};

	class Edge //边类  
	{
	public:
		int dst; //边指向的结点  
		int next; //该边的顶点的下一条边  
		TWeight weight; //边的权重  
	};

	std::vector<Vtx> vtcs; //存放所有的结点  
	std::vector<Edge> edges; //存放所有的边  
	TWeight flow; //图的流量  
};

template <class TWeight>
GCGraphMy<TWeight>::GCGraphMy()
{
	flow = 0;
}
template <class TWeight>
GCGraphMy<TWeight>::GCGraphMy(unsigned int vtxCount, unsigned int edgeCount)
{
	create(vtxCount, edgeCount);
}

template <class TWeight>
GCGraphMy<TWeight>::~GCGraphMy()
{
}
template <class TWeight>
void GCGraphMy<TWeight>::create(unsigned int vtxCount, unsigned int edgeCount) //构造函数的实际内容,根据节点数和边数  
{
	vtcs.reserve(vtxCount);
	edges.reserve(edgeCount + 2);
	flow = 0;
}

/*
函数功能:添加一个空结点,所有成员初始化为空
参数说明:无
返回值:当前结点在集合中的编号
*/
template <class TWeight>
int GCGraphMy<TWeight>::addVtx()
{
	Vtx v;
	memset(&v, 0, sizeof(Vtx)); //将结点申请到的内存空间全部清0(第二个参数0)  目的:由于结点中存在成员变量为指针,指针设置为null保证安全  
	v.first = -1;
	vtcs.push_back(v);
	return (int)vtcs.size() - 1; //返回值:当前结点在集合中的编号  
}

/*
函数功能:添加一条结点i和结点j之间的边n-link(普通结点之间的边)
参数说明:
int---i: 弧头结点编号
int---j: 弧尾结点编号
Tweight---w: 正向弧权值
Tweight---reww: 逆向弧权值
返回值:无
*/
template <class TWeight>
void GCGraphMy<TWeight>::addEdges(int i, int j, TWeight w, TWeight revw)
{
	assert(i >= 0 && i < (int)vtcs.size());
	assert(j >= 0 && j < (int)vtcs.size());
	assert(w >= 0 && revw >= 0);
	assert(i != j);

	Edge fromI, toI; // 正向弧:fromI, 反向弧 toI  

	fromI.dst = j; // 正向弧指向结点j  
	fromI.next = vtcs[i].first; //每个结点所发出的全部n-link弧0(4个方向)都会被连接为一个链表,采用头插法插入所有的弧  
	fromI.weight = w; // 正向弧的权值w   
	vtcs[i].first = (int)edges.size(); //修改结点i的第一个弧为当前正向弧  
	edges.push_back(fromI); //正向弧加入弧集合  

	toI.dst = i;
	toI.next = vtcs[j].first;
	toI.weight = revw;
	vtcs[j].first = (int)edges.size();
	edges.push_back(toI);
}

/*
函数功能:为结点i的添加一条t-link弧(到终端结点的弧),添加节点的时候,同时调用此函数
参数说明:
int---i: 结点编号
Tweight---sourceW: 正向弧权值
Tweight---sinkW: 逆向弧权值
返回值:无
*/
template <class TWeight>
void GCGraphMy<TWeight>::addTermWeights(int i, TWeight sourceW, TWeight sinkW)
{
	assert(i >= 0 && i < (int)vtcs.size());

	TWeight dw = vtcs[i].weight;
	if (dw > 0)
		sourceW += dw;
	else
		sinkW -= dw;
	flow += (sourceW < sinkW) ? sourceW : sinkW;
	vtcs[i].weight = sourceW - sinkW;
}


/*
函数功能:最大流函数,将图的所有结点分割为源点类(前景)还是汇点类(背景)
参数:无
返回值:图的成员变量--flow
*/
template <class TWeight>
TWeight GCGraphMy<TWeight>::maxFlow()
{
	const int TERMINAL = -1, ORPHAN = -2;
	Vtx stub, *nilNode = &stub, *first = nilNode, *last = nilNode;//先进先出队列,保存当前活动结点,stub为哨兵结点  
	int curr_ts = 0; //当前时间戳  
	stub.next = nilNode; //初始化活动结点队列,首结点指向自己  
	Vtx *vtxPtr = &vtcs[0]; //结点l指针  
	Edge *edgePtr = &edges[0]; //弧指针  

	vector<Vtx*> orphans; //孤立点集合  

	// 遍历所有的结点,初始化活动结点(active node)队列    
	for (int i = 0; i < (int)vtcs.size(); i++)
	{
		Vtx* v = vtxPtr + i;
		v->ts = 0;
		if (v->weight != 0) //当前结点t-vaule(即流量)不为0  
		{
			last = last->next = v; //入队,插入到队尾  
			v->dist = 1; //路径长度记1  
			v->parent = TERMINAL; //标注其双亲为终端结点  
			v->t = v->weight < 0;
		}
		else
			v->parent = 0; //孤结点  
	}
	first = first->next; //首结点作为哨兵使用,本身无实际意义,移动到下一节点,即第一个有效结点  
	last->next = nilNode; //哨兵放置到队尾了。。。检测到哨兵说明一层查找结束  
	nilNode->next = 0;


	//很长的循环,每次都按照以下三个步骤运行:    
	//搜索路径->拆分为森林->树的重构  
	for (;;)
	{

		Vtx* v, *u; // v表示当前元素,u为其相邻元素  
		int e0 = -1, ei = 0, ej = 0;
		TWeight minWeight, weight; // 路径最小割(流量), weight当前流量  
		uchar vt; // 流向标识符,正向为0,反向为1  

		//----------------------------              第一阶段: S 和 T 树的生长,找到一条s->t的路径             -------------------------//    
		while (first != nilNode)
		{
			v = first; // 取第一个元素存入v,作为当前结点  
			if (v->parent) // v非孤儿点  
			{
				vt = v->t; // 纪录v的流向  

				// 广度优先搜索,以此搜索当前结点所有相邻结点, 方法为:遍历所有相邻边,调出边的终点就是相邻结点    
				for (ei = v->first; ei != -1; ei = edgePtr[ei].next)
				{
					// 每对结点都拥有两个反向的边,ei^vt表明检测的边是与v结点同向的  
					if (edgePtr[ei^vt].weight == 0)
						continue;
					u = vtxPtr + edgePtr[ei].dst; // 取出邻接点u  
					if (!u->parent) // 无父节点,即为孤儿点,v接受u作为其子节点  
					{
						u->t = vt; // 设置结点u与v的流向相同  
						u->parent = ei ^ 1; // ei的末尾取反。。。  
						u->ts = v->ts; // 更新时间戳,由于u的路径长度通过v计算得到,因此有效性相同    
						u->dist = v->dist + 1; // u深度等于v加1  
						if (!u->next) // u不在队列中,入队,插入位置为队尾  
						{
							u->next = nilNode; // 修改下一元素指针指向哨兵  
							last = last->next = u; // 插入队尾  
						}
						continue;
					}

					if (u->t != vt) // u和v的流向不同,u可以到达另一终点,则找到一条路径  
					{
						e0 = ei ^ vt;
						break;
					}

					// u已经存在父节点,但是如果u的路径长度大于v+1,且u的时间戳较早,说明u走弯路了,修改u的路径,使其成为v的子结点      
					if (u->dist > v->dist + 1 && u->ts <= v->ts)
					{
						// reassign the parent  
						u->parent = ei ^ 1; // 从新设置u的父节点为v(编号ei),记录为当前的弧  
						u->ts = v->ts; // 更新u的时间戳与v相同  
						u->dist = v->dist + 1; // u为v的子结点,路径长度加1  
					}
				}
				if (e0 >= 0)
					break;
			}
			// exclude the vertex from the active list  
			first = first->next;
			v->next = 0;
		}

		if (e0 < 0)
			break;

		//-----------------------------------                第二阶段: 流量统计与树的拆分           ---------------------------------------//    
		//第一节: 查找路径中的最小权值   
		minWeight = edgePtr[e0].weight;
		assert(minWeight > 0);
		// 遍历整条路径分两个方向进行,从当前结点开始,向前回溯s树,向后回溯t树    
		// 2次遍历, k=1: 回溯s树, k=0: 回溯t树  
		for (int k = 1; k >= 0; k--)
		{
			//回溯的方法为:取当前结点的父节点,判断是否为终端结点    
			for (v = vtxPtr + edgePtr[e0^k].dst;; v = vtxPtr + edgePtr[ei].dst)
			{
				if ((ei = v->parent) < 0)
					break;
				weight = edgePtr[ei^k].weight;
				minWeight = MIN(minWeight, weight);
				assert(minWeight > 0);
			}
			weight = fabs((float)v->weight);
			minWeight = MIN(minWeight, weight);
			assert(minWeight > 0);
		}

		/*第二节:修改当前路径中的所有的weight权值
		任何时候s和t树的结点都只有一条边使其连接到树中,当这条弧权值减少为0则此结点从树中断开,
		若其无子结点,则成为孤立点,若其拥有子结点,则独立为森林,但是ei的子结点还不知道他们被孤立了!
		*/
		edgePtr[e0].weight -= minWeight; //正向路径权值减少  
		edgePtr[e0 ^ 1].weight += minWeight; //反向路径权值增加  
		flow += minWeight; //修改当前流量  

		// k = 1: source tree, k = 0: destination tree  
		for (int k = 1; k >= 0; k--)
		{
			for (v = vtxPtr + edgePtr[e0^k].dst;; v = vtxPtr + edgePtr[ei].dst)
			{
				if ((ei = v->parent) < 0)
					break;
				edgePtr[ei ^ (k ^ 1)].weight += minWeight;
				if ((edgePtr[ei^k].weight -= minWeight) == 0)
				{
					orphans.push_back(v);
					v->parent = ORPHAN;
				}
			}

			v->weight = v->weight + minWeight*(1 - k * 2);
			if (v->weight == 0)
			{
				orphans.push_back(v);
				v->parent = ORPHAN;
			}
		}

		//----------------------------                第三阶段: 树的重构 寻找新的父节点,恢复搜索树               -----------------------------//  
		curr_ts++;
		while (!orphans.empty())
		{
			Vtx* v = orphans.back(); //取一个孤儿  
			orphans.pop_back(); //删除栈顶元素,两步操作等价于出栈  

			int d, minDist = INT_MAX;
			e0 = 0;
			vt = v->t;

			//  遍历当前结点的相邻点,ei为当前弧的编号  
			for (ei = v->first; ei >= 0; ei = edgePtr[ei].next)
			{
				if (edgePtr[ei ^ (vt ^ 1)].weight == 0)
					continue;
				u = vtxPtr + edgePtr[ei].dst;
				if (u->t != vt || u->parent == 0)
					continue;

				// 计算当前点路径长度  
				for (d = 0;;)
				{
					if (u->ts == curr_ts)
					{
						d += u->dist;
						break;
					}
					ej = u->parent;
					d++;
					if (ej < 0)
					{
						if (ej == ORPHAN)
							d = INT_MAX - 1;
						else
						{
							u->ts = curr_ts;
							u->dist = 1;
						}
						break;
					}
					u = vtxPtr + edgePtr[ej].dst;
				}

				// update the distance  
				if (++d < INT_MAX)
				{
					if (d < minDist)
					{
						minDist = d;
						e0 = ei;
					}
					for (u = vtxPtr + edgePtr[ei].dst; u->ts != curr_ts; u = vtxPtr + edgePtr[u->parent].dst)
					{
						u->ts = curr_ts;
						u->dist = --d;
					}
				}
			}

			if ((v->parent = e0) > 0)
			{
				v->ts = curr_ts;
				v->dist = minDist;
				continue;
			}

			/* no parent is found */
			v->ts = 0;
			for (ei = v->first; ei >= 0; ei = edgePtr[ei].next)
			{
				u = vtxPtr + edgePtr[ei].dst;
				ej = u->parent;
				if (u->t != vt || !ej)
					continue;
				if (edgePtr[ei ^ (vt ^ 1)].weight && !u->next)
				{
					u->next = nilNode;
					last = last->next = u;
				}
				if (ej > 0 && vtxPtr + edgePtr[ej].dst == v)
				{
					orphans.push_back(u);
					u->parent = ORPHAN;
				}
			}
		}
		//第三阶段结束  

	}
	return flow; //返回最大流量  
}

/*
函数功能:判断结点是不是源点类(前景)
参数:结点在容器中位置
返回值:1或0,1:结点为前景,0:结点为背景
*/
template <class TWeight>
bool GCGraphMy<TWeight>::inSourceSegment(int i)
{
	assert(i >= 0 && i < (int)vtcs.size());
	return vtcs[i].t == 0;
};

4. 源文件

main.cpp

#include <iostream>
#include "gcgraphMy.h"
#include "opencv2/opencv.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include<opencv2/imgproc/imgproc_c.h>

#include <iostream>

using namespace std;
using namespace cv;

/*
This section shows how to use the library to compute
a minimum cut on the following graph :

*/
///

#include <stdio.h>

const int nDownSample = 1;
const Scalar RED = Scalar(0, 0, 255);
const Scalar PINK = Scalar(230, 130, 255);
const Scalar BLUE = Scalar(255, 0, 0);
const Scalar LIGHTBLUE = Scalar(255, 255, 160);
const Scalar GREEN = Scalar(0, 255, 0);
#define MASK_BG_COLOR   128
#define MASK_FG_COLOR   255
const Scalar FG_MASK_COLOR = Scalar(255, 255, 255);
const Scalar BG_MASK_COLOR = Scalar(128, 128, 128);

const int BGD_KEY = EVENT_FLAG_CTRLKEY;
const int FGD_KEY = EVENT_FLAG_SHIFTKEY;

static void getBinMask(const Mat& comMask, Mat& binMask)
{
    if (comMask.empty() || comMask.type() != CV_8UC1)
        CV_Error(Error::StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)");
    if (binMask.empty() || binMask.rows != comMask.rows || binMask.cols != comMask.cols)
        binMask.create(comMask.size(), CV_8UC1);
    binMask = comMask & 1;
}

static void showImageS2(const Mat& image, const string& winName)
{
    resizeWindow(winName.c_str(), image.cols / nDownSample, image.rows / nDownSample);
    imshow(winName, image);
}

class GCApplication
{
public:
    enum{ NOT_SET = 0, IN_PROCESS = 1, SET = 2 };
    static const int radius = 2;
    static const int thickness = -1;

    void reset();
    void setImageAndWinName(const Mat& _image, const string& _winName);
    void showImage(int x, int y, int FgPoint);
    void mouseClick(int event, int x, int y, int flags, void* param);
    void graphConstruct(const Mat& img, GCGraphMy<double>& graph);
    void estimateSegmentation(GCGraphMy<double>& graph);
    int nextIter();
    int getIterCount() const { return iterCount; }
    void calSeedPHist(const Mat& img, const Mat& mask);
private:
    void setRectInMask();
    void fillSeedToMask(Mat& mask);
    void setLblsInMask(int x, int y, bool isFg);
    double calFgdPrioriCost(Vec3b &color);
    double calBgdPrioriCost(Vec3b &color);
    const string* winName;
    const Mat* image;
    Mat mask;
    Mat imgShowPg;
    Mat bgdModel, fgdModel;
    double FgPHist[3][256];
    double BgPHist[3][256];
    double gamma;
    double lambda;
    double beta;
    Mat leftW, upleftW, upW, uprightW;
    GCGraphMy<double> graphMy;
    uchar rectState, lblsState, prLblsState;
    bool isInitialized;
    Rect rect;
    vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls;
    int iterCount;
};


void GCApplication::reset()
{
    if (!mask.empty())
    {
        mask.setTo(Scalar::all(GC_BGD));
        namedWindow("mask", 0);
    }
    bgdPxls.clear(); fgdPxls.clear();
    prBgdPxls.clear();  prFgdPxls.clear();
    this->image->copyTo(imgShowPg);
    isInitialized = false;
    rectState = NOT_SET;
    lblsState = NOT_SET;
    prLblsState = NOT_SET;
    iterCount = 0;
}

void GCApplication::setImageAndWinName(const Mat& _image, const string& _winName)
{
    if (_image.empty() || _winName.empty())
        return;
    image = &_image;
    winName = &_winName;
    mask.create(image->size(), CV_8UC1);
    reset();
}

void GCApplication::showImage(int x, int y, int FgPoint)
{
    static Point pre_pt = (-1, -1);//初始坐标  
    if (image->empty() || winName->empty())
        return;
    pre_pt = Point(x, y);
    if (FgPoint == 1)
    {
        circle(imgShowPg, pre_pt, 3, BLUE, FILLED, CV_AA, 0);//划圆
        circle(mask, pre_pt, 3, FG_MASK_COLOR, FILLED, CV_AA, 0);//划圆
    }
    else if (FgPoint == 2)
    {
        circle(imgShowPg, pre_pt, 3, GREEN, FILLED, CV_AA, 0);//划圆
        circle(mask, pre_pt, 3, BG_MASK_COLOR, FILLED, CV_AA, 0);//划圆
    }

    showImageS2(imgShowPg, *(this->winName));
    showImageS2(mask, "mask");
}


void GCApplication::setRectInMask()
{
    assert(!mask.empty());
    mask.setTo(GC_BGD);
    rect.x = max(0, rect.x);
    rect.y = max(0, rect.y);
    rect.width = min(rect.width, image->cols - rect.x);
    rect.height = min(rect.height, image->rows - rect.y);
    (mask(rect)).setTo(Scalar(GC_PR_FGD));
}

void GCApplication::setLblsInMask(int x, int y, bool isFg)
{
    vector<Point> *bgpxls, *fgpxls;
    uchar bvalue, fvalue;
    bgpxls = &bgdPxls;
    fgpxls = &fgdPxls;
    Point p(x, y);
    //x,y就是原始图像中的,不需要上采样回去
    //p.x = p.x * nDownSample;//上采样回去
    //p.y = p.y * nDownSample;//上采样回去
    if (isFg)
    {
        fgpxls->push_back(p);
    }
    else
    {
        bgpxls->push_back(p);
    }

}

void GCApplication::mouseClick(int event, int x, int y, int flags, void*)
{
    // TODO add bad args check
    switch (event)
    {

    case EVENT_LBUTTONDOWN: // set rect or GC_BGD(GC_FGD) labels
    {
        setLblsInMask(x, y, 1);
        showImage(x, y, 1);
        lblsState = SET;
    }
        break;
    case EVENT_RBUTTONDOWN: // set GC_PR_BGD(GC_PR_FGD) labels
    {
        setLblsInMask(x, y, 0);
        showImage(x, y, 2);
        prLblsState = SET;
    }
        break;
    case EVENT_LBUTTONUP:
        lblsState = NOT_SET;
        break;
    case EVENT_RBUTTONUP:
        prLblsState = NOT_SET;
        break;
    case EVENT_MOUSEMOVE:
        if (lblsState != NOT_SET && flags & EVENT_FLAG_LBUTTON)
        {
            setLblsInMask(x, y, 1);
            showImage(x, y, 1);
        }
        else if (prLblsState != NOT_SET && flags & EVENT_FLAG_RBUTTON)
        {
            setLblsInMask(x, y, 0);
            showImage(x, y, 2);
        }
        break;
    default:
        lblsState = NOT_SET;
        prLblsState = NOT_SET;
        break;
    }
}
/*
Calculate beta - parameter of GrabCut algorithm.
beta = 1 / (2 * avg(sqr(|| color[i] - color[j] || )))
*/
static double calcBeta(const Mat& img)
{
    double beta = 0;
    for (int y = 0; y < img.rows; y++)
    {
        for (int x = 0; x < img.cols; x++)
        {
            Vec3d color = img.at<Vec3b>(y, x);
            if (x>0) // left
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y, x - 1);
                beta += diff.dot(diff);
            }
            if (y>0 && x>0) // upleft
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x - 1);
                beta += diff.dot(diff);
            }
            if (y>0) // up
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x);
                beta += diff.dot(diff);
            }
            if (y>0 && x<img.cols - 1) // upright
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x + 1);
                beta += diff.dot(diff);
            }
        }
    }
    if (beta <= std::numeric_limits<double>::epsilon())
        beta = 0;
    else
        beta = 1.f / (2 * beta / (4 * img.cols*img.rows - 3 * img.cols - 3 * img.rows + 2));

    return beta;
}

/*
Calculate weights of noterminal vertices of graph.
beta and gamma - parameters of GrabCut algorithm.
*/
static void calcNWeights(const Mat& img, Mat& leftW, Mat& upleftW, Mat& upW, Mat& uprightW, double beta, double gamma)
{
    const double gammaDivSqrt2 = gamma / std::sqrt(2.0f);
    leftW.create(img.rows, img.cols, CV_64FC1);
    upleftW.create(img.rows, img.cols, CV_64FC1);
    upW.create(img.rows, img.cols, CV_64FC1);
    uprightW.create(img.rows, img.cols, CV_64FC1);
    for (int y = 0; y < img.rows; y++)
    {
        for (int x = 0; x < img.cols; x++)
        {
            Vec3d color = img.at<Vec3b>(y, x);
            if (x - 1 >= 0) // left
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y, x - 1);
                leftW.at<double>(y, x) = gamma * exp(-beta*diff.dot(diff));
            }
            else
                leftW.at<double>(y, x) = 0;
            if (x - 1 >= 0 && y - 1 >= 0) // upleft
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x - 1);
                upleftW.at<double>(y, x) = gammaDivSqrt2 * exp(-beta*diff.dot(diff));
            }
            else
                upleftW.at<double>(y, x) = 0;
            if (y - 1 >= 0) // up
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x);
                upW.at<double>(y, x) = gamma * exp(-beta*diff.dot(diff));
            }
            else
                upW.at<double>(y, x) = 0;
            if (x + 1<img.cols && y - 1 >= 0) // upright
            {
                Vec3d diff = color - (Vec3d)img.at<Vec3b>(y - 1, x + 1);
                uprightW.at<double>(y, x) = gammaDivSqrt2 * exp(-beta*diff.dot(diff));
            }
            else
                uprightW.at<double>(y, x) = 0;
        }
    }
}

void GCApplication::calSeedPHist(const Mat& img, const Mat& mask)
{
    int nFgNum = 0;//
    int nBgNum = 0;//
    memset(&FgPHist[0][0], 0, 256 * 3 * sizeof(double));
    memset(&BgPHist[0][0], 0, 256 * 3 * sizeof(double));
  
    Point p;
    for (p.y = 0; p.y < img.rows; p.y++)
    {
        for (p.x = 0; p.x < img.cols; p.x++)
        {
            uchar pMaskV = mask.at<uchar>(p);
            //背景像素值如直方图
            if (MASK_BG_COLOR == pMaskV)
            {
                Vec3b color = img.at<Vec3b>(p);
                nBgNum++;
                BgPHist[0][color[0]]++;
                BgPHist[1][color[1]]++;
                BgPHist[2][color[2]]++;
            }
            //前景像素值如直方图
            else if (MASK_FG_COLOR == pMaskV)
            {
                Vec3b color = img.at<Vec3b>(p);
                nFgNum++;
                FgPHist[0][color[0]]++;
                FgPHist[1][color[1]]++;
                FgPHist[2][color[2]]++;
            }
        }//
    }//

    nFgNum = nFgNum > 0 ? nFgNum : 1;//
    nBgNum = nBgNum > 0 ? nBgNum : 1;//

    //归一化并防止除0
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 256; j++)
        {
            FgPHist[i][j] = FgPHist[i][j] / nFgNum;
            FgPHist[i][j] = FgPHist[i][j] < 0.00001 ? 0.00001 : FgPHist[i][j];
            BgPHist[i][j] = BgPHist[i][j] / nBgNum;
            BgPHist[i][j] = BgPHist[i][j] < 0.00001 ? 0.00001 : BgPHist[i][j];
        }       
    }

}

double GCApplication::calFgdPrioriCost(Vec3b &color)
{
    double p = FgPHist[0][color[0]] * FgPHist[1][color[1]] * FgPHist[2][color[2]];
    return p;
}

double GCApplication::calBgdPrioriCost(Vec3b &color)
{
    double p = BgPHist[0][color[0]] * BgPHist[1][color[1]] * BgPHist[2][color[2]];
    return p;
}

void GCApplication::fillSeedToMask(Mat& mask)
{
    Point p;
    for (p.y = 0; p.y < mask.rows; p.y++)
    {
        for (p.x = 0; p.x < mask.cols; p.x++)
        {
            if (mask.at<uchar>(p) != MASK_BG_COLOR 
                && mask.at<uchar>(p) != MASK_FG_COLOR)
            {
                mask.at<uchar>(p) = 0;
            }
        }//
    }//
}


void GCApplication::graphConstruct(const Mat& img, GCGraphMy<double>& graph)
{
    gamma = 50;
    lambda = 1000;
    beta = calcBeta(*(this->image));

    Mat leftW, upleftW, upW, uprightW;
    calcNWeights(img, leftW, upleftW, upW, uprightW, beta, gamma);

    int vtxCount = img.cols*img.rows,
        edgeCount = 2 * (4 * img.cols*img.rows - 3 * (img.cols + img.rows) + 2);

    fillSeedToMask(this->mask);
    calSeedPHist(img, this->mask);

    graph.create(vtxCount, edgeCount);
    Point p;
    double a = 1.5;
    for (p.y = 0; p.y < img.rows; p.y++)
    {
        for (p.x = 0; p.x < img.cols; p.x++)
        {
            // add node
            int vtxIdx = graph.addVtx();
            Vec3b color = img.at<Vec3b>(p);

            // set t-weights
            double fromSource, toSink;
            if (mask.at<uchar>(p) == 0)
            {
                fromSource = -a*log(calBgdPrioriCost(color));
                toSink = -a*log(calFgdPrioriCost(color));
            }
            else if (mask.at<uchar>(p) == MASK_BG_COLOR)
            {
                fromSource = 0;
                toSink = lambda;
            }
            else if (mask.at<uchar>(p) == MASK_FG_COLOR) // GC_FGD
            {
                fromSource = lambda;
                toSink = 0;
            }
            graph.addTermWeights(vtxIdx, fromSource, toSink);

            // set n-weights,每个点只需要与左上4个点进行边连接即可,这样可以不重复的添加所有的N-8-edge
            if (p.x>0)
            {
                double w = leftW.at<double>(p);
                graph.addEdges(vtxIdx, vtxIdx - 1, w, w);
            }
            if (p.x>0 && p.y>0)
            {
                double w = upleftW.at<double>(p);
                graph.addEdges(vtxIdx, vtxIdx - img.cols - 1, w, w);
            }
            if (p.y>0)
            {
                double w = upW.at<double>(p);
                graph.addEdges(vtxIdx, vtxIdx - img.cols, w, w);
            }
            if (p.x<img.cols - 1 && p.y>0)
            {
                double w = uprightW.at<double>(p);
                graph.addEdges(vtxIdx, vtxIdx - img.cols + 1, w, w);
            }
        }
    }
}

/*
Estimate segmentation using MaxFlow algorithm
*/
void GCApplication::estimateSegmentation(GCGraphMy<double>& graph)
{
    graph.maxFlow();
    mask.setTo(GC_BGD);
    Point p;

    for (p.y = 0; p.y < mask.rows; p.y++)
    {
        for (p.x = 0; p.x < mask.cols; p.x++)
        {
            if (1 == graph.inSourceSegment(p.y*mask.cols + p.x /*vertex index*/))
            {
                mask.at<uchar>(p) = MASK_FG_COLOR;
            }
        }//
    }//
    showImageS2(mask, "mask");
    waitKey();
    destroyWindow("mask");
}


GCApplication gcapp;

static void on_mouse(int event, int x, int y, int flags, void* param)
{
    gcapp.mouseClick(event, x, y, flags, param);
}

int main()
{
    string filename = "E:/documents/vs2019/opencv-test/build/Debug/femur.png";//分割图像路径
    Mat image = imread(filename, 1);
    if (image.empty())
    {
        cout << "\n Durn, couldn't read image filename " << filename << endl;
        return 1;
    }
    const string winName = "image";
	//缩放图像,避免图像太大,界面无法显示,且分辨率大的图,对本方法没有帮助
    resize(image, image, Size(image.cols / 3, image.rows / 3), 0, 0, INTER_LINEAR);
    namedWindow(winName, 0);
    resizeWindow(winName.c_str(), image.cols / nDownSample, image.rows / nDownSample);
    gcapp.setImageAndWinName(image, winName);

    setMouseCallback(winName, on_mouse, 0);

    imshow(winName, image);
    waitKey();

    GCGraphMy <double>stGraphMy;

    gcapp.graphConstruct(image, stGraphMy);

    gcapp.estimateSegmentation(stGraphMy);
    destroyWindow(winName);

    //gcapp.setImageAndWinName(image, winName);
    //gcapp.showImage();

    system("pause");

    return 0;
}


///

5. 测试图像

上述代码核心部分不是调用opencv实现的。
代码有冗余,是没有完成的Grabcut部分。可运行部分是GraphCut部分。
在这里插入图片描述

参考:经典图割算法中图的构建及实现:Graph-Cut

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落花逐流水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值