45、C++项目实战:条形码识别器
条形码识别,听起来挺高大上吧?其实说白了,就是让计算机看懂那些黑白条纹。我最早接触这个项目是在一个仓储管理系统里,当时客户要求用摄像头扫描快递单上的条形码,自动录入系统。嗯,那会儿我还在想,这玩意儿是不是得用深度学习?后来发现,用C++配合图像处理库,完全能搞定。
今天我们就来手撸一个条形码识别器。别怕,核心逻辑并不复杂。你想想看,条形码本质上就是一组宽窄不同的黑白条纹,我们只需要找到它们的排列规律,就能解码出数字。
条形码的结构
先说说条形码长什么样。以最常见的EAN-13码为例,它由13位数字组成。前两位是国家代码,中间几位是厂商代码和产品代码,最后一位是校验码。
视觉上,它由以下几部分组成:
- 起始符:左侧的三个条纹(黑-白-黑)
- 左侧数据区:6位数字,每位用7个模块表示
- 中间分隔符:五个条纹(白-黑-白-黑-白)
- 右侧数据区:6位数字,每位用7个模块表示
- 终止符:右侧的三个条纹(黑-白-黑)
每个模块可以是黑色或白色,宽度有1倍、2倍、3倍、4倍四种。解码的关键,就是测量每个条纹的宽度,然后对照编码表查出对应的数字。
核心思路:条形码识别 = 图像预处理 + 条纹宽度测量 + 编码匹配
整体流程
我习惯把整个流程分成四个步骤。每一步都不难,但环环相扣。下面这张图能帮你快速建立全局认知:
图像预处理
这一步很关键。我见过不少新手直接拿原始图像去识别,结果各种噪声干扰,解码成功率极低。预处理做得好,后面就轻松了。
具体来说,我们需要做三件事:
- 灰度化:把彩色图变成灰度图。公式很简单:Gray = 0.299*R + 0.587*G + 0.114*B
- 二值化:把灰度图变成黑白图。我推荐用大津法(OTSU),它能自动找到最佳阈值
- 去噪:用中值滤波去掉椒盐噪声。窗口大小选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、3、5...位)的数字乘以3
- 偶数位(第2、4、6...位)的数字乘以1
- 把所有乘积加起来
- 用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