opencv4.0.1 qr二维码定位识别源码详解(一)

一、概述

opencv4.0版本以后,加入了二维码定位解码的功能,其主要功能基于quirc开源库,下载地址GitHub。约1200行代码,识别与定位占了约800行,解码部分不作赘述,直接调用quric库解码。
之前版本不包括定位功能,也有博主做了相关的功能二维码特征定位,这篇中主要是根据二位码三个定位图案的轮廓特征取得三个定位点,由于三个图案都具有两个子轮廓,通过findcontours()函数可以很快找到。之后再进行线性变换得到第四个点,但这篇博文中寻找第四个点的方法不太准确,因为找到四个点后需要进行透视变换,属于非线性变换,这篇中的方法在大多数情况下还是挺好用的。
得到二维码后可通过zbar,zxing库解码,这个不多说了。直接看4.0中的源码来分析定位方法吧!

二、QRCodeDetector 类结构

包括QRCodeDetctor()、~QRCodeDetector():构造与析构函数;
setEpsx()、setEpsx():设置水平、垂直扫描检测的系数,默认值分别是0.2,0.1。
bool detect(InputArray img, OutputArray points):重点函数,用于定位。
参数img:输入图像;points:返回包括二维码的最小四边形的点集。返回bool值为是否检测到二维码。
decode():对二维码进行解码,返回包含二维码内容的字符串。
string detectAndDecode(InputArray img, OutputArray points=noArray(), OutputArray straight_qrcode = noArray()):将之前函数结合,定位加解码返回字符串。

class CV_EXPORTS_W QRCodeDetector
{
public:
    CV_WRAP QRCodeDetector();
    ~QRCodeDetector();

    /** @brief sets the epsilon used during the horizontal scan of QR code stop marker detection.
     @param epsX Epsilon neighborhood, which allows you to determine the horizontal pattern
     of the scheme 1:1:3:1:1 according to QR code standard.
    */
    CV_WRAP void setEpsX(double epsX);
    /** @brief sets the epsilon used during the vertical scan of QR code stop marker detection.
     @param epsY Epsilon neighborhood, which allows you to determine the vertical pattern
     of the scheme 1:1:3:1:1 according to QR code standard.
     */
    CV_WRAP void setEpsY(double epsY);

    /** @brief Detects QR code in image and returns the quadrangle containing the code.
     @param img grayscale or color (BGR) image containing (or not) QR code.
     @param points Output vector of vertices of the minimum-area quadrangle containing the code.
     */
    CV_WRAP bool detect(InputArray img, OutputArray points) const;

    /** @brief Decodes QR code in image once it's found by the detect() method.
     Returns UTF8-encoded output string or empty string if the code cannot be decoded.

     @param img grayscale or color (BGR) image containing QR code.
     @param points Quadrangle vertices found by detect() method (or some other algorithm).
     @param straight_qrcode The optional output image containing rectified and binarized QR code
     */
    CV_WRAP std::string decode(InputArray img, InputArray points, OutputArray straight_qrcode = noArray());

    /** @brief Both detects and decodes QR code

     @param img grayscale or color (BGR) image containing QR code.
     @param points opiotnal output array of vertices of the found QR code quadrangle. Will be empty if not found.
     @param straight_qrcode The optional output image containing rectified and binarized QR code
     */
    CV_WRAP std::string detectAndDecode(InputArray img, OutputArray points=noArray(),
                                        OutputArray straight_qrcode = noArray());
protected:
    struct Impl;
    Ptr p;
};

//! @} objdetect
}

三、QRDetect类结构

主要函数:
init():初始化,对图像大小进行处理,二值化处理转为黑白图;
localization():定位,确定三个定位图块的中心点;
computeTransformationPoints():计算四个特征点,即左上、左下、右上、右下四个点;
intersectionLines():计算两条直线交叉点;

