最近师兄跟我提到我二维码定位,参考了许多大佬的程序,写了这个小程序

目的:

用opencv的库实现QRcode定位

环境:

  • Windows 10
  • VS2015
  • opencv3.4.0

基本原理

下图为二维码的其中一个黑色正方形,二维码定位主要是根据这个正方形的位置进行定位识别

opencv 获得二维码中心位置 opencv 二维码检测定位_opencv 获得二维码中心位置

这个正方形提供了两个特征:

  • 该正方形有三个轮廓特征,因此我们可以找到一个符合该特征的轮廓,便可以节省许多操作。如一个父轮廓内含有两个子轮廓。因此我们需要建立轮廓等级hierarchy
  • 该正方形的黑白比例为1:1:3:1:1。

下面我介绍一下RotatedRect类的angle参数:

opencv 获得二维码中心位置 opencv 二维码检测定位_二维码定位_02

这幅图是我认为最符合RotatedRect类的angle的描述。我的程序需要将黑色正方形旋转回正,因此需要了解这个angle 、width和heigh。

各算法输出图像

1.滤波、直方图均值化、二值化

opencv 获得二维码中心位置 opencv 二维码检测定位_opencv 获得二维码中心位置_03


这张图的三个黑色框非常明显2.识别位置、填充、连通三个区域

opencv 获得二维码中心位置 opencv 二维码检测定位_opencv 获得二维码中心位置_04

3.将图片找轮廓 找出最小包围矩形,即可框出大致二维码位置

opencv 获得二维码中心位置 opencv 二维码检测定位_二维码定位_05


opencv 获得二维码中心位置 opencv 二维码检测定位_二维码定位_06

代码实现

#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,欢迎在评论区提出,一起讨论学习!