45、C++项目实战:条形码识别器

条形码识别,听起来挺高大上吧?其实说白了,就是让计算机看懂那些黑白条纹。我最早接触这个项目是在一个仓储管理系统里,当时客户要求用摄像头扫描快递单上的条形码,自动录入系统。嗯,那会儿我还在想,这玩意儿是不是得用深度学习?后来发现,用C++配合图像处理库,完全能搞定。

今天我们就来手撸一个条形码识别器。别怕,核心逻辑并不复杂。你想想看,条形码本质上就是一组宽窄不同的黑白条纹,我们只需要找到它们的排列规律,就能解码出数字。

条形码的结构

先说说条形码长什么样。以最常见的EAN-13码为例,它由13位数字组成。前两位是国家代码,中间几位是厂商代码和产品代码,最后一位是校验码。

视觉上,它由以下几部分组成:

  • 起始符:左侧的三个条纹(黑-白-黑)
  • 左侧数据区:6位数字,每位用7个模块表示
  • 中间分隔符:五个条纹(白-黑-白-黑-白)
  • 右侧数据区:6位数字,每位用7个模块表示
  • 终止符:右侧的三个条纹(黑-白-黑)

每个模块可以是黑色或白色,宽度有1倍、2倍、3倍、4倍四种。解码的关键,就是测量每个条纹的宽度,然后对照编码表查出对应的数字。

核心思路:条形码识别 = 图像预处理 + 条纹宽度测量 + 编码匹配

整体流程

我习惯把整个流程分成四个步骤。每一步都不难,但环环相扣。下面这张图能帮你快速建立全局认知:

条形码识别器核心流程 步骤1:图像预处理 灰度化 → 二值化 → 去噪 步骤2:定位条形码 边缘检测 → 轮廓查找 步骤3:条纹宽度测量 扫描线 → 宽度计数 步骤4:解码输出 编码匹配 ① 图像预处理 将彩色图像转为灰度图,再用大津法(OTSU)自动二值化,最后用中值滤波去噪。 ② 定位条形码 用Canny边缘检测找到图像中的边缘,然后通过轮廓查找找到条形码区域。 ③ 条纹宽度测量 在条形码区域中间画一条水平扫描线,记录每个像素的颜色变化,统计连续黑白条纹的宽度。 ④ 解码输出 将测得的宽度序列与EAN-13编码表匹配,逐位解码出13位数字,并验证校验码。

图像预处理

这一步很关键。我见过不少新手直接拿原始图像去识别,结果各种噪声干扰,解码成功率极低。预处理做得好,后面就轻松了。

具体来说,我们需要做三件事:

  1. 灰度化:把彩色图变成灰度图。公式很简单:Gray = 0.299*R + 0.587*G + 0.114*B
  2. 二值化:把灰度图变成黑白图。我推荐用大津法(OTSU),它能自动找到最佳阈值
  3. 去噪:用中值滤波去掉椒盐噪声。窗口大小选3x3就够了

小技巧:如果条形码图像光照不均匀,可以先做一次自适应直方图均衡化(CLAHE),能明显提升二值化效果。

定位条形码区域

图像预处理完了,接下来要找到条形码在图像中的位置。你想想看,条形码有什么特征?对,它有很多密集的垂直边缘。

我的做法是:

  • 用Canny边缘检测提取所有边缘
  • 计算水平方向的梯度投影
  • 找到梯度最密集的区域,那就是条形码的位置

我曾经遇到过一个坑:如果图像背景里有类似条纹的纹理(比如百叶窗),算法会误判。后来我加了一个校验——条形码区域的宽高比通常在2:1到3:1之间,不符合这个比例的排除掉。

条纹宽度测量

定位到条形码区域后,我们在它的中间画一条水平扫描线。沿着这条线从左到右扫描,记录每个像素的颜色值。遇到颜色变化(黑变白或白变黑)就记一次,同时统计连续相同颜色的像素个数。

举个例子,扫描结果可能是这样的:

// 扫描到的条纹宽度序列(单位:像素)
int widths[] = {3, 2, 5, 4, 2, 3, 6, 2, 3, 4, ...};
// 对应的颜色:黑、白、黑、白、黑、白、黑、白、黑、白...

注意,这里的宽度是像素个数,不是模块数。我们需要把它转换成模块数。怎么做呢?找到最窄的条纹宽度,把它当作1个模块的基准宽度。然后其他宽度除以这个基准值,四舍五入取整。

注意:如果图像有倾斜,扫描线可能不是完全水平的。我建议在条形码区域的上、中、下各取三条扫描线,取平均值,这样更稳定。

解码实现

有了模块宽度序列,接下来就是查表了。EAN-13的编码表分左侧和右侧,左侧还有奇偶性之分。这里我直接给出核心代码:

// EAN-13 左侧编码表(奇性)
const int LEFT_ODD[10][7] = {
    {0,0,0,1,1,0,1}, // 0
    {0,0,1,1,0,0,1}, // 1
    {0,0,1,0,0,1,1}, // 2
    {0,1,1,1,1,0,1}, // 3
    {0,1,0,0,0,1,1}, // 4
    {0,1,1,0,0,0,1}, // 5
    {0,1,0,1,1,1,1}, // 6
    {0,1,1,1,0,1,1}, // 7
    {0,1,1,0,1,1,1}, // 8
    {0,0,0,1,0,1,1}  // 9
};

// 解码函数
std::string decodeBarcode(const std::vector<int>& modules) {
    std::string result;
    // 跳过起始符(3个模块)
    int pos = 3;
    
    // 解码左侧6位
    for (int i = 0; i < 6; i++) {
        int digit = matchPattern(modules, pos, LEFT_ODD);
        if (digit == -1) return "";
        result += '0' + digit;
        pos += 7;
    }
    
    // 跳过中间分隔符(5个模块)
    pos += 5;
    
    // 解码右侧6位(右侧用偶性编码)
    for (int i = 0; i < 6; i++) {
        int digit = matchPattern(modules, pos, RIGHT_EVEN);
        if (digit == -1) return "";
        result += '0' + digit;
        pos += 7;
    }
    
    return result;
}

matchPattern函数的作用是:从当前位置取7个模块,与编码表中的10个模式逐一比对,找到匹配的那个。比对时用汉明距离,距离最小的就是结果。

校验码验证

解码出12位数字后,最后一位是校验码。EAN-13的校验算法很简单:

  1. 从右往左,奇数位(第1、3、5...位)的数字乘以3
  2. 偶数位(第2、4、6...位)的数字乘以1
  3. 把所有乘积加起来
  4. 用10减去和的个位数,结果就是校验码

举个例子,假设前12位是"690123456789":

计算过程:
奇数位:9+7+5+3+1+9 = 34,乘以3得102
偶数位:8+6+4+2+0+6 = 26,乘以1得26
总和:102 + 26 = 128
校验码:10 - 8 = 2
所以完整码是:6901234567892

经验之谈:校验码验证一定要做。我遇到过条形码印刷质量差导致解码错误的情况,校验码能帮我们过滤掉大部分错误结果。

完整代码框架

下面是一个完整的条形码识别器类框架,你可以在此基础上填充具体实现:

class BarcodeRecognizer {
public:
    BarcodeRecognizer() {}
    
    // 主入口:传入图像路径,返回解码结果
    std::string recognize(const std::string& imagePath) {
        // 1. 读取图像
        cv::Mat img = cv::imread(imagePath);
        if (img.empty()) return "";
        
        // 2. 预处理
        cv::Mat gray = preprocess(img);
        
        // 3. 定位条形码区域
        cv::Rect barcodeRect = locateBarcode(gray);
        if (barcodeRect.area() == 0) return "";
        
        // 4. 提取扫描线
        cv::Mat roi = gray(barcodeRect);
        std::vector<int> widths = scanLine(roi);
        
        // 5. 宽度转模块
        std::vector<int> modules = normalizeWidths(widths);
        
        // 6. 解码
        std::string code = decodeBarcode(modules);
        
        // 7. 校验
        if (!verifyChecksum(code)) return "";
        
        return code;
    }
    
private:
    cv::Mat preprocess(const cv::Mat& img);
    cv::Rect locateBarcode(const cv::Mat& gray);
    std::vector<int> scanLine(const cv::Mat& roi);
    std::vector<int> normalizeWidths(const std::vector<int>& widths);
    std::string decodeBarcode(const std::vector<int>& modules);
    bool verifyChecksum(const std::string& code);
};

性能优化建议

在实际项目中,条形码识别往往需要实时处理。我总结了几条优化经验:

优化点 做法 效果
图像缩放 将图像缩放到宽度600像素以内 处理速度提升3-5倍
ROI裁剪 只处理条形码区域,不处理全图 减少80%计算量
查表法 预计算编码表,用查表代替循环匹配 解码速度提升10倍
多线程 同时处理多个候选区域 充分利用CPU多核

我的习惯:在开发阶段先用OpenCV的imshow把每一步的中间结果可视化出来,这样调试起来特别直观。等算法稳定了再关掉可视化,追求性能。

好了,条形码识别器的核心内容就这些。从图像预处理到解码输出,每一步都有它的道理。你动手实现一遍,遇到问题再回头看这篇文章,相信会有更深的理解。


公众号:蓝海资料掘金营,微信deep3321