class QRDetect
{
public:
    void init(const Mat& src, double eps_vertical_ = 0.2, double eps_horizontal_ = 0.1);
    bool localizationAT(); // use apriltag to locate key points
    bool localization();
    bool computeTransformationPoints();
    Mat getBinBarcode() { return bin_barcode; }
    Mat getStraightBarcode() { return straight_barcode; }
    vector getTransformationPoints() { return transformation_points; }
    static Point2f intersectionLines(Point2f a1, Point2f a2, Point2f b1, Point2f b2);
protected:
    vector searchHorizontalLines();
    vector separateVerticalLines(const vector &list_lines);
    void fixationPoints(vector &local_point);
    vector getQuadrilateral(vector angle_list);
    bool testBypassRoute(vector hull, int start, int finish);
    inline double getCosVectors(Point2f a, Point2f b, Point2f c);

    Mat barcode, bin_barcode, straight_barcode;
    vector localization_points, transformation_points;
    double eps_vertical, eps_horizontal, coeff_expansion;
};

四、原理分析

opencv4.0中的定位,根据定位图案的黑白间隔固定比例:1:1:3:1:1,即水平或垂直扫描得到五条具有近似比例的线段就将其视为定位图案的一部分。
1、水平、垂直扫描后得到三个图案中心点的集合;
2、利用kmeans聚类可以将三个集合迭代为三个中心点,这样就得到了定位图案的中心;
3、再利用角度和面积关系确定定位点的顺序。
4、floodfill获得定位图案的外框,根据对角线距离最远确定左下和右上两个特征点,再根据距离关系确定左上特征点;
5、还是利用距离确定左下和右上图案中距离左上角最远的两个点,之前的特征点连线相交即为右下角特征点;
6、进行透视变换;
7、解码。
第二张图直接用的别人的:)定位图案结构
在这里插入图片描述

五、函数分析

水平扫描函数:QRDetect::searchHorizontalLines()
由于图像已转为黑白二值图,通过分析每行黑边像素分布特征即可得到中心点的位置。
pixels_position用来储存黑白边界像素点的位置,test_line储存了某个像素点前后连续五条黑白线段的长度,根据比例来检测是否符合,系数eps_vertical限定了包容程度,毕竟图片可能有变形扭曲,比例不会完全符合。通过这个方法,得到的点总是第三个交界点,减二就是最左边边界点。
其次,如果图像不是横平竖直的,倾斜状态下根据相似性这个比例还是符合的。
最后返回的是Vec3d的向量,分别储存了该行中定位图案最左边线的点的列数(第一个交界点)、行数、扫描所得图案横向像素长度(非标准长度,通过比例计算具体值不重要)。
在这里插入图片描述在这里插入图片描述

