【C++ 图像处理 OpenCV】20230825-条码图片检测

6 篇文章 0 订阅
4 篇文章 0 订阅

条码检测

一、准备

数据集下载

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,就是调参,不断尝试。其结果为:

在这里插入图片描述
调整后,示例图片的检测结果也会出现变化:
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要在 C++ 中使用 OpenCV 来识别条形码,可以使用 ZBar 库。ZBar 是一个流行的开源条形码扫描库,可以在 Linux、Windows 和 Mac OS X 等操作系统上运行。它支持多种条码类型,包括 EAN/UPC、Code 128、Code 39、Interleaved 2 of 5 等。 以下是使用 OpenCV 和 ZBar 库来识别条形码的步骤: 1. 安装 ZBar 库:可以从官方网站(http://zbar.sourceforge.net)下载最新的 ZBar 库并进行安装。 2. 配置 OpenCV:确保已经正确配置了 OpenCV 并且可以在 C++ 中使用。 3. 在 C++ 代码中引入 ZBar 库的头文件:`#include <zbar.h>` 4. 创建一个 ZBar 扫描器:`zbar::ImageScanner scanner;` 5. 加载图像并将其转换为灰度图像:`cv::Mat image = cv::imread("barcode.jpg", cv::IMREAD_GRAYSCALE);` 6. 将图像数据传递给 ZBar 扫描器:`zbar::Image zbarImage(image.cols, image.rows, "Y800", image.data, image.cols * image.rows);` 7. 扫描图像并获取结果:`scanner.scan(zbarImage);` 8. 遍历结果并输出条码数据:`for(zbar::Image::SymbolIterator symbol = zbarImage.symbol_begin(); symbol != zbarImage.symbol_end(); ++symbol) { std::cout << "Data: " << symbol->get_data() << std::endl; }` 完整的代码示例如下: ``` #include <opencv2/opencv.hpp> #include <zbar.h> int main(int argc, char** argv) { // 创建 ZBar 扫描器 zbar::ImageScanner scanner; // 加载图像并将其转换为灰度图像 cv::Mat image = cv::imread("barcode.jpg", cv::IMREAD_GRAYSCALE); // 将图像数据传递给 ZBar 扫描器 zbar::Image zbarImage(image.cols, image.rows, "Y800", image.data, image.cols * image.rows); // 扫描图像并获取结果 scanner.scan(zbarImage); // 遍历结果并输出条码数据 for(zbar::Image::SymbolIterator symbol = zbarImage.symbol_begin(); symbol != zbarImage.symbol_end(); ++symbol) { std::cout << "Data: " << symbol->get_data() << std::endl; } return 0; } ``` 注意,这只是一个简单的示例,实际应用中可能需要添加更多的错误处理和图像预处理步骤来提高识别的准确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豆得儿不是猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值