Abstract
Keywords Opencv  C++  Qrcode  Opencv  Qrcode  C++ 
Citation Yao Qing-sheng.基于 Opencv 识别、定位二维码 (C++ 版).FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20240808. https://yaoqs.github.io/20240808/ji-yu-opencv-shi-bie-ding-wei-er-wei-ma-c-ban/

转载自基于 opencv 识别、定位二维码 (c++ 版)

前言

因工作需要,需要定位图片中的二维码;我遂查阅了相关资料,也学习了 opencv 开源库。通过一番努力,终于很好的实现了二维码定位。本文将讲解如何使用 opencv 定位二维码。

定位二维码不仅仅是为了识别二维码;还可以通过二维码对图像进行水平纠正以及相邻区域定位。定位二维码,不仅需要图像处理相关知识,还需要分析二维码的特性,本文先从二维码的特性讲起。

二维码特性

二维码在设计之初就考虑到了识别问题,所以二维码有一些特征是非常明显的。二维码有三个 “回 “” 字形图案,这一点非常明显。中间的一个点位于图案的左上角,如果图像偏转,也可以根据二维码来纠正。

思考题:为什么是三个点,而不是一个、两个或四个点。

一个点:特征不明显,不易定位。不易定位二维码倾斜角度。

两个点:两个点的次序无法确认,很难确定二维码是否放正了。

四个点:无法确定 4 个点的次序,从而无法确定二维码是否放正了。

识别二维码,就是识别二维码的三个点,逐步分析一下这三个点的特性

  1. 每个点有两个轮廓。就是两个口,大 “口” 内部有一个小 “口”,所以是两个轮廓。
  2. 如果把这个 “回” 放到一个白色的背景下,从左到右,或从上到下画一条线。这条线经过的图案黑白比例大约为:黑白比例为 1:1:3:1:1。
  3. 如何找到左上角的顶点?这个顶点与其他两个顶点的夹角为 90 度。

通过上面几个步骤,就能识别出二维码的三个顶点,并且识别出左上角的顶点。

使用 opencv 识别二维码

查找轮廓,筛选出三个二维码顶点

opencv 一个非常重要的函数就是查找轮廓,就是可以找到一个图中的缩所有的轮廓,“回” 字形图案是一个非常的明显的轮廓,很容易找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int QrParse::FindQrPoint(Mat& srcImg, vector<vector<Point>>& qrPoint)
{
//彩色图转灰度图
Mat src_gray;
cvtColor(srcImg, src_gray, CV_BGR2GRAY);
namedWindow("src_gray");
imshow("src_gray", src_gray);

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

//调用查找轮廓函数
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));

//通过黑色定位角作为父轮廓,有两个子轮廓的特点,筛选出三个定位角
int parentIdx = -1;
int ic = 0;

for (int i = 0; i < contours.size(); i++)
{
if (hierarchy[i][2] != -1 && ic == 0)
{
parentIdx = i;
ic++;
}
else if (hierarchy[i][2] != -1)
{
ic++;
}
else if (hierarchy[i][2] == -1)
{
ic = 0;
parentIdx = -1;
}


45 {47
//保存找到的三个黑色定位角
if (isQr)
qrPoint.push_back(contours[parentIdx]);

ic = 0;
parentIdx = -1;
}
}

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//横向和纵向黑白比例判断
bool QrParse::IsQrColorRate(cv::Mat& image, int flag)
{
bool x = IsQrColorRateX(image, flag);
if (!x)
return false;
bool y = IsQrColorRateY(image, flag);
return y;
}
//横向黑白比例判断
bool QrParse::IsQrColorRateX(cv::Mat& image, int flag)
{
int nr = image.rows / 2;
int nc = image.cols * image.channels();

vector<int> vValueCount;
vector<uchar> vColor;
int count = 0;
uchar lastColor = 0;

uchar* data = image.ptr<uchar>(nr);
for (int i = 0; i < nc; i++)
{
vColor.push_back(data[i]);
uchar color = data[i]; 28
if (i == 0)
{
lastColor = color;
count++;
}
else
{
if (lastColor != color)
{
vValueCount.push_back(count);
count = 0;
}
count++;
lastColor = color;
}
}

if (count != 0)
vValueCount.push_back(count);

if (vValueCount.size() < 5)
return false;

//横向黑白比例1:1:3:1:1
int index = -1;
int maxCount = -1;
for (int i = 0; i < vValueCount.size(); i++)
{
if (i == 0)
{
index = i;
maxCount = vValueCount[i];
}
else
{
if (vValueCount[i] > maxCount)
{
index = i;
maxCount = vValueCount[i];
}
}
}

//黑白比例1:1:3:1:1
float rate = ((float)maxCount) / 3.00;

cout << "flag:" << flag << " ";

float rate2 = vValueCount[index - 2] / rate;
cout << rate2 << " ";
if (!IsQrRate(rate2))
return false;

rate2 = vValueCount[index - 1] / rate;
cout << rate2 << " ";
if (!IsQrRate(rate2))
return false;

rate2 = vValueCount[index + 1] / rate;
cout << rate2 << " ";
if (!IsQrRate(rate2))
return false;

rate2 = vValueCount[index + 2] / rate;
cout << rate2 << " ";
if (!IsQrRate(rate2))
return false;

return true;
}
//纵向黑白比例判断 省略
bool QrParse::IsQrColorRateY(cv::Mat& image, int flag)
{
}
bool QrParse::IsQrRate(float rate)
{
//大概比例 不能太严格
return rate > 0.6 && rate < 1.9;
}

确定三个二维码顶点的次序

通过如下原则确定左上角顶点:二维码左上角的顶点与其他两个顶点的夹角为 90 度。

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

for (int i = 0; i < 3; i++)
{
int *n = index1;
if(i==0)
n = index1;
else if (i == 1)
n = index2;
else
n = index3;

23 if (angle > 80 && angle < 99)
{
pointDest[0] = pointSrc[n[2]];
if (clockwise)
{
pointDest[1] = pointSrc[n[0]];
pointDest[2] = pointSrc[n[1]];
}
else
{
pointDest[1] = pointSrc[n[1]];
pointDest[2] = pointSrc[n[0]];
}
return true;
}
}
return true;
}

通过二维码对图片矫正。

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

1
2
3
4
5
6
7
8
9
//二维码倾斜角度
Point hor(pointAdjust[0].x+300,pointAdjust[0].y); //水平线
double qrAngle = QrParse::Angle(pointAdjust[1], hor, pointAdjust[0], clockwise);

//以二维码左上角点为中心 旋转
Mat drawingRotation = Mat::zeros(Size(src.cols,src.rows), CV_8UC3);
double rotationAngle = clockwise? -qrAngle:qrAngle;
Mat affine_matrix = getRotationMatrix2D(pointAdjust[0], rotationAngle, 1.0);//求得旋转矩阵
warpAffine(src, drawingRotation, affine_matrix, drawingRotation.size());

二维码相邻区域定位

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

后记

作者通过查找大量资料,仔细研究了二维码的特征,从而找到了识别二维码的方法。网上也有许多识别二维码的方法,但是不够严谨。本文是将二维码的多个特征相结合来识别,这样更准确。这种识别方法已应用在公司的产品中,识别效果还是非常好的

References