vector QRDetect::searchHorizontalLines()//水平扫描
{
    vector result;
    const int height_bin_barcode = bin_barcode.rows;//行
    const int width_bin_barcode  = bin_barcode.cols;//列数
    const size_t test_lines_size = 5;
    double test_lines[test_lines_size];
    vector pixels_position;

    for (int y = 0; y < height_bin_barcode; y++)
    {
        pixels_position.clear();
        const uint8_t *bin_barcode_row = bin_barcode.ptr(y);//行指针

        int pos = 0;
        for (; pos < width_bin_barcode; pos++) { if (bin_barcode_row[pos] == 0) break; }//检测像素,黑点跳出循环
        if (pos == width_bin_barcode) { continue; }

        pixels_position.push_back(pos);
        pixels_position.push_back(pos);
        pixels_position.push_back(pos);//每行黑色起始点记录三次?

        uint8_t future_pixel = 255;
        for (int x = pos; x < width_bin_barcode; x++)//继续处理该行像素
        {
            if (bin_barcode_row[x] == future_pixel)//检测白色,黑色转换点
            {
                future_pixel = 255 - future_pixel;//future_pixel,值为0
                pixels_position.push_back(x);//记录点
            }
        }
        pixels_position.push_back(width_bin_barcode - 1);//每行最后点,pixels_position每行点元素
        for (size_t i = 2; i < pixels_position.size() - 4; i+=2)//size-4,避免越界
        {
            test_lines[0] = static_cast(pixels_position[i - 1] - pixels_position[i - 2]);
            test_lines[1] = static_cast(pixels_position[i    ] - pixels_position[i - 1]);
            test_lines[2] = static_cast(pixels_position[i + 1] - pixels_position[i    ]);
            test_lines[3] = static_cast(pixels_position[i + 2] - pixels_position[i + 1]);
            test_lines[4] = static_cast(pixels_position[i + 3] - pixels_position[i + 2]);//五条直线长度具有固定比例,用来区分定位图案

            double length = 0.0, weight = 0.0;

            for (size_t j = 0; j < test_lines_size; j++) { length += test_lines[j]; }

            if (length == 0) { continue; }
            for (size_t j = 0; j < test_lines_size; j++)
            {
                if (j != 2) { weight += fabs((test_lines[j] / length) - 1.0/7.0); }
                else        { weight += fabs((test_lines[j] / length) - 3.0/7.0); }//定位图案黑白间隔1:1:3:1:1,第三条线最长,
            }

            if (weight < eps_vertical)//eps_vertical,0.2,衡量是否水平的指数?
            {
                Vec3d line;
                line[0] = static_cast(pixels_position[i - 2]);//找到定位图案的边缘点,最左点
                line[1] = y;
                line[2] = length;
                result.push_back(line);//Vector<3d>
            }
        }
    }
    return result;
}

垂直扫描函数:QRDetect::separateVerticalLines(const vector &list_lines)
继续处理水平扫描所得点集,原理和水平扫描相似,先向下扫描,后向上扫描。这时得到的是六条线段而不是五条,x = cvRound(list_lines[pnt][0] + list_lines[pnt][2] * 0.5)这里是得到图案中心的近似列数,理论上是位于黑色小方块中间,向上向下都会得到三条线段。其中比例检测中weight += fabs((test_lines[i] / length) - 3.0/14.0)这部分的意思应该是只取中心方块垂直方向一小部分的像素点,3.0/14.0衡量了靠近中心的程度,test_[line0]是向下扫描的第一段,test_line[3]是向上扫描的第一段,越靠近中心weight值越小。
最后返回的是符合水平、垂直要求的像素点的集合。
这个方法很好,得到了三个中心点的可能特征点的集合。
在这里插入图片描述

