一、概述
opencv4.0版本以後,加入了二維碼定位解碼的功能,其主要功能基於quirc開源庫,下載地址GitHub。約1200行代碼,識別與定位佔了約800行,解碼部分不作贅述,直接調用quric庫解碼。
之前版本不包括定位功能,也有博主做了相關的功能二維碼特徵定位,這篇中主要是根據二位碼三個定位圖案的輪廓特徵取得三個定位點,由於三個圖案都具有兩個子輪廓,通過findcontours()函數可以很快找到。之後再進行線性變換得到第四個點,但這篇博文中尋找第四個點的方法不太準確,因爲找到四個點後需要進行透視變換,屬於非線性變換,這篇中的方法在大多數情況下還是挺好用的。
得到二維碼後可通過zbar,zxing庫解碼,這個不多說了。直接看4.0中的源碼來分析定位方法吧!
二、QRCodeDetector 類結構
包括QRCodeDetctor()、~QRCodeDetector():構造與析構函數;
setEpsx()、setEpsx():設置水平、垂直掃描檢測的係數,默認值分別是0.2,0.1。
bool detect(InputArray img, OutputArray points):重點函數,用於定位。
參數img:輸入圖像;points:返回包括二維碼的最小四邊形的點集。返回bool值爲是否檢測到二維碼。
decode():對二維碼進行解碼,返回包含二維碼內容的字符串。
string detectAndDecode(InputArray img, OutputArray points=noArray(), OutputArray straight_qrcode = noArray()):將之前函數結合,定位加解碼返回字符串。
class CV_EXPORTS_W QRCodeDetector
{
public:
CV_WRAP QRCodeDetector();
~QRCodeDetector();
/** @brief sets the epsilon used during the horizontal scan of QR code stop marker detection.
@param epsX Epsilon neighborhood, which allows you to determine the horizontal pattern
of the scheme 1:1:3:1:1 according to QR code standard.
*/
CV_WRAP void setEpsX(double epsX);
/** @brief sets the epsilon used during the vertical scan of QR code stop marker detection.
@param epsY Epsilon neighborhood, which allows you to determine the vertical pattern
of the scheme 1:1:3:1:1 according to QR code standard.
*/
CV_WRAP void setEpsY(double epsY);
/** @brief Detects QR code in image and returns the quadrangle containing the code.
@param img grayscale or color (BGR) image containing (or not) QR code.
@param points Output vector of vertices of the minimum-area quadrangle containing the code.
*/
CV_WRAP bool detect(InputArray img, OutputArray points) const;
/** @brief Decodes QR code in image once it's found by the detect() method.
Returns UTF8-encoded output string or empty string if the code cannot be decoded.
@param img grayscale or color (BGR) image containing QR code.
@param points Quadrangle vertices found by detect() method (or some other algorithm).
@param straight_qrcode The optional output image containing rectified and binarized QR code
*/
CV_WRAP std::string decode(InputArray img, InputArray points, OutputArray straight_qrcode = noArray());
/** @brief Both detects and decodes QR code
@param img grayscale or color (BGR) image containing QR code.
@param points opiotnal output array of vertices of the found QR code quadrangle. Will be empty if not found.
@param straight_qrcode The optional output image containing rectified and binarized QR code
*/
CV_WRAP std::string detectAndDecode(InputArray img, OutputArray points=noArray(),
OutputArray straight_qrcode = noArray());
protected:
struct Impl;
Ptr<Impl> p;
};
//! @} objdetect
}
三、QRDetect類結構
主要函數:
init():初始化,對圖像大小進行處理,二值化處理轉爲黑白圖;
localization():定位,確定三個定位圖塊的中心點;
computeTransformationPoints():計算四個特徵點,即左上、左下、右上、右下四個點;
intersectionLines():計算兩條直線交叉點;
class QRDetect
{
public:
void init(const Mat& src, double eps_vertical_ = 0.2, double eps_horizontal_ = 0.1);
bool localizationAT(); // use apriltag to locate key points
bool localization();
bool computeTransformationPoints();
Mat getBinBarcode() { return bin_barcode; }
Mat getStraightBarcode() { return straight_barcode; }
vector<Point2f> getTransformationPoints() { return transformation_points; }
static Point2f intersectionLines(Point2f a1, Point2f a2, Point2f b1, Point2f b2);
protected:
vector<Vec3d> searchHorizontalLines();
vector<Point2f> separateVerticalLines(const vector<Vec3d> &list_lines);
void fixationPoints(vector<Point2f> &local_point);
vector<Point2f> getQuadrilateral(vector<Point2f> angle_list);
bool testBypassRoute(vector<Point2f> hull, int start, int finish);
inline double getCosVectors(Point2f a, Point2f b, Point2f c);
Mat barcode, bin_barcode, straight_barcode;
vector<Point2f> localization_points, transformation_points;
double eps_vertical, eps_horizontal, coeff_expansion;
};
四、原理分析
opencv4.0中的定位,根據定位圖案的黑白間隔固定比例:1:1:3:1:1,即水平或垂直掃描得到五條具有近似比例的線段就將其視爲定位圖案的一部分。
1、水平、垂直掃描後得到三個圖案中心點的集合;
2、利用kmeans聚類可以將三個集合迭代爲三個中心點,這樣就得到了定位圖案的中心;
3、再利用角度和麪積關係確定定位點的順序。
4、floodfill獲得定位圖案的外框,根據對角線距離最遠確定左下和右上兩個特徵點,再根據距離關係確定左上特徵點;
5、還是利用距離確定左下和右上圖案中距離左上角最遠的兩個點,之前的特徵點連線相交即爲右下角特徵點;
6、進行透視變換;
7、解碼。
第二張圖直接用的別人的:)
五、函數分析
水平掃描函數:QRDetect::searchHorizontalLines()
由於圖像已轉爲黑白二值圖,通過分析每行黑邊像素分佈特徵即可得到中心點的位置。
pixels_position用來儲存黑白邊界像素點的位置,test_line儲存了某個像素點前後連續五條黑白線段的長度,根據比例來檢測是否符合,係數eps_vertical限定了包容程度,畢竟圖片可能有變形扭曲,比例不會完全符合。通過這個方法,得到的點總是第三個交界點,減二就是最左邊邊界點。
其次,如果圖像不是橫平豎直的,傾斜狀態下根據相似性這個比例還是符合的。
最後返回的是Vec3d的向量,分別儲存了該行中定位圖案最左邊線的點的列數(第一個交界點)、行數、掃描所得圖案橫向像素長度(非標準長度,通過比例計算具體值不重要)。
vector<Vec3d> QRDetect::searchHorizontalLines()//水平掃描
{
vector<Vec3d> result;
const int height_bin_barcode = bin_barcode.rows;//行
const int width_bin_barcode = bin_barcode.cols;//列數
const size_t test_lines_size = 5;
double test_lines[test_lines_size];
vector<size_t> pixels_position;
for (int y = 0; y < height_bin_barcode; y++)
{
pixels_position.clear();
const uint8_t *bin_barcode_row = bin_barcode.ptr<uint8_t>(y);//行指針
int pos = 0;
for (; pos < width_bin_barcode; pos++) { if (bin_barcode_row[pos] == 0) break; }//檢測像素,黑點跳出循環
if (pos == width_bin_barcode) { continue; }
pixels_position.push_back(pos);
pixels_position.push_back(pos);
pixels_position.push_back(pos);//每行黑色起始點記錄三次?
uint8_t future_pixel = 255;
for (int x = pos; x < width_bin_barcode; x++)//繼續處理該行像素
{
if (bin_barcode_row[x] == future_pixel)//檢測白色,黑色轉換點
{
future_pixel = 255 - future_pixel;//future_pixel,值爲0
pixels_position.push_back(x);//記錄點
}
}
pixels_position.push_back(width_bin_barcode - 1);//每行最後點,pixels_position每行點元素
for (size_t i = 2; i < pixels_position.size() - 4; i+=2)//size-4,避免越界
{
test_lines[0] = static_cast<double>(pixels_position[i - 1] - pixels_position[i - 2]);
test_lines[1] = static_cast<double>(pixels_position[i ] - pixels_position[i - 1]);
test_lines[2] = static_cast<double>(pixels_position[i + 1] - pixels_position[i ]);
test_lines[3] = static_cast<double>(pixels_position[i + 2] - pixels_position[i + 1]);
test_lines[4] = static_cast<double>(pixels_position[i + 3] - pixels_position[i + 2]);//五條直線長度具有固定比例,用來區分定位圖案
double length = 0.0, weight = 0.0;
for (size_t j = 0; j < test_lines_size; j++) { length += test_lines[j]; }
if (length == 0) { continue; }
for (size_t j = 0; j < test_lines_size; j++)
{
if (j != 2) { weight += fabs((test_lines[j] / length) - 1.0/7.0); }
else { weight += fabs((test_lines[j] / length) - 3.0/7.0); }//定位圖案黑白間隔1:1:3:1:1,第三條線最長,
}
if (weight < eps_vertical)//eps_vertical,0.2,衡量是否水平的指數?
{
Vec3d line;
line[0] = static_cast<double>(pixels_position[i - 2]);//找到定位圖案的邊緣點,最左點
line[1] = y;
line[2] = length;
result.push_back(line);//Vector<3d>
}
}
}
return result;
}
垂直掃描函數:QRDetect::separateVerticalLines(const vector &list_lines)
繼續處理水平掃描所得點集,原理和水平掃描相似,先向下掃描,後向上掃描。這時得到的是六條線段而不是五條,x = cvRound(list_lines[pnt][0] + list_lines[pnt][2] * 0.5)這裏是得到圖案中心的近似列數,理論上是位於黑色小方塊中間,向上向下都會得到三條線段。其中比例檢測中weight += fabs((test_lines[i] / length) - 3.0/14.0)這部分的意思應該是隻取中心方塊垂直方向一小部分的像素點,3.0/14.0衡量了靠近中心的程度,test_[line0]是向下掃描的第一段,test_line[3]是向上掃描的第一段,越靠近中心weight值越小。
最後返回的是符合水平、垂直要求的像素點的集合。
這個方法很好,得到了三個中心點的可能特徵點的集合。
vector<Point2f> QRDetect::separateVerticalLines(const vector<Vec3d> &list_lines)//list_lines,之前的水平掃描所得result
{
vector<Vec3d> result;
int temp_length = 0;
uint8_t next_pixel;
vector<double> test_lines;
for (size_t pnt = 0; pnt < list_lines.size(); pnt++)
{
const int x = cvRound(list_lines[pnt][0] + list_lines[pnt][2] * 0.5);//中心點座標?定位圖案最左邊的點加圖片中圖案長度的一半即爲中心座標
const int y = cvRound(list_lines[pnt][1]);//像素點所在行
// --------------- Search vertical up-lines --------------- //
test_lines.clear();
uint8_t future_pixel_up = 255;
for (int j = y; j < bin_barcode.rows - 1; j++)//這段循環寫得好,反覆計算黑色、白色線段的長度,向下檢測垂直線
{
next_pixel = bin_barcode.ptr<uint8_t>(j + 1)[x];
temp_length++;
if (next_pixel == future_pixel_up)//顏色相反,更改future__pixel_up,
{
future_pixel_up = 255 - future_pixel_up;
test_lines.push_back(temp_length);
temp_length = 0;
if (test_lines.size() == 3) { break; }
}
}
// --------------- Search vertical down-lines --------------- //
uint8_t future_pixel_down = 255;
for (int j = y; j >= 1; j--)//向上檢測
{
next_pixel = bin_barcode.ptr<uint8_t>(j - 1)[x];
temp_length++;
if (next_pixel == future_pixel_down)
{
future_pixel_down = 255 - future_pixel_down;
test_lines.push_back(temp_length);
temp_length = 0;
if (test_lines.size() == 6) { break; }
}
}
// --------------- Compute vertical lines --------------- //
if (test_lines.size() == 6)
{
double length = 0.0, weight = 0.0;
for (size_t i = 0; i < test_lines.size(); i++) { length += test_lines[i]; }
CV_Assert(length > 0);
for (size_t i = 0; i < test_lines.size(); i++)//又是計算權重??看不懂。。。
{
if (i % 3 != 0) { weight += fabs((test_lines[i] / length) - 1.0/ 7.0); }//假設圖像局域變形不大
else { weight += fabs((test_lines[i] / length) - 3.0/14.0); }//只取一部分點符合abs(test_line[3]-test[0])/length<0.1
}
if(weight < eps_horizontal)
{
result.push_back(list_lines[pnt]);
}
}
}
vector<Point2f> point2f_result;
for (size_t i = 0; i < result.size(); i++)
{
point2f_result.push_back(
Point2f(static_cast<float>(result[i][0] + result[i][2] * 0.5),
static_cast<float>(result[i][1])));
}
return point2f_result;//得到三個定位圖案中心點的集合
}
六、獲取中心點
QRDetect::localization():中心點獲取只是其中一部分內容,後續還有特徵點獲取處理。
這個函數中使用了Kmeans聚類的方法,使用交換迭代最後獲取三個中心點,K-均指聚類大致原理:a、初始化k個不同中心點;b、將每個訓練樣本分配到最近中心點所代表的聚類;c、將中心點更新爲該聚類中所有訓練樣本的均值;d、重複b,c;
在這裏由於水平、垂直掃描後得到的總是三個定位圖案中心點的一系列近似點集,使用kmeans聚類最後就會的到三個中心點的較爲準確的位置。如果聚類得不到三個點,將返回false。
kmeans(list_lines_y, 3, labels,
TermCriteria( TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1),
3, KMEANS_PP_CENTERS, localization_points);
七、一些問題
4.0中好像挺喜歡用adaptiveThreshold()這個函數,二值化處理後的效果自然比threshold()好,處理後的圖像輪廓更爲清晰,但是檢測的像素塊如果取得比較大,速度可能會有點慢。
對比開篇提到的那篇文章的方法添加鏈接描述二者的區別我覺得主要是對三個定位圖案的識別思路不同,opencv4.0是一種從內向外的方法,先找中心點,再找外圍輪廓的特徵點,這篇文章是從外向內搜索,即先確定有兩個子輪廓的輪廓即爲定位圖案最外層輪廓,再通過輪廓周長確定中心點座標,其中計算中心的Center_cal()函數如下:
Point Center_cal(vector<vector<Point> > contours,int i)
{
int centerx=0,centery=0,n=contours[i].size();
//在提取的小正方形的邊界上每隔週長個像素提取一個點的座標,
//求所提取四個點的平均座標(即爲小正方形的大致中心)
centerx = (contours[i][n/4].x + contours[i][n*2/4].x + contours[i][3*n/4].x + contours[i][n-1].x)/4;
centery = (contours[i][n/4].y + contours[i][n*2/4].y + contours[i][3*n/4].y + contours[i][n-1].y)/4;
Point point1=Point(centerx,centery);
return point1;
}
我覺得這個方法中心座標沒有kmeans聚類得到的準確,有興趣的可以測一下。
八、下一篇
fixationPoints()函數:對聚類後的中心點通過角度和麪積進行排序;
computeTransformPoints()函數:計算透視變換所需的特徵點。