条码检测
一、准备
数据集下载
Muenster BarcodeDB条形码数据集第一部分数据包下载
Muenster BarcodeDB条形码数据集第二部分数据包下载
Muenster BarcodeDB条形码数据集第三部分数据包下载
示例图片
二、详细步骤
2.1首先做灰度处理,看灰度直方图
//读取图像
srcImage = imread(imagePath);
if (srcImage.empty())
{
cout << "image file read error" << endl;
return -1;
}
//1. 图像转换为灰度图像
if (srcImage.channels() == 3)
{
cvtColor(srcImage, grayImage, CV_RGB2GRAY);
}
else
{
grayImage = srcImage.clone();
}
histogram_grayImage(grayImage,true)
其中histogram_grayImage(grayImage,true)为展示直方图,其详细代码为:
Mat histogram_grayImage(
const Mat& image,
bool showFlag = true)
{
//定义求直方图的通道数目,从0开始索引
int channels[] = { 0 };
//定义直方图的在每一维上的大小
const int histSize[] = { 256 };
//每一维bin的变化范围
float range[] = { 0,256 };
//所有bin的变化范围,个数跟channels应该跟channels一致
const float* ranges[] = { range };
//定义直方图,这里求的是直方图数据
Mat hist;
//opencv中计算直方图的函数,hist大小为256*1,每行存储的统计的该行对应的灰度值的个数
calcHist(&image, 1, channels, Mat(), hist, 1, histSize, ranges, true, false);
//找出直方图统计的个数的最大值,用来作为直方图纵坐标的高
double maxValue = 0;
//找矩阵中最大最小值及对应索引的函数
minMaxLoc(hist, 0, &maxValue, 0, 0);
//最大值取整
int rows = cvRound(maxValue);
//定义直方图图像,直方图纵坐标的高作为行数,列数为256(灰度值的个数)
//因为是直方图的图像,所以以黑白两色为区分,白色为直方图的图像
Mat histImage = Mat::zeros(rows, 256, CV_8UC1);
//直方图图像表示
for (int i = 0; i < 256; i++)
{
//取每个bin的数目
int temp = (int)(hist.at<float>(i, 0));
//如果bin数目为0,则说明图像上没有该灰度值,则整列为黑色
//如果图像上有该灰度值,则将该列对应个数的像素设为白色
if (temp)
{
//由于图像坐标是以左上角为原点,所以要进行变换,使直方图图像以左下角为坐标原点
histImage.col(i).rowRange(Range(rows - temp, rows)) = 255;
}
}
//由于直方图图像列高可能很高,因此进行图像对列要进行对应的缩减,使直方图图像更直观
Mat resizeImage;
resize(histImage, resizeImage, Size(256, 256));
if (true == showFlag) {
imshow("灰度直方图", resizeImage);
}
return resizeImage;
}
其展示结果为:
2.2试错
2.2.1 直接做二值化处理
threshold(grayImage, thresholdImage, 180, 256, THRESH_BINARY);
imshow("根据灰度直方图直接二值化结果图", thresholdImage);
其展示结果如下图所示:
可以看到效果并不好。因此需要找到其他的方法
2.2.2先做平滑处理
blur(grayImage, blurImage, Size(20, 20));
imshow("blurImage", blurImage);
其展示结果对比如下
可以看到效果并不理想,因此先考虑锐化后平滑
2.3 建立图像的梯度幅值
条码的特点是多条直线组成,因此考虑使用梯度,
2. 建立图像的梯度幅值
Scharr(grayImage, gradientXImage, CV_32F, 1, 0);
Scharr(grayImage, gradientYImage, CV_32F, 0, 1);
//3. 因为我们需要的条形码在需要X方向水平,所以更多的关注X方向的梯度幅值,而省略掉Y方向的梯度幅值
subtract(gradientXImage, gradientYImage, gradientImage);
imshow("梯度幅值", gradientImage);
其结果对比图如下
可以看到边缘都检测出来了。但还是有许多的噪点。使用平滑处理,消除噪点
2.4 bulr平滑处理,消除噪点
imshow("before convertScaleAbs", gradientImage);
convertScaleAbs(gradientImage, gradientImage);
imshow("after convertScaleAbs", gradientImage);
//5. 对图片进行相应的模糊化,使一些噪点消除
blur(gradientImage, blurImage, Size(9, 9));
imshow("blurImage", blurImage);
因为上一步做了梯度的减法,所以结果会出现小于0的值。我们所作的处理都是在0-255的8位图像基础上。所以需要将其先转换为8位图像——convertScaleAbs。
该步骤的对比结果图如下
可以看到我们的目标区域已经基本呈现白色了。
2.5做二值化处理,消除低频
imshow("blurImage", blurImage);
6. 模糊化以后进行阈值化,得到到对应的黑白二值化图像,二值化的阈值可以根据实际情况调整
threshold(blurImage, thresholdImage, 125, 255, THRESH_BINARY);
imshow("thresholdImage", thresholdImage);
和上一步的效果对比图如下:
2.6 形态学运算,消除缝隙
这张示例图片的上述处理,其实效果已经不错了,已经能够使用其他工具检测出目标区域了。但是为了能够多适应一些其他的图片,所以再做一些处理。
//7. 二值化以后的图像,条形码之间的黑白有可能没有连接起来,就要进行形态学运算,消除缝隙,相当于小型的黑洞,选择闭运算
// 因为是长条之间的缝隙,所以需要选择宽度大于长度
imshow("thresholdImage", thresholdImage);
Mat kernel = getStructuringElement(MORPH_RECT, Size(21, 7));
morphologyEx(thresholdImage, morphImage, MORPH_CLOSE, kernel);
imshow("morphImage", morphImage);
2.7腐蚀膨胀
2.6和2.7因为示例原因,差异不大,所以就不展示结果对比图了。
//8. 现在要让条形码区域连接在一起,所以选择膨胀腐蚀,而且为了保持图形大小基本不变,应该使用相同次数的膨胀腐蚀
// 先腐蚀,让其他区域的亮的地方变少最好是消除,然后膨胀回来,消除干扰,迭代次数根据实际情况选择
erode(morphImage, morphImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
dilate(morphImage, morphImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
imshow("after erode and dilate", morphImage);
2.8计算面积,找最大矩形面积
最大矩形的面积就是条形码区域。
vector<vector<Point2i>>contours;
vector<float>contourArea;
//9. 接下来对目标轮廓进行查找,目标是为了计算图像面积
findContours(morphImage, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//10. 计算轮廓的面积并且存放
for (int i = 0; i < contours.size(); i++)
{
contourArea.push_back(cv::contourArea(contours[i]));
}
//11. 找出面积最大的轮廓
double maxValue; Point maxLoc;
minMaxLoc(contourArea, NULL, &maxValue, NULL, &maxLoc);
//12. 计算面积最大的轮廓的最小的外包矩形
RotatedRect minRect = minAreaRect(contours[maxLoc.x]);
Rect myRect = boundingRect(contours[maxLoc.x]);
//把这个矩形在源图像中画出来
rectangle(srcImage, myRect, Scalar(0, 255, 255), 3, LINE_AA);
//看看显示效果,找的对不对
imshow("rectangle", srcImage);
//将扫描的图像裁剪下来,并保存为相应的结果,保留一些X方向的边界,所以对rect进行一定的扩张
myRect.x = myRect.x - (myRect.width / 20);
myRect.width = myRect.width * 1.1;
Mat resultImage = Mat(srcImage, myRect);
imshow("resultImage", resultImage);
最后的结果展示
三、完整代码
3.1 detectBarCodeByImagePath方法
void detectBarCodeByImagePath
(
string path,
string imageName,
)
{
string imagePath = path + "\\" + imageName;
Mat srcImage, grayImage, blurImage, thresholdImage,
gradientXImage, gradientYImage, gradientImage,
morphImage;
//读取图像
srcImage = imread(imagePath);
if (srcImage.empty())
{
cout << "image file read error" << endl;
return -1;
}
//1. 图像转换为灰度图像
if (srcImage.channels() == 3)
{
cvtColor(srcImage, grayImage, CV_RGB2GRAY);
}
else
{
grayImage = srcImage.clone();
}
//imshow("原图",srcImage);
//imshow("灰度图", grayImage);
//Graphs::histogram_grayImage(grayImage,true);
2. 建立图像的梯度幅值
Scharr(grayImage, gradientXImage, CV_32F, 1, 0);
Scharr(grayImage, gradientYImage, CV_32F, 0, 1);
//3. 因为我们需要的条形码在需要X方向水平,所以更多的关注X方向的梯度幅值,而省略掉Y方向的梯度幅值
subtract(gradientXImage, gradientYImage, gradientImage);
//imshow("梯度幅值", gradientImage);
//4. 归一化为八位图像
//imshow("before convertScaleAbs", gradientImage);
convertScaleAbs(gradientImage, gradientImage);
//imshow("agter convertScaleAbs", gradientImage);
//5. 对图片进行相应的模糊化,使一些噪点消除
blur(gradientImage, blurImage, Size(9, 9));
//imshow("blurImage", blurImage);
//Graphs::histogram_grayImage(blurImage, true);
//dilate(blurImage, blurImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
//imshow("before morphImage", blurImage);
//Mat kernel = getStructuringElement(MORPH_RECT, Size(21, 7));
//morphologyEx(blurImage, morphImage, MORPH_CLOSE, kernel);
//imshow("after morphImage", morphImage);
//erode(morphImage, morphImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
//imshow("after erode", morphImage);
//threshold(morphImage, morphImage, 70, 255, THRESH_BINARY);
//imshow("threshold", morphImage);
imshow("blurImage", blurImage);
6. 模糊化以后进行阈值化,得到到对应的黑白二值化图像,二值化的阈值可以根据实际情况调整
threshold(blurImage, thresholdImage, 125, 255, THRESH_BINARY);
7. 二值化以后的图像,条形码之间的黑白没有连接起来,就要进行形态学运算,消除缝隙,相当于小型的黑洞,选择闭运算
因为是长条之间的缝隙,所以需要选择宽度大于长度
imshow("thresholdImage", thresholdImage);
Mat kernel = getStructuringElement(MORPH_RECT, Size(21, 7));
morphologyEx(thresholdImage, morphImage, MORPH_CLOSE, kernel);
imshow("morphImage", morphImage);
8. 现在要让条形码区域连接在一起,所以选择膨胀腐蚀,而且为了保持图形大小基本不变,应该使用相同次数的膨胀腐蚀
先腐蚀,让其他区域的亮的地方变少最好是消除,然后膨胀回来,消除干扰,迭代次数根据实际情况选择
erode(morphImage, morphImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
dilate(morphImage, morphImage, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 4);
imshow("after erode and dilate", morphImage);
vector<vector<Point2i>>contours;
vector<float>contourArea;
//9. 接下来对目标轮廓进行查找,目标是为了计算图像面积
findContours(morphImage, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//10. 计算轮廓的面积并且存放
for (int i = 0; i < contours.size(); i++)
{
contourArea.push_back(cv::contourArea(contours[i]));
}
//11. 找出面积最大的轮廓
double maxValue; Point maxLoc;
minMaxLoc(contourArea, NULL, &maxValue, NULL, &maxLoc);
//12. 计算面积最大的轮廓的最小的外包矩形
RotatedRect minRect = minAreaRect(contours[maxLoc.x]);
Rect myRect = boundingRect(contours[maxLoc.x]);
//把这个矩形在源图像中画出来
rectangle(srcImage, myRect, Scalar(0, 255, 255), 3, LINE_AA);
//看看显示效果,找的对不对
imshow("rectangle", srcImage);
//将扫描的图像裁剪下来,并保存为相应的结果,保留一些X方向的边界,所以对rect进行一定的扩张
myRect.x = myRect.x - (myRect.width / 20);
myRect.width = myRect.width * 1.1;
Mat resultImage = Mat(srcImage, myRect);
imshow("resultImage", resultImage);
waitKey(0);
}
3.2 main函数
int main()
{
string path = "D:\\OpenCV_Training\\otherData\\Muenster BarcodeDB\\N95-2592x1944_scaledTo640x480bilinear";
string imageName = "3.jpg";
detectBarCodeByImagePath(path,imageName);
}
3.3 灰度直方图函数
Mat histogram_grayImage(
const Mat& image,
bool showFlag = true)
{
//定义求直方图的通道数目,从0开始索引
int channels[] = { 0 };
//定义直方图的在每一维上的大小
const int histSize[] = { 256 };
//每一维bin的变化范围
float range[] = { 0,256 };
//所有bin的变化范围,个数跟channels应该跟channels一致
const float* ranges[] = { range };
//定义直方图,这里求的是直方图数据
Mat hist;
//opencv中计算直方图的函数,hist大小为256*1,每行存储的统计的该行对应的灰度值的个数
calcHist(&image, 1, channels, Mat(), hist, 1, histSize, ranges, true, false);
//找出直方图统计的个数的最大值,用来作为直方图纵坐标的高
double maxValue = 0;
//找矩阵中最大最小值及对应索引的函数
minMaxLoc(hist, 0, &maxValue, 0, 0);
//最大值取整
int rows = cvRound(maxValue);
//定义直方图图像,直方图纵坐标的高作为行数,列数为256(灰度值的个数)
//因为是直方图的图像,所以以黑白两色为区分,白色为直方图的图像
Mat histImage = Mat::zeros(rows, 256, CV_8UC1);
//直方图图像表示
for (int i = 0; i < 256; i++)
{
//取每个bin的数目
int temp = (int)(hist.at<float>(i, 0));
//如果bin数目为0,则说明图像上没有该灰度值,则整列为黑色
//如果图像上有该灰度值,则将该列对应个数的像素设为白色
if (temp)
{
//由于图像坐标是以左上角为原点,所以要进行变换,使直方图图像以左下角为坐标原点
histImage.col(i).rowRange(Range(rows - temp, rows)) = 255;
}
}
//由于直方图图像列高可能很高,因此进行图像对列要进行对应的缩减,使直方图图像更直观
Mat resizeImage;
resize(histImage, resizeImage, Size(256, 256));
if (true == showFlag) {
imshow("灰度直方图", resizeImage);
}
return resizeImage;
}
if (temp)
{
//由于图像坐标是以左上角为原点,所以要进行变换,使直方图图像以左下角为坐标原点
histImage.col(i).rowRange(Range(rows - temp, rows)) = 255;
}
}
//由于直方图图像列高可能很高,因此进行图像对列要进行对应的缩减,使直方图图像更直观
Mat resizeImage;
resize(histImage, resizeImage, Size(256, 256));
if (true == showFlag) {
imshow("灰度直方图", resizeImage);
}
return resizeImage;
}
# 四、一些问题
## 4.1
最后一步,上述2.8中的12步中,对于有些图片会出错,如下图的报错
![image-20230826140506855](20230825-条码图片检测.assets/image-20230826140506855.png)
```c++
OpenCV(4.5.5) Error: Assertion failed (0 <= roi.x && 0 <= roi.width && roi.x + roi.width <= m.cols && 0 <= roi.y && 0 <= roi.height && roi.y + roi.height <= m.rows) in cv::Mat::Mat, file D:\OpenCV-4.5.5\opencv\sources\modules\core\src\matrix.cpp, line 811
D:\Microsoft Visual Studio\WorkSpace\OpenCV_demo\x64\Release\opencv_demo.exe (进程 11680)已退出,代码为 -1073740791。
其原因是,剪尺寸边界超出原始图像,可以选择对这两句代码进行频闭,或增加一些边界判断,进行调整。
调整后的就可以展示图片了,如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6KbGFmO-1693030388895)(20230825-条码图片检测.assets/image-20230826140717478.png)]
4.2
对于有些图片识别不太准确,比如如下这张原图
其展示结果为:
可以看到其范围扩大了很多,原因有很多,可能是二值化也可能是形态学运算,或者其他,可以尝试更改二值化,如下图
至于为什么是180,就是调参,不断尝试。其结果为:
调整后,示例图片的检测结果也会出现变化: