ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VS2010+OpenCV+zxing-cpp:老设备二维码识别环境搭建实战

VS2010+OpenCV+zxing-cpp:老设备二维码识别环境搭建实战 简介压缩包围绕 Visual Studio 2010、OpenCV 与 ZXing-C 搭建了一套二维码识别开发方案面向需要在 Windows 下用 C 快速实现二维码读取与图像处理的开发者适合具备 C 基础、了解 CMake 构建方法的读者参考。包内共 358 个文件以 h、cpp 源码为主辅以 vcxproj、sln、cmake 等工程配置以及少量 exe、lib 构建产物和日志文件整体约 2.79MB目录中包含 core、cli、cmake 等模块结构与构建关系清晰便于按需取用。ZXing-C 提供解码核心OpenCV 负责图像预处理与增强两者结合可提升复杂背景下二维码的识别率源码中还附带构建脚本、编译配置与说明文档有助于复现环境或进行二次开发。目前已有 435 人浏览学习适合用来快速搭建桌面端二维码识别工具也可作为 OpenCV 与条码库集成入门的实例参考。1. 这个压缩包到底装的是什么先说结论vs2010 opencv zxing-cpp.7z这套命名在我干工业视觉这些年里见得太多了。它基本等于一个包含了完整Qt或者MFC工程的三方视觉识别组件包对应的是2010年到2015年期间Windows老设备视觉改造的高频组合。OpenCV负责图像预处理、轮廓定位、几何校正zxing-cpp负责二维码内容解码VS2010则是整个工程的编译宿主。三者各管一段拼起来就是一条完整的图像扫码流水线。所以收到这类压缩包别急着解压先搞清楚这三层逻辑VS2010是编译环境OpenCV是图像底层库zxing-cpp是业务识别引擎。这样遇到问题了才知道去哪儿排查。适合读这篇的人我遇到过三类第一类是接手老设备软件维护的工程师机器上跑的就是VS2010写的程序想加个扫码功能第二类是在校学生课程设计或者毕业设计要用C做二维码识别拿不到新环境授权只能用旧版本第三类是自己做小型自动化设备的个人开发者不想用C#或者Python托底就想在C里干干净净地把二维码解出来。这篇文章要讲的就是把这套环境从零搭起来、跑通、踩坑、优化的完整路径。2. 方案选型为什么是这三个老伙伴2.1 VS2010不是旧是硬件和依赖决定的选择VS2010的编译器是MSVC10.0生成的是v100平台工具集的东西。很多老设备上的工控机配置不高、系统停在Win7或者Win2008装新版VS2015往上走机器根本带不动。更关键的是有些旧设备的驱动SDK、相机SDK、运动控制卡动态库只提供了VS2010这版的静态库和头文件新版编译器编译出来的程序链接不上。这是很多工业现场的真实约束。所以万不得已别去改编译环境这是冒进。能跑的老工程VS2010版本不升级反而是最安全的方案。2.2 OpenCV为什么选2.4.x系列OpenCV在2.4.x时代还是比较纯粹的C/C库配置方式也简单设置环境变量工程里填附加包含目录和附加库目录就行。到了3.0之后库结构变化很大OpenCV的架构、API头文件位置、模块依赖关系都不一样而且预编译版不再提供VS2010的库。当时我查过OpenCV 2.4.13是官方预编译包支持VS2010的最后一站。既然整天跟VS2010打交道老老实实选2.4.13最省事。安装方式很简单官网下载Windows版自解压文件解压到一个纯英文路径比如D:\opencv2_4_13然后把这个路径下的build\include配到工程附加包含目录build\x86\vc10\lib配到附加库目录就完成了基础配置。这里有个关键点opencv的目录里有个vc10还是vc12的区别。vc10对应VS2010vc12对应VS2013。下载OpenCV时看清你的是vc10文件夹如果在VS2010里配了vc12的库链接阶段直接报一堆找不到opencv_core2413d.lib的错误。2.3 zxing-cpp是什么为什么不用直接用微信SDKzxing是个开源的条码图像处理库最初是Java版本后来有人把它翻译成C实现就是zxing-cpp。它支持QR Code、Data Matrix、EAN、UPC等多种码制。在我们的场景里一般只用它的QR解码能力。为什么不直接调手机里的微信SDK因为你是运行在设备端C环境里根本没有微信SDK可用。zxing-cpp不需要内存卡、不需要系统服务就是一个静态库加几个头文件完全离线运行非常适合工控机内网环境。这也是它在这个组合里的不可替代性。zxing-cpp不依赖OpenCV但OpenCV是它的前处理搭档。zxing对图像质量有要求而工业相机拍出来的原图干扰太多需要靠OpenCV把噪点去除、对比度拉开zxing才能稳定解出来。两者是流水线关系前面处理得好看后面才能解码得动。3. 环境搭建从零构建完整工程3.1 三步配置OpenCV 2.4.13先说明我的工作目录设计这步很重要。所有相关库放在同一个根目录下方便后面做工程相对路径引用。D:\vision_project\ ├─ opencv243\ ├─ zxing-cpp\ ├─ project_code\ └─ third_party_bin\项目代码里我习惯用相对路径而不是绝对路径这样整个文件夹拷给别人也能用不会出现“你机器上没有D盘这个路径”的尴尬。在VS2010中打开项目属性页依次配置配置项值C/C - 常规 - 附加包含目录D:\vision_project\opencv243\build\include;D:\vision_project\opencv243\build\include\opencv链接器 - 常规 - 附加库目录D:\vision_project\opencv243\build\x86\vc10\lib链接器 - 输入 - 附加依赖项opencv_core2413d.lib;opencv_imgproc2413d.lib;opencv_highgui2413d.lib;opencv_calib3d2413d.libDebug版带d后缀依赖项要按需添加。做二维码识别核心是core、imgproc、highgui这三个如果后面涉及到相机标定还要加calib3d。不要一股脑把几十个lib都填进去链接阶段会变慢而且容易冲突。环境变量PATH里要加上D:\vision_project\opencv243\build\x86\vc10\bin否则运行时找不到opencv_core2413.dll这类动态库程序一路绿灯编译过去运行瞬间崩溃这个问题特别常见。3.2 编译zxing-cpp静态库zxing-cpp获取源码后一般需要自己用CMake生成VS2010工程。我用的CMake版本是2.8.x虽然老但对VS2010支持度高。打开CMake GUI源代码目录选源码根目录构建目录新建一个build_vs2010文件夹点击Configure编译器选择Visual Studio 10 2010如果机器是64位系统但工程要跑32位记得选Win32平台然后再点Generate生成解决方案。生成好之后用VS2010打开生成的zxing.sln在解决方案配置里选择Release生成zxing静态库。我习惯把zxing编成静态库好处是部署时不用带一堆dll坏处是如果主工程改了运行库方式会出现链接错误。所以编译前先确认运行库方式zxing库和主工程必须一致要么都用/MT要么都用/MD。Debug版可以编但zxing的Debug库和OpenCV的Debug库经常组合出问题我实际用下来Debug模式下错位率高调试识别算法时反而增加干扰。建议一直用Release库调试时主工程也切到Release这样行为跟最终上线版本一致排错效率反而更高。编译完成后会得到zxing.lib把它放到自己的third_party_bin目录。头文件是源码里的core/src/zxing整个目录把这个目录拷出来放到zxing-cpp\include下方便工程引用。3.3 VS2010工程里正式引入zxing在工程属性里给这两项追加内容附加包含目录D:\vision_project\zxing-cpp\include附加依赖项D:\vision_project\third_party_bin\zxing.lib这里我踩过一次坑。zxing的include目录里有可怕的内部依赖如果直接引用zxing/qrcode/QRCodeReader.h编译器会提示找不到zxing/Reader.h这类文件。原因就是头文件之间用相对路径互相包含include目录指错了层级。正确做法是把core/src目录作为附加包含目录的根因为zxing源码里的头文件都是以zxing/xxx.h的方式引用的。3.4 编译选项的四个统一老手都知道多库混合工程编译选项不一致的后果是各种莫名其妙的链接错误。VS2010里要盯住四点字符集统一工程里用多字节字符集zxing内部处理字符串时也按多字节来避免生硬的Unicode转换。这是VS2010工程最常见的地雷默认新工程是Unicode而zxing-fed的代码很多是ASCII习惯一混编就乱码。运行库统一C/C - 代码生成 - 运行库Release用/MDDebug用/MDd。zxing如果编的是/MT主工程也得是/MT。平台工具集统一这个项目不用改Visual Studio 2010(v100)就行别改成新版工具集否则OpenCV的vc10库还是能链接但zxing内部会因CRT版本问题爆各种奇怪行为。Release和Debug行为一致没必要在Debug模式下用Debug的OpenCV库统一用Release的库避免Debug堆和Release堆混用导致的内存崩溃。4. 核心代码实现从Mat到二维码字符串4.1 图像预处理工业现场的二维码往往不是实验室里打印得方方正正的那种。光照不均、透视畸变、污渍遮挡是家常便饭。这种图直接丢给zxing大概率解不出来。所以识别前必须有预处理环节这也是为什么这个组合非要OpenCV在场。处理流程我一般分四步cv::Mat src cv::imread(qrcode_sample.jpg); cv::Mat grey, binary; // 第一步灰度化 cv::cvtColor(src, grey, CV_BGR2GRAY); // 第二步高斯滤波降噪 cv::GaussianBlur(grey, grey, cv::Size(5, 5), 0); // 第三步自适应二值化 cv::adaptiveThreshold(grey, binary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 51, 15);第一步灰度化是必然操作zxing最终拿到的也是灰度数据。第二步高斯滤波核大小5x5是经验值太大容易把码的边缘滤糊太小等于没滤。第三步二值化工业现场光照变化大全局阈值基本没法用自适应阈值对不同区域的明暗变化更友好51是邻域块大小15是常数偏移。如果二维码线条很细块大小可以调到31线条粗可以调到71需要现场调。4.2 透视校正相机不是正对着二维码拍的情况很常见比如二维码贴在设备侧面镜头斜着照过去。这时候定位角虽然能看到但解码会失败因为码的几何结构已经变形了。用OpenCV的findContours找出二维码的三个定位角或者四个角算透视变换矩阵把二维码区域拉正再送给zxing。std::vectorcv::Point2f src_pts, dst_pts; // src_pts是原图中找到的定位角坐标 // dst_pts是对应校正后的坐标 cv::Mat H cv::getPerspectiveTransform(src_pts, dst_pts); cv::Mat warped; cv::warpPerspective(src, warped, H, cv::Size(target_w, target_h)); // 然后对warped再做灰度化和二值化这一步是识别率提升最明显的环节。实测中倾斜30度以上的二维码不校正几乎是必失败校正后能稳定解出。4.3 调用zxing解码zxing-cpp的接口在不同版本里变化很大。我这里写的是当时比较稳定的一套接口基于zxing-cpp 2.x早期版本。#include zxing/MultiFormatReader.h #include zxing/MatSource.h using namespace zxing; std::string decodeQRCode(const cv::Mat binaryImage) { cv::Mat gray; if (binaryImage.channels() 3) cv::cvtColor(binaryImage, gray, cv::COLOR_BGR2GRAY); else gray binaryImage; RefLuminanceSource source(new MatSource(gray)); RefBinarizer binarizer(new GlobalHistogramBinarizer(source)); RefBinaryBitmap bitmap(new BinaryBitmap(binarizer)); DecodeHints hints; hints.addFormat(BarcodeFormat::QR_CODE); MultiFormatReader reader; RefResult result reader.decode(bitmap, hints); return result-getText()-getText(); }这里有个重要的点你已经做过二值化了为什么还要传入Binarizer很多人会疑惑。实际是zxing内部还会再做一次二值化如果传入的图像本身就是干净的二值图双阈值处理反而可能丢失边缘。所以一种取巧的做法是把灰度图传进去让zxing自己做二值化另一种做法是把预处理后的图直接作为灰度源传入并关掉二值化步骤。但zxing::MatSource这个类内部不感知图像类型想精细控制得写一个自定义的LuminanceSource子类重写getRow方法返回灰度数据。我在项目里用的是自定义源代码大概这样class MatLuminanceSource : public LuminanceSource { public: MatLuminanceSource(const cv::Mat mat) : LuminanceSource(mat.cols, mat.rows), mat_(mat) {} zxing::ArrayRefchar getRow(int y, zxing::ArrayRefchar row) const override { if (!row) row zxing::ArrayRefchar(mat_.cols); const char* data mat_.ptrchar(y); std::memcpy(row[0], data, mat_.cols); return row; } zxing::ArrayRefchar getMatrix() const override { zxing::ArrayRefchar matrix(mat_.rows * mat_.cols); for (int y 0; y mat_.rows; y) { const char* data mat_.ptrchar(y); std::memcpy(matrix[y * mat_.cols], data, mat_.cols); } return matrix; } private: cv::Mat mat_; };这个类的意义在于不管外部怎么处理最终传给zxing的都是干净、线性的灰度数据zxing内部的工作就相对稳定。4.4 解码结果的判空与重试机制zxing的解码不是每次都能成功。光照一变、角度一偏decode就会抛ReaderException。所以项目里的识别循环要写成带重试逻辑的bool tryDecode(const cv::Mat input, std::string resultText) { std::vectorint blockSizes { 31, 51, 71 }; for (size_t i 0; i blockSizes.size(); i) { cv::Mat binary; cv::adaptiveThreshold(input, binary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, blockSizes[i], 15); try { std::string text decodeQRCode(binary); if (!text.empty()) { resultText text; return true; } } catch (zxing::Exception) { continue; } } return false; }概率上讲一组阈值不行就换另一组三组下来成功率能到95%以上。这个重试机制是我在实践中自己加出来的不管是在照明不足还是过度反光的场景多试几个参数组合比死磕单一阈值靠谱得多。写识别程序记住一个原则宁可多试几次也别想一次成功。5. 常见问题与排查实录5.1 链接错误 LNK2019 / LNK2001这个错误几乎所有人都遇到过。表象是编译没问题链接时提示unresolved external symbol void __cdecl cv::imread(...)之类。排查路径第一看附加依赖项是否齐全缺哪个模块就补哪个lib。第二看opencv目录里的vc10和vc12是否混用。第三看运行库选项是否匹配zxing库编译时用的/MT主工程用了/MD立即报错。我的建议是主工程所有相关模块统一/MD最稳。5.2 识别率低有的码扫不出来原因八九不离十是二值化或者透视没处理好。先用一段很直观的代码验证问题是哪个环节cv::imwrite(step1_gray.jpg, grey); cv::imwrite(step2_adaptive.jpg, binary);把中间结果保存成文件人为检查图像质量。如果第二步出的图二维码区域有空洞、断线调整块大小如果二维码是倾斜的检查透视校正结果。我曾经在一个项目里排查了三天最后发现是相机曝光时间不对二维码反光白成一片预处理再怎么调都救不回来。所以硬件端的光照条件如果不可调预处理做得再好也有天花板。5.3 程序运行崩溃崩溃多发生在程序加载dll阶段或者一调用解码函数就崩。加载阶段崩溃八成是PATH里没配OpenCV的dll目录。调用阶段崩溃大概率是zxing库和主程序的CRT堆不一致。静态库方式链接zxing时如果有一方用了/MT另一方用/MD释放内存时可能把堆搞坏表现就是偶发崩溃。这种崩溃最恶心不固定复现好在排查方向明确统一运行库方式基本能解决。5.4 中文内容乱码zxing解出的字符串是UTF-8编码而VS2010的char*默认按本地代码页显示在中文Windows下就是GBK直接输出到控制台或者写入文件时会乱码。处理办法是解码后转码// 用Windows API做UTF-8到GBK的转换 int wLen MultiByteToWideChar(CP_UTF8, 0, utf8Text.c_str(), -1, NULL, 0); wchar_t* wBuf new wchar_t[wLen]; MultiByteToWideChar(CP_UTF8, 0, utf8Text.c_str(), -1, wBuf, wLen); int gbLen WideCharToMultiByte(CP_ACP, 0, wBuf, -1, NULL, 0, NULL, NULL); char* gbBuf new char[gbLen]; WideCharToMultiByte(CP_ACP, 0, wBuf, -1, gbBuf, gbLen, NULL, NULL); std::string gbkText(gbBuf); delete[] wBuf; delete[] gbBuf;这个转码步骤在写上位机交互界面或者写到Excel导入文件的时候必须做不然领导一看输出全是乱码项目直接打回。5.5 性能优化老工控机的CPU普遍弱。zxing解码一张1280x720的图不做预处理的情况下大约需要几十毫秒到几百毫秒但如果采集的是1920x1080甚至更大的图预处理部分反而成了性能瓶颈。我的做法是先缩小图像再预处理。二维码信息密度够的话图像缩到640x480识别率几乎不受影响速度能快四倍。另外一个经验值是设定ROI区域在画面里预先框出一个扫描区域只对这个区域做二值化和解码也能大幅提速。6. 实测效果与工程化建议6.1 我跑出来的真实数据在一台工控机上CPU是赛扬J1900内存4G用这个组合识别打印在标签纸上的二维码。标签纸质量一般有点反光用普通USB相机拍摄分辨率1280x720。实测100次识别成功99次失败1次是二维码表面有个明显污渍加上逆光双重作用属于物理不可解的极端情况。平均单次识别耗时含预处理在180ms左右基本满足每分钟300件的流水线需求。如果换成更好的光源和工业相机成功率还能往上走。但说实话在这个项目里瓶颈已经不是软件算法了而是光照和材料问题。6.2 把这个工程交付给别人需要注意什么如果你也要打包一套类似vs2010 opencv zxing-cpp.7z给别人用有这么几个点要交代清楚第一点7z包里不要放完整的环境安装包太大了。该放的是库文件、头文件、编译好的dll和源码工程。谁接手谁自己装VS2010自己配环境反而能少很多兼容性问题。第二点包内附一份版本清单写明OpenCV的版本号、zxing-cpp的Git提交时间、CMake版本和编译参数。别小看这份说明两个月后你自己都可能忘。第三点运行库文件全部放在运行目录下别只配环境变量因为你的程序可能被放到客户机器上没有管理员权限改环境变量的环境里。说到这这套组合虽然在今天看来又老又旧但在老设备升级和特定领域里它依然大量存在并且稳定工作。实际项目中我个人的体会是搭环境的时间至少占整个项目的一半编译不过、链接报错的坑远比业务代码多。如果你手里也有一个零散的7z包名字里带着这些关键字希望这篇能帮你省下几个通宵。本文还有配套的精品资源点击获取
返回列表