形狀識別之直線檢測(LSD)
轉自一篇基於直線檢測的形狀識別方法,不同於霍夫直線檢測。
原文網址:https://blog.csdn.net/liujiabin076/article/details/74917605
LSD官網(原始碼下載):http://www.ipol.im/pub/art/2012/gjmr-lsd/
LSD官網(線上測試):http://demo.ipol.im/demo/gjmr_line_segment_detector/
作者檢測四邊形的思路主要為:
- 直線檢測
- 直線聚類
- 直線篩選
- 交點計算
- 交點排序
=================以下為轉發網址原內容====================
形狀識別中常見的即是矩形框的識別,識別的主要步驟通常是:影象二值化,查詢輪廓,四邊形輪廓篩選等。當識別的目標矩形有一條邊被部分遮擋,如圖1所示,傳統的識別方法就不能達到識別的目的。
圖1
在這裡,提供一種識別的思路,僅供參考。識別的最終目標就是想識別出身份證的四條邊,通過計算四條邊的交點最後得到四邊形的輪廓。主要涉及的問題有如下幾點:
- 直線檢測
- 直線聚類
- 直線篩選
- 交點計算
- 交點排序
1.直線檢測
常規直線檢測方法即是Hough。這裡推薦使用一種比較新的直線檢測演算法LSD。
演算法的具體使用請參考網站提供的原始碼。
圖2和圖3分別是Hough直線檢測與LSD直線檢測的結果示意圖。
對於LSD演算法得到的結果,可以根據直線的長度進行初步的篩選,得到更好的檢測結果,提高後期處理效率。如圖4所示。
圖2
圖3
圖4
2.直線聚類
由圖4可以看出,身份證的每條邊緣被分割成幾段短線段,這裡給出將每條邊上的短線段聚為一類的方法。
在極座標系下的一點(ρ,θ)(ρ,θ)即定義一條直線,其中ρρ表示極座標原點到直線的距離,θθ為如圖所示夾角。如圖5。
圖5
此時不難看出,身份證同一邊上的線段應該具有相近的極座標點。
具體做法是,先選取極座標系的原點O為影象的重點(w/2,h/2)。建立笛卡爾座標系x=u−w/2,y=h/2−vx=u−w/2,y=h/2−v;其中(u,v)(u,v)是影象座標系。極座標系(ρ,θ)(ρ,θ)與笛卡爾座標系(x,y)(x,y)的轉換關係為ρ=xcos(θ)+ysin(θ)ρ=xcos(θ)+ysin(θ)
程式碼如下:
//p[0] u1 p[1] v1
//p[2] u2 p[3] v2
Vec2d getPolarLine(Vec4d p )
{
if(fabs(p[0]-p[2]) < 1e-5 )//垂直直線
{
if(p[0] > 0)
return Vec2d(p[0],0);
else
return Vec2d(p[0],CV_PI);
}
if(fabs(p[1]-p[3]) < 1e-5 ) //水平直線
{
if(p[1] > 0)
return Vec2d(p[1],CV_PI/2);
else
return Vec2d(p[1],3*CV_PI/2);
}
float k = (p[1]-p[3])/(p[0]-p[2]);
float y_intercept = p[1] - k*p[0];
float theta;
if( k < 0 && y_intercept > 0)
theta = atan(-1/k);
else if( k > 0 && y_intercept > 0)
theta = CV_PI + atan(-1/k);
else if( k< 0 && y_intercept < 0)
theta = CV_PI + atan(-1/k);
else if( k> 0 && y_intercept < 0)
theta = 2*CV_PI +atan(-1/k);
float _cos = cos(theta);
float _sin = sin(theta);
float r = p[0]*_cos + p[1]*_sin;
return Vec2d(r,theta);
}
- 將圖4中檢測到的所有直線線段利用極座標表示,然後進行分類,同類的直線分配相同的標籤號。然後對相同標籤號的線段對應的極座標進行加權平均,即為對應直線。
演算法如下:
// vector<Vec2d> polarLines 是檢測出的所有線段對應的極座標表示
bool getIndexWithPolarLine(vector<int>& _index)
{
int polar_num = polarLines.size();
if(polar_num == 0)
{
return false;
}
_index.clear();
_index.resize(polar_num);
//初始化標籤號
for (int i=0; i < polar_num;i++)
_index[i] = i;
for (int i=0; i < polar_num-1 ;i++)
{
float minTheta = CV_PI;
float minR = 50;
Vec2d polar1 = polarLines[i];
for (int j = i+1; j < polar_num; j++)
{
Vec2d polar2 = polarLines[j];
float dTheta = fabs(polar2[1] - polar1[1]);
float dR = fabs(polar2[0] - polar1[0]);
if(dTheta < minTheta )
minTheta = dTheta;
if(dR < minR)
minR = dR;
//同類直線角度誤差不超過1.8°,距離誤差不超過8%
if(dTheta < 1.8*CV_PI/180 && dR < polar1[0]*0.08)
_index[j] = _index[i];
}
}
return true;
}
由於身份證邊緣長度是大於一定閾值的,此時,如果同類線段的長度和小於某閾值,則可以剔除掉該線段。
如圖6紅色線段為LSD檢測結果,紅色直線為線段對應極座標表示的直線。
圖6
3.直線篩選
由圖6可以看出,圖中不僅有身份證邊緣的直線,同樣存在其他干擾直線,並且背景環境越複雜,干擾的直線會越多。此時就需要對直線進行篩選。這裡進行篩選的思路是,採集圖6中所示紅色線段兩側的影象資料,計算顏色特徵H,S,V。針對圖6,手上的顏色特徵明顯區別於身份證邊緣的特徵,很容易去除。資料獲取如圖7所示,圖中紅色和藍色區域即是對應線段的採集樣本區域。
圖7
具體程式碼如下,輸入是一條線段,輸出是布林型別,表示該線段是否符合要求。
// 直線兩側取樣,計算特徵:亮度與對比度,色彩等
// Vec6d data:
// data[0] u1, data[1] v1 first line point
// data[2] u2, data[3] v2 second line point
// data[4] line width
// data[5] line length
// ksize 取樣引數
bool lineSidesFeature(Mat _input,Vec6d data,int ksize )
{
//_input 輸入的影象資料,為彩色圖
Mat gray;
if(_input.channels() == 3)
cvtColor(_input,gray,CV_BGR2GRAY);
else
_input.copyTo(gray);
// Mat drawIm = _input.clone();
float x1 = data[0];
float y1 = data[1];
float x2 = data[2];
float y2 = data[3];
// line(_drawIm,Point2f(x1,y1),Point2f(x2,y2),Scalar(0,255,0));
// imshow("Sample line",drawIm);
// waitKey(10);
//直線左右兩側的灰度值
vector<int> left_side;
vector<int> right_side;
//灰度值和
int left_sum = 0;
int right_sum = 0;
//色調與飽和度
int left_H = 0;
float left_S = 0.0;
int right_H = 0;
float right_S = 0.0;
//彩色畫素個數
int color_pix_num = 0;
//取樣總數
int sample_num = 0;
//垂直直線?
int vertical = 0;
//直線斜率 截距
float _k = 0;
float _b = 0;
//計算直線表示式
if(fabs(x1-x2) < 1e-2) // x = b;
{
vertical = 1;
_b = x1;
}
else
{
_k = (y1-y2)/(x1-x2); //直線方程 kx + b = y;
_b= y1 - _k*x1;
}
// cout<<"vertical = "<<vertical<<endl;
// fabs(_k) > 1,取樣的直線是水平的,對應圖7中紅色區域
// fabs(_k) < 1, 取樣的直線是垂直的,對應圖7中藍色區域
int sample_line_type = fabs(_k) > 1 ? 1 : 0;
// cout<<"sample_line_type = "<< sample_line_type<<endl;
//設定取樣點的起點,取線段端點,並且是x值最小或者y值最小,依據sample_line_type 的值,
//這樣迴圈取下一個點時可以不斷遞增+1
float u1,u2;
int step = 1; // 這裡step = 1,如果step = -1;那麼起點又得是最大值。
if(vertical == 1)
{
if(y1 < y2)
{
u1 = y1;
u2 = y2;
}
else
{
u1 = y2;
u2 = y1;
}
}
else
{
if(sample_line_type == 1)
{
if(y1 < y2)
{
u1 = y1;
u2 = y2;
}
else
{
u1 = y2;
u2 = y1;
}
}
else
{
if(x1 < x2)
{
u1 = x1;
u2 = x2;
}
else
{
u1 = x2;
u2 = x1;
}
}
}
// cout <<"step = "<<step <<endl;
// 從直線的一個端點開始進行取樣,該端點要麼離影象座標u最近,要麼離影象座標v最近,步長為step
// 得到取樣點後,計算過該點垂直於直線的法線,法線上的點作為樣本點。
// 在法線上採集的樣本點個數為 2*ksize+ 1, 步進的方向依據直線的斜率決定是沿x方向還是y方向,步長為1。
for (float u = u1; u<= u2; u += step)
{
float v0 ;
float v1,v2;
//取樣直線的斜率與截距
float sk,sb;
if(vertical == 1)
{
v0 = x1;
}
else
{
if(sample_line_type == 1)
{
v0 = (u - _b)/_k;
sk = -1/(1e-6 +_k);
sb = u - sk*v0;
}
else
{
v0 = _k*u + _b;
sk = -1/(1e-6 +_k);
sb = v0 - sk*u;
}
}
v1 = v0 - ksize;
v2 = v0 + ksize;
// cout<<"v1 = "<<v1<<", v2 = "<<v2<<endl;
if(vertical == 1)
{
line(_drawIm,Point2f(v1,u),Point2f(v2,u),Scalar(0,0,255));
}
else
{
if(sample_line_type == 1)
{
line(_drawIm,Point2f(v1,sk*v1 + sb),Point2f(v2,sk*v2 + sb),Scalar(0,0,255));
}
else
line(_drawIm,Point2f((v1-sb)/sk,v1),Point2f((v2-sb)/sk,v2),Scalar(255,0,0));
}
// sample_line_type = 1 ,點P0(v0,u)在直線上,垂直於該直線並過點P0,進行取樣,按x方向步長為1進行步進,起點x = v1,終點x =v2;
// sample_line_tpye = 0, 點P0(u,v0)在直線上,垂直於該直線並過點P0,進行取樣,按y方向步長為1進行步進, 起點y = v1, 終點y = v2。
for (float v = v1; v <= v2; v += 1)
{
sample_num++;
int x , y;
if(vertical == 1) //垂直線段
{
x = (int)v;
y = (int)u;
}
else
{
if(sample_line_type == 1) //“水平”取樣,v按照x方向遞增
{
x = (int)v;
y = (int)(sk*v + sb);
}
else //“垂直”取樣, v按照y方向遞增
{
x = (int)((v-sb)/sk);
y = (int)v;
}
}
//這一句很重要。
if(x < 0 || x > gray.cols-1 || y < 0 || y > gray.rows-1)
continue;
int nx = MAX(0,x);
nx = min(nx,gray.cols-1);
int ny = MAX(0,y);
ny = min(ny,gray.rows-1);
int val = gray.at<uchar>(ny,nx);
Vec3b pixel = _input.at<Vec3b>(ny,nx);
int b = pixel[0];
int g = pixel[1];
int r = pixel[2];
int _max = MAX(b,MAX(g,r));
int _min = MIN(b,MIN(g,r));
int C = _max - _min ;
float S = 0;
int H = 0;
if(C > 10)
{
S = (float)C/_max;
int vr = _max == r ? -1 : 0;
int vg = _max == g ? -1 : 0;
H = (vr & (g - b)) +
(~vr & ((vg & (b - r + 2 * C)) + ((~vg) & (r - g + 4 * C))));
H = (H * hdiv_table180[C] + (1 << (hsv_shift-1))) >> hsv_shift;
H += H < 0 ? 180 : 0;
//色度判斷直線兩邊相似的顏色
if( S > 0.1 && H > 10 )
color_pix_num++;
}
if(vertical == 1)
{
if(nx > _b)
{
right_side.push_back(val);
right_sum += val;
right_H += H;
right_S += S;
}
else
{
left_side.push_back(val);
left_sum += val;
left_H += H;
left_S += S;
}
}
else
{
float d = _k*nx + _b - ny;
if(d > 0 )
{
right_side.push_back(val);
right_sum += val;
right_H += H;
right_S += S;
}
else
{
left_side.push_back(val);
left_sum += val;
left_H += H;
left_S += S;
}
}
} //v
}//u
int l_num = left_side.size();
int r_num = right_side.size();
// cout << l_num <<" "<< r_num << endl;
float left_mean = (float) (left_sum)/l_num;
float right_mean = (float) (right_sum)/ r_num;
float left_H_mean = (float)(left_H)/l_num;
float left_S_mean = (float)(left_S)/l_num;
float right_H_mean = (float)(right_H)/r_num;
float right_S_mean = (float)(right_S)/r_num;
float left_var = 0, right_var = 0;
for (int m = 0; m < l_num; m++)
{
left_var += (left_side[m] - left_mean)*(left_side[m] - left_mean);
}
if(l_num > 2)
left_var = sqrtf(left_var)/(l_num-1);
for (int n = 0; n < r_num; n++)
{
right_var += (right_side[n] - right_mean)*(right_side[n] - right_mean);
}
if(r_num > 2)
right_var = sqrtf(right_var)/(r_num-1);
cout<<"亮度: "<<left_mean <<" "<<right_mean<<endl;
cout<<"飽和度:"<<left_S_mean<<" "<<right_S_mean<<" "<<fabs(left_S_mean - right_S_mean)/(1e-5+MAX(left_S_mean,right_S_mean))<<endl; // 篩選直線兩側顏色差異 參考值0.4
cout<<left_H_mean <<" "<<right_H_mean<<" "<<fabs(left_H_mean - right_H_mean)/(1e-5+MAX(left_H_mean,right_H_mean))<<endl; // 篩選直線兩側顏色差異 參考值 0.15
cout<<"方差/均值:"<<left_var/left_mean<<" "<<right_var/right_mean<<endl; //篩選平滑區域
// imshow("Sample line",drawIm);
// waitKey(0);
return false;
}
由於待測身份證的邊緣鄰域顏色特徵是穩定的,可以作為初始經驗值,當識別線段的顏色特徵不符合經驗值要求即可剔除掉,最後得到想要的邊緣線段以及對應的極座標表示直線。然而,有時候可能得到滿足條件的直線比較多,此時可以考慮為每一類直線進行評分,然後根據得分排序,取出前4條得分最高的直線,大部分情況下都是所求邊緣直線。具體情況可具體對待,此處不再展開。
4.交點計算
這裡給出極座標系下直線的求交點方法,這裡主要注意兩點:首先,兩條直線不是平行的,其次,直線的交點在影象範圍內。
Point2f polarLinesCorss(Vec2d l0, Vec2d l1,Size sz)
{
int w = sz.width;
int h = sz.height;
float r0 = l0[0];
float theta0 = l0[1];
float _cos0 = cos(theta0);
float _sin0 = sin(theta0);
float r1 = l1[0];
float theta1 = l1[1];
float _cos1 = cos(theta1);
float _sin1 = sin(theta1);
if(fabs(_cos0*_sin1 - _sin0*_cos1) < 1e-5) //兩條平行的直線
return Point2f(0,0);
float y = (r0*_cos1 - r1*_cos0) / (_sin0*_cos1 - _cos0*_sin1);
float x = (r0*_sin1 - r1*_sin0) / (_cos0*_sin1 - _cos1*_sin0);
if(x > - w/2 && x < w/2 && y > -h/2 && y < h/2)
return Point2f(x+w/2,h/2-y);
else
return Point2f(0,0);
}
5.交點排序
得到四個交點,此時點的順序可能是錯亂的,需要對點進行排序,起點選擇為左上角的點,並按逆時針方向對點排序。方法如下:
// 以左上角點為起點逆時針排序
static void sortPoints(vector<Point2f> & points )
{
vector<Point2f> minXpoints;
vector<Point2f> maxXpoints;
minXpoints.push_back(points[0]);
minXpoints.push_back(points[1]);
maxXpoints.push_back(points[2]);
maxXpoints.push_back(points[3]);
for(int i =0; i< 2; i++)
{
float x = minXpoints[i].x;
if(x > maxXpoints[0].x)
{
if(x >= maxXpoints[1].x)
{
(maxXpoints[0].x > maxXpoints[1].x ) ? swap(maxXpoints[1],minXpoints[i]) : swap(maxXpoints[0],minXpoints[i]);
continue;
}
if(x < maxXpoints[1].x)
{
swap(maxXpoints[0],minXpoints[i]);
continue;
}
}
if(x <= maxXpoints[0].x)
if(x > maxXpoints[1].x )
{
swap(minXpoints[i], maxXpoints[1]);
}
}
if(minXpoints[0].y > minXpoints[1].y)
{
points[0] = minXpoints[1];
points[1] = minXpoints[0];
}
else
{
points[0] = minXpoints[0];
points[1] = minXpoints[1];
}
if(maxXpoints[0].y > maxXpoints[1].y)
{
points[2] = maxXpoints[0];
points[3] = maxXpoints[1];
}
else
{
points[2] = maxXpoints[1];
points[3] = maxXpoints[0];
}
}
最後,檢測結果如圖8所示。
圖8