最近在一些機器視覺羣中的一些小夥伴們多次問到opencv是否集成了LBP算法,據我瞭解opencv沒有單獨的LBP特徵描述算法實現,都是和一些應用結合,如人臉識別,檢測等,這些都是一些論文的研究成果,針對於特定的應用,這對於想將LBP特徵描述用到自己的應用中的夥伴來說,或許不太方便。opencv也沒有一個單獨的特徵描述這樣的一個module,這或許限制了使用opencv的靈活性,而且個人體會視覺問題最終根本的問題都落在了特徵描述這樣的一個最基本的問題,包括近幾年很火的深度學習,其解決的最根本問題也是特徵表達。我們已有的機器學習理論非常的豐富,一個區分能力強,不變性強的特徵往往比改進機器學習算法的到的識別效果要好的多。期待opencv開源者在特徵描述這塊增加更多的內容,也期待自己某天也作出一點貢獻。不過現階段的特徵描述與我們語義上的特徵在實際應用中差別還很大,多數都是更具經驗和知識進行手工描述的,近些年也有一些data driven的方法,但是還有很長的一段路要走,還需要大量研究者作出新的貢獻,或許某天我們能夠將人的視覺系統如何處理和存儲信息一步一步的描述清楚,機器視覺問題才能最終得到解決。呵呵,這都是自己的一些淺顯看法,希望大家批評指正。下面開始進入正題吧。
局部二值模式(local binary pattern,LBP)算子是一個描述圖像局部空間結構的非參數模型算子,是由T.Ojala於1996年首先提出的,而且Ojala等又於2002年對LBP算子在紋理分類中的高區分能力予以了證明。LBP算法思想簡單容易理解、計算複雜度小、對單調的灰度變換不敏感並且能夠很好地描述圖像的局部紋理特徵,廣泛的應用到識別領域。LBP算子經過了一系列的改進,到目前爲止,或許CLBP中提出的3種差異描述算子組合的改進是最好的。這篇blog在CLBP基礎之上又提出了4種其它的差異算子,其中有些算子的組合也作爲研究成果發表了,這裏基於OpenCV2將7種算子用C++實現,共享出來,你可以用你的“想象力”將這些算子組合作爲特徵提取,應用到實際應用中。下面先對LBP及相關改進進行簡單描述,然後給出7種差異算子的源碼及實驗結果。
1.基本LBP
下圖給出了一個基本LBP算子,應用LBP算子的過程類似於濾波過程中的模板操作。逐行掃描圖像中的每一個像素點,以改點的灰度值作爲閾值,對其周圍3×3的8鄰域進行二值化,按照一定的順序將二值化的結果組成一個8位二進制數,以此二進制數的值作爲改點的響應。
例如對與上圖中的3×3區域的中心點,以其灰度值88作爲閾值,對其8鄰域進行二值化,並且從左上角點按照順時針方向將二值化的結果組成一個二進制數10001011,即十進制的139,作爲中心點的響應。在整個逐行掃描過程結束後,會得到一個LBP響應圖像,這個響應圖像的直方圖被稱爲LBP統計直方圖,或LBP直方圖,它常常作爲後續識別工作的特徵,以此也叫做LBP特徵。
LBP的主要思想是以某一點與其鄰域像素的相對灰度作爲響應,正是這種相對機制使LBP算子對於單調的灰度變化具有不變性(對光照變化具有較強的魯棒性,可用實驗結果驗證)。
2.圓形鄰域的LBP
採用圓形的鄰域並結合雙線性插值運算使我們可以獲得任意半徑和任意數目的鄰域像素點。例如一個半徑爲2的8鄰域像素的圓形鄰域,每一個方格對應一個像素,對於正好處於方格中心的鄰域點(左、上、右、下四個黑點),直接以改點所在方格的像素值作爲它的值;對於不在像素中心位置的鄰域點,通過雙線性插值確定其值。如下圖所示:
3.均勻LBP
由於LBP直方圖大多都是針對圖像中的各個分區分別計算的,對於一個普通大小的分塊區域,標準LBP算子得到的二值模式數目較多,而實際位於該分塊區域中的像素數目卻相對較少,這將會得到一個過於稀疏的直方圖,從而使直方圖失去統計意義。以此應設法減少一些冗餘的LBP模式,同時又保留足夠的具有重要描繪能力的模式。
正是基於上述考慮,研究者提出了均勻模式(uniform patterns)的概念,這是對LBP算子的由一重大改進。對於一個局部二值模式,在將其二進制位串視爲循環的情況下,如果其中包含的從0到1或者從1到0轉變不多於兩個,則稱這個局部二值模式爲均勻模式。例如,模式00000000(0個轉變),01110000(2個轉變)和11001111(2個轉變)都是均勻模式,而模式11001001(4個轉變)和01010011(6個轉變)則不是。
均勻模式的意義在於:在隨後的LBP直方圖的計算過程中,只爲均勻模式分配獨立的直方圖收集箱,而所有的非均勻模式都放入一個公用收集箱,這就使得LBP特徵的數目大大減少。一般來說,保留的均勻模式往往都是反映重要信息的那些模式,而那些非均勻模式中過多的轉變往往有噪聲引起,不具有良好的統計意義。
假設圖像分塊區域大小爲18×20,像素總數爲360.如果採用8鄰域像素的標準LBP算子,收集箱數目爲256個,平均到每個收集箱的像素數目還不到2個,沒有統計意義;而均勻LBP算子的收集箱數目爲59(58個均勻模式收集箱加上1個非均勻模式收集箱),平均到每個收集箱中的像素個數爲6個左右,更具有統計意義。對16鄰域像素而言,標準LBP算子和均勻LBP算子的收集箱數目分別爲65536和243。
4.旋轉不變的LBP
不斷旋轉圓形鄰域得到一系列初始定義的LBP值,取其最小值作爲該鄰域的LBP值。通過引入旋轉不變的定義,使得LBP算子對於圖像旋轉表現得更爲魯棒,並且LBP統計模式的數量進一步減少。
5.旋轉不變的均勻LBP
將旋轉不變的均勻LBP,進一步減少了LBP統計模式的數量。
6.完整LBP(CompleteLBP,CLBP)
CLBP是12年被提出的,用三種局部差異算子描述紋理特徵:局部灰度差異描述算子(CLBP-Sign, CLBP_S)、局部梯度差異描述算子(CLBP-Magnitude, CLBP_M)以及中心像素點描述算子(CLBP-Center, CLBP_C)。三個算子的計算方式如下:
其中,gc爲中心像素灰度值,gp爲中心像素點鄰域灰度值,N爲圖像局部中心像素個數。從公式中可以看出:CLBP_SP,R即爲傳統意義上的LBP;CLBP_MP,R通過兩像素點的灰度差異幅值與全局灰度差異幅值的均值比較,描述了局部梯度差異信息;CLBP_CP,R反應中心像素點的灰度信息。相比於傳統LBP及其變種,此三種描述算子聯合構成的CLBP_SMC對紋理的描述更加精細,對紋理的識別準確率有了大幅度提高。
7.基於OpenCV2實現7種差異描述算子
在CLBP中的3種算子基礎上提出了另外4種差異描述算子,它們分別在CLBP_M和CLBP_C算子基礎之上各提出了另外2種類CLBP_M和CLBP_C差異描述算子。如果你在熟悉CLBP的基礎之上理解這些算子是相對較容易的,可以參考相關論文:《A completed modeling of local binary pattern operator for textureclassification》,《用於紋理特徵提取的改進的LBP算法》。下面就直接給出代碼好了。
class ICLBP { private: double global_average_difference(const cv::Mat& src, int radius=1, int neighbors=8) { assert(radius>0 && neighbors>0); //compute global average difference double averageDiff = 0; for(int n=0; n<neighbors; n++){ // position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring and ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // processing image data for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { // compute interpolation value float t = static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); averageDiff += abs(t-src.at<uchar>(i,j)); } }//processing data for }//outer for averageDiff /= (neighbors*(src.rows-radius)*(src.cols-radius)); return averageDiff; } double local_average_difference(const cv::Mat& src, int i, int j, int radius=1, int neighbors=8) { assert(i>=0 && i<src.cols && j>=0 && j<src.rows); assert(radius>0 && neighbors>0); double averageDiff = 0; for(int n=0; n<neighbors; n++){ // compute position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring and ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // sum of interpolation value float t = static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); averageDiff += abs(t-src.at<uchar>(i,j)); } averageDiff /= neighbors; return averageDiff; } double global_average_gray(const cv::Mat& src) { double whole = 0; for(int i=0; i<src.rows; i++) { for(int j=0; j<src.cols; j++) { whole += src.at<uchar>(i,j); } } whole /= (src.rows)*(src.cols); return whole; } double local_average_gray(const cv::Mat& src, int i, int j, int radius=1, int neighbors=8) { assert(i>=0 && i<src.cols && j>=0 && j<src.rows); assert(radius>0 && neighbors>0); double t = 0; for(int n=0; n<neighbors; n++){ // compute position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // sum of interpolation value t += static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); } t /= neighbors; return t; } public: //local gray difference code cv::Mat lbps(const cv::Mat& src, int radius=1, int neighbors=8){ assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(radius>0 && neighbors>0); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); for(int n=0; n<neighbors; n++){ // position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring and ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // processing image data for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { // compute interpolation value float t = static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); // coding dst.at<uchar>(i-radius,j-radius) += ((t > src.at<uchar>(i,j)) || (std::abs(t-src.at<uchar>(i,j)) < std::numeric_limits<float>::epsilon())) << n; } }//processing data for }//outer for return dst; } //differnece between local differnce and global average differnce code cv::Mat lbpm(const cv::Mat& src, int radius=1, int neighbors=8) { assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(radius>0 && neighbors>0); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); double averageDiff = global_average_difference(src, radius, neighbors); for(int n=0; n<neighbors; n++){ // position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring and ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // processing image data for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { // compute interpolation value float t = static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); // coding t -= src.at<uchar>(i,j); dst.at<uchar>(i-radius,j-radius) += ((abs(t) > averageDiff) || (std::abs(abs(t)-averageDiff) < std::numeric_limits<float>::epsilon())) << n; } }//processing data for }//outer for return dst; } //differnece between local differnce and local average differnce code cv::Mat lbpm_local(const cv::Mat& src, int radius=1, int neighbors=8) { assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(radius>0 && neighbors>0); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { double averageDiff = local_average_difference(src, i, j, radius, neighbors); for(int n=0; n<neighbors; n++){ // compute position of sample point float x = static_cast<float>(-radius * sin(2.0*CV_PI*n/static_cast<float>(neighbors))); float y = static_cast<float>(radius * cos(2.0*CV_PI*n/static_cast<float>(neighbors))); // flooring and ceiling int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // decimals float ty = y - fy; float tx = x - fx; // weight of interpolation float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // sum of interpolation value float t = static_cast<float>(w1*src.at<uchar>(i+fy,j+fx) + w2*src.at<uchar>(i+fy,j+cx) + w3*src.at<uchar>(i+cy,j+fx) + w4*src.at<uchar>(i+cy,j+cx)); // coding t -= src.at<uchar>(i,j); dst.at<uchar>(i-radius,j-radius) += ((abs(t) > averageDiff) || (std::abs(abs(t)-averageDiff) < std::numeric_limits<float>::epsilon())) << n; } }//for }//for return dst; } //differnece between local average differnce and local average differnce cv::Mat lbpm_local_whole(const cv::Mat& src, int radius=1, int neighbors=8) { assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(radius>0 && neighbors>0); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); double global = global_average_difference(src, radius, neighbors); for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { double local = local_average_difference(src, i, j, radius, neighbors); if(local > global) { dst.at<uchar>(i, j) = 255; }else { dst.at<uchar>(i, j) = 0; } } } return dst; } //diffrence between local gray and global average gray cv::Mat lbpc(const cv::Mat& src) { assert(src.data); assert((src.type()==CV_8UC1)); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); double whole = global_average_gray(src); for(int i=0; i<src.rows; i++) { for(int j=0; j<src.cols; j++) { if(src.at<uchar>(i,j) > whole) { dst.at<uchar>(i, j) = 255; }else { dst.at<uchar>(i, j) = 0; } } } return dst; } //difference between local gray and local average gray cv::Mat lbpc_local(const cv::Mat& src, int radius=1, int neighbors=8) { assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { double local = local_average_gray(src, i, j, radius, neighbors); if(src.at<uchar>(i, j) > local) { dst.at<uchar>(i-radius, j-radius) = 255; }else { dst.at<uchar>(i-radius, j-radius) = 0; } } } return dst; } //difference between local average gray and local average gray cv::Mat lbpc_local_whole(const cv::Mat& src, int radius=1, int neighbors=8) { assert(src.data); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(radius>0 && neighbors>0); cv::Mat dst(src.rows, src.cols, CV_8UC1); dst.setTo(0); double whole = global_average_gray(src); for(int i=radius; i < src.rows-radius;i++){ for(int j=radius;j < src.cols-radius;j++) { double local = local_average_gray(src, i, j, radius, neighbors); if(local > whole) { dst.at<uchar>(i-radius, j-radius) = 255; }else { dst.at<uchar>(i-radius, j-radius) = 0; } } } return dst; } };
8.LBP特徵描述實驗結果
lbps:
lbpm:
lbpm_local:
lbpm_local_whole:
lbpc:
lbpc_local:
lbpc_local_whole:
9.LBP統計特徵
爲了方便統計均勻LBP特徵和旋轉不變LBP特徵及旋轉不變均勻LBP特徵模式,先將常規的LBP特徵模式映射到相應均勻,旋轉不變及旋轉不變均模式,方便後續的特徵組合的統計特徵的提取工作。下面的mapping結構和相關函數完成了這個工作。
struct Mapping { int* table; int samples; int num; Mapping() { samples =0; num = 0; table = NULL; } ~Mapping() { if (table != NULL) delete[] table; } private: Mapping& operator=(const Mapping&); Mapping(const Mapping&); }; static int GetTransU2(int i, int samples) { int trans = 0; int temp = i; temp = temp>>1; int pre = i - (temp<<1); int next; int j = 2; do { int temp2 = temp; temp2 = temp2>>1; next = temp -(temp2<<1); if(pre != next) { trans++; } pre = next; temp = temp2; j++; }while(j <= samples); return trans; } static int GetMinRi(int i, int samples) { int rm = i; int r = i; for(int j=0; j<samples-1; j++) { int temp = r; temp = temp>>1; int out = r - (temp<<1); out = out<<(samples-1); r = out + temp; if(r < rm) { rm = r; } } return rm; } static int GetNumOf1(int i, int samples) { int num = 0; for(int j=0; j<samples; j++) { int temp = i; temp = temp>>1; int out = i - (temp<<1); num += out; i = temp; } return num; } void GetMapping(Mapping& map, int samples=8, std::string mappingType="u2") { assert(samples >0); assert(mappingType == "u2" || mappingType == "ri" || mappingType == "riu2"); int newMax = 0; //number of new patterns int tableNum = 1; for(int i=0; i<samples; i++) { tableNum <<= 1; } map.table = new int[tableNum]; if(mappingType == "u2") { newMax = samples*(samples-1) + 3; int index = 0; for(int i=0; i<tableNum; i++) { int trans = GetTransU2(i, samples); if(trans <= 2) { map.table[i] = index; index++; }else { map.table[i] = newMax-1; } }//for }else if(mappingType == "ri") { int *tempTable = new int[tableNum]; for(int i=0; i<tableNum; i++) { tempTable[i] = -1; } for(int i=0; i<tableNum; i++) { int rm = GetMinRi(i, samples); if (tempTable[rm] < 0) { tempTable[rm] = newMax; newMax = newMax + 1; } map.table[i] = tempTable[rm]; }//for delete[] tempTable; }else if(mappingType == "riu2") { newMax = samples + 2; for(int i=0; i<tableNum; i++) { int trans = GetTransU2(i, samples); if(trans <= 2) { map.table[i] = GetNumOf1(i, samples); }else { map.table[i] = samples + 1; } } } map.samples = samples; map.num = newMax; }
10.一個特徵組合的統計特徵提取例子
//get statistical feature form combination of lbps_r and lbpc_r static std::vector<double> GetLBP_S_C(const cv::Mat& src, Mapping& Map, int radius=1, int neighbors=8, std::string histType="hs") { assert(src.data != NULL); assert(radius > 0 && neighbors > 0); assert((src.type()==CV_8UC1)); assert(histType == "u2" || histType == "ri" || histType == "riu2"); //initialize feature vector std::vector<double> hist; for(int i=0; i<Map.num*2; i++) { hist.push_back(0); } ICLBP clbp; cv::Mat lbps = clbp.lbps(src, radius, neighbors); cv::Mat lbpc = clbp.lbpc(src, radius, neighbors); for(int i=1; i<src.rows-1; i++) { for(int j=1; j<src.cols-1; j++) { int pos = Map.table[lbps.at<uchar>(i-1, j-1)]; if(lbpc.at<uchar>(i-1, j-1) != 0) { hist[2*pos] ++; }else { hist[2*pos+1]++; } } } if(histType == "hs") { double whole = 0; for(int i=0; i<Map.num*2; i++) { whole += hist[i]; } for(int i=0; i<Map.num*2; i++) { hist[i] /= whole; } } return hist; }
reference:
《A completed modeling of local binary pattern operator for textureclassification》
《用於紋理特徵提取的改進的LBP算法》