简介:本资源是一套基于C# WinForm开发的车牌识别完整实现方案,面向计算机视觉初学者、智能交通系统开发者及.NET桌面应用工程师,解决车辆图像中车牌定位、颜色判别与字符OCR识别的一体化需求。压缩包共66个文件,包含16个核心C#源码文件(如Yolov7.cs、PlateManager.cs、Form1.cs等)、7个ONNX模型文件(YOLOv7检测+CRNN识别双模型)、14个依赖DLL及配置文件,整体大小为141.54MB,结构清晰,模块划分明确,便于理解目标检测与序列识别的协同流程。已有597人学习下载,配套提供B站视频演示与CSDN技术博文详解,涵盖VS2019+NET Framework 4.7.2环境配置、模型加载调试、UI交互逻辑及识别结果可视化等关键环节,可直接运行exe快速验证效果,亦支持二次开发与算法替换。
1. 项目概述:一个桌面端车牌识别系统的诞生
最近在做一个工业园区的车辆出入管理项目,客户明确要求系统要能离线运行,并且识别速度要快,最好能直接集成到他们现有的C# WinForm上位机里。市面上现成的车牌识别SDK要么是收费的,要么就是依赖网络API,不符合离线部署和成本控制的要求。于是,我就琢磨着自己动手,把YOLOv7和CRNN这两个在学术界和工业界都经受过考验的模型组合起来,用C# WinForm搭一个本地化的车牌识别系统。这不仅仅是一个“调用库”的活儿,更涉及到从Python模型训练、转换,到C#端推理框架集成、界面交互设计,再到性能优化的一整套流程。如果你也在寻找一个能脱离网络、部署在Windows工控机或普通PC上,且识别准确率有保障的车牌识别方案,那么我这次从零到一的踩坑和实现经验,或许能给你提供一个清晰的参考路径。
2. 技术选型与整体架构设计
2.1 为什么是YOLOv7 + CRNN?
选择这个组合,是基于车牌识别任务的两个核心阶段:定位(Detection)和识别(Recognition)。
YOLOv7负责定位:我们需要从一张复杂的车辆图像中,精准地框出车牌的位置。YOLO系列模型以其速度和精度平衡著称,而v7版本在保持YOLO实时性的优势下,通过更高效的网络架构和训练策略,进一步提升了小目标检测的精度。对于车牌这种在整张图片中占比可能很小的目标,YOLOv7的表现比前几代更稳定。相较于两阶段的Faster R-CNN,单阶段的YOLO在速度上优势明显,能满足实时视频流处理的需求。
CRNN负责识别:裁剪出的车牌图像,包含了字符序列信息。传统的分割+分类方法对字符粘连、倾斜、光照不均等情况非常敏感。CRNN(Convolutional Recurrent Neural Network)将CNN的特征提取能力与RNN的序列建模能力结合,端到端地输出字符序列,避免了繁琐且易错的分割步骤。它特别适合像车牌号、验证码这类不定长的文本识别任务。
C# WinForm作为载体:客户环境是Windows,且已有基于WinForm的管理系统。WinForm开发快速,界面控件丰富,与.NET生态结合紧密,便于集成硬件(如摄像头、道闸)和控制逻辑。虽然WPF在UI表现力上更强,但WinForm在工控、嵌入式上位机领域因其稳定性和低资源消耗,依然有不可替代的地位。
2.2 系统核心架构拆解
整个系统可以清晰地分为离线训练和在线应用两条线。
离线训练线(Python环境):
- 数据准备:收集并标注车牌数据集,包含车牌位置框(用于YOLOv7训练)和对应的车牌号码文本(用于CRNN训练)。
- 模型训练:
- YOLOv7训练:使用官方或改进的YOLOv7代码,在车牌检测数据集上训练,得到最优的权重文件(通常是
.pt格式)。 - CRNN训练:使用CRNN模型(如基于Pytorch的实现),在裁剪出的车牌图片和对应文本标签上训练。
- YOLOv7训练:使用官方或改进的YOLOv7代码,在车牌检测数据集上训练,得到最优的权重文件(通常是
- 模型转换:这是打通Python和C#的关键一步。需要将训练好的PyTorch模型(
.pt或.pth)转换为ONNX格式。ONNX是一个开放的模型交换格式,可以被多种推理引擎加载。
在线应用线(C# WinForm项目):
- 推理引擎集成:在C#中,我们需要一个能加载并运行ONNX模型的推理引擎。这里我选择了ONNX Runtime。它是一个高性能推理引擎,对.NET有原生支持(
Microsoft.ML.OnnxRuntimeNuGet包),并且可以利用CPU或GPU进行加速推理,非常适合生产环境。 - 图像预处理与后处理:
- 预处理:将从摄像头或文件读取的图像,按照模型训练时的要求进行缩放、归一化、颜色通道转换(如BGR转RGB)、并转换为Tensor。
- 后处理:
- 对YOLOv7的输出,进行非极大值抑制(NMS)过滤掉重叠框,得到最终的车牌位置坐标。
- 对CRNN的输出(一个序列概率矩阵),使用CTC解码算法或简单的贪心解码,将概率序列转换为最终的字符串(车牌号码)。
- WinForm界面与业务逻辑:设计主界面,集成图像显示框、按钮控件、结果展示区域。编写代码处理视频流捕获(如使用AForge.NET/OpenCvSharp)、按钮点击事件、识别结果展示与保存等。
注意:模型转换这一步至关重要。务必确保Python端导出ONNX模型时的输入输出节点名称、维度与C#端代码中
InferenceSession的输入输出定义完全一致,否则会导致运行时错误。
3. 关键模块实现与核心代码解析
3.1 模型训练与转换实操要点
YOLOv7训练注意事项:
- 数据集标注:使用LabelImg等工具标注时,标注框应紧密贴合车牌四角。建议对倾斜、透视畸变较大的车牌也进行标注,以增强模型鲁棒性。
- 配置文件修改:需要修改
data/custom.yaml,指定你的数据集路径和类别(通常只有‘license_plate’一类)。修改models/yolov7.yaml(或你选择的其他变体),将nc(类别数)改为1。 - 训练技巧:如果数据集较小,强烈建议使用迁移学习。从官方预训练模型(如
yolov7.pt)开始训练,可以大幅缩短训练时间并提升收敛效果。命令示例:python train.py --weights yolov7.pt --data data/custom.yaml --epochs 100 --batch-size 8。
CRNN训练与转换:
- 数据准备:CRNN需要的是单张车牌图片和对应的文本标签文件。图片需要统一高度(如32像素),宽度按比例缩放。标签文件可以是每行“图片路径 标签”的格式。
- 字符集定义:必须明确定义所有可能出现的字符,例如“0123456789ABCDEFGHJKLMNPQRSTUVWXYZ京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新”。注意排除容易混淆的字符,如‘I’和‘1’,‘O’和‘0’,通常车牌字符集中会去掉‘I’和‘O’。
- 导出ONNX:在PyTorch训练脚本中,使用
torch.onnx.export函数导出。关键是要提供一个正确的dummy_input(模拟输入张量),并指定动态维度(尤其是宽度维度,因为车牌图片宽度不一)。示例代码片段:import torch dummy_input = torch.randn(1, 3, 32, 100, device='cuda') # 批次,通道,高,宽(宽是动态的) torch.onnx.export(model, dummy_input, "crnn_plate.onnx", input_names=['input'], output_names=['output'], dynamic_axes={'input': {3: 'width'}, 'output': {1: 'seq_len'}})
3.2 C#端ONNX Runtime集成与推理
首先,通过NuGet为你的WinForm项目安装Microsoft.ML.OnnxRuntime和Microsoft.ML.OnnxRuntime.Gpu(如果使用GPU)。
创建推理会话与预处理:
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // 初始化推理会话,可指定使用CPU或GPU SessionOptions options = new SessionOptions(); // options.AppendExecutionProvider_CUDA(0); // 如果使用NVIDIA GPU InferenceSession yoloSession = new InferenceSession("yolov7_plate.onnx", options); InferenceSession crnnSession = new InferenceSession("crnn_plate.onnx", options); // 图像预处理函数(示例,需根据模型要求调整) public static DenseTensor<float> PreprocessForYolo(Mat srcImage, int targetSize) { // 使用OpenCvSharp进行resize,BGR转RGB,归一化等操作 Mat resized = new Mat(); Cv2.Resize(srcImage, resized, new Size(targetSize, targetSize)); // ... 更多预处理步骤 // 将数据转换为NCHW格式的DenseTensor<float> DenseTensor<float> tensor = new DenseTensor<float>(new[] { 1, 3, targetSize, targetSize }); // ... 填充tensor数据 return tensor; }执行YOLOv7推理与后处理:
// 准备输入 var yoloInputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", inputTensor) // “images”需与导出模型时的输入名一致 }; // 运行推理 using (IDisposableReadOnlyCollection<DisposableNamedOnnxValue> yoloResults = yoloSession.Run(yoloInputs)) { var outputTensor = yoloResults.First().AsTensor<float>(); // 后处理:解析outputTensor,应用置信度阈值和NMS,得到车牌框列表List<Rect> List<Rect> plates = ParseYoloOutput(outputTensor, srcImage.Width, srcImage.Height); return plates; }ParseYoloOutput函数是核心,需要根据你使用的YOLOv7输出层结构(例如,是包含检测框、置信度、类别的单一输出,还是多个输出头)来编写。通常需要遍历所有预测框,过滤掉置信度低于阈值(如0.5)的,再对剩余的框进行NMS。
执行CRNN推理与解码:
// 从原图中根据Rect裁剪出车牌区域 Mat plateImage = srcImage.Clone(plateRect); // 对plateImage进行CRNN所需的预处理(如高度缩放到32,保持宽高比填充,归一化) DenseTensor<float> crnnInputTensor = PreprocessForCRNN(plateImage); var crnnInputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", crnnInputTensor) // “input”需与导出模型时的输入名一致 }; using (var crnnResults = crnnSession.Run(crnnInputs)) { var crnnOutput = crnnResults.First().AsTensor<float>(); // 形状可能是 [1, seq_len, num_classes] // CTC解码或贪心解码 string plateNumber = DecodeCRNNOutput(crnnOutput, charset); // charset是定义的字符集 return plateNumber; }DecodeCRNNOutput函数实现解码。贪心解码简单直接:在序列的每一个时间步,取概率最大的字符索引,然后合并重复字符,最后去掉空白符(如果用了CTC)。CTC解码更准确,但实现稍复杂,可以搜索现成的.NET CTC解码实现。
3.3 WinForm界面与视频流处理
界面设计:主窗体可以包含一个PictureBox用于显示视频帧,一个ListBox或DataGridView用于展示识别结果,以及“开始”、“停止”、“拍照识别”等按钮。
视频流捕获:这里我推荐使用OpenCvSharp,它比AForge.NET功能更强大,与我们的图像处理流程结合更紧密。
- 安装NuGet包
OpenCvSharp4和OpenCvSharp4.runtime.win。 - 使用
VideoCapture类捕获摄像头或视频文件。
using OpenCvSharp; private VideoCapture capture; private bool isCapturing = false; private void btnStart_Click(object sender, EventArgs e) { if (capture == null) capture = new VideoCapture(0); // 0代表默认摄像头 if (!capture.IsOpened()) return; isCapturing = true; // 使用Timer或异步任务循环读取帧 Task.Run(() => { Mat frame = new Mat(); while (isCapturing) { capture.Read(frame); if (frame.Empty()) break; // 在UI线程上更新PictureBox this.Invoke(new Action(() => { pictureBox1.Image = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame); })); // 可选:进行实时识别 // var plates = DetectAndRecognize(frame); // UpdateResultUI(plates); } }); } private void btnStop_Click(object sender, EventArgs e) { isCapturing = false; capture?.Release(); }性能优化提示:实时识别时,不要对每一帧都进行识别,可以设置一个识别间隔(如每秒识别3-5帧),或者使用一个后台线程专门处理识别任务,避免阻塞UI导致界面卡顿。
4. 开发中的常见坑点与解决方案实录
4.1 模型转换与加载失败
问题1:C#加载ONNX模型时抛出“Failed to load model”或节点不匹配错误。
- 排查:首先检查ONNX模型文件路径是否正确。更常见的原因是Python导出ONNX和C#加载时的输入输出名称、维度不匹配。
- 解决:使用Netron工具(一个可视化神经网络模型的开源工具)打开你的ONNX模型,精确查看输入输出层的名称(
name)和形状(shape)。确保C#代码中NamedOnnxValue.CreateFromTensor的第一个参数与输入层名称完全一致。对于动态维度,在创建InferenceSession时可能需要特别处理,或者确保输入Tensor的维度与模型期望的静态部分匹配。
问题2:Microsoft.ML.OnnxRuntime.Gpu包在调用GPU时失败。
- 排查:错误信息可能类似“未能加载DLL ‘onnxruntime_providers_cuda.dll’”。
- 解决:
- 确认你的电脑有NVIDIA GPU并安装了合适的CUDA和cuDNN驱动,且版本与
Microsoft.ML.OnnxRuntime.Gpu包要求的版本匹配。查看NuGet包详情页面的依赖说明。 - 对于WinForm项目,有时需要将CUDA相关的运行时DLL(如
cudart64_1xx.dll,cublas64_1xx.dll等)复制到你的程序输出目录(bin\Debug或bin\Release)下。这些DLL通常位于CUDA安装目录的bin文件夹内。
- 确认你的电脑有NVIDIA GPU并安装了合适的CUDA和cuDNN驱动,且版本与
4.2 推理结果异常
问题1:YOLOv7检测不到车牌或框的位置不准。
- 排查:首先用训练时使用的测试图片在Python环境下验证模型效果,排除模型本身的问题。如果Python下正常,C#下异常,问题大概率出在图像预处理上。
- 解决:仔细对比Python训练/推理时和C#端的预处理流程,必须做到完全一致。包括:
- Resize算法:是
cv2.INTER_LINEAR还是INTER_NEAREST?在OpenCvSharp中对应InterpolationFlags.Linear或InterpolationFlags.Nearest。 - 颜色通道顺序:模型训练时用的是RGB还是BGR?OpenCV默认读图是BGR顺序。
- 归一化:是否进行了
/255.0归一化?是否还减了均值、除了标准差?具体数值是多少? - 数据布局:PyTorch模型通常是NCHW(批次,通道,高,宽),而OpenCV的Mat数据是HWC布局,需要进行转换。
- Resize算法:是
问题2:CRNN识别出的车牌号码乱码或字符错误。
- 排查:
- 字符集不匹配:C#解码时使用的字符集顺序必须与Python训练时定义的字符集顺序完全一致,包括空白符(blank)的位置(如果使用CTC)。
- 预处理不一致:同样是高度缩放、填充方式的问题。CRNN对输入图像的高度非常敏感,必须与训练时保持一致(如32像素)。
- 解码方式错误:贪心解码和CTC解码的结果会有差异。确认训练时用的什么解码方式,推理时就用什么。
- 解决:在Python端写一个简单的测试脚本,用几幅已知车牌图片进行推理,打印出模型输出的原始概率矩阵。然后在C#端对同一幅图片进行推理,也打印出原始输出矩阵,对比两者是否一致。如果不一致,回溯预处理步骤。如果一致,则检查解码算法。
4.3 WinForm界面与性能问题
问题1:视频显示卡顿,UI无响应。
- 原因:在UI线程中进行耗时的图像处理或识别推理,阻塞了消息循环。
- 解决:
- 异步处理:将视频捕获和识别逻辑放在
Task.Run或BackgroundWorker中。 - 双缓冲:为
PictureBox设置DoubleBuffered属性为true(可能需要继承自定义控件)。 - 控制识别频率:不要每帧都识别,可以设置一个计时器,每隔200-300毫秒对当前最新帧进行一次识别。
- 降低显示分辨率:用于显示的
PictureBox图像可以适当缩小,而传递给识别模型的图像保持原分辨率或模型要求的分辨率。
- 异步处理:将视频捕获和识别逻辑放在
问题2:内存泄漏,程序运行一段时间后崩溃。
- 排查:在频繁处理图像时,
Mat或Bitmap对象如果没有正确释放(.Dispose()),会导致内存泄漏。 - 解决:
- 对所有实现了
IDisposable接口的对象(如Mat,InferenceSession,Bitmap)使用using语句确保释放。 - 在循环中创建的临时
Mat对象,在循环末尾手动调用.Dispose()。 - 对于UI上显示的
Bitmap,在更新PictureBox.Image前,将旧的Image赋值给一个临时变量并调用.Dispose()。
// 更新PictureBox图像的正确做法 Bitmap oldBmp = pictureBox1.Image as Bitmap; pictureBox1.Image = newBitmap; // newBitmap是新生成的位图 oldBmp?.Dispose(); - 对所有实现了
5. 项目部署与优化进阶思考
当核心功能跑通后,我们可以从工程化和用户体验角度进行更深层次的优化。
模型轻量化与加速:
- 模型量化:ONNX Runtime支持将FP32模型量化为INT8模型,在几乎不损失精度的情况下,显著提升推理速度并减少内存占用。可以使用ONNX Runtime提供的量化工具在部署前对模型进行静态量化。
- 模型剪枝与蒸馏:在训练阶段,可以考虑对YOLOv7和CRNN模型进行剪枝,移除不重要的神经元连接,或者使用知识蒸馏训练一个更小的学生模型,以进一步减小模型体积,提升速度。
工程结构优化:
- 依赖管理:将所有模型文件(
.onnx)、配置文件、字符集文件等放入项目的Resources文件夹,并将其“生成操作”属性设置为“内容”,并“如果较新则复制”,确保发布时能一并输出。 - 配置化:将置信度阈值、NMS阈值、识别间隔、模型路径等参数提取到
App.config配置文件中,方便后期调整,而无需重新编译代码。 - 日志记录:集成如NLog或log4net等日志框架,记录程序运行状态、识别结果和错误信息,便于线上问题排查。
功能扩展:
- 车牌颜色识别:可以在YOLOv7检测到车牌后,在车牌区域内统计主要颜色(HSV色彩空间),通过设定阈值来判断是蓝牌、黄牌、绿牌还是白牌。这是一个相对独立的图像处理模块。
- 多线程并行处理:如果工控机CPU核心较多,可以设计一个生产者-消费者队列。视频捕获线程作为生产者,将待识别帧放入队列;多个消费者线程从队列取帧进行识别,充分利用多核性能。
- 结果缓存与去重:对于视频流,连续帧中的车牌可能是同一个。可以设计一个简单的缓存机制,根据车牌位置和识别结果,在短时间内(如2秒)对同一车牌只记录一次识别结果,避免重复记录。
关于网络热词中一些问题的快速回应:
c# aforge设置摄像头视频属性和控制属性:AForge.NET的VideoCaptureDevice类提供了SetCameraProperty等方法,但功能有限且已停止维护。对于复杂的摄像头控制(如曝光、聚焦),建议直接使用摄像头厂商的SDK或转向OpenCV(通过VideoCapture的Set方法)。c# hoperatorset.queryavailabledldevices("runtime", "gpu", out hv_dld);失败:这看起来是Halcon算子,与本文的ONNX Runtime无关。确保Halcon运行时库正确安装且许可证有效。winform的 propertygrid 只能查看不能修改怎么现实:将你想要展示的对象的属性设置为具有Browsable(true)和ReadOnly(false)特性,并确保属性有公共的set访问器。遇见网络环境不好怎么办:这正是我们开发本地化、离线车牌识别系统的核心优势所在。所有计算都在本地完成,完全不依赖网络,彻底避免了因网络延迟、抖动、中断导致的识别服务不可用问题,特别适合工厂、停车场、园区等网络条件可能不佳或要求高可靠性的封闭环境。
本文还有配套的精品资源,点击获取