全世界常用和不常用的条码类型大概有一百多种,常用的条码类型一般是指在世界上的多个国家或者地区使用比如EAN-13码、UPC-A码、Code-128码、Code-39码、EAN/UCC-128码、ITF-14码等等,而不常用的条形码可能只是在某些国家地区,或者仅在某一个行业使用,比较少见。
根据条形码的长边的黑条长度是否一致,本文将条形码简单的区分为等长和不等长两种类型,例如Code 128每个黑条的长度都是一致的,为等长条形码,而EAN - 8、EAN - 13左右两边的黑条比中间的多出来一截,为不等长条形码。
最近工作中的项目有识别条形码的需求,在条形码的图片较理想时,目前的开源库(openCV、ZXing、ZBar等)都可以满足需求,但是实际应用后发现当条码存在于图片中的某个位置时所有的开源库都定位不到了,当我手动截取条形码的ROI区域后又可以识别成功,并且openCV目前还没有提供对code 128的支持,所以在开发过程中,条形码图片预处理和定位比条形码的识别更重要。
条形码的定位不同于QR码的定位,有三个回字的轮廓可以作为显著的特征被提取,而条码则缺少这种特征,在背景环境较复杂时,很难被精准的定位。在参考了网上很多开源的定位方案后,发现很多方案都存在一定的局限性,例如被采取最多的方案是对原图像灰度并高斯模糊处理,在x方向上对灰度图像求取sobel边缘并二值化,利用形态学运算(闭运算、膨胀等)时离散的条形轮廓合并为完整的四边形条形码轮廓,通过findContours函数实现对轮廓的提取,最后通过形态学特征(面积、惯量比、外接矩形等)筛选出条形码实现定位。
这种方法的局限性在于,由于只对水平方向求取边缘,当条形码倾斜放置时将无法定位,又因为形态学计算,当图像背景复杂时,很容易产生粘连现象从而引入干扰,并且当条形码的大小、拍摄角度发生变化时,相邻的黑条之间距离也会发生变化,因此需要开发者反复调整形态学计算的参数,增加了开发者的负担,此方法仅适用于场景单一的环境。基于原作者的思路,经过三天的摸索,以code 128为例,提供一种通用性更强的改进方法,并开放完整源代码。
【图像预处理】
cv::Mat src = cv::imread("path");
cv::Mat gray,bilateral,laplacian,thresh;
cv::cvtColor(src,gray,cv::COLOR_BGR2GRAY);
cv::bilateralFilter(gray,bilateral,25,25,25);
cv::Laplacian(bilateral,laplacian,bilateral.depth(),3,3);
cv::threshold(laplacian,thresh,64,255,cv::THRESH_BINARY);
对输入图像src灰度变换,进行双边滤波在减少图像颗粒噪声的同时保留图像边缘,求取拉普拉斯导数得到边缘图像,最后通过threshold对图像二值化,得到黑白轮廓图像,这边不建议使用Canny作为边缘提取的算子,因为Canny算子提取出的边缘太细,且当条形码黑条间距过小时存在粘连,不利于后续处理
【轮廓提取】
for(size_t index = 0;index < contours.size();index++){
std::vector<cv::Point> blob = contours[index];
cv::RotatedRect minRect = cv::minAreaRect(blob);
double area = cv::contourArea(blob);
if(area < 20){
continue;
}
float min = std::fmin(minRect.size.width,minRect.size.height);
float max = std::fmax(minRect.size.width,minRect.size.height);
float inertia_ratio = max/min;
if(inertia_ratio < 5){
continue;
}
if(area / double(min * max) < 0.1){
continue;
}
ptSet.push_back(minRect.center);
rectSet.push_back(minRect);
contourSet.push_back(blob);
cv::drawContours(contourImage,contours,int(index),cv::Scalar(0,0,255),-1);
}
使用findContours函数进行轮廓提取,筛选出所有符合条件的轮廓,例如这里我们筛选掉面积不足20像素,惯量比(长轴与短轴尺寸的比值)不足5,并且轮廓实际面积不及最小外接矩形面积的十分之一的轮廓,一轮筛选后,我们得到了可能满足是条形码的所有轮廓,并记录下它们的中心坐标
【条形码拟合】
上一步中,得到了所有可能的条形的中心坐标,因为本文以code 128为例,黑条的长度相等,那么所有黑条的中心点一定位于同一条直线上,那么如何拟合出这一条直线呢,因为其他干扰项的存在,最小二乘法的效果往往不好,所以采用RANSAC(随机抽样一致算法),通过随机抽样拟合,多次迭代后排除掉异常点即得到我们想要的直线,本文不对算法做详细的介绍,只在末尾提供源代码。经过拟合后根据中心点是否位于直线上即可得到目标条形码的轮廓。
如图所示,红色轮廓为干扰项,绿色轮廓为条形码,圆圈为每个轮廓的中心点,水平的黄色直线为拟合出的直线,对所有绿色的轮廓作为一个点集合,使用cv::minAreaRect函数得到最小外接矩形,如蓝色矩形所示,垂直的黄色直线为其垂线,以这两条直线为坐标系,根据中学就学到的点与直线的关系,即可得到条形码四个象限的各自角点坐标
这个过程还会存在一个问题:有些干扰项的中心点正好落在了直线上,因此也被拟合了进去,针对这个现象采用中位数过滤方法,具体思路是根据条形码每个黑条轮廓的长轴尺寸、以及最小外接矩形的旋转角度基本一致,可以作为一个很好的二维特征作为区分条码和干扰项的依据,对这个特征各自取中位数,想象把他们放在一个二维坐标系上,条码的特征点是非常密集的聚集在一起的,如黄色圈内的绿点所示,而干扰项就是离黄圈圆心(也就是中位数)非常远的红色点,根据每个特征点到中位数的距离,设定一个阈值,即可排除掉所有的干扰
【透视变换实现条形码校正】
经过了三轮筛选,我们终于得到了非常具体的条形码位置信息,但是对于不够成熟的开源读码库来说还是需要做最后一步处理,也就是用过透视变换将条形码校正,上一步中提到了我们要精准的获取条形码各个象限的角点坐标,也是为了透视变换而服务的。透视变换的前后四个角点必须一一对应,而一旦角点找错了,进行透视变换的时候就会出错,由于条形码的长度、旋转角度无法确定,故不能通过简单的方式判断哪个点是左上角、哪个点是右下角,必须根据点与直线的关系来确定。为了给变换后的条形码保留一片空白边框区域,变换前的四个角点应该往外点,也就是等比例适当放大其外接矩形,取放大后的角点。经过测试,在不同条码大小、拍摄角度、不同背景环境下,本方法均能够较好的实现定位,本文示例图片定位的结果如下图所示:
解决了单一等长条形码的定位问题,针对更加复杂的情况,例如不等长条码、或者多条形码同时提取的问题,暂无具体的源代码,有需要的读者可以尝试下面的思路:
针对定位不等长的条形码,由于条形码的不等长,所有中心点的位置无法拟合成一条完美的直线,那么数量少的长边就会被过滤掉,针对这个现象,可以调整RANCAC算法的参数,也可以根据点到直线的距离公式,单独遍历每一个中心点到拟合直线的距离,再根据实际情况筛选。
对于多条形码的定位,我们可以按部就班使用上面的方法,仅需要实现每定位一个条形码,就把其相关的轮廓、中心点等信息从集合中剔除,循环以上的过程,直到图像中再也找不到其他条形码为止。
【完整代码】
RANSAC直线拟合、中位数滤波代码
#ifndef RANSAC_H
#define RANSAC_H
#include <opencv2/opencv.hpp>
class Filter
{
public:
static Filter * GetInstance();
public:
void filterLineRANSAC(std::vector<cv::Point2d> ptSet,double & a,double & b,double & c,std::vector<bool> & inlierFlag);
void filterOutLierPointMAD(std::vector<cv::Point2d> ptSet,double threshold,std::vector<bool> & inlierFlag);
private:
Filter() = default;
~Filter() = default;
private:
//直线样本中两随机点位置不能太近
bool verifyComposition(const std::vector<cv::Point2d> pts);
//得到直线拟合样本,即在直线采样点集上随机选2个点
bool getSample(std::vector<int> set, std::vector<int> &sset);
//根据点集拟合直线ax+by+c=0,res为残差
void calcLinePara(std::vector<cv::Point2d> pts, double & a, double & b, double & c, double & res);
//生成[0,1]之间符合高斯分布的数
double gaussianRandom();
//生成[0,1]之间符合均匀分布的数
double uniformRandom();
};
#endif // RANSAC_H
#include "Filter.h"
Filter * Filter::GetInstance()
{
static Filter obj;
return &obj;
}
double Filter::gaussianRandom(void)
{
static int next_gaussian = 0;
static double saved_gaussian_value;
double fac, rsq, v1, v2;
if (next_gaussian == 0) {
do {
v1 = 2 * uniformRandom() - 1;
v2 = 2 * uniformRandom() - 1;
rsq = v1*v1 + v2*v2;
} while (rsq >= 1.0 || rsq == 0.0);
fac = sqrt(-2 * log(rsq) / rsq);
saved_gaussian_value = v1*fac;
next_gaussian = 1;
return v2*fac;
}
else {
next_gaussian = 0;
return saved_gaussian_value;
}
}
double Filter::uniformRandom(void)
{
return double(rand()) / double(RAND_MAX);
}
void Filter::calcLinePara(std::vector<cv::Point2d> pts, double & a, double & b, double & c, double & res)
{
res = 0;
cv::Vec4f line;
std::vector<cv::Point2f> ptsF;
for (unsigned int i = 0; i < pts.size(); i++){
ptsF.push_back(pts[i]);
}
fitLine(ptsF, line, cv::DIST_L2, 0, 1e-2, 1e-2);
a = double(line[1]);
b = double(-line[0]);
c = double(line[0] * line[3] - line[1] * line[2]);
for (unsigned int i = 0; i < pts.size(); i++)
{
double resid_ = fabs(pts[i].x * a + pts[i].y * b + c);
res += resid_;
}
res /= pts.size();
}
bool Filter::getSample(std::vector<int> set, std::vector<int> &sset)
{
int i[2];
if (set.size() > 2)
{
do
{
for (int n = 0; n < 2; n++)
i[n] = int(uniformRandom() * (set.size() - 1));
} while (!(i[1] != i[0]));
for (int n = 0; n < 2; n++)
{
sset.push_back(i[n]);
}
}
else
{
return false;
}
return true;
}
bool Filter::verifyComposition(const std::vector<cv::Point2d> pts)
{
cv::Point2d pt1 = pts[0];
cv::Point2d pt2 = pts[1];
if (abs(pt1.x - pt2.x) < 5 && abs(pt1.y - pt2.y) < 5)
return false;
return true;
}
void Filter::filterLineRANSAC(std::vector<cv::Point2d> ptSet, double & a, double & b, double & c, std::vector<bool> & inlierFlag)
{
double residual_error = 2.99; //内点阈值
bool stop_loop = false;
int maximum = 0; //最大内点数
//最终内点标识及其残差
inlierFlag = std::vector<bool>(ptSet.size(), false);
std::vector<double> resids_(ptSet.size(), 3);
int sample_count = 0;
int N = 500;
double res = 0;
// Filter
srand(uint(time(nullptr))); //设置随机数种子
std::vector<int> ptsID;
for (unsigned int i = 0; i < ptSet.size(); i++){
ptsID.push_back(int(i));
}
while (N > sample_count && !stop_loop)
{
std::vector<bool> inlierstemp;
std::vector<double> residualstemp;
std::vector<int> ptss;
int inlier_count = 0;
if (!getSample(ptsID, ptss))
{
stop_loop = true;
continue;
}
std::vector<cv::Point2d> pt_sam;
pt_sam.push_back(ptSet[uint(ptss[0])]);
pt_sam.push_back(ptSet[uint(ptss[1])]);
if (!verifyComposition(pt_sam))
{
++sample_count;
continue;
}
// 计算直线方程
calcLinePara(pt_sam, a, b, c, res);
//内点检验
for (unsigned int i = 0; i < ptSet.size(); i++)
{
cv::Point2d pt = ptSet[i];
double resid_ = fabs(pt.x * a + pt.y * b + c);
residualstemp.push_back(resid_);
inlierstemp.push_back(false);
if (resid_ < residual_error)
{
++inlier_count;
inlierstemp[i] = true;
}
}
// 找到最佳拟合直线
if (inlier_count >= maximum)
{
maximum = inlier_count;
resids_ = residualstemp;
inlierFlag = inlierstemp;
}
// 更新Filter迭代次数,以及内点概率
if (inlier_count == 0)
{
N = 500;
}
else
{
double epsilon = 1.0 - double(inlier_count) / double(ptSet.size()); //野值点比例
double p = 0.99; //所有样本中存在1个好样本的概率
double s = 2.0;
N = int(log(1.0 - p) / log(1.0 - pow((1.0 - epsilon), s)));
}
++sample_count;
}
}
void Filter::filterOutLierPointMAD(std::vector<cv::Point2d> ptSet,double threshold,std::vector<bool> & inlierFlag)
{
double * data_x = new double[ptSet.size()];
double * data_y = new double[ptSet.size()];
for(size_t index = 0;index < ptSet.size();index++){
data_x[index] = ptSet[index].x;
data_y[index] = ptSet[index].y;
}
auto on_sort = [](double v1,double v2){
return v1 > v2;
};
std::sort(data_x,&data_x[ptSet.size()],on_sort);
std::sort(data_y,&data_y[ptSet.size()],on_sort);
double median_x = (ptSet.size() % 2 == 1) ? data_x[ptSet.size() / 2] : (data_x[ptSet.size() / 2] + data_x[ptSet.size() / 2 - 1]) / 2;
double median_y = (ptSet.size() % 2 == 1) ? data_y[ptSet.size() / 2] : (data_y[ptSet.size() / 2] + data_y[ptSet.size() / 2 - 1]) / 2;
cv::Point2d median = cv::Point2d(median_x,median_y);
auto distance = [](cv::Point2d p1,cv::Point2d p2) -> double{
return std::pow(std::pow(p1.x - p2.x,2) + std::pow(p1.y - p2.y,2),0.5);
};
inlierFlag.clear();
for(size_t index = 0;index < ptSet.size();index++){
inlierFlag.push_back(distance(ptSet[index],median) < threshold);
}
delete[] data_x;
delete[] data_y;
}
条形码定位识别代码,本文采用QZxing库
QString decodeBarCode(cv::Mat & src)
{
cv::Mat gray,bilateral,laplacian,thresh;
cv::cvtColor(src,gray,cv::COLOR_BGR2GRAY);
cv::bilateralFilter(gray,bilateral,25,25,25);
cv::Laplacian(bilateral,laplacian,bilateral.depth(),3,3);
cv::threshold(laplacian,thresh,64,255,cv::THRESH_BINARY);
cv::imshow("gray",gray);
cv::imshow("bilateral",bilateral);
cv::imshow("laplacian",laplacian);
cv::imshow("thresh",thresh);
cv::Mat contourImage(src.rows,src.cols,CV_8UC3,cv::Scalar(0,0,0));
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
std::vector<cv::Point2d> ptSet;
std::vector<cv::RotatedRect> rectSet;
std::vector<std::vector<cv::Point>> contourSet;
cv::findContours(thresh,contours,hierarchy,cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
for(size_t index = 0;index < contours.size();index++){
std::vector<cv::Point> blob = contours[index];
cv::RotatedRect minRect = cv::minAreaRect(blob);
double area = cv::contourArea(blob);
if(area < 20){
continue;
}
float min = std::fmin(minRect.size.width,minRect.size.height);
float max = std::fmax(minRect.size.width,minRect.size.height);
float inertia_ratio = max/min;
if(inertia_ratio < 5){
continue;
}
if(area / double(min * max) < 0.1){
continue;
}
ptSet.push_back(minRect.center);
rectSet.push_back(minRect);
contourSet.push_back(blob);
cv::drawContours(contourImage,contours,int(index),cv::Scalar(0,0,255),-1);
}
if(ptSet.size() < 10){
return QString();
}
double A, B, C;
std::vector<bool> inliers_LINE;
Filter::GetInstance()->filterLineRANSAC(ptSet, A, B, C, inliers_LINE);
std::vector<std::vector<cv::Point>> contour_ok;
std::vector<cv::Point2d> keypoint_set;
for(size_t index = 0;index < inliers_LINE.size();index++){
if(inliers_LINE[index]){
double angle = std::min(double(rectSet[index].angle),90 - double(rectSet[index].angle));
double length = std::max(double(rectSet[index].size.width),double(rectSet[index].size.height));
keypoint_set.push_back(cv::Point2d(angle,length));
contour_ok.push_back(contourSet[index]);
cv::circle(contourImage,ptSet[index],10,cv::Scalar(255,255,255),2);
} else {
cv::circle(contourImage,ptSet[index],10,cv::Scalar(128,128,128),2);
}
}
cv::drawContours(contourImage,contour_ok,-1,cv::Scalar(0,255,0),-1);
std::vector<bool> inliers_POINT;
Filter::GetInstance()->filterOutLierPointMAD(keypoint_set,10,inliers_POINT);
std::vector<cv::Point> contours_barCode;
for(size_t index = 0;index < keypoint_set.size();index++){
if(inliers_POINT[index]){
contours_barCode.insert(contours_barCode.end(), contour_ok[index].begin(), contour_ok[index].end());
}
}
if(contours_barCode.empty()){
return QString();
}
cv::RotatedRect barCodeRect = cv::minAreaRect(contours_barCode);
cv::Size2f size = barCodeRect.size;
size.width = std::min(size.width * 1.2f,size.width + 30);
size.height = std::min(size.height * 1.2f,size.height + 30);
barCodeRect = cv::RotatedRect(barCodeRect.center,size,barCodeRect.angle);
drawRotatedRect(contourImage,barCodeRect,cv::Scalar(255,255,0),2);
cv::Point2f arr[4];
barCodeRect.points(arr);
cv::Mat dst_warp,dst_warpRotateScale,dst_warpTransformation,dst_warpFlip;
cv::Point2f srcPoints[4];
cv::Point2f dstPoints[4];
cv::Point2f center = barCodeRect.center;
if(fabs(B) > 0){
double v_A = B;
double v_B = -A;
double v_C = A * double(center.y) - B * double(center.x);
auto h_function = [A,B,C](double x)->double{
return -A/B*x - C/B;
};
auto v_function = [v_A,v_B,v_C](double y)->double{
return - y * v_B / v_A - v_C / v_A;
};
for(size_t index = 0;index < 4;index++){
double x = double(arr[index].x);
double y = double(arr[index].y);
if(y < h_function(x) && x > v_function(y)){
srcPoints[0] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,255,128),-1);
}
if(y < h_function(x) && x < v_function(y)){
srcPoints[1] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,128,255),-1);
}
if(y > h_function(x) && x < v_function(y)){
srcPoints[2] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,128,128),-1);
}
if(y > h_function(x) && x > v_function(y)){
srcPoints[3] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(0,128,255),-1);
}
}
cv::Point2d ptStart, ptEnd;
ptStart.x = 0;
ptStart.y = h_function(ptStart.x);
ptEnd.x = contourImage.cols;
ptEnd.y = h_function(ptEnd.x);
cv::line(contourImage, ptStart, ptEnd, cv::Scalar(0, 255, 255), 2, 8);
ptStart.x = v_function(0);
ptStart.y = 0;
ptEnd.x = v_function(contourImage.rows);
ptEnd.y = contourImage.rows;
cv::line(contourImage, ptStart, ptEnd, cv::Scalar(0, 255, 255), 2, 8);
} else {
for(size_t index = 0;index < 4;index++){
double x = double(arr[index].x);
double y = double(arr[index].y);
if(x > -C/A && y < double(center.y)){
srcPoints[0] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,255,128),-1);
}
if(x < -C/A && y < double(center.y)){
srcPoints[1] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,128,255),-1);
}
if(x < -C/A && y > double(center.y)){
srcPoints[2] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(255,128,128),-1);
}
if(x > -C/A && y > double(center.y)){
srcPoints[3] = arr[index];
cv::circle(contourImage,arr[index],10,cv::Scalar(0,128,255),-1);
}
cv::circle(contourImage,arr[index],10,cv::Scalar(255,255,255),2);
}
cv::Point2d ptStart, ptEnd;
ptStart.x = -C/A;
ptStart.y = 0;
ptEnd.x = -C/A;
ptEnd.y = contourImage.rows;
cv::line(contourImage, ptStart, ptEnd, cv::Scalar(0, 255, 255), 2, 8);
ptStart.x = 0;
ptStart.y = double(center.y);
ptEnd.x = contourImage.cols;
ptEnd.y = double(center.y);
cv::line(contourImage, ptStart, ptEnd, cv::Scalar(0, 255, 255), 2, 8);
}
auto distance = [](cv::Point2f p1,cv::Point2f p2)->double{
return std::pow(std::pow(p1.x - p2.x,2) + std::pow(p1.y - p2.y,2),0.5);
};
int width = int(distance(srcPoints[0],srcPoints[1])) + int(distance(srcPoints[2],srcPoints[3]));
int height = int(distance(srcPoints[1],srcPoints[2])) + int(distance(srcPoints[0],srcPoints[3]));
dstPoints[0] = cv::Point2f(width,0);
dstPoints[1] = cv::Point2f(0,0);
dstPoints[2] = cv::Point2f(0,height);
dstPoints[3] = cv::Point2f(width,height);
cv::Mat M1 = cv::getPerspectiveTransform(srcPoints, dstPoints);
warpPerspective(src, dst_warp, M1, cv::Size(width,height));
cv::imshow("dst_warp",dst_warp);
cv::waitKey(0);
QZXing qzxing;
qzxing.setDecoder(QZXing::DecoderFormat_CODE_128);
return qzxing.decodeImage(Mat2Image(dst_warp));
}
参考博客:opencv练习--条形码定位识别_opencv识别条形码位置_ZZU-Hanqi_Duan的博客-CSDN博客