• 形状识别之直线检测


    形状识别中常见的即是矩形框的识别,识别的主要步骤通常是:图像二值化,查找轮廓,四边形轮廓筛选等。当识别的目标矩形有一条边被部分遮挡,如图1所示,传统的识别方法就不能达到识别的目的。

    图1
    图1


    在这里,提供一种识别的思路,仅供参考。识别的最终目标就是想识别出身份证的四条边,通过计算四条边的交点最后得到四边形的轮廓。主要涉及的问题有如下几点:

    1. 直线检测
    2. 直线聚类
    3. 直线筛选
    4. 交点计算
    5. 交点排序

    1.直线检测

    常规直线检测方法即是Hough。这里推荐使用一种比较新的直线检测算法LSD

    算法的具体使用请参考网站提供的源码。

    图2和图3分别是Hough直线检测与LSD直线检测的结果示意图。

    对于LSD算法得到的结果,可以根据直线的长度进行初步的筛选,得到更好的检测结果,提高后期处理效率。如图4所示。

    Hough
    图2

    LSD
    图3

    lsd2
    图4


    2.直线聚类

    由图4可以看出,身份证的每条边缘被分割成几段短线段,这里给出将每条边上的短线段聚为一类的方法。
    在极坐标系下的一点 (ρ,θ) 即定义一条直线,其中 ρ 表示极坐标原点到直线的距离, θ 为如图所示夹角。如图5。

    5
    图5

    此时不难看出,身份证同一边上的线段应该具有相近的极坐标点。

    具体做法是,先选取极坐标系的原点O为图像的重点(w/2,h/2)。建立笛卡尔坐标系 x=uw/2,y=h/2v ;其中 (u,v) 是图像坐标系。极坐标系 (ρ,θ) 与笛卡尔坐标系 (x,y) 的转换关系为 ρ=xcos(θ)+ysin(θ) 。因此,当已知一线段的两个端点 (u1,v1),(u2,v2) ,即可求解出对应的 (ρ,θ) 。具体角度的计算请参考直线检测之极坐标表示

    代码如下:

    //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
    图6


    3.直线筛选

    由图6可以看出,图中不仅有身份证边缘的直线,同样存在其他干扰直线,并且背景环境越复杂,干扰的直线会越多。此时就需要对直线进行筛选。这里进行筛选的思路是,采集图6中所示红色线段两侧的图像数据,计算颜色特征H,S,V。针对图6,手上的颜色特征明显区别于身份证边缘的特征,很容易去除。数据获取如图7所示,图中红色和蓝色区域即是对应线段的采集样本区域。

    sample
    图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所示。
    result
    图8

  • 相关阅读:
    工程结构
    生活决策
    工作原则概要与列表
    生活原则概要与列表
    在Windows2008下安装SQL Server 2005无法启动服务的解决办法
    MySQL启动提示High Severity Error解决方案
    知乎页面颜色个性化修改
    博客园 CodingLife 模板 翻页样式美化方法
    【翻译】GitHub Pages Basics 基本使用帮助【一】GitHub Pages 是什么?
    【翻译】GitHub Pages Basics 基本使用帮助【首页】
  • 原文地址:https://www.cnblogs.com/brother-louie/p/13976565.html
Copyright © 2020-2023  润新知