簡介:最近研究了一下二維碼的定位與解碼,看了很多大神的博客文章,現在將總結的方法與心得體會寫在博客中。源程序見本博客的最下方。
一、二維碼的結構
通過很多博客文章的介紹,我們可以知道二維碼有三個匹配模式區域,又叫PositionDetectionPattern。這三個區域位於二維碼的三個頂點(下圖中左下、左上、右上頂點)。與大多數方法類似,我們也是通過尋找這三個匹配模式區域來對二維碼進行定位。
二、使用opencv對二維碼進行定位
下面我們使用opencv對下圖中的二維碼進行定位。我們主要使用opencv中的findContours()函數,顧名思義,其主要作用便是在二值圖像中尋找輪廓。
vector<vector<Point>> contours;
vector<Vec4i> hireachy;
findContours(binary, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE,
Point());
輸入的第一個參數爲二值圖像;
第二個參數爲輪廓,函數檢測到的輪廓存在該參數中;
第三個參數表示圖像中輪廓的拓撲信息,我們主要是根據輪廓的拓撲信息來對二維碼的匹配模式區域進行定位;每一個輪廓contours[ i ]對應4個hierarchy元素hierarchy [ i ][ 0 ]~hierarchy [ i ][ 3 ],分別表示後一個輪廓、前一個輪廓、父輪廓、內嵌輪廓的索引編號。如果沒有對應項,對應的hierarchy [ i ] 值設置爲負數。
第四個參數表示輪廓的檢測模式,RETR_TREE表示提取所有輪廓,建立網狀的輪廓結構,對檢測到的輪廓建立等級關係。
現在我們把所有檢測到的輪廓在原圖中畫出進行觀察,如下圖所示。
注意觀察,在匹配模式區域,我們可以看出輪廓是有包圍關係的。即在該區域,有一個稍微大一點的矩形包圍這兩個小一些的矩形,三個矩形的中心基本上是同一個點。其實這樣一個約束關係是比較強,於是我們就根據這個約束關係對查找到的輪廓進行篩選,再根據輪廓的邊長以及邊長比例進行篩選,最終我們成功找到了二維碼的匹配模式區域,結果如下圖所示。
補充 :
在上述方法的基礎上,我們增加了一個篩選函數
void check_center(vector<vector<Point> >c, vector<int>& index)
第一個參數爲上述方法篩選出來的輪廓;
第二個參數爲一個索引向量;
我們假設二維碼附近區域的背景比較乾淨,干擾較少。
該函數的作用是,首先提取每一個輪廓的中心,然後分別求每兩個中心間的距離,最後將距離最短的兩個中心的索引 和 距離第二短的兩個中心的索引值 賦予 索引向量index
我們是基於這樣的假設 如果圖片中有類似於二維碼三個頂點這樣的圖形,只要不是集中在一起出現,那麼其中心間的距離很大可能高於二維碼三個頂點間的距離,則會被check_center()函數排除掉。
當然這種方法的有效性還有待驗證,僅供參考!
比較嚴謹的篩選方法可以參考下面這篇博客:
https://blog.callmewhy.com/2016/04/23/opencv-find-qrcode-position/
三、使用zbar進行識別
上一步,我們已經找到了二維碼的三個匹配模式區域,並且爲每一個區域生成了一個矩形。現在我們定義一個vector<Point>類型的對象final,將每一個矩形的四個頂點都添加進這個對象中,在根據final對象生成一個ROI區域。
Rect ROI=boundingRect(final);
當使用zbar包進行二維碼的解碼時,不需要圖片中二維碼的區域爲100%。二維碼的區域佔圖片比例的50~100%時,使用zbar包便能夠成功解碼二維碼的信息。因此,這大大增加了算法對二維碼定位的容錯率。我們無需對二維進行精準定位,只需要保證整個二維碼出現在我們的定位區域中並且比例高於50% 。
本實驗所使用的圖片中二維碼背景比較簡單,干擾較少,因此通過上一節所說的方法能夠對二維碼進行精準定位。實際上使用時,二維碼背景較爲複雜,通過上述方法只能對二維碼進行一個粗略的定位。但是通過我們的觀察,二維碼大部分情況下可以完整的出現在我們的定位區域中,並且比例高於50% 。
考慮到二維碼的邊緣部分有可能出現在定位區域的交界處,造成信息的缺失,無法對二維碼進行解碼。我們對定位區域進行一定比例的放大,這有利於後面二維碼的解碼。
最後我們調用zbar包中的函數對二維碼進行解碼。具體過程見完整源程序。
#include <stddef.h>
#include <zbar.h>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <math.h>
#include <iostream>
using namespace cv;
using namespace std;
using namespace zbar;
double getDistance (CvPoint pointO,CvPoint pointA )
{
double distance;
distance = powf((pointO.x - pointA.x),2) + powf((pointO.y - pointA.y),2);
distance = sqrtf(distance);
return distance;
}
void check_center(vector<vector<Point> >c, vector<int>& index)
{
float dmin1=10000;
float dmin2=10000;
for(int i=0;i<c.size();i++)
{
RotatedRect rect_i = minAreaRect(c[i]);
for(int j=i+1;j<c.size();j++)
{
RotatedRect rect_j = minAreaRect(c[j]);
float d=getDistance(rect_i.center,rect_j.center);
if(d<dmin2 && d>10)
{
if(d<dmin1 && d>10)
{
dmin2=dmin1;
dmin1=d;
index[2]=index[0];
index[3]=index[1];
index[0]=i;
index[1]=j;
}
else
{
dmin2=d;
index[2]=i;
index[3]=j;
}
}
}
}
}
int main(int argc, char** argv) {
Mat src = imread(argv[1]);
int hight=src.rows;
int width=src.cols;
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
vector<vector<Point>> contours;
vector<Vec4i> hireachy;
findContours(binary.clone(), contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
vector<int> found;
vector<vector<Point>> found_contours;
for (size_t t = 0; t < contours.size(); t++) {
double area = contourArea(contours[t]);
if (area < 100) continue;
RotatedRect rect = minAreaRect(contours[t]);
// 根據矩形特徵進行幾何分析
float w = rect.size.width;
float h = rect.size.height;
float rate = min(w, h) / max(w, h);
Point2f fourPoint2f[4];
rect.points(fourPoint2f);
if(w<width/4 && h<hight/4)
{
int k=t;
int c=0;
while (hireachy[k][2]!=-1)
{
k=hireachy[k][2];
c=c+1;
}
if (c>=2)
{
found.push_back(t);
found_contours.push_back(contours[t]);
}
}
}
if(found.size()>=3)
{
vector<int> indexs(4,-1);
check_center(found_contours,indexs);
vector<Point> final;
for(int i=0;i<4;i++)
{
RotatedRect part_rect=minAreaRect(found_contours[indexs[i]]);
Point2f p[4];
part_rect.points(p);
for (int j=0;j<4;j++)
{
final.push_back(p[j]);
}
}
//region of qr
Rect ROI=boundingRect(final);
Point left_top=ROI.tl();
Point right_down=ROI.br();
if(left_top.x>=20 || left_top.y>=20 || right_down.x<=width-20 || right_down.y<=hight-20 )
{
ROI=ROI+Point(-20,-20)+Size(40,40);
}
if (ROI.tl().x >0 && ROI.tl().y>0 && ROI.br().x<width && ROI.br().y<hight)
{
rectangle( src, ROI.tl(), ROI.br(), Scalar(0, 0, 255));
Mat ROI_image;
ROI_image=src(ROI);
Mat gray_qr;
cvtColor(ROI_image, gray_qr, CV_BGR2GRAY);
//對定位區域的二維碼進行解碼
ImageScanner scanner;
scanner.set_config(ZBAR_NONE, ZBAR_CFG_ENABLE, 1);
int qr_width = gray_qr.cols;
int qr_height = gray_qr.rows;
uchar *raw = (uchar *)(gray_qr.data);
Image image(qr_width, qr_height, "Y800", raw, qr_width * qr_height);
scanner.scan(image);
if(image.symbol_begin()==image.symbol_end())
{
cout<<"查詢條碼失敗,請檢查圖片!"<<endl;
}
for (Image::SymbolIterator symbol = image.symbol_begin(); symbol != image.symbol_end(); ++symbol) {
// do something useful with results
cout<< " symbol \"" << symbol->get_data() << '"' << endl;
}
}
}
imshow("【繪製結束後的圖像】", src);
waitKey(0);
return 0;
}
CMakeLists.txt文件如下
cmake_minimum_required( VERSION 2.8 )
project( QR )
# 添加c++ 11標準支持
set( CMAKE_CXX_FLAGS "-std=c++11" )
# 尋找OpenCV庫
find_package( OpenCV REQUIRED )
# 添加頭文件
include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( QR_detect QR.cpp )
# 鏈接OpenCV庫
target_link_libraries( QR_detect ${OpenCV_LIBS} zbar )
參考文章:
《OpenCV3編程入門》
https://blog.callmewhy.com/2016/04/23/opencv-find-qrcode-position/