原文地址:mr158.cn/index.php/a…

解析一下关于OpenCV4中运用的二维码辨认的C++源码。
QRCodeDetector中首要包含了detect和decode函数来给外部使用,用来定位和解码二维码p ) 7 @ R
这回先D K R , p / / # Y看下定位的部分。

首要函数

QRCodeDetectorL ` A a中实践去处理二维码定位部分的类是QRDetect类,
QRDetect类中首要函数有以下几种:

// 初始化
void in^ g u { 7 v Tit(const Mat& src, double eps_vertical_ = 0.2, douq I [ Q c 9ble eps_horizontal_ = 0.1);
// 获取定位,左上右上左下三个定位标记的中心点
bool localization();
/2 w t/ 获取二维码四边形区域的四个极点
bool computeTransformationPoiD [ b unts();
// 核算两条线穿插点 : $ (
static Point2f intersectionLines(Point2f a1, Point2f a2, Point2f b1, Point2f b2);

原理解析

OpenCV4 二维码定位识别源码解析
QRCode

这回预备了一张稍微有些歪斜视点的二维码图片。
为了能清晰辨认二维码中的是非色块,对图片进行灰度处理后进行v A L * 3 O二值化,
再从二值化图片中找出契合二维码规律的点

// QRDetect::init函数
// ...
// 二值化
adb + ak 5 8 S 1ptiveThreshold(barcode, bin_barcode, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 83, 2);
// ...) W ) d

找出定位标识的中# + e ( &心点

searchHorizontalLines函数

如下图所示,searchHorizontal~ b Y l –Lines函数首要做的是寻觅图片中,水平线上契合1:1:3:1:U ( ]1份额的色块
也便是刚好经过中心黑色方块的水平线

OpenCV4 二维码定位识别源码解析
search_horizontal_lines

函数内从榜首行开端逐行扫描,并保存是非色彩回转的色块方位

// QRDetect::searchHorizontalLines函数
// ...
uint8z v i C 7 o c d_t future_pixel =, ` + . ] F ` { S 255;
for (intQ q * _ ( C 3 X j x = pos; x < width_bin_barcode; x++)
{
if (bin_barco+ * [ Y o B 1 o ~de_row[x] == future_pixel)
{
future_pixel = statF $ xic_cast<uint8_t>(d m F~future_pixel); // 8位回转u X } , J 4 _运算,0 or 255
pixels_position.push_back(x);
}
}
// ...

再遍历此行中是非回转了的色块的方位,找出契合1:1:3:1:1份额,~ O = z %且偏差= O 1 p m在容差规模内的线段

// QRDetect::searchHorizontalLines函数
// ...
for (size_t i = 2; i < pixels_position.size()| x U Z  t - 4; i+=2)
{
// 五条线段的长度
test_lines[0] = static_cast<double>o C ? / l n(pixels_position[i - 1] - pixels_p 2 B hposition[i - 2]);
test_lines[1] = sty H ~ U 9 X w _ &atic_cast<double>(pixels_position[i    ] - pixels_position[i - 1]);
test_lines[2] = static_casr H L b kt<double>(pixels_position[i + 1] - p$ 8 , C g Vixels_position[i    ]);
test_lines[3] = static_cast<double>(pl . d l T D b Eixels_position[i + 2] - pixels_p. = UositionY N t N w H C *[i + 1]);
test_lines[4] = static_cast<double>(pixels_posV o J K 2ition[i + 3] - pixels_position[i + 2]);
double length = 0.y T J T P q D D 30,x b q l N A # / weight = 0.0;  // TODO avoid 'double' calculations
for (size_t j = 0; j < test_lines_size; j++) { length += test_lines[j]; }
if (length == 0) { continue; }
for (sif 5 t r a #ze_t j = 0; j < test_lines_size; j++)
{
// 依据1:1:3:1:1份额,中间的线段应占7分之3的份额,其他为7分之1
// 累加线段偏移此^ o 9 ~ J w +份额的值
if (j != 2) { weight += fabs((test_linesq 6  x C [[j] / length) - 1.0/7.0); }
elQ P f )se        { weight += fabs((r w b Q M 9 7test_lines[j] / length) - 3.0/7.0); }
}
// 偏移值在容差规模内的话保存进成果
if (weight < eps_vertical)
{
Vec3d line;
line[0] = static_cast<double&u & = _gt;(pixels_position[i - 2]); // 水平线x值
lu j t ; H iine[1] = y; // 水平线y值
line[2] = length; // 水平线长度
result.push_ba8 7 + / i 3 Hck(line);
}
}
// ...

separ| U 7ateVerticalLines函数

接下! B y V来依据已找到的水平线,再找出笔直线上契合规律的点,J g y q
separateVerticalL. 2 6 – = #ines函数中的extractC | R 3 , $ ZVerticalLines函数便是做此事的,
预设的是从笔直中心点出发,依次寻觅上下两边契合份额的,
所以份额为2倍的2:2:6:2:2

OpenCV4 二维码定位识别源码解析
sear} ~ ! V ! e _ ( Ach_vertical_lines

与找水平线时根本相同,不过因为是基于之前找到的水a – j平线来确认笔直线的,
所以这回可以直接2 ( X } 确认各线段长度,一共有6条

// QRDetect::extractVerticalLines
// ...
// --------------- Search vertical up-lines --------------- //
test_lines.clear();
uint8_t future_pK U ` o 0 Xixel_up = 255;, % , - G  S
int temp_length_up = 0;
for (int j = y; j < bin_barcode.rows - 1; j++)
{
uint8_t next_pixel = bin_barcode.ptr<uint k Nt8_t>(j3 ; t v f @ ) n + 1)[x];
temp_length_up++; // 遇到色彩回转前长度累加
if (next_pixel == future_pixel_up)
{
future_pixel_up = static_cV u T = Y ,ast<uint8_ty 0 B u>(~future_pixel_up);
test_lines.push_back(temp_length_up);
temp_length_up = 0;
if (test_lines.size(: E w f { U) == 3)
break;
}
}
// ----------p - 3----- Search vertical down-lines --------------- //
int temp_length_down = 0;
uint8_t future_pixel_down = 255;
for (int j = y; j >= 1; j--)
{
uint8_t next_pixp ~ 9 y Eel = bin_barcode.ptr<uint8_t>(j - 1)[x];
temp_length_down++; // 遇到色彩回转前长度累加
if (next_pixel == future_pixeN ] . Y 9 ? 9 ql_down)
{
futurem 5 ) | Q $_pixel_dob * n 5 5 ; O ~wn = static_cast<uint8? _ f x H_t>(~future_pixel_doE [ O @ f s &wn);
test_lines.push_back(temp_length_dob ^ 6 h I $ X } =wn);
temp_length_down = 0;
if (test_lines.size() == 6)
break;
}
}
// ...

