OpenCV中的HAAR與LBP資料
HAAR特征資料 參看 haarcascade_frontalface_alt.xml 各标簽
LBP特征資料 參看 lbpcascade_frontalface.xml 各标簽
HAAR與LBP的差別:
HAAR特征是浮點數計算
LBP特征是整數計算
LBP訓練需要的樣本數量要比HAAR大
同樣的樣本空間, HAAR訓練出來的資料檢測結果要比LBP準确
擴大LBP的樣本資料,訓練結果可以跟HAAR一樣
LBP的速度一般可以比HAAR快幾倍
LBP特征的背景介紹
LBP指局部二值模式,英文全稱:Local Binary Pattern,是一種用來描述圖像局部特征的算子,LBP特征具有灰階不變性和旋轉不變性等顯著優點。它是由T. Ojala, M.Pietikäinen, 和 D. Harwood在1994年提出,由于LBP特征計算簡單、效果較好,是以LBP特征在計算機視覺的許多領域都得到了廣泛的應用,LBP特征比較出名的應用是用在人臉識别和目标檢測中,LBP提取局部特征作為判别依據,LBP方法顯著的優點是對光照不敏感,但是依然沒有解決姿态和表情的問題。不過相比于特征臉方法,LBP的識别率已經有了很大的提升。在計算機視覺開源庫OpenCV中有使用LBP特征進行人臉識别的接口,也有用LBP特征訓練目标檢測分類器的方法,Opencv實作了LBP特征的計算,但沒有提供一個單獨的計算LBP特征的接口。
LBP特征的原理
1、原始LBP特征描述及計算方法
原始的LBP算子定義在像素3*3的鄰域内,以鄰域中心像素為門檻值,相鄰的8個像素的灰階值與鄰域中心的像素值進行比較,若周圍像素大于中心像素值,則該像素點的位置被标記為1,否則為0。這樣,3*3鄰域内的8個點經過比較可産生8位二進制數,将這8位二進制數依次排列形成一個二進制數字,這個二進制數字就是中心像素的LBP值(通常轉換為十進制數即LBP碼,共256種)。中心像素的LBP值反映了該像素周圍區域的紋理資訊。
計算LBP特征的圖像必須是灰階圖,如果是彩色圖,需要先轉換成灰階圖。
上述過程用圖像表示為:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL1MzN4MzMxEjMzEzMwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
用比較正式的公式來定義的話:
其中
代表3x3鄰域的中心元素,它的像素值為ic,ip代表鄰域内其他像素的值。s(x)是符号函數,定義如下:
原始LBP特征計算代碼(Opencv下):
//原始LBP特征計算
template <typename _tp>
void getOriginLBPFeature(InputArray _src,OutputArray _dst)
{
Mat src = _src.getMat();
_dst.create(src.rows-2,src.cols-2,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
for(int i=1;i<src.rows-1;i++)
{
for(int j=1;j<src.cols-1;j++)
{
_tp center = src.at<_tp>(i,j);
unsigned char lbpCode = 0;
lbpCode |= (src.at<_tp>(i-1,j-1) > center) << 7;
lbpCode |= (src.at<_tp>(i-1,j ) > center) << 6;
lbpCode |= (src.at<_tp>(i-1,j+1) > center) << 5;
lbpCode |= (src.at<_tp>(i ,j+1) > center) << 4;
lbpCode |= (src.at<_tp>(i+1,j+1) > center) << 3;
lbpCode |= (src.at<_tp>(i+1,j ) > center) << 2;
lbpCode |= (src.at<_tp>(i+1,j-1) > center) << 1;
lbpCode |= (src.at<_tp>(i ,j-1) > center) << 0;
dst.at<uchar>(i-1,j-1) = lbpCode;
}
}
}
LBP算子利用了周圍點與該點的關系對該點進行量化。量化後可以更有效地消除光照對圖像的影響。隻要光照的變化不足以改變兩個點像素值之間的大小關系,那麼LBP算子的值不會發生變化,是以一定程度上,基于LBP的識别算法解決了光照變化的問題,但是當圖像光照變化不均勻時,各像素間的大小關系被破壞,對應的LBP模式也就發生了變化。
二、LBP特征的改進版本
在原始的LBP特征提出以後,研究人員對LBP特征進行了很多的改進,是以産生了許多LBP的改進版本。
2.1 圓形LBP特征(Circular LBP or Extended LBP)
由于原始LBP特征使用的是固定鄰域内的灰階值,是以當圖像的尺度發生變化時,LBP特征的編碼将會發生錯誤,LBP特征将不能正确的反映像素點周圍的紋理資訊,是以研究人員對其進行了改進。基本的 LBP 算子的最大缺陷在于它隻覆寫了一個固定半徑範圍内的小區域,隻局限在3*3的鄰域内,對于較大圖像大尺度的結構不能很好的提取需要的紋理特征,是以研究者們對LBP算子進行了擴充。
新的LBP算子LBP(P,R) 可以計算不同半徑鄰域大小和不同像素點數的特征值,其中P表示周圍像素點個數,R表示鄰域半徑,同時把原來的方形鄰域擴充到了圓形,下圖給出了四種擴充後的LBP例子,其中,R可以是小數,對于沒有落到整數位置的點,根據軌道内離其最近的兩個整數位置像素灰階值,利用雙線性內插補點的方法可以計算它的灰階值:
這種LBP特征叫做Extended LBP,也叫Circular LBP。使用可變半徑的圓對近鄰像素進行編碼,可以得到如下的近鄰:
通過LBP特征的定義可以看出,LBP特征對光照變化是魯棒的,其效果如下圖所示:
//圓形LBP特征計算,效率優化版本,聲明時預設neighbors=8
template <typename _tp>
void getCircularLBPFeatureOptimization(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征圖像的行數和列數的計算要準确
_dst.create(src.rows-2*radius,src.cols-2*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
for(int k=0;k<neighbors;k++)
{
//計算采樣點對于中心點坐标的偏移量rx,ry
float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
//為雙線性插值做準備
//對采樣點偏移量分别進行上下取整
int x1 = static_cast<int>(floor(rx));
int x2 = static_cast<int>(ceil(rx));
int y1 = static_cast<int>(floor(ry));
int y2 = static_cast<int>(ceil(ry));
//将坐标偏移量映射到0-1之間
float tx = rx - x1;
float ty = ry - y1;
//根據0-1之間的x,y的權重計算公式計算權重,權重與坐标具體位置無關,與坐标間的內插補點有關
float w1 = (1-tx) * (1-ty);
float w2 = tx * (1-ty);
float w3 = (1-tx) * ty;
float w4 = tx * ty;
//循環處理每個像素
for(int i=radius;i<src.rows-radius;i++)
{
for(int j=radius;j<src.cols-radius;j++)
{
//獲得中心像素點的灰階值
_tp center = src.at<_tp>(i,j);
//根據雙線性插值公式計算第k個采樣點的灰階值
float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
+ src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
//LBP特征圖像的每個鄰居的LBP值累加,累加通過與操作完成,對應的LBP值通過移位取得
dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-1);
}
}
}
}
2.2 旋轉不變LBP特征
從上面可以看出,上面的LBP特征具有灰階不變性,但還不具備旋轉不變性,是以研究人員又在上面的基礎上進行了擴充,提出了具有旋轉不變性的LBP特征。首先不斷的旋轉圓形鄰域内的LBP特征,根據選擇得到一系列的LBP特征值,從這些LBP特征值選擇LBP特征值最小的作為中心像素點的LBP特征。具體做法如下圖所示:
假設一開始得到的LBP特征為10010000,那麼将這個二進制特征,按照順時針方向旋轉,可以轉化為00001001的形式,這樣得到的LBP值是最小的。無論圖像怎麼旋轉,對點提取的二進制特征的最小值是不變的,用最小值作為提取的LBP特征,這樣LBP就是旋轉不變的了。當P=8時,能産生的不同的二進制特征數量是2^8個,經過上述表示,就變為36個。(我以為應當是2^8/8=32個)
//旋轉不變圓形LBP特征計算,聲明時預設neighbors=8
template <typename _tp>
void getRotationInvariantLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征圖像的行數和列數的計算要準确
_dst.create(src.rows-2*radius,src.cols-2*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
for(int k=0;k<neighbors;k++)
{
//計算采樣點對于中心點坐标的偏移量rx,ry
float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
//為雙線性插值做準備
//對采樣點偏移量分别進行上下取整
int x1 = static_cast<int>(floor(rx));
int x2 = static_cast<int>(ceil(rx));
int y1 = static_cast<int>(floor(ry));
int y2 = static_cast<int>(ceil(ry));
//将坐标偏移量映射到0-1之間
float tx = rx - x1;
float ty = ry - y1;
//根據0-1之間的x,y的權重計算公式計算權重,權重與坐标具體位置無關,與坐标間的內插補點有關
float w1 = (1-tx) * (1-ty);
float w2 = tx * (1-ty);
float w3 = (1-tx) * ty;
float w4 = tx * ty;
//循環處理每個像素
for(int i=radius;i<src.rows-radius;i++)
{
for(int j=radius;j<src.cols-radius;j++)
{
//獲得中心像素點的灰階值
_tp center = src.at<_tp>(i,j);
//根據雙線性插值公式計算第k個采樣點的灰階值
float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
+ src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
//LBP特征圖像的每個鄰居的LBP值累加,累加通過與操作完成,對應的LBP值通過移位取得
dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-1);
}
}
}
//進行旋轉不變處理
for(int i=0;i<dst.rows;i++)
{
for(int j=0;j<dst.cols;j++)
{
unsigned char currentValue = dst.at<uchar>(i,j);
unsigned char minValue = currentValue;
for(int k=1;k<neighbors;k++)
{
//循環左移
unsigned char temp = (currentValue>>(neighbors-k)) | (currentValue<<k);
if(temp < minValue)
{
minValue = temp;
}
}
dst.at<uchar>(i,j) = minValue;
}
}
}
2.3 Uniform Pattern LBP特征
Uniform Pattern,也被稱為等價模式或均勻模式,由于一個LBP特征有多種不同的二進制形式,對于半徑為R的圓形區域内含有P個采樣點的LBP算子将會産生2^P種模式。很顯然,随着鄰域集内采樣點數的增加,二進制模式的種類是以指數形式增加的。例如:5×5鄰域内20個采樣點,有2^20=1,048,576種二進制模式。這麼多的二進制模式不利于紋理的提取、分類、識别及存取。例如,将LBP算子用于紋理分類或人臉識别時,常采用LBP模式的統計直方圖來表達圖像的資訊,而較多的模式種類将使得資料量過大,且直方圖過于稀疏。是以,需要對原始的LBP模式進行降維,使得資料量減少的情況下能最好的表示圖像的資訊。
為了解決二進制模式過多的問題,提高統計性,Ojala提出了采用一種“等價模式”(Uniform Pattern)來對LBP算子的模式種類進行降維。Ojala等認為,在實際圖像中,絕大多數LBP模式最多隻包含兩次從1到0或從0到1的跳變。是以,Ojala将“等價模式”定義為:當某個LBP所對應的循環二進制數從0到1或從1到0最多有兩次跳變時,該LBP所對應的二進制就稱為一個等價模式類。如00000000(0次跳變),00000111(隻含一次從0到1的跳變),10001111(先由1跳到0,再由0跳到1,共兩次跳變)都是等價模式類。除等價模式類以外的模式都歸為另一類,稱為混合模式類,例如10010111(共四次跳變)。
通過這樣的改進,二進制模式的種類大大減少,而不會丢失任何資訊。模式數量由原來的2^P種減少為 P ( P-1)+2種,其中P表示鄰域集内的采樣點數。對于3×3鄰域内8個采樣點來說,二進制模式由原始的256種減少為58種,即:它把值分為59類,58個uniform pattern為一類,其它的所有值為第59類。這樣直方圖從原來的256維變成59維。這使得特征向量的維數更少,并且可以減少高頻噪聲帶來的影響。
具體實作:采樣點數目為8個,即LBP特征值有2^8種,共256個值,正好對應灰階圖像的0-255,是以原始的LBP特征圖像是一幅正常的灰階圖像,而等價模式LBP特征,根據0-1跳變次數,将這256個LBP特征值分為了59類,從跳變次數上劃分:跳變0次—2個,跳變1次—0個,跳變2次—56個,跳變3次—0個,跳變4次—140個,跳變5次—0個,跳變6次—56個,跳變7次—0個,跳變8次—2個。共9種跳變情況,将這256個值進行配置設定,跳變小于2次的為等價模式類,共58個,他們對應的值按照從小到大分别編碼為1—58,即它們在LBP特征圖像中的灰階值為1—58,而除了等價模式類之外的混合模式類被編碼為0,即它們在LBP特征中的灰階值為0,是以等價模式LBP特征圖像整體偏暗。
//等價模式LBP特征計算
template <typename _tp>
void getUniformPatternLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征圖像的行數和列數的計算要準确
_dst.create(src.rows-2*radius,src.cols-2*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
//LBP特征值對應圖像灰階編碼表,直接預設采樣點為8位
uchar temp = 1;
uchar table[256] = {0};
for(int i=0;i<256;i++)
{
if(getHopTimes(i)<3)
{
table[i] = temp;
temp++;
}
}
//是否進行UniformPattern編碼的标志
bool flag = false;
//計算LBP特征圖
for(int k=0;k<neighbors;k++)
{
if(k==neighbors-1)
{
flag = true;
}
//計算采樣點對于中心點坐标的偏移量rx,ry
float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
//為雙線性插值做準備
//對采樣點偏移量分别進行上下取整
int x1 = static_cast<int>(floor(rx));
int x2 = static_cast<int>(ceil(rx));
int y1 = static_cast<int>(floor(ry));
int y2 = static_cast<int>(ceil(ry));
//将坐标偏移量映射到0-1之間
float tx = rx - x1;
float ty = ry - y1;
//根據0-1之間的x,y的權重計算公式計算權重,權重與坐标具體位置無關,與坐标間的內插補點有關
float w1 = (1-tx) * (1-ty);
float w2 = tx * (1-ty);
float w3 = (1-tx) * ty;
float w4 = tx * ty;
//循環處理每個像素
for(int i=radius;i<src.rows-radius;i++)
{
for(int j=radius;j<src.cols-radius;j++)
{
//獲得中心像素點的灰階值
_tp center = src.at<_tp>(i,j);
//根據雙線性插值公式計算第k個采樣點的灰階值
float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
+ src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
//LBP特征圖像的每個鄰居的LBP值累加,累加通過與操作完成,對應的LBP值通過移位取得
dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-1);
//進行LBP特征的UniformPattern編碼
if(flag)
{
dst.at<uchar>(i-radius,j-radius) = table[dst.at<uchar>(i-radius,j-radius)];
}
}
}
}
}
//計算跳變次數
int getHopTimes(int n)
{
int count = 0;
bitset<8> binaryCode = n;
for(int i=0;i<8;i++)
{
if(binaryCode[i] != binaryCode[(i+1)%8])
{
count++;
}
}
return count;
}
4、LBP特征用于檢測的原理
顯而易見的是,上述提取的LBP算子在每個像素點都可以得到一個LBP“編碼”,那麼,對一幅圖像(每個像素點的灰階值)提取其原始的LBP算子之後,得到的原始LBP特征依然是“一幅圖檔”(每個像素點的LBP值)。
LBP的應用中,如紋理分類、人臉分析等,一般都不将LBP圖譜作為特征向量用于分類識别,而是采用LBP特征譜的統計直方圖作為特征向量用于分類識别。
因為,從上面的分析我們可以看出,這個“特征”跟位置資訊是緊密相關的。直接對兩幅圖檔提取這種“特征”,并進行判别分析的話,會因為“位置沒有對準”而産生很大的誤差。後來,研究人員發現,可以将一幅圖檔劃分為若幹的子區域,對每個子區域内的每個像素點都提取LBP特征,然後,在每個子區域内建立LBP特征的統計直方圖。如此一來,每個子區域,就可以用一個統計直方圖來進行描述;整個圖檔就由若幹個統計直方圖組成;
例如:一幅100*100像素大小的圖檔,劃分為10*10=100個子區域(可以通過多種方式來劃分區域),每個子區域的大小為10*10像素;在每個子區域内的每個像素點,提取其LBP特征,然後,建立統計直方圖;這樣,這幅圖檔就有10*10個子區域,也就有了10*10個統計直方圖,利用這10*10個統計直方圖,就可以描述這幅圖檔了。之後,我們利用各種相似性度量函數,就可以判斷兩幅圖像之間的相似性了,LBP即可以用于人臉檢測,也可用于人臉識别。
對LBP特征向量進行提取的步驟:
(1)首先将檢測視窗劃分為16×16的小區域(cell);
(2)對于每個cell中的一個像素,将相鄰的8個像素的灰階值與其進行比較,若周圍像素值大于中心像素值,則該像素點的位置被标記為1,否則為0。這樣,3*3鄰域内的8個點經比較可産生8位二進制數,即得到該視窗中心像素點的LBP值;
(3)然後計算每個cell的直方圖,即每個數字(假定是十進制數LBP值)出現的頻率;然後對該直方圖進行歸一化處理。
(4)最後将得到的每個cell的統計直方圖進行連接配接成為一個特征向量,也就是整幅圖的LBP紋理特征向量;
然後便可利用SVM或者其他機器學習算法進行分類了。
from:LBP原理加源碼解析
from:Haar ,LBP 級聯分類器
from:目标檢測的圖像特征提取之(二)LBP特征
from:人臉識别經典算法二:LBP方法
OpenCV實作了LBP+Adaboost+級聯器檢測方法
LBP+Adaboost方法用在目标檢測中的效果比Haar特征、HOG特征都要好(HOG特征用的不多,主要是Haar和LBP),而且LBP特征的訓練速度比Haar和HOG都要快很多。在LBP+Adaboost中,LBP特征主要是用作輸入的訓練資料(特征),使用的LBP特征應該是DLBP(維基百科上說的,沒太看明白Cascade中LBP特征的計算方式),具體用法需要看源碼。Opencv的TrainCascade中使用的LBP特征是MB-LBP。
OpenCV附帶了一個名為traincascade的工具,用于訓練LBP,Haar和HOG。特别是對于人臉檢測,他們甚至以traincascade所需的格式處理了24x24像素大小的3000幅圖像。
檢測器以24x24的滑動視窗來尋找面部。從級聯分類器的第1階段到第20級步進,如果它可以顯示目前24x24視窗可能不是面部,則它拒絕它并在視窗上移動一到兩個像素到下一個位置;否則它将進入下一階段。
在每個階段,檢查3-10個左右的LBP特征。每個LBP特征在視窗内都有一個偏移量和一個大小,它覆寫的區域完全包含在目前視窗中。計算每個給定位置LBP特征,可能導緻通過或失敗。根據LBP功能是成功還是失敗,将特定于該功能的正或負權重添加到累加器。Evaluating an LBP feature at a given position can result in either a pass or fail.
Depending on whether an LBP feature succeeds or fails, a positive or negative weight particular to that feature is added to an accumulator.
将每個階段的輸出的值與每個階段的門檻值進行比較。如果輸出低于門檻值則該階段失敗,如果輸出高于門檻值則通過。同樣如果一個階段失敗,則退出級聯并且視窗移動到下一個位置。
與haar基本類似,不在贅述!
執行個體:
//cascades+LBP檢測器的運用,用于人臉識别和人眼識别
#include<opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
CascadeClassifier face_cascade;
CascadeClassifier eyes_cascade;
void detectAndDisplay(Mat frame);
int main(int argc, char** argv)
{
Mat srcImage;
srcImage = imread("1.jpg", 1);
imshow("原圖", srcImage);
//============加載分類器=========
if (!face_cascade.load("D:\\Program Files\\OpenCV\\opencv\\sources\\data\\lbpcascades\\lbpcascade_frontalface.xml"))//也可用Haar分類器
{
printf("人臉檢測器加載失敗\n");
return -1;
}
if (!eyes_cascade.load("D:\\Program Files\\OpenCV\\opencv\\sources\\data\\haarcascades_cuda\\haarcascade_eye.xml"))
{
printf("人眼檢測器加載失敗\n");
return -1;
};
//============調用人臉檢測函數 =========
detectAndDisplay(srcImage);
waitKey(0);
}
void detectAndDisplay(Mat dispFace)
{
//定義變量
std::vector<Rect> faces;
std::vector<Rect>eyes;
Mat srcFace, grayFace, eqlHistFace;
int eye_number = 0;
cvtColor(dispFace, grayFace, CV_BGR2GRAY);
equalizeHist(grayFace, eqlHistFace); //直方圖均衡化
//人臉檢測******************
face_cascade.detectMultiScale(eqlHistFace, faces, 1.1, 3, 0 | CV_HAAR_SCALE_IMAGE, Size(10, 10));
//增大第四個參數可以提高檢測精度,但也可能會造成遺漏
//人臉尺寸minSize和maxSize,關鍵參數,自行設定,随圖檔尺寸有很大關系,
for (unsigned int i = 0; i < faces.size(); i++)
{
//用藍色橢圓标記檢測到的人臉
Point center(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height / 2);
ellipse(dispFace, center, Size(faces[i].width / 2, faces[i].height * 65 / 100), 0, 0, 360, Scalar(255, 0, 0), 2, 8, 0);
//人眼檢測*****************
Mat faceROI = eqlHistFace(faces[i]);
eyes_cascade.detectMultiScale(faceROI, eyes, 1.2, 3, 0 | CV_HAAR_SCALE_IMAGE, Size(15, 15), Size(80, 80));
eye_number += eyes.size();//人眼計數
//用綠色圓标記檢測到的人眼*****************
for (unsigned int j = 0; j <eyes.size(); j++)
{
Point center(faces[i].x + eyes[j].x + eyes[j].width / 2, faces[i].y + eyes[j].y + eyes[j].height / 2);
int radius = cvRound((eyes[j].width + eyes[i].height)*0.25);
circle(dispFace, center, radius, Scalar(0, 255, 0), 2, 8, 0);
}
}
//*****************3.0檢測結果輸出*****************
cout << "檢測結果\n人臉: " << faces.size() << " 張" << endl;
cout << "人眼: " << eye_number << " 隻" << endl;
imshow("人臉識别結果", dispFace);
}