序言
在一些质量检测中,由于二维码不是直接打印上去的,而是由人工张贴,则有可能存在质量问题。需要对二维码的张贴是否为正向,以及是否贴偏进行自动检测。
二维码的结构及基本原理
有关二维码的结构和基本原理可以参考这一篇博客。 基于OpenCV实现二维码发现与定位
简单来说就是通过二维码的三个定位框,可以确切地定位到一个二维码的位置。
二维码的定位框的黑白区域面积比是确定的。
设计思路
- 在全图中搜索二维码的定位框。若定位框数量为3个,求出它们的质心坐标,进入下一步; 否则表明图中没有一个完整的二维码;
- 通过三个定位框的质心坐标,构建直角三角形。若可以构建,即其中一个角近似为直角,则进入下一步; 否则表明三个定位框不是来自一个二维码;
- 计算非直角边的斜率。若斜率不为负数,进入下一步; 否则表明贴偏;
- 以图像(0, 0)点为原点,判断非直角边的对应点是否位于线段内测。若是,进入下一步; 否则表明贴反;
- 根据非直角边的斜率,计算二维码偏移的角度。若偏移角在允许的范围内,则二维码贴正; 否则二维码贴偏。
代码实现
这里参考了这一篇博客,在其基础上增加了我们需求的功能。 通过二维码定位框确定二维码位置
按照上一步的设计思路,输入参数为图像和两个容器,返回二维码的偏移角。
- 此处设计当已检测到二维码不为正向时,会直接返回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为求两条线段夹角的函数,可自行实现。