C# OnnxRuntime部署DAMO-YOLO人体检测全攻略
2026/9/8 23:01:03 网站建设 项目流程

简介:C# OnnxRuntime部署DAMO-YOLO人体检测示例包,面向需要在Windows平台集成高效人体检测能力的C#开发者。示例基于阿里巴巴达摩院的DAMO-YOLO模型,借助OnnxRuntime推理引擎实现本地化部署,完整演示了从ONNX模型加载、推理参数配置到检测结果输出的流程。压缩包共302个文件,容量约452.27MB,包含Visual Studio解决方案(.sln)、C#工程文件、ONNX模型、可执行演示程序以及大量依赖库(dll、xml、nupkg等),同时附带文本说明、图片和配置文件,便于快速还原工程环境并对照学习。已有204人学习下载,适合具备一定C#和深度学习基础、希望在自己的应用中接入人体检测功能的开发者参考。通过该示例,可以掌握模型转换、运行时配置、数据预处理与后处理等关键环节,为实际项目中的模型部署提供可复用的代码骨架与排错思路。 最近总有人问我,从网上下到的那份《C# OnnxRuntime部署DAMO-YOLO人体检测》资源包到底怎么用,为什么照着敲还是报错。其实问题大多不在模型本身,而在C#这一侧的工程化细节。DAMO-YOLO是阿里达摩院开源的目标检测模型,想要在Windows上位机里落地做人体检测,用C#调用OnnxRuntime确实是一条非常顺的路,但很多人卡在模型选型、环境配置、预处理后处理这些关键节点上,网上资料又零零散散,翻半天也拼不出一个完整可用的流程。

这篇就把我从环境搭建到推理管线、从上位机联调到性能优化的整个链路讲一遍。想在上位机里加人体检测功能、或者刚接触C#调用AI模型的,按这个思路走能少踩很多坑。

1. 为什么我先把选票投给DAMO-YOLO

1.1 部署侧更看重的是“导出ONNX后还好不好用”

很多刚开始做检测的人第一反应是上YOLOv5或YOLOv8,这两个生态确实好,教程满天飞。但DAMO-YOLO在部署上有个容易被忽略的优势:它的解耦头和重参数化设计,让导出到ONNX后的模型结构相对规整,推理引擎跑起来更顺畅。它的核心改进主要是NAS搜索出来的backbone、CSPRepResStage、TinyNAS,以及对齐感知蒸馏,这些听起来偏学术,可落到工程上,结果就是模型在相同精度下体积和耗时都控制得不错,特别是T和S这两个尺寸,非常适合做CPU推理。

需要注意一点,DAMO-YOLO里的D2Conv在导出ONNX时,最好用官方仓库的tools/export_onnx.py脚本完成重参数化再导出,不要自己拿pth文件直接torch.onnx.export硬转。那样容易导出一堆非标准的小算子和额外结构,OnnxRuntime跑起来反而是负优化。官方脚本会把这些结构折叠成普通卷积,部署侧拿到的是干净的推理图。

1.2 人体检测这个任务,DAMO-YOLO-T/S其实够用了

人体检测在工业、商业场景里的需求很明确:闸机通行计数、安全区域入侵告警、工位有人存在检测、AGV避障感知前端。这些场景的共同点是“检测框不需要那么极致精准,但要稳定、快、能长时间跑”。DAMO-YOLO-T在640x640输入下,COCO上能到40多mAP,检测人体这种大目标,用T版完全够。用一个更大更慢的模型,只会让上位机的CPU和内存更紧张,收益却很小。

在C#里做人体检测,另一个隐性优势是不用引一堆笨重的视觉框架。纯OnnxRuntime加System.Drawing或SkiaSharp就能解决图像解码和显示,依赖越少,上位机分发越省事。

1.3 拿到资源包,先确认这三样东西

如果你手里已经有一个来路不明的压缩包,先别急着跑。打开看有没有这老三样:

  • onnx模型文件:一般是damo_yolo_*.onnx这类命名,对应T/S/M/L不同尺寸。
  • C#工程源码:有工程文件可以自己编译调试,比直接跑exe靠谱。
  • 说明文档或依赖清单:有些包里会写OnnxRuntime版本,这个很重要,版本不匹配会出现莫名其妙的内存报错。

缺了onnx模型文件的话,就自己去官方仓库按README导出。模型文件是整个项目的核心,后面所有代码都是围着它转的。

2. 把ONNX模型接进C#前的工程准备

2.1 NuGet包与原生DLL的关系

C#调用OnnxRuntime,本质是通过NuGet包里的托管API去调用native层的onnxruntime.dll。所以我会建议直接用Microsoft.ML.OnnxRuntime这个包,它会自动把对应平台的原生库拷到输出目录。

这里有几个高频翻车点,先说结论:

  • 平台目标选x64,不要用AnyCPU。项目里一旦有原生DLL,AnyCPU在64位系统上可能触发BadImageFormatException。
  • 不要手动去复制onnxruntime.dll,NuGet会在编译后自动把runtimes/win-x64/native下的文件带过去。手动复制反而容易版本错乱。
  • 发布到客户机器时,别漏了runtimes目录。用单文件发布也要把原生库包含进去,否则客户机器上会报找不到DLL。

如果你要在Windows上用显卡加速,可以装Microsoft.ML.OnnxRuntime.DirectML这个包,后面在SessionOptions里追加DirectML执行提供程序就行。但注意它是独立包,不能和普通包混着乱引。

2.2 拿到ONNX文件后先做的第一件事

我每次部署新模型,不管来源是哪,都先用Netron打开看一眼输入输出。这一步能避免后面90%的维度错误。

DAMO-YOLO官方导出的ONNX,常见输入名是images,形状是[1, 3, 640, 640],对应NCHW的Batch、Channel、Height、Width。输出常见是一个名为output的张量,形状类似[1, 8400, 85],代表8400个候选框,每个框有x1、y1、x2、y2、目标分数和80个类别分数。不过DAMO-YOLO也有变体把分类分支单独输出,所以真的别猜,用Netron看两眼最稳。

如果模型输出名或维度顺序跟你预期不一样,后处理代码要跟着改。这里说的都是最常见形态,实际以你的onnx文件为准。

2.3 20行冒烟测试,过滤掉90%的环境问题

拿到模型环境里先写个控制台项目,做一次最小加载,确认Session能建起来、输入输出名能打出来。这一步跑通了,再谈后面那些花活。

using Microsoft.ML.OnnxRuntime; var sessionOptions = new SessionOptions(); sessionOptions.IntraOpNumThreads = 4; using var session = new InferenceSession(@"D:\models\damo_yolo_t.onnx", sessionOptions); Console.WriteLine("Inputs:"); foreach (var name in session.InputNames) { Console.WriteLine(name); } Console.WriteLine("Outputs:"); foreach (var name in session.OutputNames) { Console.WriteLine(name); }

如果这段能打印出输入输出名,说明DLL加载正常、模型文件没损坏。如果连这一步都过不了,先检查平台目标和NuGet包版本,别急着往后写。

3. 完整推理链路:图像预处理、Session调用、后处理

3.1 预处理:letterbox缩放到640x640

模型训练时输入是640x640,推理时也要保持一致,否则精度会明显掉。最常用的做法是letterbox,保持图像宽高比缩放,两边不足的区域用灰色(114,114,114)填充。这里要记住缩放比例scale和填充偏移padX/padY,后处理还原坐标时要用。

using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; float scale; int padX, padY; Bitmap Letterbox(Bitmap src, int targetSize = 640) { scale = Math.Min((float)targetSize / src.Width, (float)targetSize / src.Height); int newW = (int)(src.Width * scale); int newH = (int)(src.Height * scale); padX = (targetSize - newW) / 2; padY = (targetSize - newH) / 2; var dst = new Bitmap(targetSize, targetSize, PixelFormat.Format24bppRgb); using var g = Graphics.FromImage(dst); g.Clear(Color.FromArgb(114, 114, 114)); g.InterpolationMode = InterpolationMode.HighQualityBilinear; g.DrawImage(src, padX, padY, newW, newH); return dst; }

这里用System.Drawing是因为Windows上位机部署简单,跨平台需求不强烈。如果你要部署到Linux或ARM设备,建议换成SkiaSharp,API逻辑差不多。

3.2 图像数据转成模型输入张量

模型的输入是[1, 3, 640, 640]的float张量,顺序是NCHW,也就是每个通道的像素要连续排列。同时注意模型期望的是RGB顺序,而Bitmap内部通常是BGR,必须做通道顺序调整。

为了后续性能优化,我习惯先把数据填到普通float数组里,再用DenseTensor包装,这样后面想换成OrtValue复用内存也方便。

