1. 程式人生 > >【圖割】opencv中構建圖和最大流/最小割的gcgraph.h原始碼解讀

【圖割】opencv中構建圖和最大流/最小割的gcgraph.h原始碼解讀

本文對opencv中構建圖和最大流/最小割的原始碼進行解讀,添加了中文註釋

opencv中gcgraph.h原始碼(也許有些許改動),需要用的同學,可以新增.h標頭檔案,直接複製粘下面的程式碼

#include <vector>

using namespace std;

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

typedef unsigned char uchar;

template <class TWeight>
class GCGraph
{
public:
	GCGraph();
	GCGraph(unsigned int vtxCount, unsigned int edgeCount);
	~GCGraph();
	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>
GCGraph<TWeight>::GCGraph()
{
	flow = 0;
}
template <class TWeight>
GCGraph<TWeight>::GCGraph(unsigned int vtxCount, unsigned int edgeCount)
{
	create(vtxCount, edgeCount);
}

template <class TWeight>
GCGraph<TWeight>::~GCGraph()
{
}
template <class TWeight>
void GCGraph<TWeight>::create(unsigned int vtxCount, unsigned int edgeCount) //建構函式的實際內容,根據節點數和邊數
{
	vtcs.reserve(vtxCount);
	edges.reserve(edgeCount + 2);
	flow = 0;
}

/*
函式功能:新增一個空結點,所有成員初始化為空
引數說明:無
返回值:當前結點在集合中的編號
*/
template <class TWeight>
int GCGraph<TWeight>::addVtx()
{
	Vtx v;
	memset(&v, 0, sizeof(Vtx)); //將結點申請到的記憶體空間全部清0(第二個引數0)  目的:由於結點中存在成員變數為指標,指標設定為null保證安全
	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 GCGraph<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弧(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 GCGraph<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 GCGraph<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]; //結點指標
	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 != 0; 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(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 GCGraph<TWeight>::inSourceSegment(int i)  
{
	assert(i >= 0 && i < (int)vtcs.size());
	return vtcs[i].t == 0;
};

圖類宣告和定義都在標頭檔案gcgraph.h中,是因為使用了模板類,如果把成員函式放在.cpp檔案中定義,編譯時會出現

下一篇部落格是關於GrabCuts演算法的詳解