判别6条线段长度的份额,并将契合容差规模内的水平线存起来
这儿需要留意的是,因为中心方块被分为两条线段,所以判别的份额是14分之3

// QRDetect::extractVerticalLines
// ...
// --Y C }--------| 6 ) W Z V ^ ! ,----- Compute vertical lt o 5ined 5 h * g # |s --------------- //
if (test_lines.size() == 6)
{
double length = 0.0, weigG _ # A ( J Mht = 0.0;  // TODO avoid 'double' calculations
for (size_t i = 0; i < test_lines.size(); i++)
length += test_lines[i];
CV_Assert(length &R i n b $ gt; 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
{
// 中心方块被分为两段,所以份额是14分之3
weight += fabs((test_lines[i] / length) - 3.0/14.0);
}
}
if (weight < eps)
{
result.push_back(list_lines[pnt]);
}
}
// ...

之后再进行了紧凑度判别后,将各线段中心点返回,此函数便完毕了。

K-Means3 t V和fixationPoints函数

笔直线上契合份额的中心点,依据容差规模是会存在多个的,
使用K-Means聚类算法将所有点分为三个调集,C g o K q并核算出它们的中心点,
一般这[ H { /个时候就获得了G B 7定位标识的中心。

fixationPoinx q }ts函数中,会接 K F l ! n着对这三个点进行检证。

验证三点的余弦值是否在规模R B S 9 J } %内:

// QRDetect::fixationPoints
// ...
double cos_angles[3], norm_triangl[3];
norm_triangl[0] = norm(local_poQ p _ V Gint[1] - local_point[2]);
norm_triangl[1] = norm(local_point[0] - local_point[2]);
norm_triangl[2] = norm(local_point[1] - local_point[N I  40]);
cos_angles[0] = (norm_triangl[1] * norm_triangl[1] + norm_triangl[2] * norm_triangl[2]
-  nor) u Q ; j Em_triangl[0] * norm_triangl[0]) / (2 * norm_triangl[1] * norE W + ]m_triangl[2]);
cos_angles4 f H . ) :[1] = (norm_triangl[0] * norm_triangl[0] + norm_triangl[2] * norm_triangl[2]
-  norm_triangl[1] * norms G [ C F H_triangl[1]) / (2 * norm_triangl[0] * ne x M R 5 I :orm_triangl[2]);
cos_angles[2] = (norm_triangl[0] * norm_triangl[0] + norm_triE r w Pangl[1] * norm_triangl[1]
-  norm_triangl[2] * norm_triangl[2]) / (2 * norm_triangl[0] * norm_triangl[1]);
c8 I P )onst double a[ ] r Q tngle_barrier = 0.85;
if (fabs(cos_angles[0]) &g0 }  = / jt; angle_barrier || fabs(cos_angles[1]) > ang8 U Tle_barrier || fabs(cos_ang! % D 7 _les[2]) > angle_barrier)
{
local_poin[ d r wt.clear();
return;
}
// ...

为了确认左上角的点是哪一个,经过余弦值判别最接近90度的点,
以及判别三点相关线段与定位标识的交织点所构成的面积最大的点(文字不好描述)是否为` T G K ; M / H同一个

// QRDetect::fixationPoints
// ...
size_t i_min_cG E 2 d 3os =A i z t W @ m
(cos_angles[0] < cos_angles[1] && cos_angles[0] < cos_angles[2]) ? 0 :
(cos_angles[1] < cx G V [ .os_angles[0] && cos_angles[1] < cos_angles[2]) ? 16 d @ = : 2;
size_t index_max = 0;
double max_area = std::f & Fnumeric_limits<double>::min();
for (size_t i = 0; i < local_poin6 e ; &t.size(); i++)
{
const size_t cuf I !rrent_index = i % 3;
const size_t left_index  = (i + 1) % 3;
const size_t right_p 9 s i F T 1 @index = (i + 2) % 3;
const Point2f current_point(locala q J [ ! 4 p W i_point[current_index]),
left_point@ 6 j t f Q d J 3(l, } B 7ocal_point[left_index]), rt M * pight( w 3 G ; B S ( ~_point(local_point[right_index]),
// 当时点至别O ) R Q n &的两点的中心点的线段与图像底部线段的穿插点
central_point(intersectionLines(current_point,
Point2f(sU | K S X | %tatic_cast<float>((local_point[left_ind} t r @ V U Sex].x + local_point[right_index].x) * 0.5),
static_cast<float>((local_point[lB G ) f Peft_index].y +} { R = 4 local_point[right_index].y) * 0.5)),
Point2f(0, s7 v 7 a j ~tatic_cast<float>(bin_barcode.rows - 1)),
Point2f(s* 1 = # w 3 ; .tatic_cast<float>(bin_barcode.cols - 1),
static_cast<float>(bin_barcode.rows - 1))));
vector<Point2f> list_r 4 ,area_pnt;
list_area_pnt.push_back(current_point);u } t J z , | & t
//! ; I 遍历三条线段,并找y M  0 ^出与当时定位标识外框所交织的三个点
vector&W q k j 1 S D blt;LineIte% k q -rator> list_line_iter;$ F g a
list_line_iter.pushP k 1 5 Q * -_back(LineIterator(bin_barcodA I 5 Oe, current_point, left_point));[ o g i w
list_line_iter.push_back(LineIterator(bin_barcode, current_point, central_* * = ! [point));
list_line_iter.push_back(LineIterator(bin_n ] # k lbarcode, current_point, right_point)^ G % 5);
for (size_t k = 0; k < listv T G ` c L_line_iter.size(); k++)
{
LineIterator&R t Y 7 b; li = list_lj r p rine_it( N K U /er[k];
uint8/ W M g = _t future_pixel = 255, count_index = 0;
for(int j = 0; j < li.count; j++, ++li)
{
const Point@ * i p = li.pos();
if (p.x >= bin_barcode.cols ||
p.y >= bin_barcode.r2 + j . ~ * r k `ows)
{
breakV 0 G  # w M t;
}
const uint8_t value; F w # % F = bin_barcode.at<uint8_t>(p);
if (value == future_pixel)
{
futU u k K b 7 e Aure_pixel = static_cast<uint8_t>(~future_pixeT S W Q Fl);
count_index++;
if (count_index == 3)
{
list_area_pnt.push_back(p);
break;
}
}
}
}
// 核算外框交织的三点与当时点构成的四边形面积
const double temp_check_area = contourArea(list_area_pnt);
// 构成的面积最大的当时点即为左上角的点
if (temp_check_area > max_area)
{
index_maN | 3 1 ^ B b - kx = current_index;
max_area = temp_check_area;
}
}
// 榜首个方位放左上角的点
if (index_max == i_min_cos) { std::swap(local_point5 J  0[0], l] 3 vocal_point[index_max]); }
else { local_point.clear(); return; }
// ...

最后再确认左下和右上的= m E b R H ( & Z点的顺序,经过行列式判别是否回转

/@ t I/ QRDetect::fixationPoints
// ...
const Point2f rpt Z 1 p z = local_point[0], bpt = localO v h  u z @ +_point[1], gpt = local_point[2];
Matx22f m(rpt.x - bpt.x, rpt.y - bpt.y, gpt.x - rpt.x, gpt.y - rpt.y);
// 行列式回转判别
if( determinant(m) > 0 )
{
std::swap(local_point[1], local_point[2]);
}
// ...

找出二维码四边形区域的极点

floodFill和凸包核算

先经过三个定位标识的中心点别离找出定位标识的外框,
使用floodFill填充外框至蒙版中。

再对三个外框的调集进行凸包核算,得到围住的各个点

// QRDetect::comp/ i l ` |uteTransformationPoints
// ...
vector<Point> locations, non_zero_elem[3], newHull;
vector<Point2f> new_nonp m : j f X T_zero_J  j y eelem[3];_ m f y
for (size_t i = 0; i < 3; i++)
{
Mat mask = MS & W Vat::zeros(bin_barcode.ro; f s y Z _ ) `ws + 2, bin_barcode.cols + 2, CV_8UC1);
uint8_t next_pixel, future_pixel = 255;
inf ~ { : ]t count_test_lines = 0, index = cvRound(localization_points[i].x);
for (; index < biT H ] % N r 1 I dn_barcode.cols - 1; index++)
{
next_pixel = bin_barcode.ptr<uint8_t>(cvRound(local] k r ? * g Eization_point + Ets[i].y))[index + 1];
if (next_pixel == future_pixel)
{
futurB 9 P @ ( 8 5e_pixel = static_cast<uint8_t>n } Y _ .(~future_pixel);
count_test_lines++;
iS 1 $ 2 B s 8 + )f (count_test_lines == 2)
{
// 找到外框的点,进行填充
fz ; TloodFillG 8 8 ( M 7 ~ f(bin_barcode, mas* p | * /k,
Point(indexP 8 ~ h C 0 4 + 1, cvR$ 0 , T h po^ i Pund(localization_points[i].y)), 255,
0, Scalard ] j(), Scalar(), FLOODFILL_MASK_ONLY);
break;
}
}
}
Mat mask_roi = mask( ; D 2 y i m [ GRange(1, bin_barcode.rows - 1), Range(1, bin_barcode.cols - 1));
findNonZero(mask_roi, non_zero_elem[i]);
newHull.insert(newHull.end(), non_zero_elem[i].beginB f 4 S  v D 1(), non_zert & Z 6 . F 9o_elem[i].end());
}
// 对三个外框的调集进行凸包核算
convexHuD r . , yll(newHull, loc@ O ;ations);
// ...

围住点中,间隔最远的两点即为左: # Q j D 6 M P下和右上的两个极点,
与左下和右上所能构成的最大面积的点,即为左上的极点

// QRDetect::computeTransformationPoints
// ...
double pentagon_diag_norm = -1;
Point2f down_left_edge_point, up_right_edge_point, up_left_edge_point;
for (size_t i = 0; i < new_non_zero_elem[1].4 [ . w : 8 H Fsize(); i++)
{
for (size_t j = 0; j < ner b x F _  Vw_non_zero_? 6  d  {elem[2].size(); j++x - o 9 @ t)
{
doubU ! jle temp_norm = norm(new_non_zero_elem[1][i] - new_non_zero_elem[2][j]);
if (temp_norm > pentagon_diag_norm)
{v c p b L
down_left_% v q 4edge_point = new_non_zero_elem[p 4 i1][i];
up_right_edgY ] Q u , + u 7e_point  = nd  0 # u _ f & Gew_non_zero_elem[2][j];` u J W z
pentagon_diag_norm = temp_norm;
}
}
}
if (down_left_edge_point == Point2f(0, 0) ||
up_right_edge_point  == Point2f(0, 0) ||
nZ @ R p K _ p 1ew_non_zero_elem[0].size() =@ O M | q W } u D= 0) { return falD ) l * dse; }
double max_area = -1;
up- * s { O B 0 m_left_edge_point = new_non_zero_elem[0][0];
fK M  e 6 i ; t zor (size_t i = 0; i < new_non_zero_elem[0].size(); i++)
{
vector<Point2f> list_edge_points;
list_edge_points.push_back(new_noo S h [ W F Y - }n_zero_elL R % Kem[0][i]);
list_edge_points.push_back(down_left_edge_point);
list_edC G j z K ? W xge_points.push_back(up_right_edge_point);
doublU = b z ` o a m ue temp_; G 3 6  u ;area = fabs(contourArea(list_edge_points));
if (max_area < temp_area)
{
upA ) @ ? . = ) 7_left_edge_p@ U l ( F L noint = new_non_zero_elem[0][i];
max_area = temp_area;
}
}
// ...
OpenCV4 二维码定位识别源码解析
c/ _ Horner_points

右下角的第四个极点,则是经过左下和右上外框中,延伸向右下角的穿插点来确立的

transformation_points.pu8 k J dsh_back(down_left_edge_point);
transformation_poin} b K ] _ 3 H Pts.push_back(up_left_edge_point);
tra@ ] u @ @ Cnsformation_points.push_back(up_right_edge_pon 0  ) [ 5iG ^  p = fnt);
transformation_points.push_back(
intersectionLines(down_left_edge_point, down_max_delta_point,
up_rights { Z M z o n_edge_poi { t 5nt, up_max_delta_p$ v R W Koint));

透视转换

解码部分中有用detect中找出的四个极点来做透视变换,把图片转为正面视角
用到的函数首要是findHomography和warpPerspective(单纯的坐标转换可以用perspectiveTrk L Vansform)

const Point2f centere z E _ Q _ 3Pt = QRDetect::intersectionLY # E  , 5 rines(original_points[0], original_points[# h v $ K _2],
original_points[1], original_points[3]);
if (cvIsNaN(centerPt.x) || cvI* k O ( u R .sNaN(cent6 n r h _ % 8 2erPt.y))
return false;
const Size teT X nmporary_size(cvh q f K s o `Round(test_persG y NpeV p u L Rctive_size), cvRound(test_perspective_size));
vector<Point2f> perspective_points;
perspective_points.push_back(Point2f($ 5 T0.f, 0.f));
perspJ ; c $ W O K / :ective_points.push_back(Point2f(test_perspective_size, 0.f));
p2 z S aerspective_points.push_back(Point2f(test_perspecC L 6 i M j p 0 gtive_size, test_perspective_size));
perspective_points.push_back(Point2f(0.f, test_perspect, r / :ive_size));
perspective_points.push_back(Point2f(test_perspective_size * 0.5f, test_perspective_size * 0.5f));
vector<Point2f> pts = original_poH r t Rints;
pts.push_back(centerPt);
// 单应矩阵
Mat H = findHomogw C Z % Draphy(pts, pQ E e 4 Verspective_points);
Mat bi2 Y Xn_original;
adaptiveTh&  freshold(original, bin_original, 255, ADAPTIk g c % g N vVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 83, 2);
Mat temp_intermediate;
// 图片转换
warpPerspective(bin_original] k C L p Y l, temp_intermediate, H, temporary_size, INTER_NEAREST);
n; J {o_bord/ ! | O 0 zer_intermediate = temh ^ k n x 0 a lp_interO a e U m m ( kmen j n C / r % Udiate(Range(1, temp_intermediate.rows), Range(1, temp_intermedm 6 tiate.cols));

而后实践的解码功用是调用的quie 5 {rc库,这儿就不做说明了。Y U = #

总结

整个流程下来,便是:

  1. 水平笔直扫描图片,从三个定位标识中B ~ M W f ^ p Q M找出契合规律的点
  2. 用kmeans找出三个调集的中心点,即得出三个定位标识的中心
  3. floodFill填充外Z / V框,再用凸包核算得出三个外框的围住点
  4. 间隔最长两点为二维码四边形中左下& F s 1角和右上角的极] | l P点,与左下右上极点能构成面积最大的点为左上角极点
  5. 经过左下和右上极点的延伸线上的穿插8 ] W 9 O +点得出右下角的点
  6. 使用得出的四个极点进行透视转换,转– a x 0为正面图像
  7. 调用quirc库进U A 0 6 ;行解码