轉自 zouxy09node
GrabCut 原理參考這裏,如下爲 GrabCut 源碼:算法
——看別人寫的好的代碼也很享受,乾淨利落,有些處理的細節也學習一下。express
/*M/////////////////////////////////////////////////////////////////////////////////////// // // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. // // By downloading, copying, installing or using the software you agree to this license. // If you do not agree to this license, do not download, install, // copy or use the software. // // // Intel License Agreement // For Open Source Computer Vision Library // // Copyright (C) 2000, Intel Corporation, all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistribution's of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Redistribution's in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * The name of Intel Corporation may not be used to endorse or promote products // derived from this software without specific prior written permission. // // This software is provided by the copyright holders and contributors "as is" and // any express or implied warranties, including, but not limited to, the implied // warranties of merchantability and fitness for a particular purpose are disclaimed. // In no event shall the Intel Corporation or contributors be liable for any direct, // indirect, incidental, special, exemplary, or consequential damages // (including, but not limited to, procurement of substitute goods or services; // loss of use, data, or profits; or business interruption) however caused // and on any theory of liability, whether in contract, strict liability, // or tort (including negligence or otherwise) arising in any way out of // the use of this software, even if advised of the possibility of such damage. // //M*/ #include "precomp.hpp" #include "gcgraph.hpp" #include <limits> using namespace cv; /* This is implementation of image segmentation algorithm GrabCut described in "GrabCut — Interactive Foreground Extraction using Iterated Graph Cuts". Carsten Rother, Vladimir Kolmogorov, Andrew Blake. */ /* GMM - Gaussian Mixture Model */ class GMM { public: static const int componentsCount = 5; GMM( Mat& _model ); double operator()( const Vec3d color ) const; double operator()( int ci, const Vec3d color ) const; int whichComponent( const Vec3d color ) const; void initLearning(); void addSample( int ci, const Vec3d color ); void endLearning(); private: void calcInverseCovAndDeterm( int ci ); Mat model; double* coefs; double* mean; double* cov; double inverseCovs[componentsCount][3][3]; //協方差的逆矩陣 double covDeterms[componentsCount]; //協方差的行列式 double sums[componentsCount][3]; double prods[componentsCount][3][3]; int sampleCounts[componentsCount]; int totalSampleCount; }; //背景和前景各有一個對應的GMM(混合高斯模型) GMM::GMM( Mat& _model ) { //一個像素的(惟一對應)高斯模型的參數個數或者說一個高斯模型的參數個數 //一個像素RGB三個通道值,故3個均值,3*3個協方差,共用一個權值 const int modelSize = 3/*mean*/ + 9/*covariance*/ + 1/*component weight*/; if( _model.empty() ) { //一個GMM共有componentsCount個高斯模型,一個高斯模型有modelSize個模型參數 _model.create( 1, modelSize*componentsCount, CV_64FC1 ); _model.setTo(Scalar(0)); } else if( (_model.type() != CV_64FC1) || (_model.rows != 1) || (_model.cols != modelSize*componentsCount) ) CV_Error( CV_StsBadArg, "_model must have CV_64FC1 type, rows == 1 and cols == 13*componentsCount" ); model = _model; //注意這些模型參數的存儲方式:先排完componentsCount個coefs,再3*componentsCount個mean。 //再3*3*componentsCount個cov。 coefs = model.ptr<double>(0); //GMM的每一個像素的高斯模型的權值變量起始存儲指針 mean = coefs + componentsCount; //均值變量起始存儲指針 cov = mean + 3*componentsCount; //協方差變量起始存儲指針 for( int ci = 0; ci < componentsCount; ci++ ) if( coefs[ci] > 0 ) //計算GMM中第ci個高斯模型的協方差的逆Inverse和行列式Determinant //爲了後面計算每一個像素屬於該高斯模型的機率(也就是數據能量項) calcInverseCovAndDeterm( ci ); } //計算一個像素(由color=(B,G,R)三維double型向量來表示)屬於這個GMM混合高斯模型的機率。 //也就是把這個像素像素屬於componentsCount個高斯模型的機率與對應的權值相乘再相加, //具體見論文的公式(10)。結果從res返回。 //這個至關於計算Gibbs能量的第一個能量項(取負後)。 double GMM::operator()( const Vec3d color ) const { double res = 0; for( int ci = 0; ci < componentsCount; ci++ ) res += coefs[ci] * (*this)(ci, color ); return res; } //計算一個像素(由color=(B,G,R)三維double型向量來表示)屬於第ci個高斯模型的機率。 //具體過程,即高階的高斯密度模型計算式,具體見論文的公式(10)。結果從res返回 double GMM::operator()( int ci, const Vec3d color ) const { double res = 0; if( coefs[ci] > 0 ) { CV_Assert( covDeterms[ci] > std::numeric_limits<double>::epsilon() ); Vec3d diff = color; double* m = mean + 3*ci; diff[0] -= m[0]; diff[1] -= m[1]; diff[2] -= m[2]; double mult = diff[0]*(diff[0]*inverseCovs[ci][0][0] + diff[1]*inverseCovs[ci][1][0] + diff[2]*inverseCovs[ci][2][0]) + diff[1]*(diff[0]*inverseCovs[ci][0][1] + diff[1]*inverseCovs[ci][1][1] + diff[2]*inverseCovs[ci][2][1]) + diff[2]*(diff[0]*inverseCovs[ci][0][2] + diff[1]*inverseCovs[ci][1][2] + diff[2]*inverseCovs[ci][2][2]); res = 1.0f/sqrt(covDeterms[ci]) * exp(-0.5f*mult); } return res; } //返回這個像素最有可能屬於GMM中的哪一個高斯模型(機率最大的那個) int GMM::whichComponent( const Vec3d color ) const { int k = 0; double max = 0; for( int ci = 0; ci < componentsCount; ci++ ) { double p = (*this)( ci, color ); if( p > max ) { k = ci; //找到機率最大的那個,或者說計算結果最大的那個 max = p; } } return k; } //GMM參數學習前的初始化,主要是對要求和的變量置零 void GMM::initLearning() { for( int ci = 0; ci < componentsCount; ci++) { sums[ci][0] = sums[ci][1] = sums[ci][2] = 0; prods[ci][0][0] = prods[ci][0][1] = prods[ci][0][2] = 0; prods[ci][1][0] = prods[ci][1][1] = prods[ci][1][2] = 0; prods[ci][2][0] = prods[ci][2][1] = prods[ci][2][2] = 0; sampleCounts[ci] = 0; } totalSampleCount = 0; } //增長樣本,即爲前景或者背景GMM的第ci個高斯模型的像素集(這個像素集是來用估 //計計算這個高斯模型的參數的)增長樣本像素。計算加入color這個像素後,像素集 //中全部像素的RGB三個通道的和sums(用來計算均值),還有它的prods(用來計算協方差), //而且記錄這個像素集的像素個數和總的像素個數(用來計算這個高斯模型的權值)。 void GMM::addSample( int ci, const Vec3d color ) { sums[ci][0] += color[0]; sums[ci][1] += color[1]; sums[ci][2] += color[2]; prods[ci][0][0] += color[0]*color[0]; prods[ci][0][1] += color[0]*color[1]; prods[ci][0][2] += color[0]*color[2]; prods[ci][1][0] += color[1]*color[0]; prods[ci][1][1] += color[1]*color[1]; prods[ci][1][2] += color[1]*color[2]; prods[ci][2][0] += color[2]*color[0]; prods[ci][2][1] += color[2]*color[1]; prods[ci][2][2] += color[2]*color[2]; sampleCounts[ci]++; totalSampleCount++; } //從圖像數據中學習GMM的參數:每個高斯份量的權值、均值和協方差矩陣; //這裏至關於論文中「Iterative minimisation」的step 2 void GMM::endLearning() { const double variance = 0.01; for( int ci = 0; ci < componentsCount; ci++ ) { int n = sampleCounts[ci]; //第ci個高斯模型的樣本像素個數 if( n == 0 ) coefs[ci] = 0; else { //計算第ci個高斯模型的權值係數 coefs[ci] = (double)n/totalSampleCount; //計算第ci個高斯模型的均值 double* m = mean + 3*ci; m[0] = sums[ci][0]/n; m[1] = sums[ci][1]/n; m[2] = sums[ci][2]/n; //計算第ci個高斯模型的協方差 double* c = cov + 9*ci; c[0] = prods[ci][0][0]/n - m[0]*m[0]; c[1] = prods[ci][0][1]/n - m[0]*m[1]; c[2] = prods[ci][0][2]/n - m[0]*m[2]; c[3] = prods[ci][1][0]/n - m[1]*m[0]; c[4] = prods[ci][1][1]/n - m[1]*m[1]; c[5] = prods[ci][1][2]/n - m[1]*m[2]; c[6] = prods[ci][2][0]/n - m[2]*m[0]; c[7] = prods[ci][2][1]/n - m[2]*m[1]; c[8] = prods[ci][2][2]/n - m[2]*m[2]; //計算第ci個高斯模型的協方差的行列式 double dtrm = c[0]*(c[4]*c[8]-c[5]*c[7]) - c[1]*(c[3]*c[8]-c[5]*c[6]) + c[2]*(c[3]*c[7]-c[4]*c[6]); if( dtrm <= std::numeric_limits<double>::epsilon() ) { //至關於若是行列式小於等於0,(對角線元素)增長白噪聲,避免其變 //爲退化(降秩)協方差矩陣(不存在逆矩陣,但後面的計算須要計算逆矩陣)。 // Adds the white noise to avoid singular covariance matrix. c[0] += variance; c[4] += variance; c[8] += variance; } //計算第ci個高斯模型的協方差的逆Inverse和行列式Determinant calcInverseCovAndDeterm(ci); } } } //計算協方差的逆Inverse和行列式Determinant void GMM::calcInverseCovAndDeterm( int ci ) { if( coefs[ci] > 0 ) { //取第ci個高斯模型的協方差的起始指針 double *c = cov + 9*ci; double dtrm = covDeterms[ci] = c[0]*(c[4]*c[8]-c[5]*c[7]) - c[1]*(c[3]*c[8]-c[5]*c[6]) + c[2]*(c[3]*c[7]-c[4]*c[6]); //在C++中,每一種內置的數據類型都擁有不一樣的屬性, 使用<limits>庫能夠獲 //得這些基本數據類型的數值屬性。由於浮點算法的截斷,因此使得,當a=2, //b=3時 10*a/b == 20/b不成立。那怎麼辦呢? //這個小正數(epsilon)常量就來了,小正數一般爲可用給定數據類型的 //大於1的最小值與1之差來表示。若dtrm結果不大於小正數,那麼它幾乎爲零。 //因此下式保證dtrm>0,即行列式的計算正確(協方差對稱正定,故行列式大於0)。 CV_Assert( dtrm > std::numeric_limits<double>::epsilon() ); //三階方陣的求逆 inverseCovs[ci][0][0] = (c[4]*c[8] - c[5]*c[7]) / dtrm; inverseCovs[ci][1][0] = -(c[3]*c[8] - c[5]*c[6]) / dtrm; inverseCovs[ci][2][0] = (c[3]*c[7] - c[4]*c[6]) / dtrm; inverseCovs[ci][0][1] = -(c[1]*c[8] - c[2]*c[7]) / dtrm; inverseCovs[ci][1][1] = (c[0]*c[8] - c[2]*c[6]) / dtrm; inverseCovs[ci][2][1] = -(c[0]*c[7] - c[1]*c[6]) / dtrm; inverseCovs[ci][0][2] = (c[1]*c[5] - c[2]*c[4]) / dtrm; inverseCovs[ci][1][2] = -(c[0]*c[5] - c[2]*c[3]) / dtrm; inverseCovs[ci][2][2] = (c[0]*c[4] - c[1]*c[3]) / dtrm; } } //計算beta,也就是Gibbs能量項中的第二項(平滑項)中的指數項的beta,用來調整 //高或者低對比度時,兩個鄰域像素的差異的影響的,例如在低對比度時,兩個鄰域 //像素的差異可能就會比較小,這時候須要乘以一個較大的beta來放大這個差異, //在高對比度時,則須要縮小自己就比較大的差異。 //因此咱們須要分析整幅圖像的對比度來肯定參數beta,具體的見論文公式(5)。 /* 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 >0的判斷是爲了不在圖像邊界的時候還計算,致使越界 { 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) ); //論文公式(5) return beta; } //計算圖每一個非端點頂點(也就是每一個像素做爲圖的一個頂點,不包括源點s和匯點t)與鄰域頂點 //的邊的權值。因爲是無向圖,咱們計算的是八鄰域,那麼對於一個頂點,咱們計算四個方向就行, //在其餘的頂點計算的時候,會把剩餘那四個方向的權值計算出來。這樣整個圖算完後,每一個頂點 //與八鄰域的頂點的邊的權值就都計算出來了。 //這個至關於計算Gibbs能量的第二個能量項(平滑項),具體見論文中公式(4) /* 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 ) { //gammaDivSqrt2至關於公式(4)中的gamma * dis(i,j)^(-1),那麼能夠知道, //當i和j是垂直或者水平關係時,dis(i,j)=1,當是對角關係時,dis(i,j)=sqrt(2.0f)。 //具體計算時,看下面就明白了 const double gammaDivSqrt2 = gamma / std::sqrt(2.0f); //每一個方向的邊的權值經過一個和圖大小相等的Mat來保存 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; } } } //檢查mask的正確性。mask爲經過用戶交互或者程序設定的,它是和圖像大小同樣的單通道灰度圖, //每一個像素只能取GC_BGD or GC_FGD or GC_PR_BGD or GC_PR_FGD 四種枚舉值,分別表示該像素 //(用戶或者程序指定)屬於背景、前景、可能爲背景或者可能爲前景像素。具體的參考: //ICCV2001「Interactive Graph Cuts for Optimal Boundary & Region Segmentation of Objects in N-D Images」 //Yuri Y. Boykov Marie-Pierre Jolly /* Check size, type and element values of mask matrix. */ static void checkMask( const Mat& img, const Mat& mask ) { if( mask.empty() ) CV_Error( CV_StsBadArg, "mask is empty" ); if( mask.type() != CV_8UC1 ) CV_Error( CV_StsBadArg, "mask must have CV_8UC1 type" ); if( mask.cols != img.cols || mask.rows != img.rows ) CV_Error( CV_StsBadArg, "mask must have as many rows and cols as img" ); for( int y = 0; y < mask.rows; y++ ) { for( int x = 0; x < mask.cols; x++ ) { uchar val = mask.at<uchar>(y,x); if( val!=GC_BGD && val!=GC_FGD && val!=GC_PR_BGD && val!=GC_PR_FGD ) CV_Error( CV_StsBadArg, "mask element value must be equel" "GC_BGD or GC_FGD or GC_PR_BGD or GC_PR_FGD" ); } } } //經過用戶框選目標rect來建立mask,rect外的所有做爲背景,設置爲GC_BGD, //rect內的設置爲 GC_PR_FGD(可能爲前景) /* Initialize mask using rectangular. */ static void initMaskWithRect( Mat& mask, Size imgSize, Rect rect ) { mask.create( imgSize, CV_8UC1 ); mask.setTo( GC_BGD ); rect.x = max(0, rect.x); rect.y = max(0, rect.y); rect.width = min(rect.width, imgSize.width-rect.x); rect.height = min(rect.height, imgSize.height-rect.y); (mask(rect)).setTo( Scalar(GC_PR_FGD) ); } //經過k-means算法來初始化背景GMM和前景GMM模型 /* Initialize GMM background and foreground models using kmeans algorithm. */ static void initGMMs( const Mat& img, const Mat& mask, GMM& bgdGMM, GMM& fgdGMM ) { const int kMeansItCount = 10; //迭代次數 const int kMeansType = KMEANS_PP_CENTERS; //Use kmeans++ center initialization by Arthur and Vassilvitskii Mat bgdLabels, fgdLabels; //記錄背景和前景的像素樣本集中每一個像素對應GMM的哪一個高斯模型,論文中的kn vector<Vec3f> bgdSamples, fgdSamples; //背景和前景的像素樣本集 Point p; for( p.y = 0; p.y < img.rows; p.y++ ) { for( p.x = 0; p.x < img.cols; p.x++ ) { //mask中標記爲GC_BGD和GC_PR_BGD的像素都做爲背景的樣本像素 if( mask.at<uchar>(p) == GC_BGD || mask.at<uchar>(p) == GC_PR_BGD ) bgdSamples.push_back( (Vec3f)img.at<Vec3b>(p) ); else // GC_FGD | GC_PR_FGD fgdSamples.push_back( (Vec3f)img.at<Vec3b>(p) ); } } CV_Assert( !bgdSamples.empty() && !fgdSamples.empty() ); //kmeans中參數_bgdSamples爲:每行一個樣本 //kmeans的輸出爲bgdLabels,裏面保存的是輸入樣本集中每個樣本對應的類標籤(樣本聚爲componentsCount類後) Mat _bgdSamples( (int)bgdSamples.size(), 3, CV_32FC1, &bgdSamples[0][0] ); kmeans( _bgdSamples, GMM::componentsCount, bgdLabels, TermCriteria( CV_TERMCRIT_ITER, kMeansItCount, 0.0), 0, kMeansType ); Mat _fgdSamples( (int)fgdSamples.size(), 3, CV_32FC1, &fgdSamples[0][0] ); kmeans( _fgdSamples, GMM::componentsCount, fgdLabels, TermCriteria( CV_TERMCRIT_ITER, kMeansItCount, 0.0), 0, kMeansType ); //通過上面的步驟後,每一個像素所屬的高斯模型就肯定的了,那麼就能夠估計GMM中每一個高斯模型的參數了。 bgdGMM.initLearning(); for( int i = 0; i < (int)bgdSamples.size(); i++ ) bgdGMM.addSample( bgdLabels.at<int>(i,0), bgdSamples[i] ); bgdGMM.endLearning(); fgdGMM.initLearning(); for( int i = 0; i < (int)fgdSamples.size(); i++ ) fgdGMM.addSample( fgdLabels.at<int>(i,0), fgdSamples[i] ); fgdGMM.endLearning(); } //論文中:迭代最小化算法step 1:爲每一個像素分配GMM中所屬的高斯模型,kn保存在Mat compIdxs中 /* Assign GMMs components for each pixel. */ static void assignGMMsComponents( const Mat& img, const Mat& mask, const GMM& bgdGMM, const GMM& fgdGMM, Mat& compIdxs ) { Point p; for( p.y = 0; p.y < img.rows; p.y++ ) { for( p.x = 0; p.x < img.cols; p.x++ ) { Vec3d color = img.at<Vec3b>(p); //經過mask來判斷該像素屬於背景像素仍是前景像素,再判斷它屬於前景或者背景GMM中的哪一個高斯份量 compIdxs.at<int>(p) = mask.at<uchar>(p) == GC_BGD || mask.at<uchar>(p) == GC_PR_BGD ? bgdGMM.whichComponent(color) : fgdGMM.whichComponent(color); } } } //論文中:迭代最小化算法step 2:從每一個高斯模型的像素樣本集中學習每一個高斯模型的參數 /* Learn GMMs parameters. */ static void learnGMMs( const Mat& img, const Mat& mask, const Mat& compIdxs, GMM& bgdGMM, GMM& fgdGMM ) { bgdGMM.initLearning(); fgdGMM.initLearning(); Point p; for( int ci = 0; ci < GMM::componentsCount; ci++ ) { for( p.y = 0; p.y < img.rows; p.y++ ) { for( p.x = 0; p.x < img.cols; p.x++ ) { if( compIdxs.at<int>(p) == ci ) { if( mask.at<uchar>(p) == GC_BGD || mask.at<uchar>(p) == GC_PR_BGD ) bgdGMM.addSample( ci, img.at<Vec3b>(p) ); else fgdGMM.addSample( ci, img.at<Vec3b>(p) ); } } } } bgdGMM.endLearning(); fgdGMM.endLearning(); } //經過計算獲得的能量項構建圖,圖的頂點爲像素點,圖的邊由兩部分構成, //一類邊是:每一個頂點與Sink匯點t(表明背景)和源點Source(表明前景)鏈接的邊, //這類邊的權值經過Gibbs能量項的第一項能量項來表示。 //另外一類邊是:每一個頂點與其鄰域頂點鏈接的邊,這類邊的權值經過Gibbs能量項的第二項能量項來表示。 /* Construct GCGraph */ static void constructGCGraph( const Mat& img, const Mat& mask, const GMM& bgdGMM, const GMM& fgdGMM, double lambda, const Mat& leftW, const Mat& upleftW, const Mat& upW, const Mat& uprightW, GCGraph<double>& graph ) { int vtxCount = img.cols*img.rows; //頂點數,每個像素是一個頂點 int edgeCount = 2*(4*vtxCount - 3*(img.cols + img.rows) + 2); //邊數,須要考慮圖邊界的邊的缺失 //經過頂點數和邊數建立圖。這些類型聲明和函數定義請參考gcgraph.hpp graph.create(vtxCount, edgeCount); Point p; 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 //計算每一個頂點與Sink匯點t(表明背景)和源點Source(表明前景)鏈接的權值。 //也即計算Gibbs能量(每個像素點做爲背景像素或者前景像素)的第一個能量項 double fromSource, toSink; if( mask.at<uchar>(p) == GC_PR_BGD || mask.at<uchar>(p) == GC_PR_FGD ) { //對每個像素計算其做爲背景像素或者前景像素的第一個能量項,做爲分別與t和s點的鏈接權值 fromSource = -log( bgdGMM(color) ); toSink = -log( fgdGMM(color) ); } else if( mask.at<uchar>(p) == GC_BGD ) { //對於肯定爲背景的像素點,它與Source點(前景)的鏈接爲0,與Sink點的鏈接爲lambda fromSource = 0; toSink = lambda; } else // GC_FGD { fromSource = lambda; toSink = 0; } //設置該頂點vtxIdx分別與Source點和Sink點的鏈接權值 graph.addTermWeights( vtxIdx, fromSource, toSink ); // set n-weights n-links //計算兩個鄰域頂點之間鏈接的權值。 //也即計算Gibbs能量的第二個能量項(平滑項) 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 ); } } } } //論文中:迭代最小化算法step 3:分割估計:最小割或者最大流算法 /* Estimate segmentation using MaxFlow algorithm */ static void estimateSegmentation( GCGraph<double>& graph, Mat& mask ) { //經過最大流算法肯定圖的最小割,也即完成圖像的分割 graph.maxFlow(); Point p; for( p.y = 0; p.y < mask.rows; p.y++ ) { for( p.x = 0; p.x < mask.cols; p.x++ ) { //經過圖分割的結果來更新mask,即最後的圖像分割結果。注意的是,永遠都 //不會更新用戶指定爲背景或者前景的像素 if( mask.at<uchar>(p) == GC_PR_BGD || mask.at<uchar>(p) == GC_PR_FGD ) { if( graph.inSourceSegment( p.y*mask.cols+p.x /*vertex index*/ ) ) mask.at<uchar>(p) = GC_PR_FGD; else mask.at<uchar>(p) = GC_PR_BGD; } } } } //最後的成果:提供給外界使用的偉大的API:grabCut /* ****參數說明: img——待分割的源圖像,必須是8位3通道(CV_8UC3)圖像,在處理的過程當中不會被修改; mask——掩碼圖像,若是使用掩碼進行初始化,那麼mask保存初始化掩碼信息;在執行分割 的時候,也能夠將用戶交互所設定的前景與背景保存到mask中,而後再傳入grabCut函 數;在處理結束以後,mask中會保存結果。mask只能取如下四種值: GCD_BGD(=0),背景; GCD_FGD(=1),前景; GCD_PR_BGD(=2),可能的背景; GCD_PR_FGD(=3),可能的前景。 若是沒有手工標記GCD_BGD或者GCD_FGD,那麼結果只會有GCD_PR_BGD或GCD_PR_FGD; rect——用於限定須要進行分割的圖像範圍,只有該矩形窗口內的圖像部分才被處理; bgdModel——背景模型,若是爲null,函數內部會自動建立一個bgdModel;bgdModel必須是 單通道浮點型(CV_32FC1)圖像,且行數只能爲1,列數只能爲13x5; fgdModel——前景模型,若是爲null,函數內部會自動建立一個fgdModel;fgdModel必須是 單通道浮點型(CV_32FC1)圖像,且行數只能爲1,列數只能爲13x5; iterCount——迭代次數,必須大於0; mode——用於指示grabCut函數進行什麼操做,可選的值有: GC_INIT_WITH_RECT(=0),用矩形窗初始化GrabCut; GC_INIT_WITH_MASK(=1),用掩碼圖像初始化GrabCut; GC_EVAL(=2),執行分割。 */ void cv::grabCut( InputArray _img, InputOutputArray _mask, Rect rect, InputOutputArray _bgdModel, InputOutputArray _fgdModel, int iterCount, int mode ) { Mat img = _img.getMat(); Mat& mask = _mask.getMatRef(); Mat& bgdModel = _bgdModel.getMatRef(); Mat& fgdModel = _fgdModel.getMatRef(); if( img.empty() ) CV_Error( CV_StsBadArg, "image is empty" ); if( img.type() != CV_8UC3 ) CV_Error( CV_StsBadArg, "image mush have CV_8UC3 type" ); GMM bgdGMM( bgdModel ), fgdGMM( fgdModel ); Mat compIdxs( img.size(), CV_32SC1 ); if( mode == GC_INIT_WITH_RECT || mode == GC_INIT_WITH_MASK ) { if( mode == GC_INIT_WITH_RECT ) initMaskWithRect( mask, img.size(), rect ); else // flag == GC_INIT_WITH_MASK checkMask( img, mask ); initGMMs( img, mask, bgdGMM, fgdGMM ); } if( iterCount <= 0) return; if( mode == GC_EVAL ) checkMask( img, mask ); const double gamma = 50; const double lambda = 9*gamma; const double beta = calcBeta( img ); Mat leftW, upleftW, upW, uprightW; calcNWeights( img, leftW, upleftW, upW, uprightW, beta, gamma ); for( int i = 0; i < iterCount; i++ ) { GCGraph<double> graph; assignGMMsComponents( img, mask, bgdGMM, fgdGMM, compIdxs ); learnGMMs( img, mask, compIdxs, bgdGMM, fgdGMM ); constructGCGraph(img, mask, bgdGMM, fgdGMM, lambda, leftW, upleftW, upW, uprightW, graph ); estimateSegmentation( graph, mask ); } }
其中 gcgraph.hpp 轉自 http://blog.csdn.net/wstcegg/article/details/39495535,先存着尚未看。安全
#ifndef _CV_GCGRAPH_H_ #define _CV_GCGRAPH_H_ 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 ); void addTermWeights( int i, TWeight sourceW, TWeight sinkW ); TWeight maxFlow(); bool inSourceSegment( int i ); private: class Vtx //結點類型 { public: Vtx *next; //在maxflow算法中用於構建先進-先出隊列 int parent; //父節點發出的弧 int first; //首個相鄰弧 int ts; //time-stamp時間戳 int dist; //distance,到樹根的距離 TWeight weight; //t-value, 即到終端結點的權值 uchar t; //取值只能是0-1,s->t方向爲0, t->s方向爲1 }; class Edge //邊類型 { public: int dst; //弧指向的頂點 int next; //同一個原點的下一條弧 TWeight weight; //n-value, 弧的權值 }; std::vector<Vtx> vtcs; //結點集合 std::vector<Edge> edges; //弧集合 TWeight flow; //圖的流量 }; template <class TWeight> GCGraph<TWeight>::GCGraph() { flow = 0; //流量初始化爲0 } GCGraph<TWeight>::GCGraph( unsigned int vtxCount, unsigned int edgeCount ) { //構造函數並無實質性的內容,而是把工做交給了create函數 //你們一塊兒想一想是爲啥~ create( vtxCount, edgeCount ); } template <class TWeight> GCGraph<TWeight>::~GCGraph() { } template <class TWeight> void GCGraph<TWeight>::create( unsigned int vtxCount, unsigned int edgeCount ) { //原來creeate也沒啥東西啊 //爲了提升內存分配效率,給兩個集合預留足夠空間 vtcs.reserve( vtxCount ); edges.reserve( edgeCount + 2 ); flow = 0; } /* 函數功能: 添加一個空結點,全部成員初始化爲空 參數說明: 無 返回值: 當前結點在集合中的編號 */ template <class TWeight> int GCGraph<TWeight>::addVtx() { //添加結點 Vtx v; //將結點申請到的內存空間所有清0(第二個參數0) //目的:因爲結點中存在成員變量爲指針,指針設置爲null保證安全 memset( &v, 0, sizeof(Vtx)); vtcs.push_back(v); return (int)vtcs.size() - 1; //返回結點集合尺寸-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 ) { // 檢查結點編號有效性 CV_Assert( i>=0 && i<(int)vtcs.size() ); CV_Assert( j>=0 && j<(int)vtcs.size() ); CV_Assert( w>=0 && revw>=0 ); // 檢查弧權值有效性 CV_Assert( i != j ); // 不存在指向本身的結點 if( !edges.size() ) edges.resize( 2 ); // 正向弧:fromI, 反向弧 toI Edge fromI, toI; //===================================================// //如下爲插入正向弧的操做 fromI.dst = j; // 正向弧指向結點j fromI.weight = w; // 正向弧賦予權值w //注意,爲便於解釋,下方一行代碼與上面一行代碼交換了順序,沒有任何實際影響 //每一個結點所發出的所有n-link弧(4個方向)都會被鏈接爲一個鏈表, //採用頭插法插入全部的弧 fromI.next = vtcs[i].first; //將正向弧指向結點i的鏈表首部 vtcs[i].first = (int)edges.size(); //修改結點i的第一個弧爲當前正向弧 edges.push_back( fromI ); //正向弧加入弧集合 //===================================================// //如下爲插入反向弧的操做,與上相同,不做解釋 toI.dst = i; toI.weight = revw; toI.next = vtcs[j].first; 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 ) { CV_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; } template <class TWeight> { //本函數中僅有的可能出現的負值,下面若是存在判別某值是否小於0, //意味着判斷當前結點是否爲終端結點,或者孤立點 const int TERMINAL = -1, ORPHAN = -2; //先進先出隊列,保存當前活動結點,stub爲哨兵結點 Vtx stub, *nilNode = &stub, *first = nilNode, *last = nilNode; int curr_ts = 0; //當前時間戳 stub.next = nilNode; //初始化活動結點隊列,首結點指向本身 Vtx *vtxPtr = &vtcs[0]; //結點指針 Edge *edgePtr = &edges[0]; //弧指針 std::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; //標註其雙親爲終端結點 //t爲uchar類型, 例如: t=(1>0), t=1 // t=(1<0), t=0 //實際效果紀錄當前結點流向, 正向取0,逆向取1 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 ) { // 此路不通,跳過,一般兩個相鄰點都會有關聯的,不過若是關聯值weight太小,爲防止下溢出,置0! // 每對結點都擁有兩個反向的邊,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; //continue_for() [遍歷鄰接點] } if( u->t != vt ) // u和v的流向不一樣,u能夠到達另外一終點,則找到一條路徑 { e0 = ei ^ vt; // break; //break_for() [遍歷鄰接點] } // u已經存在父節點,可是若是u的路徑長度大於v+1,說明u走彎路了,修改u的路徑,使其成爲v的子結點 // 進入條件:u的路徑長度大於v的長度+1,且u的時間戳較早 if( u->dist > v->dist+1 && u->ts <= v->ts ) { // 重新設置u的父節點爲v(編號ei),記錄爲當前的弧 u->parent = ei ^ 1; u->ts = v->ts; // 更新u的時間戳與v相同 u->dist = v->dist + 1; // u爲v的子結點,路徑長度加1 } } if( e0 > 0 ) break; //break_while() [查找s->t路徑] } // 將剛處理完的結點從活動隊列移除 first = first->next; v->next = 0; } if( e0 <= 0 ) //所有搜索結束,e0=0說明全部結點都已經處理完畢,e0<0說明已經搜索到了終端結點了。。 break; //break_for(;;)[max_flow] //===================================================// // 第二階段: 流量統計與樹的拆分 //===第一節===// //查找路徑中的最小權值 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 ) //回溯,ei紀錄當前點的父邊,回溯至終端結點,退出 break; weight = edgePtr[ei^k].weight; //取當前弧的權值 minWeight = MIN(minWeight, weight); //紀錄當前路徑最小流 assert( minWeight > 0 ); } weight = fabs(v->weight); minWeight = MIN(minWeight, weight); //取s樹和t樹的最小流 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; //n-value逆向增長 //n-value正向減小,若是權值減小至0,則將ei標註爲孤兒 if( (edgePtr[ei^k].weight -= minWeight) == 0 ) { orphans.push_back(v); //加入孤兒集合 v->parent = ORPHAN; //修改父節點標記 } } v->weight = v->weight + minWeight*(1-k*2); //t-value修改(減小或增長) //若是權值減小至0,則將ei標註爲孤兒 if( v->weight == 0 ) { orphans.push_back(v); v->parent = ORPHAN; } } //===================================================// // 第三階段: 樹的重構 // 爲孤兒找到新的父節點,恢復樹結構 curr_ts++; while( !orphans.empty() ) //存在孤兒 { Vtx* v2 = orphans.back(); //取一個孤兒,記爲v2 orphans.pop_back(); //刪除棧頂元素,兩步操做等價於出棧 int d, minDist = INT_MAX; e0 = 0; vt = v2->t; // 遍歷當前結點的相鄰點,ei爲當前弧的編號 for( ei = v2->first; ei != 0; ei = edgePtr[ei].next ) { if( edgePtr[ei^(vt^1)].weight == 0 ) // 找到權值爲0的邊,無效,繼續找 continue; u = vtxPtr+edgePtr[ei].dst; // 鄰接點記爲u if( u->t != vt || u->parent == 0 ) // 不一樣方向的邊,或者找到的點也是孤立點,繼續找 continue; // 計算當前點路徑長度 for( d = 0;; ) { if( u->ts == curr_ts ) // 找到時間戳符合的結點,即此結點路徑長度有效 { d += u->dist; // 最終路徑長度等於到u結點的距離加u結點的路徑長度 break; } ej = u->parent; // 繼續尋找u的父節點 d++; // 距有效點距離加1 if( ej < 0 ) // TERMINAL = -1, ORPHAN = -2 { if( ej == ORPHAN ) // 找到的父節點是孤立點 d = INT_MAX-1; // 紀錄d無窮大,即沒法到達終端結點 else // 找到的是終端結點 { u->ts = curr_ts; // 更改時間戳爲當前時刻,本次大循環中其路徑長度是有效的! u->dist = 1; // 路徑長度爲1 } break; } u = vtxPtr+edgePtr[ej].dst; // u指向父節點,繼續回溯 } // 更新子結點的路徑長度 if( ++d < INT_MAX ) // 當前結點找到了父節點 { if( d < minDist ) // { minDist = d; // 更新minDist e0 = ei; // e0記錄找到的v2父弧 } // for( u = vtxPtr+edgePtr[ei].dst; u->ts != curr_ts; u = vtxPtr+edgePtr[u->parent].dst ) { u->ts = curr_ts; u->dist = --d; } } }//end_for if( (v2->parent = e0) > 0 ) { v2->ts = curr_ts; //v2時間戳爲當前時刻,本次大循環中其路徑長度是有效的! v2->dist = minDist; continue; } // 未找到父節點,將此結點和其全部子節點置爲孤兒 v2->ts = 0; // v2爲當前結點,時間戳置0 for( ei = v2->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和v反向,則加入活動隊列 (vt^1表示對vt取反) { u->next = nilNode; last = last->next = u; } if( ej > 0 && vtxPtr+edgePtr[ej].dst == v2 ) //當前節點確實是v2的子節點 { orphans.push_back(u); //加入孤立點隊列 u->parent = ORPHAN; //標記其無父節點 } } }//end_while }//end_for(;;) return flow; //返回最大流量 } template <class TWeight> bool GCGraph<TWeight>::inSourceSegment( int i ) { CV_Assert( i>=0 && i<(int)vtcs.size() ); return vtcs[i].t == 0; } #endif