通过几何关系判断二维码的朝向

序言

在一些质量检测中,由于二维码不是直接打印上去的,而是由人工张贴,则有可能存在质量问题。需要对二维码的张贴是否为正向,以及是否贴偏进行自动检测。

二维码的结构及基本原理

有关二维码的结构和基本原理可以参考这一篇博客。 基于OpenCV实现二维码发现与定位
简单来说就是通过二维码的三个定位框,可以确切地定位到一个二维码的位置。
二维码的定位框的黑白区域面积比是确定的。

设计思路

  1. 在全图中搜索二维码的定位框。若定位框数量为3个,求出它们的质心坐标,进入下一步; 否则表明图中没有一个完整的二维码;
  2. 通过三个定位框的质心坐标,构建直角三角形。若可以构建,即其中一个角近似为直角,则进入下一步; 否则表明三个定位框不是来自一个二维码;
  3. 计算非直角边的斜率。若斜率不为负数,进入下一步; 否则表明贴偏;
  4. 以图像(0, 0)点为原点,判断非直角边的对应点是否位于线段内测。若是,进入下一步; 否则表明贴反;
  5. 根据非直角边的斜率,计算二维码偏移的角度。若偏移角在允许的范围内,则二维码贴正; 否则二维码贴偏。

代码实现

这里参考了这一篇博客,在其基础上增加了我们需求的功能。 通过二维码定位框确定二维码位置

按照上一步的设计思路,输入参数为图像和两个容器,返回二维码的偏移角。

  • 此处设计当已检测到二维码不为正向时,会直接返回180度,此处没有实际意义,仅表明没贴正。
