首页 / 资讯中心 / 文章详情

C#与Halcon车牌识别开源工具:从图像定位到OCR的完整实现

C#与Halcon车牌识别开源工具:从图像定位到OCR的完整实现 ★ FEATURED ARTICLE
前阵子把几个项目里一直在用的车牌识别逻辑单独抽出来做成了一个可独立运行的开源小工具。技术栈很直接C# 负责界面、任务调度和结果输出Halcon 负责图像预处理、车牌定位、字符切分和 OCR 识别。源码是完整的一套拿到之后可以直接编译运行不用自己再补一堆依赖。这个工具解决的问题很聚焦停车场出入口、园区门禁、厂区内部车道这类固定点位抓拍场景输入一张包含车头的图像输出车牌号码、置信度、位置坐标同时保留原始截图和识别记录。如果你正在做工业视觉、安防监控或者上位机开发又不想从头去碰图像算法这套代码值得花点时间看看。1. 车牌识别工具的整体设计与思路拆解1.1 车牌识别的核心链路车牌识别本质是一条固定流水线图像读取、预处理、车牌区域定位、几何校正、字符分割、OCR 识别、业务后处理。听起来简单真正落地时每个环节都有坑。比如“定位”这一步如果抓拍图像背景里有大量蓝色物体分分钟会把车身贴膜、路边广告牌当车牌“字符分割”遇到汉字笔画断裂、字符粘连识别结果就会变成乱码“OCR 识别”面对模糊字符、反光和倾斜角度置信度会明显下降。这个工具在设计时把整条链路拆成了四个独立模块图像获取模块负责从文件目录或相机回调拿原始帧预处理模块负责灰度化、滤波和 ROI 裁剪车牌检测模块负责定位和校正字符识别模块负责切分和 OCR。模块之间只传标准化结果对象比如PlateImage和PlateResult这样后续不管是替换算法实现还是接入新的相机型号都不会牵一发动全身。这种拆法还有一个现实原因实际项目里往往不是“识别不准”而是“不知道哪里不准”。把链路拆开之后每一层都能单独输出中间状态图。我在工具里加了一个 Debug 模式开启后会依次画出预处理结果、候选区域、校正后的车牌图、字符切分边界。排错的时候不用猜直接看图就知道问题出在哪一环。1.2 为什么这次选了 C# 加 Halcon开发这类工具常见的方案还有“C OpenCV”和“Python OpenCV”。C 性能上限高但开发周期长尤其是界面、日志、数据库上报这些业务功能要写大量代码。Python 做算法验证很快但交付给现场使用时打包环境、版本兼容、性能抖动都是麻烦事。C# 恰好卡在一个很舒服的位置Windows 生态成熟WPF 做界面顺手串口、数据库、网络通信都有现成库和 Halcon 的 .NET 接口配合也特别自然。Halcon 在这个项目里起到的作用不是简单地调用几个算子而是把图像处理里最麻烦的部分做了工业级优化。它的形态学算子、连通域分析、OCR 分类器在常见的工业场景中表现稳定很多细节参数不需要自己从头调。比如字符切分之后要筛选“面积符合车牌字符大小”的区域如果纯用 OpenCV 写需要自己控制轮廓层级和几何规则而在 Halcon 里一个select_shape加几个参数就能把大多数干扰区域清掉。开发效率和鲁棒性都能兼顾。当然选型也要考虑成本。Halcon 是商业授权软件不是所有场景都愿意花这部分预算。但如果你已经有 Halcon 许可证或者正在评估工业视觉方案那这套 C# Halcon 的架构就可以直接作为参考如果完全没有预算后续可以把 Halcon 调用层替换成 OpenCV 实现整体接口不需要动这种替换成本在设计阶段就已经预留出来了。2. 工具选型解析车牌识别中的关键算法细节2.1 图像获取与预处理阶段实际项目里图像来源通常有两种网络相机抓拍帧和本地图片文件。开源工具里我两条路都保留了。测试阶段最省事的方式是直接把一批图片放到固定目录下程序启动后批量识别输出 CSV 结果现场部署时再通过相机 SDK 的回调接口把帧数据转成HImage送入识别流程。预处理阶段要控制“度”。第一步通常是转灰度如果车牌底色是蓝色也可以直接在 HSV 色彩空间里提取色相通道这样对光线变化更稳健。中值滤波要去掉压缩噪声和传感器噪点但滤波窗口不能太大我用的是 3×3否则字符边缘会被磨掉后面做二值化时笔画容易断。对比度拉伸同样要留余地尤其是在夜间抓拍图像中直接做全局直方图均衡化会让反光区域过曝车牌字符反而看不清。另一个关键点是 ROI。固定点位的相机车牌出现的位置往往高度集中在某个区域。不要一上来就对整张 1920×1080 图像跑算法先按经验把识别区域缩到画面中央偏下的一个多边形里。这样既能减少误检又能明显降低处理耗时。这个工具里 ROI 区域可以在界面里手动拖拽保存识别时直接加载配置。2.2 车牌定位的思路和参数选择这个工具里使用的定位方式是“颜色特征 形态学闭运算 连通域筛选”实际效果在大多数场景下都够用。国内常见的蓝牌、绿牌、黄牌、白牌底色相对固定而且车牌区域的字符和底色之间对比度很高提取颜色区间之后能形成一块边界清晰的色块。以蓝牌为例我会把 RGB 图像转成 HSV取色相和饱和度阈值范围过滤出蓝色像素再生成二值掩膜。掩膜上通常会包含车身、车灯、背景广告牌等蓝色区域接着做一次闭运算让内部的白色字符区域被周围的蓝色像素填充整个车牌就会连成一个接近矩形的连通域。关键参数是闭运算的卷积核尺寸我用的是宽 30、高 10 的矩形结构元素专门针对车牌这种“扁长形状”设计。核太大容易把旁边物体也连进来核太小字符区又补不上这个尺寸需要在调试环境里多试几轮。连通域筛选用select_shape主要关注面积、宽度、高度和长宽比。不同分辨率的图像面积阈值必须动态计算不能写死。比如 1920×1080 图像里车牌区域面积通常占整幅画面的 2% 到 5%我在代码里按图像总像素数换算出一个合理区间再把宽高比限制在 2.5 到 5.2 之间。经过这一步误检区域基本能清掉剩下少数候选区域再用字符纹理特征做二次确认。倾斜校正是很多人容易忽略的步骤。车牌不可能是绝对水平的常见倾斜角度在正负 15 度以内。我提取每个候选区域的最小外接矩形拿到角度值之后做仿射变换把车牌区域旋转到水平状态。这一步不能省否则后面字符切分的垂直投影会偏差极大识别率断崖式下降。2.3 字符切分与 OCR 分类字符切分我采用“连通域分析 垂直投影”组合方式。第一步是通过二值化把字符变成白色前景连通域分析能找到每个独立字符块第二步利用字符之间固定间距的特点用垂直投影直方图切分连续字符列。如果字符发生粘连连通域会把两个字符连成一个块这时候再用固定宽度滑动切分来补救。相反如果笔画断裂导致一个字符变成两个块就根据字符宽度阈值做合并。OCR 识别层是这套工具里最需要根据业务定制的地方。Halcon 自带常用的工业字符集分类器对英文和数字识别效果不错但中文车牌里的省份简称比如“京、沪、粤、苏、浙”这些汉字在标准英数分类器里是不存在的。所以在这个项目中我额外准备了一个针对省份简称的训练数据用一批真实车牌图片切出的汉字样本做了分类器训练。分类器输出每个字符候选类别和置信度后面再接一层正则校验。正则校验是车牌识别特有的是“业务常识约束”。普通字符识别可能出现的“O”和“0”、“I”和“1”混淆放到车牌规则里就很好处理第二位一定是字母最后五位通常是字母和数字混合但不能出现 I 和 O。代码里直接用正则表达式过滤把明显不符合车牌规则的候选组合直接淘汰置信度排在后面的合法组合补上来。这步改动看起来不大但整体识别准确率能提升好几个点。3. 实操过程与核心环节实现3.1 工程结构与环境准备开源工程目录分成三块PlateDetector负责定位和校正OcrRecognizer负责字符切分和分类MainApp负责界面与调度。这样分的好处是算法模块可以被其他项目直接引用不需要连带界面代码一起拷走。环境准备方面安装 Halcon 后在安装目录的bin目录下能找到halcondotnet.dll和hdevengine.dll把这两个程序集引用进 C# 工程。需要注意主程序的项目平台目标要改成 x64因为 Halcon 运行时是按 64 位发布的如果不匹配加载时会直接报BadImageFormatException。第一次运行前还要把 Halcon 运行时目录加入 PATH或者把需要的运行时文件拷贝到程序输出目录否则调用任何算子都会报“找不到动态库”。界面我用的是 WinForms代码结构更直白方便大家看原理。在主窗体上放一个PictureBox显示原始图和识别结果旁边是识别日志列表底部是统计信息。为了不阻塞界面所有识别操作都放到后台线程池里执行识别完成后再通过事件回传结果。3.2 核心算子调用定位、校正与识别在 C# 中调用 Halcon 算子最直接的方式是通过HOperatorSet静态方法。下面是一段典型的定位代码演示从图像读取到候选区域筛选using HalconDotNet; HImage image new HImage(imagePath); // 转 HSV提取蓝色通道范围 HImage hsv image.TransToRgb(hsv); HChannel hChannel hsv.Decompose3(out HChannel sChannel, out HChannel vChannel); // 蓝色车牌阈值 HRegion blueRegion hChannel.Threshold(200, 255); HRegion greenRegion hChannel.Threshold(90, 140); // 新能源绿牌 HRegion mask blueRegion.Union2(greenRegion); // 闭运算填充字符间隙 HRegion closed mask.DilationRectangle1(30, 10); closed closed.ErosionRectangle1(30, 10); // 连通域筛选 HRegion connectedRegions closed.Connection(); HRegion selected connectedRegions.SelectShape(area, and, minArea, maxArea); selected selected.SelectShape(width, and, 100, 800); selected selected.SelectShape(height, and, 30, 200);因为 Halcon 算子的输出参数使用out传递变量必须先声明再传入看清楚类型就能减少编译报错。上面代码里的Decompose3返回三个通道我用out HChannel sChannel, out HChannel vChannel的方式直接接收这是比较常见的 C# 写法。阈值范围只是示例值真实现场需要根据相机型号和画面亮度调整。拿到候选区域后再做一次找最小外接矩形和仿射变换// 计算最小外接矩形 HRegion plateRegion selected[0]; HTuple row, col, phi, len1, len2; plateRegion.SmallestRectangle2(out row, out col, out phi, out len1, out len2); // 生成旋转矩阵并校正 HHomMat2D homMat new HHomMat2D(); homMat homMat.HomMat2dRotate(-phi, row, col); HImage rotated image.AffineTransImage(homMat, constant, false);这里有个关键点phi的单位是弧度角度转正时需要取反否则会把本来就水平的图像转歪。代码注释里我特意写了这个坑之前版本漏掉取反导致现场调试时怎么都识别不对。字符切分和 OCR 识别的核心代码如下// 把校正后的区域裁剪出来转灰度并二值化 HImage plateGray rotated.Rgb1ToGray(); HRegion plateRegion plateGray.Threshold(0, 120); HRegion charRegions plateRegion.Connection(); // 用连通域筛选字符块 HRegion charBlocks charRegions.SelectShape(height, and, 20, 120); // OCR识别 HTuple ocrHandle; HOperatorSet.ReadOcrClassMlp(Industrial_0-9A-Z_NoRej.omc, out ocrHandle); HTuple classes, confidences; HOperatorSet.DoOcrMultiClassMlp(charBlocks, plateGray, ocrHandle, out classes, out confidences);这段代码里使用的训练分类器是 Halcon 自带的标准英数模型适合识别英文和数字。省份汉字的识别需要切换到我训练好的中文分类器工程里已经提供了对应的.omc文件直接替换ReadOcrClassMlp的参数即可。3.3 识别结果的工程化封装识别结果不能只返回一个字符串实际业务里至少需要四样东西车牌号、整体置信度、车牌位置坐标、识别时间。我在工程里定义了一个PlateResult类字段包括PlateText、Confidence、BoundingBox、SnapPath、IsValid。这样上层 UI 拿到的就是一个完整对象不需要再反查原始数据。置信度的计算不能简单对每个字符取平均因为汉字识别错误往往发生在低置信度字符上但英数部分可能很准。我用了加权平均汉字段权重 0.2字母段权重 0.5数字段权重 0.3。具体权重可配置在实测中比简单平均更能反映真实可靠程度。对外接口也做了异步封装。调用方传入图像路径或HImage立即返回TaskPlateResult。内部通过线程池把识别任务丢到后台识别完再回到上下文回调。这样的好处是即使一台机器接了几路相机的画面轮流识别业务层也不会被卡住。3.4 多线程调度与 UI 刷新写上位机不要在主线程里做图像算法这个原则在这个工具里贯彻得很彻底。相机抓帧线程只负责往内存队列里塞FrameItem识别线程从队列取图跑完算法后把结果投递到 UI 线程。队列使用ConcurrentQueue并且限制最大长度超过阈值直接丢弃最旧的帧。这样做可以避免现场相机帧率不稳时内存无限增长。UI 刷新使用事件方式识别完成触发OnPlateRecognized事件界面订阅后更新识别列表和图片框。注意界面控件跨线程更新要使用Invoke或BeginInvoke否则会出现线程安全问题。调试时我习惯在事件回调里加一个Stopwatch统计耗时这样能直观看到每帧的耗时瓶颈在定位还是识别。4. 常见问题与排查技巧实录4.1 Halcon 环境与集成问题最容易翻车的是 DLL 相关报错。HalconDotNet.dll引用没问题但运行时找不到hdevengine.dll原因是项目输出目录里没有拷贝运行时。解决方案很简单把安装目录里的hdevengine.dll等运行时文件设为“复制到输出目录”或者统一放到系统 PATH 中。每次升级 Halcon 版本后记得把引用路径重新指一下否则会出现版本不匹配的异常。另一个常见问题是 32 位和 64 位不匹配。C# 工程默认是AnyCPU在 64 位系统上会以 64 位运行但 Halcon 的某些配套组件还是 32 位时就会崩溃。我直接在工程属性里把平台目标改为x64彻底避开这种问题。如果现场机器是 32 位系统那就需要整套换成 32 位运行时不能混用。许可证问题也不少见。Halcon 使用授权许可证第一次运行会在控制台或日志里输出授权错误。遇到这类提示先检查两件事环境变量HALCON_LICENSE_PATH是否指向许可证目录当前系统时间是否和授权期限一致。不要想着绕过授权正确做法是联系供应商购买合适的授权类型。4.2 车牌定位不准或误检定位问题在四个环节都可能出现我整理了一个排查表方便直接对应处理现象可能原因处理方式蓝色底色区域完全没找出来色相通道阈值过窄放宽 HSV 阈值范围增加饱和度约束车身贴膜、蓝色车灯被当车牌颜色区间太宽 形态学核过大缩小结构元素尺寸增加宽高比约束定位结果倾斜严重最小外接矩形角度跳变检查phi取反增加角度平滑处理夜间反光导致区域断裂二值化阈值过低改用自适应阈值或做局部对比度增强我的习惯是优先用 Debug 模式看中间图。如果掩膜图上目标区域颜色不对说明色彩空间通道选错了如果区域没有连成完整矩形说明闭运算核太小如果筛选出的区域形状不对劲那问题在select_shape参数而不是后续算法。按这个逻辑逐个排除通常几分钟就能定位到问题源头。4.3 字符分割和 OCR 的坑字符切分最常见的现象是“两个字粘在一起识别成一个字”或者“一个字被拆成两个”。前者多数是因为二值化之后字符之间仍有连接可以用腐蚀操作断开腐蚀核不能太大否则笔画细的字符会消失。后者通常是字符内部笔画断裂比如反光让字符中段变成背景色这时需要按宽度合并相邻连通域。OCR 层面英文和数字在标准分类器下表现很好容易出问题的是汉字。车牌汉字字形相对固定但不同省份字体和笔画密度差异大我建议自己采集真实场景字符样本训练分类器至少覆盖业务范围内可能出现的那几个省份简称不要贪多。训练样本每类至少几百张保证光照、角度和清晰度的多样性。后处理能救回不少误识别。我写了一个字符级黑白名单第二位只能是字母字符长度固定为 7不能出现字母 I 和 O。最后再通过多帧校验同一辆车抓三帧三帧结果一致才输出任何一帧不同就重置计数。虽然牺牲了一点实时性但在入口道闸这种场景里稳定压倒一切。4.4 性能瓶颈与优化方向性能优化不一定非得 GPU。我先用Stopwatch测出定位、切分、识别三段的耗时发现定位阶段如果全图处理会吃掉接近一半时间。于是把 ROI 裁剪提前识别区域只保留画面中的车道范围处理后耗时降了 40%。如果 ROI 还不够再开 OpenCL 加速通道。Halcon 支持很多 GPU 算子但启用后需要监测不同硬件下的显存占用部分老显卡反而比 CPU 慢。多路相机并发识别时要避免无限制创建线程。我用的线程池固定为 4 个 worker任务队列排队处理。实际测试中4 路 1080p 画面保持 15 fps 没有问题。如果任务持续积压说明单机算力不够这时不要优化程序了直接在架构上扩展一台识别节点。5. 开源结构、后续扩展与个人经验5.1 源码工程里包含什么工程里包含的源码覆盖了完整闭环界面程序、算法封装、分类器文件、示例图片、说明文档。示例图片我放了几十张不同场景的抓拍包括白天、夜间、顺光和逆光方便第一次跑起来就能立刻看到效果。说明文档里写了环境配置步骤和每个参数的调整建议。分类器文件单独放在Resources目录如果觉得准确率不够可以直接替换成自己训练好的模型。5.2 如何接入新场景换现场时最该调整的不是算法而是 ROI 和相机参数。每个点位的相机高度和角度都不同保存一套独立的CameraProfile配置比较合适。工具里已经做了配置文件热加载切换现场时只需要换一个 JSON 配置不需要重新编译程序。如果要换相机品牌只需在图像获取模块里实现对应的相机 SDK 回调后面所有逻辑都能复用。5.3 把车牌识别封装成服务的建议实际项目中我发现车牌识别这层最容易被多个业务系统调用比如门禁系统要识别结果收费系统也要识别结果有时候监控大屏还要展示。如果在每个系统里各放一份算法代码维护成本会剧增。后来我把识别模块单独抽成一个本地 HTTP 服务对外暴露一个接口输入图片路径返回车牌结果。各业务系统只管调用接口算法升级时只需要替换服务端。再往后走如果现场设备算力足够可以把识别服务部署在边缘设备上只把识别结果回传到中心。这样中心平台负载会瞬间降下来网络带宽消耗也小很多。这套工程里的PlateDetector和OcrRecognizer本来就是无界面依赖的类库移植成独立服务只需要加一层监听封装。最后说一点我实际操作中的真实感受车牌识别这类工具算法选型只是第一步后续的取景、标定、调参和现场适配才真正决定口碑。开源版本解决了“从零到一”但“从一到一百”还需要不断积累样本和场景数据。这个工具我会持续迭代后面大概率会加入车型分类、车身颜色识别、车标识别这些方向。你现在拿到源码可以先把基础链路跑通再按自己业务需要慢慢加功能。
阅读完成 · 觉得有帮助?
咨询建站