基于opencv 识别、定位二维码 (c++版)
原文地址:https://www.cnblogs.com/yuanchenhui/p/opencv_qr.html
前言 因工作需要,需要定位图片中的二维码;我遂查阅了相关资料,也学习了opencv开源库。通过一番努力,终于很好的实现了二维码定位。本文将讲解如何使用opencv定位二维码。
定位二维码不仅仅是为了识别二维码;还可以通过二维码对图像进行水平纠正以及相邻区域定位。定位二维码,不仅需要图像处理相关知识,还需要分析二维码的特性,本文先从二维码的特性讲起。
1 二维码特性
二维码在设计之初就考虑到了识别问题,所以二维码有一些特征是非常明显的。
二维码有三个“回“”字形图案,这一点非常明显。中间的一个点位于图案的左上角,如果图像偏转,也可以根据二维码来纠正。

思考题:为什么是三个点,而不是一个、两个或四个点。
一个点:特征不明显,不易定位。不易定位二维码倾斜角度。
两个点:两个点的次序无法确认,很难确定二维码是否放正了。
四个点:无法确定4个点的次序,从而无法确定二维码是否放正了。
识别二维码,就是识别二维码的三个点,逐步分析一下这三个点的特性
1 每个点有两个轮廓。就是两个口,大“口”内部有一个小“口”,所以是两个轮廓。
2 如果把这个“回”放到一个白色的背景下,从左到右,或从上到下画一条线。这条线经过的图案黑白比例大约为:黑白比例为1:1:3:1:1。
3 如何找到左上角的顶点?这个顶点与其他两个顶点的夹角为90度。
通过上面几个步骤,就能识别出二维码的三个顶点,并且识别出左上角的顶点。
2 使用opencv识别二维码
1) 查找轮廓,筛选出三个二维码顶点
opencv一个非常重要的函数就是查找轮廓,就是可以找到一个图中的缩所有的轮廓,“回”字形图案是一个非常的明显的轮廓,很容易找到。

1 int QrParse::FindQrPoint(Mat& srcImg, vector<vector<Point>>& qrPoint)
2 {
3 //彩色图转灰度图
4 Mat src_gray;
5 cvtColor(srcImg, src_gray, CV_BGR2GRAY);
6 namedWindow("src_gray");
7 imshow("src_gray", src_gray);
8
9 //二值化
10 Mat threshold_output;
11 threshold(src_gray, threshold_output, 0, 255, THRESH_BINARY | THRESH_OTSU);
12 Mat threshold_output_copy = threshold_output.clone();
13 namedWindow("Threshold_output");
14 imshow("Threshold_output", threshold_output);
15
16 //调用查找轮廓函数
17 vector<vector<Point> > contours;
18 vector<Vec4i> hierarchy;
19 findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
20
21 //通过黑色定位角作为父轮廓,有两个子轮廓的特点,筛选出三个定位角
22 int parentIdx = -1;
23 int ic = 0;
24
25 for (int i = 0; i < contours.size(); i++)
26 {
27 if (hierarchy[i][2] != -1 && ic == 0)
28 {
29 parentIdx = i;
30 ic++;
31 }
32 else if (hierarchy[i][2] != -1)
33 {
34 ic++;
35 }
36 else if (hierarchy[i][2] == -1)
37 {
38 ic = 0;
39 parentIdx = -1;
40 }
41
43
44 45 {
46 bool isQr = QrParse::IsQrPoint(contours[parentIdx], threshold_output_copy);
47
48 //保存找到的三个黑色定位角
49 if (isQr)
50 qrPoint.push_back(contours[parentIdx]);
51
52 ic = 0;
53 parentIdx = -1;
54 }
55 }
56
57 return 0;
58 }

找到了两个轮廓的图元,需要进一步分析是不是二维码顶点,用到如下函数:

bool QrParse::IsQrPoint(vector<Point>& contour, Mat& img)
{
//最小大小限定
RotatedRect rotatedRect = minAreaRect(contour);
if (rotatedRect.size.height < 10 || rotatedRect.size.width < 10)
return false;
//将二维码从整个图上抠出来
cv::Mat cropImg = CropImage(img, rotatedRect);
int flag = i++;
//横向黑白比例1:1:3:1:1
bool result = IsQrColorRate(cropImg, flag);
return result;
}

黑白比例判断函数:

1 //横向和纵向黑白比例判断
2 bool QrParse::IsQrColorRate(cv::Mat& image, int flag)
3 {
4 bool x = IsQrColorRateX(image, flag);
5 if (!x)
6 return false;
7 bool y = IsQrColorRateY(image, flag);
8 return y;
9 }
10 //横向黑白比例判断
11 bool QrParse::IsQrColorRateX(cv::Mat& image, int flag)
12 {
13 int nr = image.rows / 2;
14 int nc = image.cols * image.channels();
15
16 vector<int> vValueCount;
17 vector<uchar> vColor;
18 int count = 0;
19 uchar lastColor = 0;
20
21 uchar* data = image.ptr<uchar>(nr);
22 for (int i = 0; i < nc; i++)
23 {
24 vColor.push_back(data[i]);
25 uchar color = data[i];
26 if (color > 0)
27 color = 255;
28
29 if (i == 0)
30 {
31 lastColor = color;
32 count++;
33 }
34 else
35 {
36 if (lastColor != color)
37 {
38 vValueCount.push_back(count);
39 count = 0;
40 }
41 count++;
42 lastColor = color;
43 }
44 }
45
46 if (count != 0)
47 vValueCount.push_back(count);
48
49 if (vValueCount.size() < 5)
50 return false;
51
52 //横向黑白比例1:1:3:1:1
53 int index = -1;
54 int maxCount = -1;
55 for (int i = 0; i < vValueCount.size(); i++)
56 {
57 if (i == 0)
58 {
59 index = i;
60 maxCount = vValueCount[i];
61 }
62 else
63 {
64 if (vValueCount[i] > maxCount)
65 {
66 index = i;
67 maxCount = vValueCount[i];
68 }
69 }
70 }
71
72 //左边 右边 都有两个值,才行
73 if (index < 2)
74 return false;
75 if ((vValueCount.size() - index) < 3)
76 return false;
77
78 //黑白比例1:1:3:1:1
79 float rate = ((float)maxCount) / 3.00;
80
81 cout << "flag:" << flag << " ";
82
83 float rate2 = vValueCount[index - 2] / rate;
84 cout << rate2 << " ";
85 if (!IsQrRate(rate2))
86 return false;
87
88 rate2 = vValueCount[index - 1] / rate;
89 cout << rate2 << " ";
90 if (!IsQrRate(rate2))
91 return false;
92
93 rate2 = vValueCount[index + 1] / rate;
94 cout << rate2 << " ";
95 if (!IsQrRate(rate2))
96 return false;
97
98 rate2 = vValueCount[index + 2] / rate;
99 cout << rate2 << " ";
100 if (!IsQrRate(rate2))
101 return false;
102
103 return true;
104 }
105 //纵向黑白比例判断 省略
106 bool QrParse::IsQrColorRateY(cv::Mat& image, int flag)

bool QrParse::IsQrRate(float rate)
{
//大概比例 不能太严格
return rate > 0.6 && rate < 1.9;
}
2) 确定三个二维码顶点的次序
通过如下原则确定左上角顶点:二维码左上角的顶点与其他两个顶点的夹角为90度。

1 // pointDest存放调整后的三个点,三个点的顺序如下
2 // pt0----pt1
3 //
4 // pt2
5 bool QrParse::AdjustQrPoint(Point* pointSrc, Point* pointDest)
6 {
7 bool clockwise;
8 int index1[3] = { 2,1,0 };
9 int index2[3] = { 0,2,1 };
10 int index3[3] = { 0,1,2 };
11
12 for (int i = 0; i < 3; i++)
13 {
14 int *n = index1;
15 if(i==0)
16 n = index1;
17 else if (i == 1)
18 n = index2;
19 else
20 n = index3;
21
22 23 if (angle > 80 && angle < 99)
24 {
25 pointDest[0] = pointSrc[n[2]];
26 if (clockwise)
27 {
28 pointDest[1] = pointSrc[n[0]];
29 pointDest[2] = pointSrc[n[1]];
30 }
31 else
32 {
33 pointDest[1] = pointSrc[n[1]];
34 pointDest[2] = pointSrc[n[0]];
35 }
36 return true;
37 }
38 }
39 return true;
40 }

3)通过二维码对图片矫正。
图片有可能是倾斜的,倾斜夹角可以通过pt0与pt1连线与水平线之间的夹角确定。二维码的倾斜角度就是整个图片的倾斜角度,从而可以对整个图片进行水平矫正。

1 //二维码倾斜角度 2 Point hor(pointAdjust[0].x+300,pointAdjust[0].y); //水平线 3 double qrAngle = QrParse::Angle(pointAdjust[1], hor, pointAdjust[0], clockwise); 4 5 //以二维码左上角点为中心 旋转 6 Mat drawingRotation = Mat::zeros(Size(src.cols,src.rows), CV_8UC3); 7 double rotationAngle = clockwise? -qrAngle:qrAngle; 8 Mat affine_matrix = getRotationMatrix2D(pointAdjust[0], rotationAngle, 1.0);//求得旋转矩阵 9 warpAffine(src, drawingRotation, affine_matrix, drawingRotation.size());

4)二维码相邻区域定位
一般情况下,二维码在整个图中的位置是确定的。识别出二维码后,根据二维码与其他图的位置关系,可以很容易的定位别的图元。

后记
作者通过查找大量资料,仔细研究了二维码的特征,从而找到了识别二维码的方法。网上也有许多识别二维码的方法,但是不够严谨。本文是将二维码的多个特征相结合来识别,这样更准确。这种识别方法已应用在公司的产品中,识别效果还是非常好的。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)