double DetectionApi::FindQrPoint(Mat& srcImg, vector<Point2f>& qrCentroids, vector<Point>& qrCodeAreaContour, double delta)
{
    /*
        寻找二维码定位点

        srcImg: 原RGB图
        qrCentroids: 二维码的三个定位框质心
        qrCodeAreaContour: 连接得到的二维码坐标区域
        
        return: 二维码偏移角
    */

	qrCentroids.clear();
	//彩色图转灰度图
	Mat src_gray;
	cvtColor(srcImg, src_gray, COLOR_RGB2GRAY);

	//二值化
	Mat threshold_output;
	threshold(src_gray, threshold_output, 0, 255, THRESH_BINARY | THRESH_OTSU);
	Mat threshold_output_copy = threshold_output.clone();

	//调用查找轮廓函数
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	//hierarchy说明:
	//-- hierarchy[0]: 同级的下一条轮廓。   hierarchy[1]: 同级的上一条轮廓。
	//-- hierarchy[2]: 下级的第一个子节点。 hierarchy[3]: 上级的父节点。
	//-- findContours算子中选择CV_RETR_TREE参数。
	findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
	
	//只找有三级轮廓的地方
	int levelsNum = 3;

	//用于寻找二维码的最小包裹矩形
	qrCodeAreaContour.clear();

	for(int contourIndex = 0; contourIndex < contours.size(); contourIndex++)
	{		
		int level = 0;
		int chirldIdx = hierarchy[contourIndex][2]; //[2]: 下级的第一个子节点
		level++;    //当前轮廓自身作为第一级

		while (chirldIdx != -1) //只要当前轮廓还有下一级就继续循环
		{
			chirldIdx = hierarchy[chirldIdx][2];
			level++;
		}

		if (level != levelsNum)
		{
			continue;
		}

		//进行父、子轮廓的面积筛选
		int firstLevel = contourIndex;
		int secondLevel = hierarchy[firstLevel][2];
		int lastLevel = hierarchy[secondLevel][2];

		//分别计算三级轮廓的面积
		float firstArea = contourArea(contours[firstLevel],false);//49
		float secondArea = contourArea(contours[secondLevel], false);//25
		float lastArea = contourArea(contours[lastLevel], false);//9

		//实际值:第一层级与第二层级的面积比
		float realFirstVsSecond = firstArea / (secondArea + 1e-5);
		//实际值:第二层级与第三层级的面积比
		float realSecondVsLast = secondArea / (lastArea + 1e-5);

		//二维码定位点的黑白宽度比例为 1 : 1 : 3 : 1 : 1。 
		//第一层级轮廓的标称边长为(1+1+3+1+1=7),标称面积为7*7=49;
		//第二层级轮廓的标称边长为(    1+3+1=5),标称面积为5*5=25;
		//第三层级轮廓的标称边长为(        3=3),标称面积为3*3=9;
		float firstVsSecond = 49.0 / 25.0;   //标称值:第一层级与第二层级的面积比
		float secondVsLast = 25.0 / 9.0;     //标称值:第二层级与第三层级的面积比
		float ratio = 0.5;   //允许偏差
		//父、子轮廓的面积筛选
		if(realFirstVsSecond > firstVsSecond / (1 + ratio) && realFirstVsSecond < firstVsSecond / (1 - ratio)
			&& realSecondVsLast > secondVsLast / (1 + ratio) && realSecondVsLast < secondVsLast / (1 - ratio))
		{
			//计算质心. 为提高稳定性,以三级轮廓的质心平均值作为最后的质心
			Moments mu;
			Point2f mc = cv::Point2f(0, 0);
			mu = moments(contours[firstLevel], false);
			mc += Point2f(mu.m10 / mu.m00, mu.m01 / mu.m00);

			mu = moments(contours[secondLevel], false);
			mc += Point2f(mu.m10 / mu.m00, mu.m01 / mu.m00);

			mu = moments(contours[lastLevel], false);
			mc += Point2f(mu.m10 / mu.m00, mu.m01 / mu.m00);
			mc /= 3.0;

			qrCentroids.push_back(mc);

			//将一个vector连接另一个vector(qrCodeAreaContour)末尾
			copy(contours[firstLevel].begin(), contours[firstLevel].end(), back_inserter(qrCodeAreaContour));
		}
	}

    //寻找直角边
    if(qrCentroids.size() == 3){
        //构建三边
        Vec4d line0(qrCentroids[1].x, qrCentroids[1].y, qrCentroids[2].x, qrCentroids[2].y);
        Vec4d line1(qrCentroids[0].x, qrCentroids[0].y, qrCentroids[2].x, qrCentroids[2].y);
        Vec4d line2(qrCentroids[0].x, qrCentroids[0].y, qrCentroids[1].x, qrCentroids[1].y);

        //求各边夹角
        Vec4d v[3] = {lines_intersection(line0, line1), lines_intersection(line0, line2), lines_intersection(line1, line2)};

        double bias = 3;    //允许的角度误差
        for(int i = 0; i < 3; i++)
        {
            //满足直角三角形的条件
            if(v[i][3] >= 90 - bias && v[i][3] <= 90 + bias)
            {
                //根据直角定位到非直角边
                double x1, y1, x2, y2, rightangle_x, rightangle_y;
                if(i == 0) {
                    x1 = qrCentroids[0].x, y1 = qrCentroids[0].y,
                    x2 = qrCentroids[1].x, y2 = qrCentroids[1].y;
                    rightangle_x = qrCentroids[2].x, rightangle_y = qrCentroids[2].y;
                }                
                else if(i == 1) {
                    x1 = qrCentroids[0].x, y1 = qrCentroids[0].y,
                    x2 = qrCentroids[2].x, y2 = qrCentroids[2].y;
                    rightangle_x = qrCentroids[1].x, rightangle_y = qrCentroids[1].y;
                }
                else if(i == 2) {
                    x1 = qrCentroids[1].x, y1 = qrCentroids[1].y,
                    x2 = qrCentroids[2].x, y2 = qrCentroids[2].y;
                    rightangle_x = qrCentroids[0].x, rightangle_y = qrCentroids[0].y;
                }
                
                //非直角边斜率
                double k = (y2 - y1) / (x2 - x1);

                //仅判断斜率为负的情况,若为正,则表明为偏
                if(k < 0)
                {
                    //判断非直角边对应点是否位于线段内测,若不在,则表明为反
                    //以图像(0,0)为原点,两点式纵截距
                    double b = y1 + (-1 * x1 * (y2 - y1)) / (x2 - x1);
                    double direction_val = k * rightangle_x - rightangle_y + b;
                    if(direction_val > 0)
                    {
                        //表明在线段内测
                        //根据斜率获取偏移角度
                        delta = abs(abs(atan(k) * 180 / CV_PI) - 45);
                    }
                    else delta = 180;
                }
                else delta = 180;
            }
        }        
    }
    else delta = 180;   //180仅代表不合格,无实际意义

	//返回角度
	return delta;
}

最后返回的二维码偏移角度,应根据个人需求,判定角度在什么范围内为合格。

  • lines_intersection为求两条线段夹角的函数,可自行实现。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值