简介:本资源面向具备一定 C# 与计算机视觉基础的开发者,提供基于 Onnx Runtime 的 PP-Vehicle 车辆分析完整源码,可解决车辆检测、车型与颜色识别、车牌检测等典型场景的落地问题,适用于智能交通、停车场管理、车辆监控等项目的二次开发与学习参考。压缩包共 138 个文件,约 53.65MB,包含 14 个 cs 源码文件、10 个 config 配置、10 个 dll 依赖库、9 个 exe 可执行程序、2 个 onnx 模型及 resx、resources 等资源文件,覆盖从模型推理到界面展示的完整工程结构。目前已有 329 人学习下载。源码以 Onnx 推理为核心,将车辆检测、车型分类、颜色识别与车牌检测串联为可运行流程,读者可据此理解模型加载、预处理、后处理与结果可视化的实现思路,并在此基础上替换模型或调整参数,快速搭建自己的车辆分析应用。
1. 从一张卡口图说起:C# Onnx PP-Vehicle 车辆分析源码能跑出什么
手里有一批卡口抓拍图,想在一台普通工控机上把车辆检测、车型识别、车身颜色分类、车牌检测四件事一次跑完,还要用 C# 直接嵌进现有桌面程序——这是我拿到这份 C# Onnx PP-Vehicle 车辆分析源码时的真实诉求。它基于 ONNX Runtime 在 .NET 里做推理,模型侧对应 PaddleDetection 的 PP-Vehicle 系列,输出四类结果:车辆框、车型、颜色、车牌框。适合做停车场出入口、地磅、园区卡口这类需要本地化、离线、低依赖的车辆结构化场景,不适合追求超大模型精度或云端批量吞吐的团队。下面按「模型怎么接、代码怎么跑、坑在哪」拆开讲。
2. PP-Vehicle 四任务拆解:模型输入输出与 C# 侧数据流
2.1 四个子任务各自吃什么、吐什么
PP-Vehicle 不是一个单模型端到端出全部结果,而是检测 + 多分类 + 车牌检测的组合流水线。常见做法是:车辆检测模型先出 vehicle 框,再把框裁剪出来分别送车型分类、颜色分类;车牌检测独立跑一遍全图或车辆 ROI。理解这一点很关键,因为后面所有坐标换算、阈值调节都围绕「谁给谁喂数据」展开。
| 子任务 | 典型输入尺寸 | 输出含义 | 常见后处理 |
|---|---|---|---|
| 车辆检测 | 640×640 letterbox | 类别 + xyxy + score | NMS、坐标反 letterbox |
| 车型识别 | 224×224 裁剪图 | 类别概率向量 | argmax + 置信度过滤 |
| 颜色识别 | 224×224 裁剪图 | 类别概率向量 | argmax + 置信度过滤 |
| 车牌检测 | 640×640 | 车牌框 xyxy + score | NMS、可加角度回归 |
车型和颜色的类别数取决于训练集,源码里一般以字符串数组形式硬编码在配置类里,改类别时数组顺序必须和训练时一致,否则会出现「明明是 SUV 却输出轿车」的玄学。
2.2 C# 侧的数据流与张量组织
C# 用 Microsoft.ML.OnnxRuntime 加载 .onnx,输入张量是float[1,3,H,W],通道顺序 NCHW,归一化一般是(pixel/255 - mean)/std,PP 系列常用 mean=[0.485,0.456,0.406]、std=[0.229,0.224,0.225]。下面这段是图像转张量的核心逻辑,抄的时候注意 BGR/RGB 顺序。
// 将 Bitmap 转为 NCHW float 张量,PP 系列通常要求 RGB public static DenseTensor<float> ToTensor(Bitmap bmp, int w, int h) { var tensor = new DenseTensor<float>(new[] { 1, 3, h, w }); using var resized = new Bitmap(bmp, new Size(w, h)); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { var c = resized.GetPixel(x, y); // 注意:Paddle 训练多为 RGB,若模型精度异常先查这里 tensor[0, 0, y, x] = (c.R / 255f - 0.485f) / 0.229f; tensor[0, 1, y, x] = (c.G / 255f - 0.456f) / 0.224f; tensor[0, 2, y, x] = (c.B / 255f - 0.406f) / 0.225f; } } return tensor; }参数说明:w/h必须和模型输入一致,检测模型一般 640,分类模型 224;mean/std若和训练脚本不一致,表现为置信度整体偏低但框位置还行,这是最容易被忽略的翻车点。GetPixel 在批量场景很慢,生产环境建议换成 LockBits 或 ImageSharp。
2.3 会话初始化与线程配置
var options = new SessionOptions(); options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL; options.IntraOpNumThreads = 4; // 按 CPU 核数调,工控机一般 4 options.InterOpNumThreads = 1; using var session = new InferenceSession("vehicle_det.onnx", options);IntraOpNumThreads设太大反而抢核,实测 4 核工控机上设 4 比设 8 帧率更稳。GPU 场景要换成SessionOptions.AppendExecutionProvider_CUDA,并确认 onnxruntime.gpu 包版本和 CUDA 匹配,否则会直接抛 provider 加载失败。
3. 从零跑通:环境、推理与四任务串联代码
3.1 环境与依赖清单
- .NET 6/8 桌面项目,x64 平台(ONNX Runtime 不支持 AnyCPU 下的原生库加载)
- NuGet:Microsoft.ML.OnnxRuntime(CPU)或 Microsoft.ML.OnnxRuntime.Gpu
- System.Drawing.Common(.NET 6 后 Windows 限定,跨平台需换 ImageSharp)
- 四个 .onnx 模型文件放输出目录,路径别写死绝对路径
提示:项目里那堆
.csproj.AssemblyReference.cache是编译缓存,删掉不影响运行,别当成模型文件。
3.2 车辆检测推理与坐标还原
// 假设输出为 [1,N,6]:x1,y1,x2,y2,score,class var outputs = session.Run(new[] { NamedOnnxValue.CreateFromTensor("images", tensor) }); var data = outputs.First().AsTensor<float>(); float scale = Math.Min(640f / srcW, 640f / srcH); float padX = (640 - srcW * scale) / 2f; float padY = (640 - srcH * scale) / 2f; for (int i = 0; i < data.Dimensions[1]; i++) { float score = data[0, i, 4]; if (score < 0.35f) continue; // 检测阈值,卡口场景 0.3~0.4 float x1 = (data[0, i, 0] - padX) / scale; float y1 = (data[0, i, 1] - padY) / scale; float x2 = (data[0, i, 2] - padX) / scale; float y2 = (data[0, i, 3] - padY) / scale; // 后续裁剪该框送分类模型 }逻辑说明:letterbox 会补边,反算时必须先减 pad 再除 scale,顺序反了框会整体偏移。阈值 0.35 是经验值,夜间逆光图可降到 0.25 但误检会上升,需要按现场光照调。
3.3 车型与颜色分类串联
// 裁剪车辆框,留 5% 边距,避免切掉车头特征 var rect = Expand(bbox, 0.05f, srcW, srcH); using var crop = srcBitmap.Clone(rect, srcBitmap.PixelFormat); var clsTensor = ToTensor(crop, 224, 224); var clsOut = clsSession.Run(new[] { NamedOnnxValue.CreateFromTensor("x", clsTensor) }); var probs = clsOut.First().AsTensor<float>(); int typeId = ArgMax(probs); // 车型 int colorId = ArgMax(colorSession.Run(...)); // 颜色参数说明:Expand的边距系数 0.05 是平衡「切太紧丢特征」和「带太多背景」的折中;分类置信度低于 0.5 时建议输出「未知」而不是硬给标签,否则颜色识别在阴天灰车场景会频繁误判。
3.4 车牌检测与结果组装
车牌检测单独跑一次全图,输出框后按 IoU 归属到最近的车辆框。最终组装成一个VehicleResult对象:车辆框、车型、颜色、车牌框、各任务置信度。车牌框若要做识别,再串一个 CRNN 或 PP-OCR 的 ONNX 模型即可,源码里只到检测这一步。
4. 避坑与排查:四类高频翻车现场
4.1 现象:框位置整体偏移或缩放错位
原因:letterbox 的 pad 和 scale 计算顺序错,或用了非等比 resize。解决:统一封装Letterbox和UnLetterbox两个函数,所有模型共用,禁止各写各的。
4.2 现象:车型/颜色全部输出同一个类别
原因:分类模型输入通道顺序错(BGR 当 RGB),或 mean/std 与训练不一致。解决:先用一张已知图打印 softmax 前 5 个概率,若全挤在一起基本就是预处理问题。
4.3 现象:CPU 占用 100% 但帧率只有个位数
原因:GetPixel 逐像素拷贝 + 每帧新建 Session。解决:Session 全局单例,图像转换改 LockBits;分类模型可合并成一个多输出模型减少调用次数。
4.4 现象:GPU 版报 provider 加载失败
原因:onnxruntime.gpu 版本与 CUDA/cuDNN 不匹配,或项目平台是 AnyCPU。解决:平台锁 x64,按官方版本矩阵对齐 CUDA 版本,别混装 CPU 和 GPU 包。
4.5 现象:车牌框在夜间图大量漏检
原因:车牌检测阈值沿用白天值,且未做图像增强。解决:按亮度分档设阈值,或对暗图先做一次 CLAHE 再推理,代价是增加约 15% 耗时。
5. 进阶:把四任务压进一条流水线并验证精度
跑通单图后,真正决定能不能上线的是吞吐和一致性。我一般会把四个 Session 做成单例池,检测和分类分两个线程队列,检测出框后异步送分类,主线程只做组装和绘制。这样在 4 核工控机上,1080P 图端到端能压到 80~120ms 一张,比串行快近一倍。
验证环节别只看单张效果图,要建一个小评测集:至少 200 张覆盖白天、夜间、逆光、雨雾、不同车型颜色。指标上,车辆检测看 mAP@0.5,车型和颜色看 top-1 准确率,车牌检测看召回。下面这个对比表是我调参时的记录方式,供参考。
| 场景 | 检测阈值 | 分类置信度下限 | 车牌召回 | 备注 |
|---|---|---|---|---|
| 白天顺光 | 0.35 | 0.5 | 0.96 | 基线 |
| 夜间补光 | 0.28 | 0.45 | 0.88 | 需 CLAHE |
| 雨雾 | 0.25 | 0.4 | 0.79 | 误检上升明显 |
调参顺序建议:先固定分类阈值调检测阈值,再回头调分类,两个一起动会分不清是谁的锅。另外,车型和颜色的类别字符串数组一定要和训练时的label_list逐行核对,我吃过一次亏——数组顺序错位,整批 SUV 被标成轿车,查了两天才发现是配置问题。从那以后我每次换模型都强制走一遍「单类别已知图」验证,确认每个类别都能被正确命中再上量。希望帮到你。
本文还有配套的精品资源,点击获取