【圖割】opencv中構建圖和最大流/最小割的gcgraph.h原始碼解讀
阿新 • • 發佈:2019-01-10
本文對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演算法的詳解