最近师兄跟我提到我二维码定位,参考了许多大佬的程序,写了这个小程序
目的:
用opencv的库实现QRcode定位
环境:
- Windows 10
- VS2015
- opencv3.4.0
基本原理
下图为二维码的其中一个黑色正方形,二维码定位主要是根据这个正方形的位置进行定位识别
这个正方形提供了两个特征:
- 该正方形有三个轮廓特征,因此我们可以找到一个符合该特征的轮廓,便可以节省许多操作。如一个父轮廓内含有两个子轮廓。因此我们需要建立轮廓等级hierarchy
- 该正方形的黑白比例为1:1:3:1:1。
下面我介绍一下RotatedRect类的angle参数:
这幅图是我认为最符合RotatedRect类的angle的描述。我的程序需要将黑色正方形旋转回正,因此需要了解这个angle 、width和heigh。
各算法输出图像
1.滤波、直方图均值化、二值化
这张图的三个黑色框非常明显2.识别位置、填充、连通三个区域
3.将图片找轮廓 找出最小包围矩形,即可框出大致二维码位置
代码实现
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
Mat transformCorner(Mat src, RotatedRect rect);
bool isCorner(Mat &image);
double Rate(Mat &count);
int main()
{
Mat src = imread("QRcode.jpg");
if (!src.data)
return -1;
double start = (double)getTickCount();
Mat srcGray, canvas; //canvas为画布 将找到的定位特征画出来
Mat canvasGray;
//pyrDown(src, src); //图片过大时使用
canvas = Mat::zeros(src.size(), CV_8UC3);
/*灰度滤波直方图均值化 提高对比度*/
cvtColor(src, srcGray, COLOR_BGR2GRAY);
blur(srcGray, srcGray, Size(3, 3));
equalizeHist(srcGray, srcGray);
/*阈值根据实际情况 如视图中已找不到特征 可适量调整*/
threshold(srcGray, srcGray, 50, 255, THRESH_BINARY);
imshow("threshold", srcGray);
/*contours是第一次寻找轮廓*/
/*contours2是筛选出的轮廓*/
vector<vector<Point>> contours, contours2;
vector<Vec4i> hierarchy;
findContours(srcGray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
int ic = 0;
int parentIdx = -1;
/*个人认为: 下面程序为寻找出有两个子轮廓的父轮廓*/
/*挺好用的 几乎筛选出来了*/
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)
{
parentIdx = -1;
ic = 0;
}
if (ic >= 2)
{
contours2.push_back(contours[parentIdx]);
//drawContours(canvas, contours, parentIdx, Scalar(0, 0, 255), 1, 8);
ic = 0;
parentIdx = -1;
}
}
vector<Point> center_all; //center_all获取特性中心
for (int i = 0; i < contours2.size(); i++)
{
//drawContours(canvas, contours, i, Scalar(0, 255, 0), 2);
double area = contourArea(contours2[i]);
if (area < 100)
continue;
/*控制高宽比*/
RotatedRect rect = minAreaRect(Mat(contours2[i]));
double w = rect.size.width;
double h = rect.size.height;
double rate = min(w, h) / max(w, h);
if (rate > 0.85)
{
Mat image = transformCorner(src, rect); //返回旋转后的图片
if (isCorner(image))
{
Point2f points[4];
rect.points(points);
for (int i = 0; i < 4; i++)
line(src, points[i], points[(i + 1) % 4], Scalar(0, 255, 0), 2);
drawContours(canvas, contours2, i, Scalar(0, 0, 255), -1);
center_all.push_back(rect.center);
}
}
}
/*连接三个黑色正方形区域,将其变成一个轮廓,即可用最小矩形框选*/
for (int i = 0; i < center_all.size(); i++)
{
line(canvas, center_all[i], center_all[(i+1) % center_all.size()], Scalar(255, 0, 0),3);
}
vector<vector<Point>> contours3;
cvtColor(canvas, canvasGray, COLOR_BGR2GRAY);
findContours(canvasGray, contours3, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
/*原程序是没有这个设置 因此但输入图片无二维码时容易报错*/
for (int i = 0; i < contours3.size(); i++)
{
RotatedRect rect = minAreaRect(contours3[i]);
Point2f boxpoint[4];
rect.points(boxpoint);
for (int i = 0; i < 4; i++)
line(src, boxpoint[i], boxpoint[(i + 1) % 4], Scalar(0, 0, 255), 3);
}
imshow("src", src);
imshow("canvas", canvas);
//imshow("srcGray", srcGray);
double time = ((double)getTickCount() - start) / getTickFrequency();
cout << "time:"<< time<< endl;
waitKey(0);
return 0;
}
/******************************************************************
该变换是假设照片是在一个平面且垂直与摄像头
(即二维码没有仿射或投影 仅仅是平移和旋转)
*******************************************************************/
Mat transformCorner(Mat src, RotatedRect rect)
{
Point center = rect.center; //旋转中心
//circle(src, center, 2, Scalar(0, 0, 255), 2);
//Size sz = Size(rect.size.width, rect.size.height);
Point TopLeft = Point(cvRound(center.x),cvRound(center.y)) - Point(rect.size.height/2,rect.size.width/2); //旋转后的目标位置
TopLeft.x = TopLeft.x > src.cols ? src.cols : TopLeft.x;
TopLeft.x = TopLeft.x < 0 ? 0 : TopLeft.x;
TopLeft.y = TopLeft.y > src.rows ? src.rows : TopLeft.y;
TopLeft.y = TopLeft.y < 0 ? 0 : TopLeft.y;
//Point ButtonRight = (Point)center - Point(rect.size.width, rect.size.height);
Rect RoiRect = Rect(TopLeft.x, TopLeft.y, rect.size.width, rect.size.height); //抠图必备矩形
double angle = rect.angle; //旋转角度
Mat mask,roi,dst; //dst是被旋转的图片 roi为输出图片 mask为掩模
Mat image; //被旋转后的图片
Size sz = src.size(); //旋转后的尺寸
mask = Mat::zeros(src.size(), CV_8U);
/************************************
为掩模上色 一般为白色
因为RotatedRect 类型的矩形不容易调取内像素 (主要是我不太懂)
因此我把矩形的四个顶点当成轮廓 再用drawContours填充
************************************/
vector<Point> contour;
Point2f points[4];
rect.points(points);
for (int i = 0; i < 4; i++)
contour.push_back(points[i]);
vector<vector<Point>> contours;
contours.push_back(contour);
drawContours(mask, contours, 0, Scalar(1), -1);
/*抠图,然后围绕中心矩阵中心旋转*/
src.copyTo(dst, mask);
//roi = dst(RoiRect);
Mat M = getRotationMatrix2D(center, angle, 1);
warpAffine(dst, image, M, sz);
roi = image(RoiRect);
//imshow("image", image);
return roi;
}
/***************判断输入图像的最底层轮廓是否有特征*********************/
bool isCorner(Mat &image)
{
/*******dstCopy作用是防止findContours修改dstGray*******/
/*******dstGray后面还需要抠图**************************/
Mat mask,dstGopy;
Mat dstGray;
mask = image.clone();
cvtColor(image, dstGray, COLOR_BGR2GRAY);
threshold(dstGray, dstGray, 100, 255, THRESH_BINARY_INV); //阈值根据情况而定
dstGopy = dstGray.clone(); //备份
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(dstGopy, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)
{
//cout << i << endl;
if (hierarchy[i][2] == -1 && hierarchy[i][3])
{
Rect rect = boundingRect(Mat(contours[i]));
rectangle(image, rect, Scalar(0, 0, 255), 2);
/******************由图可知最里面的矩形宽度占总宽的3/7***********************/
if (rect.width < mask.cols*2/ 7) //2/7是为了防止一些微小的仿射
continue;
if (Rate(dstGray(rect)) > 0.75) //0.75是我测试几张图片的经验值 可根据情况设置(测试数量并不多)
{
rectangle(mask, rect, Scalar(0, 0, 255), 2);
return true;
}
}
}
//imshow("dstGray", image);
//imshow("mask", dstGray);
return false;
}
/********统计像素点*****/
double Rate(Mat &count)
{
int number = 0;
int allpixel = 0;
for (int row = 0; row < count.rows; row++)
{
for (int col = 0; col < count.cols; col++)
{
if (count.at<uchar>(row, col) == 255)
{
number++;
}
allpixel++;
}
}
cout << (double)number / allpixel << endl;
return (double)number / allpixel;
}
总的来说,这份程序还是很多多余步骤,主要是功底不足,如有BUG,欢迎在评论区提出,一起讨论学习!