文章目录
1. 效果展示
首先先展示一下效果,左边是原图,右边是通过矫正后的图片。该算法适用于黑白较为分明的图像,但对于一些极端情况(比如大面积阴影,污点等等),效果不佳,因此有一定局限性。这里也仅仅提供一个算法思路,供人借鉴。
2. 算法流程
算法主要流程主要分为:
1)对比度亮度调整
通过对比度亮度调整,增加二维码和背景的分离度。
2)滤波降噪
通过滤波降噪,去除部分图像噪点。
3)反二值化
反二值化,进一步强化边界,增强分离度;同时为腐蚀膨胀做铺垫。
4)腐蚀膨胀处理
腐蚀操作处理图像上小的污点;膨胀操作勾画二维码大致轮廓区域(近似四边形)。
5)Canny边缘检测
Canny边缘检测可以对膨胀处理生成的轮廓区域进行边缘勾画。
6)Hough算子拟合直线
利用Hough算子可以对轮廓边缘线进行近似拟合,生成多个拟合直线。
7)计算二维码四个顶点坐标
利用算法从众多直线中,找到四根边界线,并计算出四个交点(即轮廓四边形顶点)
8)利用顶点坐标进行仿射变换
对四个顶点进行顺时针排序,并按照顶点顺序进行仿射变换。
3. 算法分析(带示例)
1)对比度亮度调整
首先将图片resize到500x500,减小后续图片处理的计算量。随后可以通过对比度因子和亮度因子,对对比度及亮度进行调整。
String srcImagePath( "./result/test.jpg" ); // 原图路径
Mat srcImage;
srcImage = imread( srcImagePath, IMREAD_COLOR ); // 载入初始图片
resize(srcImage, srcImage, Size(500, 500)); //图片缩放为500*500进行后续计算
imwrite("./result/Src_Image.jpg", srcImage); //保存图片
Mat contrastImage = Mat::zeros( srcImage.size(), srcImage.type() ); //亮度与对比度调节
double alpha = 1.8; //对比度因子
int beta = -30; //亮度因子
for( int y = 0; y < srcImage.rows; y++ ) {
for( int x = 0; x < srcImage.cols; x++ ) {
for( int c = 0; c < 3; c++ ) {
contrastImage.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*( srcImage.at<Vec3b>(y,x)[c] ) + beta );
}
}
}
imwrite("./result/Contrast_Image.jpg", contrastImage);
调整结果如下:
2)滤波降噪
将对比度调整后的图片进行灰度转化,降低通道数。随后对该灰度图进行滤波,主要作用是为了降低噪声。不同的噪声类型,可以采用不用的滤波方法,包括高斯滤波、中值滤波、甚至自定义滤波方法。这里我采用的是双边滤波,保留边缘信息。
Mat grayImage;
cvtColor( contrastImage, grayImage, COLOR_BGR2GRAY ); //转化为灰度图
imwrite("./result/Gray_Image.jpg", grayImage); //保存灰度图
Mat filterImage;
bilateralFilter( grayImage, filterImage, 13, 26, 6 ); //双边滤波
//medianBlur ( grayImage, filterImage, 3 ); //中值滤波
imwrite("./result/Filter_Image.jpg", filterImage); //保存滤波后图片
滤波后结果如下:
3)反二值化
二值化这一步,将二维码部分与背景进一步分离。同时对结果进行反色,即二维码黑色部分变成白色,背景变成黑色,以便于对后续图像进行腐蚀膨胀等操作。
Mat binaryImage;
threshold( filterImage, binaryImage, 210, 255, THRESH_BINARY_INV ); //反二值化
imwrite("./result/Binary_Image.jpg", binaryImage); //保存二值化图片
反二值化结果如下:
4)腐蚀膨胀处理
首先通过一到两次的腐蚀处理,原本图像上非二维码区域的小污点将被清理掉;随后进行多次膨胀操作,勾画二维码所在位置的大致区域,表现为一个近似的四边形。
Mat erodeImage;
erode( binaryImage, erodeImage, Mat(), Point(-1, -1), 2 ); //腐蚀化
imwrite("./result/Erode_Image.jpg", erodeImage);
Mat dilateImage;
dilate( erodeImage, dilateImage, Mat(), Point(-1, -1), 19 ); //膨胀化
imwrite("./result/Dilate_Image.jpg", dilateImage);
腐蚀膨胀操作结果如下:
5)Canny边缘检测
利用Canny算法对上述结果进行边缘检测,勾勒出近似四边形的边界。
Mat cannyImage;
Canny( dilateImage, cannyImage, 10, 100, 3, false); //canny边缘检测
imwrite("./result/Canny_Image.jpg", cannyImage);
边缘检测结果如下:
6)Hough算子拟合直线
利用Hough算子对上述结果图进行直线拟合,找到近似四边形的所有近似边的直线,并画出所有直线。(可以通过修改参数以提高或者降低拟合程度,保证每条边至少能拟合一条直线)
//********在canny图上画出所有hough拟合直线
Mat allLinesImage = cannyImage.clone();
vector<Vec2f> lines;
HoughLines( allLinesImage, lines, 5, CV_PI/180, 100 ); //hough算子拟合直线
for( size_t i = 0; i < lines.size(); i++ )
{
float rho = lines[i][0];
float theta = lines[i][1];
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
Point pt1(cvRound(x0 + 1000*(-b)),
cvRound(y0 + 1000*(a)));
Point pt2(cvRound(x0 - 1000*(-b)),
cvRound(y0 - 1000*(a)));
line( allLinesImage, pt1, pt2, Scalar(255), 1, 8 );
}
imwrite("./result/AllLines_Image.jpg", allLinesImage);
Hough拟合直线结果如下:
7)计算二维码四个顶点坐标
这里分为两个步骤:
1. 从所有直线中,删除相似的直线,保留差距较大的直线。依此得到符合要求的四条边。
其算法思路就是:对所有直线进行两两比较,如果某两条直线角度(theta)之差以及距离原点距离(rho)之差都分别小于某一阈值,则认为这两条直线相似,需要删去其中一条直线。比较完毕后,若发现剩余直线数量大于4,则提高阈值(相反,小于4,则适当降低阈值),继续进行新一轮的比较算法,直到剩余直线数量为4。
//********删除相似直线直到只剩4条拟合直线
double A = 50.0; //初始距离阈值:50
double B = CV_PI / 180 * 20; //初始角度阈值:20度
vector<Vec2f> resLines (lines);
set<size_t> removeIndex; //记录需要删除的直线编号
int countLess4 = 0; //死循环检测
int countMore4 = 0; //死循环检测
while(1){
for( size_t i = 0; i < resLines.size(); i++ ){
for( size_t j = i+1; j < resLines.size(); j++ ){
float rho1 = resLines