### `highgui`的經常使用函數: `cv::namedWindow`:一個命名窗口 `cv::imshow`:在指定窗口顯示圖像 `cv::waitKey`:等待按鍵 ### 像素級 * 在灰度圖像中,像素值表示亮度,因此0表示黑色,255表示白色; * 圖像在本質上都是一個矩陣,可是灰度圖像的值就是一個矢量,而彩色圖像則是多通道的向量,因此能夠經過`image.at<>(row,colomn)[]`來取值,灰度就是`uchar`,經常使用的RGB通道則是`cv::Vec3b`,b表明ushort,s-short, i-int, f-float. at方法自己不作任何類型轉換; * 矩陣能夠聲明爲`cv::Mat`,這是一個泛型的數據結構,因此在使用at時要指定類型,也能夠直接聲明存儲類型,如`cv::Mat_`,這樣在使用`at(i,j)`時就沒必要指明結構; * 不論是多通道仍是單通道,在內存中的實際存儲類型都是一個連續的二維數組,所以,爲了提升訪問速度,咱們一般使用`ptr<>`方法來直接訪問內存,它返回指定類型的指針。對於連續矩陣,其長度就是矩陣的行長度,其寬度則是`channels()*cols`。因此,使用指針掃描圖像,格式就是: ```cpp cv::Mat image; int nl=image.rows; int nc=image.cols*image.channels() for(int j=0;j<nl;j++) uchar *data=image.ptr(j); //取得行首指針 for(int i=0;i<nc;i++) data[i]=... //像素間距爲uchar ``` 書中這裏給出了減小像素範圍的算法(也就是像素位數),讓每一個像素值/div*div+div/2。這是一個經常使用的彩色圖像處理時的預處理步驟。 * opencv默認存放彩色圖像爲`BGR`順序,即`image.at[0]=blue`...; * 顯然,爲了內存對齊等目的,數組的行寬度和實際圖像像素的行寬度不一致,可使用`data`取得元素頭指針,`step`屬性取得數組的行字節數,使用`elemSize`方法取得一個像素的字節數,使用`channels`方法取得通道數,使用`total`方法取得總像素數; * opencv的內存是本身管理的,正常使用賦值操做,獲得的是引用。使用`clone`方法來進行深拷貝,使用`create`工廠方法來填充矩陣(用default constructor 聲明時); * 可使用`data`屬性取得低級指針,而後配合`step`等屬性|方法來進行指針低級運算,通常不推薦使用這種方法; * 迭代器,使用`cv::MatIterator_<>`或`cv::Mat<>_::iterator`來聲明,固然最好用`auto`(for c++11),使用`begin<>`和`end<>`方法來取得頭尾迭代器;只讀迭代器爲`cv::MatConstIterator_<>`,或者`cv::Mat_<>::const_iterator`;迭代器的效率不高,可是能夠配合STL使用; ```cpp cv::Mat image; auto iter=image.begin(); auto iterend=image.end(); for(;iter!=iterend;++iter) (*iter)[0]=... ``` 上面是使用迭代器進行遍歷的過程。注意迭代器的訪問速度還要快於直接使用`at(i,j)`。 * 使用`saturate_cast(value)`對`value`實行指定`type`的截斷操做; * 輔助函數`cv::getTickCount()`返回從啓動電腦以來的時鐘滴答數,配合用來得到每秒滴答數的`cv::getTickFrequency()`,能夠用做一個普通的定時器;固然,也可使用`boost::timer`; ### 空間域圖像處理 * 使用`row(n)`和`col(n)`來取得行、列的向量引用,使用`cv::Scalar()`來創建向量,使用`setTo`方法能夠對矩陣進行向量尺度的賦值。順便一提,`Scalar_`是固定長度爲4的向量,即`Vec<T,4>`,而`typedef Scalar_ Scalar`; * 使用`cv::filter2D`來進行空間域卷積濾波: ```cpp cv::Mat_ filter_kernel(3,3,0.0f); filter_kernel[1][1]=5.0f; filter_kernel[0][1]=-1.0f; filter_kernel[1][0]=-1.0f; filter_kernel[2][1]=-1.0f; filter_kernel[1][2]=-1.0f; cv::filter2D(image,result,image.depth(),filter_kernel); ``` 創建濾波核,而後應用到圖像便可,支持`inplace`處理; * 算術計算函數:加法—`cv::add` or `cv::addWeighted`,能夠應用mask(mask必須是單通道矩陣,操做僅對mask非0的像素執行);減法—`cv::subtract`,乘法—`cv::multiply`,除法—`cv::divide`,差的絕對值—`cv::absdiff`;位運算:`cv::bitwise_and`,`cv::bitwise_or`,`cv::bitwise_xor`和`cv::bitwise_not`;`cv::min`和`cv::max`用來計算像素極值;此外`cv::sqrt`, `cv::abs`, `cv::cuberoot`, `cv::exp`, `cv::log`等函數也存在; * 以上是全局函數,實際上,矩陣運算的大部分操做符已被重載。能夠直接使用 + ,- ,* ,/ ,& ,| ,~ , ^等操做符,更有`inv`(轉置),`cross`(X乘),`dot`(點乘),`determinant`(行列式)等方法進行計算;注意全部的操做符的計算結果都會被截斷,若是須要負值或者過大的值, **不能**直接用操做符計算; * 和Matlab不一樣,使用矩陣計算**並不**會比使用指針的像素計算更快,可是因爲矩陣計算更簡潔,因此在對性能要求可以知足的狀況下,可使用矩陣級計算; * 切割矩陣,使用`cv::split`能夠將多通道矩陣切割成多個單通道矩陣,其第二個參數是`std::vector`;使用`cv::merge`對切割矩陣進行合併; * ROI: 和openCV1.0中不一樣,ROI就是矩陣局部的引用,使用`cv::Rect(x,y,cols,rows)`或者`cv::row(xth,yth)`或`cv::Range(xbegin,xend)`,或者同義的`rowRange(begin,end)`方法,便可獲得對應行/列的引用。另外,`Range::all()`的意思同Matlab中的`:`操做符。 ### 面向對象的圖像處理 * 一般,咱們對算法使用`Strategy`模式,將之包裹在聽從接口的類中,使用控制器來關聯類和做用的對象; * 使用MVC模式構建GUI程序,UI交互程序調用控制器方法,控制器調用實際的數據類,並將數據類的變化反映到視圖中。 * 使用Singleton構建全局單例; * 轉換顏色空間。因爲RGB顏色空間不是視覺均勻的,在使用某些操做時,可能須要轉換色彩空間。函數`cv::cvtColor(image,result,flag)`用來完成此工做,flag是系列`CV_XX2XX`的色彩轉換常數,支持`in-place`操做,其中`CV_BGR2gray`是彩色轉灰度的參量;注意:opencv自由一些常見的顏色空間轉換函數,實際上的顏色空間多到蛋疼的地步; ### 直方圖相關操做 * 直方圖演算。直方圖計算的基本函數是`cv::calcHist()`,因爲該函數的參數過於複雜,爲簡化使用,通常將之封裝在提供默認參數的類中。對於單通道圖像,可使用: ```cpp class Histogram1D{ private: int histSize[1]; //像素值的有效值個數 float hranges[2]; //像素值有效值的上下限 const float *ranges[1]; //指向hranges int channels[1]; //通道數 public: Histogram1D{ HistSize[0]=256; hranges[0]=0.0; hranges[1]=255.0; ranges[0]=hranges; channels[0]=1; } cv::MatND getHistogram(const cv::Mat& image) { cv::MatND hist; cv::calcHist(&image,1,channels,cv::Mat(),hist,1,histSize,ranges); return hist; } //描繪直方圖對應的點狀圖(連續圖像) cv::Mat getHistogramImage(const cv::Mat& image) { cv::MatND hist=getHistgram(image); double maxVal=0.0; double minVal=0.0; cv::minMaxLoc(hist, &minVal,&maxVal,0,0); cv::Mat histImg(histSize[0],histSize[0],CV_8U,cv::Scalar(255)); int hpt=static_cast(0.9*histSize[0]); for(int h=0;h<histSize[0];++h) { float binVal=hist.at(h); int intensity=static_cast(binVal*hpt/maxVal); cv::line(histImg,cv::Point(h,histSize[0]), cv::Point(h,histSize[0]-intensity), cv::Scalar::all(0)); } return histImg; } }; ``` * 使用 `cv::threshold`取閾值,進行二值化處理。 * 使用 `cv::SparseMat`建立稀疏矩陣,減小內存佔用量(含有大量0值時); * 直方圖規格化,即將源圖像轉化爲給定直方圖分佈的圖像。使用函數`cv::LUT`進行轉換; * 直方圖均衡化,使用函數`cv::equalizeHist`進行處理,實際上直方圖均衡化是直方圖規格化的一個特例; * 使用`cv::normalize`進行直方圖歸一化; * 能夠根據ROI的直方圖分佈狀況,對圖像中有類似分佈狀況的區域進行再認,算是目標識別的一種方式。使用函數`cv::calcBackProject`獲得一個機率分佈圖(與目標直方圖分佈的匹配程度),對該圖進行二值化,便可獲得近視的目標匹配效果。須要注意:若是使用灰度直方圖,目標匹配的效果會不好,必須使用彩色直方圖。 * 用於目標追蹤的`Mean-Shift`算法(均值漂移),其跟蹤依據(特徵參數)是機率密度梯度函數,其終止條件能夠是類似度或最大迭代次數。Mean-Shift須要把圖像轉換到hue顏色空間,其特徵使用了大量顏色信息,所以不能把源圖像灰度化; * 使用直方圖比較來檢索類似圖像,主要使用`cv::compareHist`函數,其中第三個參數指定比較的算法,注意在比較以前通常要減小圖像的色彩度。 ### 圖像的形態學變換 * 腐蝕:`cv::erode`,膨脹:`cv::dilate`,支持`in-place`變換。腐蝕是用將當前像素值用形態核中最小的值代替,膨脹則是用最大值。默認使用3*3形態核; * 高等形態學變換統一使用`cv::morphologyEx`函數,指定第三個參數爲`cv::MORPH_CLOSE`則爲閉操做(先膨脹再腐蝕,填補白色前景中的孔洞), `cv::MORPH_OPEN`爲開操做(先腐蝕再膨脹,移除前景中的小物體),開運算和閉運算都是冪等操做。參數`cv::MORPH_GRADIENT`用來梯度化處理(輔以二值化),通常用來作邊緣檢測; * 將圖像的灰度看作地理圖中的海拔度,那麼邊緣就是懸崖,腐蝕該圖像會使山峯的高度下降,膨脹該圖像會使山谷的深度減小,用膨脹的圖像減去腐蝕過的圖像,就獲得了突出的邊緣部分。這就是形態學邊緣檢測的原理;而拐角探測算法,則是利用特殊的形態核,使用這些形態核進行閉操做不會影響直線邊緣,但對於拐角部分有影響(這個操做在OpenCV中不是現成的函數)。 * 使用漫水填充算法能夠將圖像快速分割爲均勻區域。所謂漫水填充,仍然是將圖像的灰度看作海拔,那麼均勻區域就是一塊相對平坦的盆地,向地圖中灌水,有水的地方就是一個個湖泊,隨着水平線的提升,不一樣的盆地可能會被鏈接起來,根據須要的不一樣調整水平線,就能夠獲得分割的一個個獨立的區域。使用`cv::watershed`完成這一操做,若是用於物體探測,咱們首先須要知道一些確切屬於某區域|物體的點,將其做爲掩膜帶入函數,函數會逐步升高水平線直到達到掩膜區域給定的邊界; * GrabCut算法也是一種經常使用的靜態圖像前景提取算法,在圖像處理軟件裏面經常使用來摳圖。這種算法和形態學毫無關係,可是使用方法有些相似漫水填充,或者說,很像PS或者光影魔術手裏面的摳圖步驟…首先標記一些屬於前景或者背景的像素,而後調用`cv::grabCut`,填充最大迭代次數和算法的flag便可。具體原理這裏不記錄。 ### 圖像濾波 * 低通濾波器會下降圖像幅值,模糊化圖像;高通則相反,會銳化圖像; * `cv::blur`,均值濾波;`cv::GaussianBlur`,高斯濾波;`cv::medianBlur`,中值濾波; * 圖像尺寸變換:`cv::pyrUp`, `cv::pyrDown`,以及更通用的`cv::resize`; * 使用邊緣濾波算子探測邊緣,`cv::Sobel`,該算子是方向性的,可選擇用於垂直或水平方向。若是選擇生成8位灰度圖像,產生的效果就是經常使用繪圖軟件中的「浮雕」效果。咱們綜合兩個方向的結果,再進行一個灰度變換,就能夠獲得邊緣檢測結果。 * `Sobel`算子能夠被看作圖像在垂直|水平方向上某個參數的度量,這個參數就是所謂「梯度」,梯度指向灰度變化最快的方向,其長度就是歐拉距離。爲了減小計算量,這裏直接用水平方向和垂直方向二者絕對值的和來近似。 * 除了`Sobel`外,還有其餘梯度算子,不一樣之處固然在於濾波核。 * 使用`cv::Laplacian`對圖像作拉普拉斯變換,本質上也是一個高通濾波器,相似Sobel,除了不用指出方向。拉普拉斯變換對噪聲很是敏感。 在拉普拉斯變換結果中,從正到負(或者相反)的地方意味着圖像的邊界。能夠經過圖像減去其拉普拉斯變換的結果加強對比度; ### 輪廓提取 * `cv::Canny`使用了兩個閾值來過濾所需的邊緣,從而能夠提取出所需物體的輪廓。`Canny`算子依賴於`Sobel`算子(或其餘邊緣提取算子)。方法的核心是先利用低閾值獲得包含正確邊緣的圖像,利用高閾值來提取包含重要輪廓的部分,而後經過一些計算保留了第二個閾值中限定的重要輪廓,同時儘可能移除第一個閾值中不重要的部分。這種算法屬於「雙閾值門限」算法。 * Hough變換用來探測圖像中的直線,有兩個版本: > `cv::HoughLines`用來檢測通過邊緣檢測的二值圖像,經過指定需探測直線的角度範圍便可; >`cv::HoughLinesP`是機率性霍夫變換,增長了兩個參數用來代表探測直線的最小長度和連續物體的最小像素間距; * 除了直線檢測外,還有一些函數專用於檢測其餘簡單形狀,包括圓(HoughCircles)和其餘不規則形狀; * 使用`cv::fitLine`對指定點集進行直線擬合,`cv::fitEllipse` 對指定點集進行橢圓擬合; * 使用`cv::findContours`在二值化的圖像中尋找連續像素組成物體的輪廓。輸入一個包含輪廓的二值圖像,輸出是一個輪廓(點集)的集合(std::vector<std::vector>); 使用`cv::drawContours`描繪出這些輪廓; * 有時候須要將輪廓包圍起來作標識,使用`cv::boundingRect`(矩形),`cv::minEnclosingCircle`(圓), `cv::approxPolyDP`(多邊形), `cv::convexHull`(凸包), `cv::Moments`(一階矩),`cv::minAreaRect`(最小旋轉矩形), `cv::contourArea`估算面積,其構造參數都是輪廓。 * 使用輪廓能夠進行形態上的特徵分類。`cv::Moments`有一堆參數,其中質心就是 $(\frag{m_{10}}{m_{00}},\frag{m_{01}{m_{00}})$ 其餘的參照公式。 * `cv::pointPolygonTest`能夠用來檢測一個點是否在輪廓內;`cv::matchShapes`用來評估輪廓的類似度,可選算法有3種。 ### 特徵探測 * 物體的輪廓是物體最基本的特徵之一,在一些簡單的object detection算法應用中,使用輪廓自己的匹配,或者輪廓相關的特徵的匹配,就能夠達到識別物體的目的。`cv::SimpleBlobDetector`就是利用了這個特性。 * Harris corners探測:`cv::cornerHarris`,用來探測角點,其原理是先計算其平均灰度變化的梯度方向(使用Sobel邊緣檢測算子),而後在計算該方向垂直方向的平均灰度變化,若是變化率也很高,那麼就是一個角點。其對應矩陣特徵是其協方差矩陣擁有高於指定閾值的最小特徵值;`cv::goodFeaturesToTrack`,經過指定角點的一些特徵(最大數量、質量等級、最小點距)來進行Harris corner探測,使特徵點的分佈更加均勻。角點具備必定的尺度不變形,而且運算量不大,在object detection中是一種很好的探測特徵。 * `cv::FeatureDetector`是一個通用的特徵探測抽象類(繼承自`cv::Algorithm`),規定了接口`detect`方法,用於返回知足條件的特徵集(`std::vector`),`cv::goodFeaturesToTrackDetector`就是這個抽象類的一個子類(顧名思義,是`cv::goodFeaturesToTrack`函數的一個包裝類。) * `cv::FastFeatureDetector`用來完成快速(Harris)特徵檢測,這個也是`cv::FeatureDetector`的一個子類,這是一種估算,所以精確度略低,但速度很快,適用於對實時性要求較高的場合。另外`cv::drawKeypoints`能夠用來直接描繪關鍵點。 * 尺度無關的特徵檢測。書裏面介紹了SURF(Speeded Up Robust Features)特徵檢測,`cv::SurfFeatureDetector`是另外一個`cv::FeatureDetector`的子類。此外還說起了SIFT檢測算法,與SURF相比,該算法的精細度更高,對應的,速度較低。 * SURF是很是重要的特徵檢測算子,由於攝像頭的角度若是不是垂直向下的,物體的尺度很難保持不變。SURF可以計算出尺度不變的特徵,同時保持較快的計算速度,可以在必定程度上兼顧準確度和實時性要求。 * 除了上文中的特徵檢測子之外,opencv還實現了一大堆其餘的檢測子,包括:`STAR`,`ORB`,`BRISK`,`MSER`,`Dense`和`SimpleBlob`等,其中`SimpleBlob`最簡單。 * 利用直方圖分佈的Mean-shift算法也是一種特徵。 ### 特徵匹配 * `cv::DescripterExtractor`和`cv::DescriptorMatcher`也是抽象類,前者用於特徵提取,有一個工廠函數,對應前面的特徵檢測子,使用方法`compute`計算特徵;後者使用`match`匹配。`match`方法的結果是`cv::DMatch`的集合,Struct DMatch包含queryIdx, trainIdx和imgIdx,以及一個distance表示兩個點間距。可使用`cv::drawMatches`來描繪這些匹配。 ```cpp cv::DescripterExtractor *extractor=new cv::SurfDescriptor(); cv::Mat descriptors1,descriptors2; extractor->compute(image1,keypoints1,descriptors1); extractor->compute(image2,keypoints2,descriptors2); std::vector matches; cv::DescriptorMatcher *matcher=new cv::BruteForceMatcher<cv::L2>(); matcher->match(descriptors1,decriptors2,matches); std::nth_element(matches.begin(),matches.begin()+24, matches.end()); matches.erase(matches.begin()+25,matches.end()); cv::drawMatches(image1,keypoints1, image2,keypoints2, matches, imageMatches, cv::Scalar(255,255,255)); ``` ### 場景重建 涉及3D建模,本章暫略。 ### 視頻處理 * 視頻就是連續的圖像幀,所以基本處理步驟是固定的: ```cpp //用視頻文件進行初始化 cv::VideoCapture capture("../xxx.avi"); //檢測是否打開成功。不過這裏沒法判斷打開不成功的緣由 if(!capture.isOpened()) { return 1; } //幀率, 使用set能夠定位 double rate=capture.get(CV_CAP_PROP_FPS); bool stop(false); cv::Mat frame; cv::namedWindow("Extracted Frame"); int delay=1000/rate; while(!stop) { //讀取下一幀,也能夠用capture>>frame, //capture.grab(), capture.retrieve(frame) if(!capture.read(frame)) { break; } cv::imshow("Extracted Frame",frame); //延遲,通常和視頻的幀率保持一致 if(cv::waitKey(delay)>=0) stop=true; } //能夠不用,由於析構自動釋放 capture.release(); } ``` 計算機裏面必須有對應的解碼器才能解碼視頻流。對於專用視頻流,opencv就無能爲力了。必須本身使用正確的SDK進行圖像提取,而後交給opencv進行處理。 寫和讀相似,用的是`cv::VideoWriter`類(提及來,對應的名字是VideoReader豈不是更好)。方法`open`須要指定輸出文件名、編碼方式(一個四字節整數)、幀率、幀大小和是否彩色。這裏值得注意的是4字節的編碼方式,貌似opencv中尚未對應的輔助函數,要本身寫。傳入-1,會彈出窗口讓你選擇編碼方式,也能夠趁機看一下系統支持哪些編碼方式。 ```cpp class VideoProcessor{ private: cv::VideoWriter writer; std::string outputFile; int currentIndex; int digits; std::string extension; bool setOutput(const std::string &filename, int codec=0,double frameRate=0.0, bool isColor=true) { outputFile=filename; extention.clear(); if(frameRate==0.0) frameRate=getFrameRate(); char c[4]; if(codec==0) { codec=getCodec(c); } return writer.open(outputFile,codec,frameRate, getFrameSize(),isColor); } int getCodec(char codec[4]){ if(images.size()!=0)return -1; union{ int value; char code[4];} returned; returned.value=static_cast( capture.get(CV_CAP_PROP_FOURCC)); codec[0]=returned.code[0]; //這裏獲得編碼的字符表示 codec[1]=returned.code[1]; codec[2]=returned.code[2]; codec[3]=returned.code[3]; return returned.value; } void writeNextFrame(cv::Mat& frame) { if(extension.length()) { std::stringstream ss; ss<<outputFile<<std::setfill('0') <<std::setw(digits)<<currentIndex++ <<extension; }else{ writer.write(frame); } } bool setOutput(const std::string& filename, const std::string &ext, int numberOfDigits=3, int startIndex=0) { if(numberOfDigits<0)return false; outputFile=filename; extension=ext; digits=numberOfDigits; currentIndex=startIndex; return true; } void run() { //... while(!isStoped()) { if(outputFile.length()!=0) writeNextFrame(output); if(windowNameOutput.length()!=0) cv::imshow(windowNameOutput,output); if(delay>=0 && cv::waitKey(delay)>=0); stopIt(); if(frameToStop>=0 && getFrameNumber()==frameToStop) stopIt(); } } } ``` 視頻打開後,就可使用`write`方法將視頻幀寫入文件(也能夠寫成連續的圖像文件)。 ### 特徵跟蹤 光流法使用示例: ```cpp class FeatureTracker : public IFrameProcessor{ //當前幀 cv::Mat gray; //上一幀 cv::Mat gray_prev; //兩幀的特徵點 std::vector points[2]; //初始特徵,繪圖使用 std::vector initial; //未過濾的當前特徵 std::vector features; //下面是過濾使用和相關函數使用的參數 int max_count; double qlevel; double minDist; std::vector status; std::vector err; public: FeatureTracker(): max_count(500),qlevel(0.01),minDist(10.){} void process(cv::Mat &frame,cv::Mat &output) { cv::cvtColor(frame,gray,CV_BGR2GRAY); //第一次使用 if(addNewPoints()) { //探測特徵點 detectFeaturePoints(); //插入特徵點 points[0].instert(points[0].end(), features.begin(),features.end()); initial.insert(initial.end(), features.begin(),features.end()); } if(gray_prev.empty()) gray.copyTo(gray_prev); //光流跟蹤 cv::calcOpticalFlowPyrLK( gray_prev,gray, points[0], points[1], //匹配的特徵點 status, err); int k=0; for(int i=0;i<points[1].size();i++){ if(acceptTrackpoint(i)){ //過濾特徵點 initial[k]=initial[i]; points[1][k++]=points[1][i]; } } points[1].resize(k); initial.resize(k); handleTrackPoints(frame,output); //處理軌跡 std::swap(points[1],points[0]); cv::swap(gray_prev,gray); } //特徵點用了角點探測 void detectFeaturePoints(){ cv::goodFeaturesToTrack(gray, features, max_count, qlevel, minDist); } //這裏只在開始的時候須要探測特徵點,後面都是跟蹤 bool addNewPoints(){ return points[0].size()<=10; } //過濾特徵點 //兩個特徵點集對應 bool acceptTrackedPoint(int i){ return status[i] && (abs(points[0][i].x-points[1][i].x)+ abs(points[0][i].y-points[1][i].y))>2; } //描繪對應點 void handleTrackedPoints(cv::Mat &frame, cv::Mat &output){ for(int i=0;i<points[1].size();i++) { cv::line(output, initial[i], points[1][i], cv::Scalar(255,255,255)); cv::circle(output,points[1][i],3, cv::Scalar(255,255,255),-1); } } ``` 光流法是基於光流場的灰度變化趨勢推算下一幀可能的位置,所以**不是**角度無關的,計算結果和攝像機與物體間的相對角度、位置都有關係。 ### 前景提取 * 若是背景一致,顯然直接相減便可。固然,這是理想狀況。通常使用running average,也就是將各幀的數據直接加權疊加到原背景中,以動態更新背景,這就是所謂的「自適應更新背景模型"。 * 相似`FeatureDetector`,背景提取也有一個公用抽象類`BackgroundSubtractor`,有一個虛函數`getBackgroundImage`用於取出當前的背景圖片,重載了函數調用操做符;現階段opencv有如下兩個實現: `BackgroundSubtractorMOG`是高斯混合算法的一種實現,`BackgroundSubtractorMOG2`也是一種高斯混合算法的實現,可是數學模型不一致。