OpenCV 二维码识别:OpenCvSharp + 相机取流 + WinForms 显示
萨达大 · 06-01 · 追溯读码是产线标配
产线追溯、物料绑定、治具校验、出货标签复核——工位软件里「读二维码」出现频率极高。很多兄弟第一反应是 ZXing,其实 OpenCvSharp 4 自带的QRCodeDetector已经能覆盖绝大多数 QR 场景:检测定位点、透视校正、解码字符串一条龙。本文按上位机工程师的真实排期来写:周一装 NuGet,周二接 USB 相机,周三把结果显示在 WinForms,周四联调 PLC 上报。全文带可复制代码、现场调参口诀和 FAQ。
一、为什么在上位机里用 OpenCV 读码
工业 WinForms 项目往往已经引了 OpenCV 做测量或模板匹配,再引一套读码库会增加部署体积和许可证心智负担。QRCodeDetector与Mat管线同源:同一帧既可以读码又可以测边,线程模型也统一。缺点也要说清楚:码制支持以 QR 为主,DataMatrix、PDF417 等仍需 ZXing 或商用读码器 SDK。若你的工位只有 QR 追溯,OpenCV 足够且与下文圆形检测、模板匹配章节无缝衔接。
二、工程准备与常见坑
NuGet 引用(版本号请与团队仓库锁定,下面用 4.x 示意):
<PackageReferenceInclude="OpenCvSharp4"Version="4.10.0.20240616"/><PackageReferenceInclude="OpenCvSharp4.runtime.win"Version="4.10.0.20240616"/>平台目标务必 x64。OpenCvSharp 的 native 依赖在产线 PC 上几乎都是 64 位 Windows;AnyCPU 在部分机器会以 32 位进程运行,然后你就看到经典的DllNotFoundException opencv_world。Release 模式下测一遍,Debug 里帧率高不代表上线后 CPU 够用。
首次集成建议在空 WinForms 里只放PictureBox+ 按钮「打开相机」「单帧解码」,确认链路再并入主界面。萨达大见过太多「一上主窗体就卡死」——其实是 UI 线程里Read()阻塞了消息泵。
三、QRCodeDetector 解码核心
usingOpenCvSharp;usingOpenCvSharp.Extensions;publicsealedclassQrReaderService{privatereadonlyQRCodeDetector_detector=new();public(boolok,stringtext,Point2f[]corners)TryDecode(Matbgr){if(bgr==null||bgr.Empty())return(false,string.Empty,Array.Empty<Point2f>());usingvargray=newMat();Cv2.CvtColor(bgr,gray,ColorConversionCodes.BGR2GRAY);// 光斑不均:可选 CLAHE(注意只对 ROI 做以省时间)// using var clahe = Cv2.CreateCLAHE(2.0, new Size(8, 8));// clahe.Apply(gray, gray);stringtext=_detector.DetectAndDecode(gray,outPoint2f[]points);boolok=!string.IsNullOrWhiteSpace(text);return(ok,ok?text.Trim():string.Empty,points??Array.Empty<Point2f>());}}解码成功时points一般为四个角点,可在图像上画绿色四边形,操作员一眼可知「对准了没有」。若角点有了但text为空,多半是对比度或模块损坏,优先调光而不是狂加 GaussianBlur(糊了更难解)。
多码场景:OpenCV 提供 multi 接口(OpenCvSharp 命名可能是DetectAndDecodeMulti),一次帧内多个 QR 返回字符串数组。产线托盘多码时要配合 ROI 分区,避免相邻码互相干扰。
四、USB 相机 VideoCapture 连续取流
publicsealedclassCameraGrabLoop:IDisposable{privateVideoCapture?_cap;privateThread?_thread;privatevolatilebool_run;publiceventAction<Mat>?FrameReady;publicboolOpen(intdeviceIndex=0,intwidth=1280,intheight=720){_cap=newVideoCapture(deviceIndex,VideoCaptureAPIs.DMSHOW);if(!_cap.IsOpened())returnfalse;_cap.Set(VideoCaptureProperties.FrameWidth,width);_cap.Set(VideoCaptureProperties.FrameHeight,height);// 固定曝光若驱动支持(不同相机属性 ID 可能无效,需实测)// _cap.Set(VideoCaptureProperties.AutoExposure, 0);returntrue;}publicvoidStart(){if(_cap==null)return;_run=true;_thread=newThread(CaptureLoop){IsBackground=true,Name="UvcGrabThread",Priority=ThreadPriority.AboveNormal};_thread.Start();}privatevoidCaptureLoop(){usingvarframe=newMat();while(_run&&_cap!=null){if(!_cap.Read(frame)||frame.Empty()){Thread.Sleep(5);continue;}FrameReady?.Invoke(frame.Clone());}}publicvoidStop(){_run=false;_thread?.Join(800);}publicvoidDispose(){Stop();_cap?.Release();_cap?.Dispose();_cap=null;}}工业相机替代方案:海康 MVS、Basler pylon 等 SDK 回调里拿到灰度或 BGR 缓冲区,构造Mat后仍调用同一个QrReaderService。读码逻辑与相机品牌解耦,这是上位机架构里该有的分层。
五、WinForms 显示:线程与去重
界面刷新铁律:后台采图 + 克隆 Mat + BeginInvoke 更新 PictureBox。解码可与采图同线程,也可丢进BlockingCollection<Mat>由专用解码线程消费,避免读码耗时拖低帧率。
privatereadonlyQrReaderService_qr=new();privatereadonlyCameraGrabLoop_cam=new();privatestring_lastCode=string.Empty;privateDateTime_lastCodeTime=DateTime.MinValue;privatevoidBindCamera(){if(!_cam.Open(0)){MessageBox.Show("相机打开失败,检查占用与驱动");return;}_cam.FrameReady+=OnFrame;_cam.Start();}privateint_frameIndex;privatevoidOnFrame(Matmat){_frameIndex++;booldecodeThisFrame=(_frameIndex%2==0);// 隔帧解码降 CPUvarresult=decodeThisFrame?_qr.TryDecode(mat):(false,string.Empty,Array.Empty<Point2f>());if(result.ok&&result.corners.Length>=4){for(inti=0;i<4;i++){vara=(Point)result.corners[i];varb=(Point)result.corners[(i+1)%4];Cv2.Line(mat,a,b,newScalar(0,255,0),2);}}usingvarbmp=BitmapConverter.ToBitmap(mat);varuiBmp=(Bitmap)bmp.Clone();mat.Dispose();BeginInvoke(newAction(()=>UpdateUi(uiBmp,result.ok,result.text)));}privatevoidUpdateUi(Bitmapbmp,boolok,stringtext){pictureBox1.Image?.Dispose();pictureBox1.Image=bmp;lblStatus.Text=ok?$"OK:{text}":"等待二维码…";if(!ok)return;if(text==_lastCode&&(DateTime.Now-_lastCodeTime).TotalSeconds<2)return;// 去重,防止连续触发 PLC/MES_lastCode=text;_lastCodeTime=DateTime.Now;OnQrAccepted(text);// 写日志、Modbus、数据库}privatevoidOnQrAccepted(stringcode){// 示例:写 Info 日志,见 07 章 NLog// Logger.Info("QR={Code}", code);}PictureBox 闪烁可开双缓冲;更专业是用OpenCvSharp.UserInterface的PictureBoxIpl(若版本提供)或直接 GDI+ 画在 Panel 上。
六、与 PLC / 追溯系统对接(思路)
读码成功后的业务动作通常是:校验格式(正则、长度、前缀)、查重(同一盘料是否重复绑定)、写寄存器或 HTTP 调 MES。视觉层只负责string code,不要做 HTTP 在解码线程里同步阻塞;用队列 + 后台 Worker,失败可重试。萨达大建议码内容进日志时打掩码(中间段***),避免完整 SN 泄露到非追溯角色账号。
七、现场调光与安装
- 工作距离固定:治具定位销比软件 ROI 更可靠。
- 曝光:读码工位尽量关自动曝光;LED 补光用 diffuse,避免 QR 表面镜面反射。
- 分辨率:模块边长至少占 20~30 像素更稳;不够就升分辨率或光学放大。
- 脏污:用超时判定 NG,并触发「请擦拭镜头」维护提示(见 07 章 7x24 自愈)。
八、FAQ
Q1:DetectAndDecode 一直空,微信能扫?
说明光学/曝光问题居多。抓 RAW 帧保存 PNG,看是否过曝一块白;调整补光角度。软件侧试 CLAHE、仅 ROI 放大 2x 再解。
Q2:CPU 占用过高?
隔帧解码;ROI 裁剪(已知码在大致区域);降低预览分辨率,解码用全分辨率单帧 SDK 抓拍。
Q3:偶发误码?
QR 有纠错级别,但工业仍应做格式校验;必要时二次确认(转治具再拍一次)。
Q4:VideoCapture 打开 index 0 失败?
相机被其他软件占用;尝试 index 1;工业相机不要用 DSHOW,换 SDK。
Q5:OpenCvSharp 与 EmguCV 混用?
不建议同进程混两套 native;选一个栈统一。
Q6:需要生成 QR 吗?
OpenCV 也可生成;打印标签一般用 Bartender/ZPL,上位机只读。
导航
- 上一篇:05-Excel/PDF 报表导出
- 下一篇:02-OpenCV圆形检测
萨达大 · CSDN 上位机系列 · 欢迎收藏本目录索引
九、ROI 与透视(进阶)
当相机斜拍时,QR 在图像里是梯形,OpenCV 检测器内部会做透视变换,但极端角度仍失败。机械上保证光轴近似垂直;软件上可用四个角点GetPerspectiveTransform拉正后再 decode 备用通道。ROI 用Mat roi = new Mat(gray, new Rect(x,y,w,h))裁剪,注意 Rect 越界要 clamp。流水线节拍紧时,先粗定位(上一工位机械限位)再小 ROI 解码,单帧耗时可从几十毫秒降到个位数毫秒。
publicMatSafeRoi(Matsrc,Rectr){r.X=Math.Max(0,r.X);r.Y=Math.Max(0,r.Y);r.Width=Math.Min(r.Width,src.Width-r.X);r.Height=Math.Min(r.Height,src.Height-r.Y);returnnewMat(src,r);}十、错误处理与资源释放
Mat必须using或显式Dispose;FrameReady里克隆出来的 Mat 在 UI 线程画完要释放,否则 GDI+ Bitmap 与 native Mat 双泄漏,夜班跑八小时 OOM。相机线程退出前先Stop()再Dispose(),FormClosing 里解绑事件,避免窗体关了线程还在 Invoke。
十一、测试清单(上线前)
- 连续运行 2 小时无句柄泄漏(任务管理器观察 GDI/句柄)。
- 强反光、弱光、偏一像素对焦各 100 次读码成功率达标。
- 断拔 USB 相机软件不崩溃,提示重连(配合 07 章自愈)。
- 与 PLC 联调:OK 脉冲宽度、NG 报警、重复码拒绝逻辑正确。
- 日志可追溯:时间戳、工位号、码内容(脱敏策略评审通过)。
十二、延伸阅读
本目录02讲圆与尺寸;03把读码纳入 OK/NG 统计与 MES。07讲 NLog 与 7x24 异常处理,读码线程的异常不要吞掉,应进全局 Handler 并计数,便于维护窗口看「今日解码失败次数」趋势。
十三、OpenCvSharp 与 OpenCV 版本对应关系
团队里若有人用 C++ OpenCV 做算法原型,再交给 C# 集成,务必对齐主版本号。minor 差异偶尔导致QRCodeDetector行为不同。升级 NuGet 前在测试工位跑 golden 样图集:同一文件夹一百张历史 OK/NG 图,统计解码率变化。萨达大习惯在仓库里放TestImages/Qr/作为回归资产,CI 若条件允许可跑 headless 批量断言。
十四、WinForms 设计器注意事项
PictureBox.SizeMode建议Zoom,避免拉伸导致操作员误判对准。状态栏显示「帧率 FPS」「解码耗时 ms」「相机名称」,现场工程师会感谢你。快捷键 F5 单帧保存、F9 重新打开相机,比鼠标点菜单快。多语言项目里,错误提示用资源文件,但日志保持中文或英文一种统一,方便远程排查。
十五、小结
OpenCvSharp 的QRCodeDetector足以支撑典型 QR 追溯工位:VideoCapture 或 SDK 供帧 → 灰度 → 检测解码 → WinForms 显示与去重 → 业务层写 PLC/MES。把线程和资源管理好,再配合工程化章节的日志与自愈,读码模块就能长期稳定跑在产线 PC 上。下一篇我们转向HoughCircles 与圆尺寸测量,那是另一类视觉刚需。
最后补一句部署习惯:读码工位 PC 不要装杀软实时扫描相机 SDK 目录;Windows 更新可设维护窗口,避免周二上午整线停产等补丁。若你司信息安全政策必须扫描,请把 OpenCvSharp native 目录加入白名单并记录在案。 现场若更换镜头焦距,记得重新评估景深与读码距离,并更新作业指导书上的示意图,避免操作员按老经验摆料导致批量 NG。