vector QRDetect::separateVerticalLines(const vector &list_lines)//list_lines,之前的水平扫描所得result
{
    vector result;
    int temp_length = 0;
    uint8_t next_pixel;
    vector test_lines;


    for (size_t pnt = 0; pnt < list_lines.size(); pnt++)
    {
        const int x = cvRound(list_lines[pnt][0] + list_lines[pnt][2] * 0.5);//中心点坐标?定位图案最左边的点加图片中图案长度的一半即为中心坐标
        const int y = cvRound(list_lines[pnt][1]);//像素点所在行

        // --------------- Search vertical up-lines --------------- //

        test_lines.clear();
        uint8_t future_pixel_up = 255;

        for (int j = y; j < bin_barcode.rows - 1; j++)//这段循环写得好,反复计算黑色、白色线段的长度,向下检测垂直线
        {
            next_pixel = bin_barcode.ptr(j + 1)[x];
            temp_length++;
            if (next_pixel == future_pixel_up)//颜色相反,更改future__pixel_up,
            {
                future_pixel_up = 255 - future_pixel_up;
                test_lines.push_back(temp_length);
                temp_length = 0;
                if (test_lines.size() == 3) { break; }
            }
        }

        // --------------- Search vertical down-lines --------------- //

        uint8_t future_pixel_down = 255;
        for (int j = y; j >= 1; j--)//向上检测
        {
            next_pixel = bin_barcode.ptr(j - 1)[x];
            temp_length++;
            if (next_pixel == future_pixel_down)
            {
                future_pixel_down = 255 - future_pixel_down;
                test_lines.push_back(temp_length);
                temp_length = 0;
                if (test_lines.size() == 6) { break; }
            }
        }

        // --------------- Compute vertical lines --------------- //

        if (test_lines.size() == 6)
        {
            double length = 0.0, weight = 0.0;

            for (size_t i = 0; i < test_lines.size(); i++) { length += test_lines[i]; }

            CV_Assert(length > 0);
            for (size_t i = 0; i < test_lines.size(); i++)//又是计算权重??看不懂。。。
            {
                if (i % 3 != 0) { weight += fabs((test_lines[i] / length) - 1.0/ 7.0); }//假设图像局域变形不大
                else            { weight += fabs((test_lines[i] / length) - 3.0/14.0); }//只取一部分点符合abs(test_line[3]-test[0])/length<0.1
            }

            if(weight < eps_horizontal)
            {
                result.push_back(list_lines[pnt]);
            }
        }
    }

    vector point2f_result;
    for (size_t i = 0; i < result.size(); i++)
    {
        point2f_result.push_back(
              Point2f(static_cast(result[i][0] + result[i][2] * 0.5),
                      static_cast(result[i][1])));
    }
    return point2f_result;//得到三个定位图案中心点的集合
}

六、获取中心点

QRDetect::localization():中心点获取只是其中一部分内容,后续还有特征点获取处理。
这个函数中使用了Kmeans聚类的方法,使用交换迭代最后获取三个中心点,K-均指聚类大致原理:a、初始化k个不同中心点;b、将每个训练样本分配到最近中心点所代表的聚类;c、将中心点更新为该聚类中所有训练样本的均值;d、重复b,c;
在这里由于水平、垂直扫描后得到的总是三个定位图案中心点的一系列近似点集,使用kmeans聚类最后就会的到三个中心点的较为准确的位置。如果聚类得不到三个点,将返回false。

kmeans(list_lines_y, 3, labels,
           TermCriteria( TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1),
           3, KMEANS_PP_CENTERS, localization_points);

七、一些问题

4.0中好像挺喜欢用adaptiveThreshold()这个函数,二值化处理后的效果自然比threshold()好,处理后的图像轮廓更为清晰,但是检测的像素块如果取得比较大,速度可能会有点慢。
对比开篇提到的那篇文章的方法添加链接描述二者的区别我觉得主要是对三个定位图案的识别思路不同,opencv4.0是一种从内向外的方法,先找中心点,再找外围轮廓的特征点,这篇文章是从外向内搜索,即先确定有两个子轮廓的轮廓即为定位图案最外层轮廓,再通过轮廓周长确定中心点坐标,其中计算中心的Center_cal()函数如下:

Point Center_cal(vector > contours,int i)
{
      int centerx=0,centery=0,n=contours[i].size();
      //在提取的小正方形的边界上每隔周长个像素提取一个点的坐标,
      //求所提取四个点的平均坐标(即为小正方形的大致中心)
      centerx = (contours[i][n/4].x + contours[i][n*2/4].x + contours[i][3*n/4].x + contours[i][n-1].x)/4;
      centery = (contours[i][n/4].y + contours[i][n*2/4].y + contours[i][3*n/4].y + contours[i][n-1].y)/4;
      Point point1=Point(centerx,centery);
      return point1;
}

我觉得这个方法中心坐标没有kmeans聚类得到的准确,有兴趣的可以测一下。

八、下一篇

fixationPoints()函数:对聚类后的中心点通过角度和面积进行排序;
computeTransformPoints()函数:计算透视变换所需的特征点。

九、参考

opencv二维码识别代码简析
opencv4.0.0中qr码定位源码分析

你可能感兴趣的:(现学现卖-opencv)