using Microsoft.ML.OnnxRuntime.Tensors; float[] inputArray = new float[1 * 3 * 640 * 640]; unsafe { var bd = letterboxed.LockBits(new Rectangle(0, 0, 640, 640), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); byte* ptr = (byte*)bd.Scan0; for (int y = 0; y < 640; y++) { for (int x = 0; x < 640; x++) { int offset = y * bd.Stride + x * 3; byte b = ptr[offset]; byte g = ptr[offset + 1]; byte r = ptr[offset + 2]; int idx = y * 640 + x; inputArray[idx] = r / 255f; inputArray[1 * 640 * 640 + idx] = g / 255f; inputArray[2 * 640 * 640 + idx] = b / 255f; } } letterboxed.UnlockBits(bd); } var tensor = new DenseTensor<float>(new Memory<float>(inputArray), new[] { 1, 3, 640, 640 });

注意这里用了unsafe,如果你不想开不安全代码,可以用Marshal.Copy把每行数据拷到byte数组里再循环,效果一样只是多一次拷贝。工程上建议直接开unsafe,代码更简洁。

3.3 Session调用与输出获取

调用Session.Run只需要把输入张量塞进NamedOnnxValue列表就行。这里先不纠结性能,写法以最直白为准,后面再讲复用优化。

var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", tensor) }; using var results = session.Run(inputs); var outputTensor = results.First().AsTensor<float>(); float[] outputData = outputTensor.ToArray();

输出张量拿到后,长度一般是1 * 8400 * 85。如果输出维度不是这个,就把上面Netron看到的shape对应改掉。

3.4 后处理:从8400个候选框里过滤出人体框

DAMO-YOLO的输出排列通常是x1, y1, x2, y2, obj_score, cls_scores...。COCO数据集中person类别索引一般是0,保险起见用coco.names核对一下,别想当然。

后处理逻辑分三步:先把目标分数和类别分数乘起来得到最终置信度,再做阈值过滤,还原坐标到原图,最后NMS去掉重叠框。

var boxes = new List<DetectionBox>(); int numAnchors = 8400; int numClasses = 80; float confThres = 0.25f; for (int i = 0; i < numAnchors; i++) { int offset = i * (5 + numClasses); float objScore = outputData[offset + 4]; float clsScore = outputData[offset + 5]; // person 索引 0 float score = objScore * clsScore; if (score < confThres) continue; float x1 = outputData[offset]; float y1 = outputData[offset + 1]; float x2 = outputData[offset + 2]; float y2 = outputData[offset + 3]; // 还原到原图坐标 x1 = (x1 - padX) / scale; y1 = (y1 - padY) / scale; x2 = (x2 - padX) / scale; y2 = (y2 - padY) / scale; boxes.Add(new DetectionBox { X1 = x1, Y1 = y1, X2 = x2, Y2 = y2, Score = score }); }

NMS可以用OpenCvSharp的Cv2.Dnn.NMSBoxes,但我不太喜欢因为一个函数引入整个OpenCvSharp原生依赖。自己写一个也就30行,还方便控制逻辑。

static List<int> NMS(List<DetectionBox> boxes, float iouThres = 0.45f) { var sorted = boxes.Select((b, i) => new { Box = b, Index = i }) .OrderByDescending(x => x.Box.Score) .ToList(); var picked = new List<int>(); var suppressed = new bool[boxes.Count]; for (int i = 0; i < sorted.Count; i++) { if (suppressed[sorted[i].Index]) continue; picked.Add(sorted[i].Index); for (int j = i + 1; j < sorted.Count; j++) { if (suppressed[sorted[j].Index]) continue; if (IoU(sorted[i].Box, sorted[j].Box) > iouThres) { suppressed[sorted[j].Index] = true; } } } return picked; }

IoU计算就是两个框交集面积除以并集面积,这个不贴了,老生常谈。最后遍历NMS选中的框,往图片上画Rectangle就行。

4. 性能校准与内存复用,跑通后真正要做的事

4.1 先摸清当前机器的底线

用Stopwatch测三个数据:模型加载耗时、第一次推理耗时、稳定后的单帧推理耗时。DAMO-YOLO-T在640x640输入下,中端桌面CPU上单帧大概几十毫秒级别,S和M会更慢。这个量级受内存带宽、CPU频率、线程数影响很大,别拿别人博客里的精确数字往自己机器上套,实测最重要。

如果稳定后的单帧耗时比第一次推理少很多,说明有初始化开销,可以在程序启动时先跑一次全黑图warm-up,把内存池、线程池激活,避免用户第一次操作时卡顿。

4.2 复用输入缓冲,减少GC压力

如果按上面最直白的写法循环跑,托管堆上会不停产生新的float数组和Tensor对象,程序跑久了GC频繁,上位机UI会跟着卡。优化思路很明确:把输入数组、输出Tensor对象都复用。

var inputBuffer = new float[1 * 3 * 640 * 640]; // 在循环前用 OrtValue.CreateTensorValueFromMemory 包装 // 每次推理前只填充 inputBuffer 数据,不再 new 新 Tensor

这里说下思路,完整OrtValue使用代码在官方示例里都有。本质是告诉OnnxRuntime这块内存我后续还会用,别每次Run完就释放。实践下来,连续推理场景内存占用会平稳很多,不再一帧一涨。

SessionOptions里还有一个值得关注的配置:IntraOpNumThreads。不是无脑设成CPU核心数,图像推理的算子对线程数不饱和,设太多反而因为线程切换和缓存失效变慢。我常用4或6,具体用Stopwatch对比着调。

4.3 想更快,DirectML是最省事的加速方案

如果CPU跑下来的延迟不满足需求,Windows上最简单的是上Microsoft.ML.OnnxRuntime.DirectML。这个包通过DirectML调用GPU,N卡A卡都能用,代码改动也很小:

sessionOptions.AppendExecutionProvider_DML(0);

只要引对包,加上这一行,同一份模型可能从几十毫秒降到几毫秒。注意DML包的版本要跟你的显卡驱动匹配,驱动太旧会报找不到执行提供程序。

如果你是想在Jetson Orin这类边缘设备上跑,那就要用对应JetPack版本的OnnxRuntime,C#侧通过.NET 6可以跑,但生态和性能验证不如Python方便,建议先在目标设备上做一轮CPU基准测试再决定。

5. 从Demo到上位机,扫码枪和UI线程都要一起干活

5.1 UI刷新卡顿的根因,是推理跑在了UI线程里

很多做上位机的朋友遇到“循环数据采集和UI刷新卡顿”,第一个怀疑的是绘图逻辑。其实很多时候是采集、推理、显示全塞在同一个Timer事件里,推理一耗几十毫秒,UI就断断续续。

正确做法是拆成三段流水线:

  • 采集线程:从相机或图像源不停取帧,转成Bitmap后丢进阻塞队列。
  • 推理线程:消费队列里的图像,跑DAMO-YOLO检测,把检测结果封装成事件抛出去。
  • UI线程:只负责接收结果并绘制框。

队列容量控制到2到3帧就够,满了直接丢最旧的帧。检测任务实时性要求高的时候,“丢帧”比“排队越积越多”合理得多,否则延迟会滚雪球。

5.2 扫码枪触发事件和检测任务的联动

上位机场景里经常要跟扫码枪配合。扫码枪现在基本都是USB HID键盘模式,上位机通过键盘事件或焦点输入框拿到条码。拿到条码后,这次检测任务就有了业务主键,可以把“条码 + 当前帧 + 检测结果”拼成一个结构化记录。

一个容易踩的坑是扫码和相机抓拍不同步。扫码枪遇到的是条码先到、相机后抓图,结果图象里工件可能已经移动了。我习惯的做法是扫码枪触发相机软触发或硬触发,强制相机在收到条码的同一时刻抓一帧,保证数据对齐。如果相机不支持硬触发,就从采集线程里取当时最新的一帧,配合时间戳做关联。

5.3 检测结果不只是画框,还要对接业务

人体检测最常见的业务动作是区域判断。比如工业现场划了一个禁止进入区,上位机拿到检测框后,把框底边中心点取出来,用点在多边形内的算法判断是否落在禁区,连续N帧都命中再触发告警。这个“连续确认”机制非常重要,单帧误检太常见,不加这个逻辑,运行几天就会被误报烦死。

结果数据可以走Modbus TCP、OPC UA或者直接写数据库,视你的PLC和MES而定。注意检测线程里不要做同步的Socket或数据库写入,应该把结果丢给独立的消息队列,由专门线程去写,避免因为网络波动把推理也拖住了。

最后说一个让我少加三天班的习惯

我自己的建议是:任何模型接进C#,先把这个模型预测一次的整个链路做成带详细日志的“诊断模式”——预处理后保存一张中间图、输入张量打印前几个数值、输出张量打印shape和一批原始值、后处理结果打印坐标。这样模型和代码哪一侧出问题,一眼就能定位。

很多所谓的“跑出来全是乱框”、“精度不对”、“内存暴涨”,最后查下来都是这种低级问题:坐标忘了还原、颜色通道顺序反了、输出维度理解错。DAMO-YOLO本身在COCO人体检测上表现非常稳,只要把C#侧这几道关口把好,跑一个可上线的上位机检测功能,其实没有想象中那么难。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询