前言 因工作需要, 需要定位图片中的二维码; 我遂查阅了相关资料, 也学习了 opencv 开源库. 通过一番努力, 终于很好的实现了二维码定位. 本文将讲解如何使用 opencv 定位二维码.
定位二维码不仅仅是为了识别二维码; 还可以通过二维码对图像进行水平纠正以及相邻区域定位. 定位二维码, 不仅需要图像处理相关知识, 还需要分析二维码的特性, 本文先从二维码的特性讲起.
1 二维码特性
二维码在设计之初就考虑到了识别问题, 所以二维码有一些特征是非常明显的.
二维码有三个 "回"" 字形图案, 这一点非常明显. 中间的一个点位于图案的左上角, 如果图像偏转, 也可以根据二维码来纠正.
思考题: 为什么是三个点, 而不是一个, 两个或四个点.
一个点: 特征不明显, 不易定位. 不易定位二维码倾斜角度.
两个点: 两个点的次序无法确认, 很难确定二维码是否放正了.
四个点: 无法确定 4 个点的次序, 从而无法确定二维码是否放正了.
识别二维码, 就是识别二维码的三个点, 逐步分析一下这三个点的特性
1 每个点有两个轮廓. 就是两个口, 大 "口" 内部有一个小 "口", 所以是两个轮廓.
2 如果把这个 "回" 放到一个白色的背景下, 从左到右, 或从上到下画一条线. 这条线经过的图案黑白比例大约为: 黑白比例为 1:1:3:1:1.
3 如何找到左上角的顶点? 这个顶点与其他两个顶点的夹角为 90 度.
通过上面几个步骤, 就能识别出二维码的三个顶点, 并且识别出左上角的顶点.
2 使用 opencv 识别二维码
1) 查找轮廓, 筛选出三个二维码顶点
opencv 一个非常重要的函数就是查找轮廓, 就是可以找到一个图中的缩所有的轮廓,"回" 字形图案是一个非常的明显的轮廓, 很容易找到.
- 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;
- }
- // 有两个子轮廓才是二维码的顶点
- if (ic>= 2)
- {
- bool isQr = QrParse::IsQrPoint(contours[parentIdx], threshold_output_copy);
- // 保存找到的三个黑色定位角
- if (isQr)
- qrPoint.push_back(contours[parentIdx]);
- ic = 0;
- parentIdx = -1;
- }
- }
- return 0;
- }
找到了两个轮廓的图元, 需要进一步分析是不是二维码顶点, 用到如下函数:
- 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;
- }
黑白比例判断函数:
- // 横向和纵向黑白比例判断
- 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];
- if (color> 0)
- color = 255;
- 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];
- }
- }
- }
- // 左边 右边 都有两个值, 才行
- if (index <2)
- return false;
- if ((vValueCount.size() - index) < 3)
- return false;
- // 黑白比例 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;
- }
2) 确定三个二维码顶点的次序
通过如下原则确定左上角顶点: 二维码左上角的顶点与其他两个顶点的夹角为 90 度.
- // 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;
- double angle = QrParse::Angle(pointSrc[n[0]], pointSrc[n[1]], pointSrc[n[2]], clockwise);
- 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;
- }
3) 通过二维码对图片矫正.
图片有可能是倾斜的, 倾斜夹角可以通过 pt0 与 pt1 连线与水平线之间的夹角确定. 二维码的倾斜角度就是整个图片的倾斜角度, 从而可以对整个图片进行水平矫正.
- // 二维码倾斜角度
- 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());
4) 二维码相邻区域定位
一般情况下, 二维码在整个图中的位置是确定的. 识别出二维码后, 根据二维码与其他图的位置关系, 可以很容易的定位别的图元.
后记
作者通过查找大量资料, 仔细研究了二维码的特征, 从而找到了识别二维码的方法. 网上也有许多识别二维码的方法, 但是不够严谨. 本文是将二维码的多个特征相结合来识别, 这样更准确. 这种识别方法已应用在公司的产品中, 识别效果还是非常好的.
来源: https://www.cnblogs.com/yuanchenhui/p/opencv_qr.html