尧图精选

C#上位机集成YOLOv8烟雾检测:ONNX Runtime部署全攻略

🕒 发布时间:2026/9/8 18:04:56 📁 来源:尧图网络
简介这是一套基于C#与ONNX Runtime的YOLOv8烟雾检测完整实现源码面向需要在Windows桌面应用中集成实时目标检测的.NET开发者。项目以Visual Studio解决方案形式组织包含C#核心逻辑模型加载、图像预处理、推理与后处理、可直接运行的ONNX模型以及配套依赖库与配置文件适合用来学习C#调用深度学习模型和YOLOv8部署流程。压缩包共45个文件以cs源码、dll依赖、cache缓存、config配置及resx/resources资源文件为主还包括exe可执行程序与sln解决方案整体大小约99.98MB。已有784人学习下载可作为安全监控、工业巡检等场景中实现烟雾检测功能的基础框架帮助开发者快速理解端到端推理链路节省从零搭建环境与模型调试的时间。 做C#上位机的朋友迟早会碰到一个需求把深度学习模型集成到自己的工业软件里做实时检测。烟雾检测是里面比较典型的一类场景工厂车间、仓库、机房、野外监控都有早期火灾预警的需求。你不可能让现场还装一套Python环境更不可能让操作人员去跑命令行。最务实的路线就是C#直接调用ONNX模型而YOLOv8目前是目标检测里综合效果和部署便利性最平衡的选择。这篇文章把我实际跑通的这套C# ONNX Runtime YOLOv8烟雾检测方案完整拆开从模型导出到C#推理从后处理优化到现场踩坑一次说清楚给正在做类似需求的朋友一个可以照着抄的作业。1. 方案选型解析为什么是C# ONNX Runtime YOLOv8先说结论这套组合不是唯一选择但在Windows桌面端、工业上位机场景里它是最省心的。C#这边的优势不用多说WinForm、WPF做界面快与PLC、扫码枪、数据库、串口网口通信的生态非常成熟。你做一个烟雾检测模块最终一定是要和整个工控系统联动的C#能让你把检测结果直接推到报警界面、写入日志、触发联动设备。换成Python来做那头程通信和UI开发反而成了麻烦事。ONNX Runtime是微软主推的推理引擎。它的核心价值在于模型一旦导出为ONNX格式就脱离了PyTorch、TensorFlow这些训练框架可以在不安装任何深度学习框架的机器上运行。这正好契合工控机的部署环境——现场机器一般配置不高系统环境五花八门装了杀毒软件、工控驱动你敢随便往上装Python和PyTorch吗不敢。但ONNX Runtime只是一个运行库几个DLL拷过去就能用。YOLOv8选它是因为部署生态实在太完善了。Ultralytics官方直接支持一键导出ONNX不需要多余的中间步骤。而且YOLOv8的检测精度和速度平衡得不错nano版本在CPU上也能跑到几十毫秒一帧对烟雾这种目标来说完全够用。还有个实际原因烟雾检测的客户现场往往没有NVIDIA显卡。工控机上大概率是集成显卡甚至没有显卡纯CPU推理是常态。ONNX Runtime在CPU上的优化做得很好配合正确设置线程数小模型推理帧率完全可以接受。这套方案能解决的问题也很明确在不依赖训练框架、不依赖GPU、不重写算法逻辑的前提下把YOLOv8的检测能力嵌入到C#上位机里实现本地化、离线化、实时化的烟雾识别。2. YOLOv8模型导出PyTorch到ONNX的关键细节2.1 导出前的模型与数据准备模型导出的前提是你手里已经有一个训练好的烟雾检测权重比如best.ptPyTorch格式或者直接用官方预训练COCO权重里的person、fire等类别做二次开发。如果是自己训练的烟雾数据集我建议训练时重点关注几个方向烟雾在不同光照下的形态、透明烟与浓烟的区别、与蒸汽/雾气/灯光的混淆情况。数据集质量直接决定后续检测效果这一点在模型导出前就要心里有数。导出前还要确认模型输入尺寸。YOLOv8默认是640x640如果你的场景中烟雾目标普遍很小可以考虑把输入提升到960甚至1280但代价是推理时间成倍增长。我实测下来640x640在大多数烟雾场景里已经够用尤其是配合后续的帧间判别逻辑比盲目提高分辨率划算得多。2.2 导出命令与输出结构解读导出本身很简单Ultralytics库一条命令搞定yolo export modelbest.pt formatonnx opset12 dynamicFalse这里有几个参数值得单独说明。opset12是兼容性最好的算子版本ONNX Runtime全版本都支持dynamicFalse固定输入尺寸导出后模型推理速度更快、显存和内存占用更稳定。如果你需要适配不同分辨率输入可以开dynamicTrue但C#端处理起来会多一层麻烦我建议没特殊需求不要开。导出完成后用Netron打开best.onnx你会看到网络结构。对C#开发来说最关键的只有两头输入节点的名称通常是images和输出节点的维度。YOLOv8的输出是一个三维张量形状类似[1, 84, 8400]。其中8400是模型在不同尺度特征图上生成的候选框总数84 4 80对应4个坐标值和80个COCO类别概率。如果是自训练的烟雾单类别模型第二个维度就是54个坐标 1个类别概率。这个维度信息在C#端写代码时要用到建议导出后先记下来不同模型输出维度可能不一样直接写死在代码里最省事。2.3 C#端必不可少的输入预处理模型导出后C#端的输入预处理往往会劝退新手。YOLOv8要求输入是归一化后的RGB图像顺序是[1, 3, 640, 640]而OpenCV读出来的图像是BGR、HWC格式尺寸也不一定匹配。所以推理前必须做三件事将图像Resize到640x640建议用LetterBox等比例缩放填充而不是直接拉伸否则目标形变会影响检测效果把BGR通道顺序换成RGB像素值从0~255归一化到0.0~1.0这三个步骤看着简单但顺序和细节错了检测结果就会稀烂。我见过不少朋友把RGB和BGR搞反结果模型什么都检测不出来还以为是模型没训练好。实际上烟雾检测在这种情况下尤其明显烟雾是半透明的颜色通道顺序一错特征全乱了。3. C#端推理代码实战从加载模型到输出结果3.1 项目配置与NuGet依赖先在Visual Studio里创建一个.NET 6/8的类库或者WinForm项目然后通过NuGet安装两个包Microsoft.ML.OnnxRuntime OpenCvSharp4Microsoft.ML.OnnxRuntime是推理引擎本体OpenCvSharp4用来做图像读取和预处理。OpenCvSharp4在Windows下需要额外把OpenCvSharp4.runtime.win和OpenCvSharp4.Extensions如果用到WinForm显示也装上否则运行时会报找不到OpenCvSharpNative.dll。这个坑很常见装上就好了。如果你要跑GPU版本要装的是Microsoft.ML.OnnxRuntime.Gpu不是同一个包。CPU版和GPU版的API基本一致只是初始化时SessionOptions略有不同。3.2 推理引擎的封装与初始化我建议把整个推理逻辑封装成一个类一方面方便后续复用另一方面可以统一管理资源释放。类里核心的成员就两个InferenceSession和SessionOptions。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class SmokeDetector : IDisposable { private InferenceSession _session; private string _inputName; public SmokeDetector(string modelPath) { var options new SessionOptions(); options.LogSeverityLevel OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING; options.IntraOpNumThreads Environment.ProcessorCount; _session new InferenceSession(modelPath, options); _inputName _session.InputMetadata.Keys.First(); } public void Dispose() { _session?.Dispose(); } }注意IntraOpNumThreads这个参数。默认情况下ONNX Runtime会根据系统自动选线程数但在工控机上它往往选得不够合理。我实测过把这个值设为Environment.ProcessorCount推理速度能提升20%到30%。AMD的CPU上尤其明显。InferenceSession是线程安全的可以多个线程同时调用Run方法这在后面做多路视频流检测时特别有用。但是要注意Run方法内部会做内存分配频繁调用时会触发GC。这个问题后面单独讲。3.3 完整推理流程与代码实现推理流程拆成四步读图预处理 - 转Tensor - Run推理 - 解析输出。预处理部分借助OpenCvSharp实现public float[] Preprocess(string imagePath, int inputSize 640) { using var mat Cv2.ImRead(imagePath, ImReadModes.Color); using var resized new Mat(); // LetterBox等比例缩放 int newW, newH; float ratio Math.Min((float)inputSize / mat.Width, (float)inputSize / mat.Height); newW (int)(mat.Width * ratio); newH (int)(mat.Height * ratio); Cv2.Resize(mat, resized, new OpenCvSharp.Size(newW, newH)); // 填充到640x640 using var canvas new Mat(inputSize, inputSize, MatType.CV_8UC3, Scalar.All(114)); resized.CopyTo(canvas[new OpenCvSharp.Rect(0, 0, newW, newH)]); // BGR - RGB HWC - CHW 归一化 float[] inputData new float[3 * inputSize * inputSize]; for (int y 0; y inputSize; y) { for (int x 0; x inputSize; x) { Vec3b pixel canvas.AtVec3b(y, x); inputData[0 * inputSize * inputSize y * inputSize x] pixel[2] / 255f; inputData[1 * inputSize * inputSize y * inputSize x] pixel[1] / 255f; inputData[2 * inputSize * inputSize y * inputSize x] pixel[0] / 255f; } } return inputData; }这段代码看起来长但每一步都有明确目的。填充成640x640时填充值用114是YOLO系列训练时的惯例你换成其他值影响不大但保持训练分布总归是最稳妥的。推理部分更直接public ListDetection Infer(float[] inputData) { var inputTensor new DenseTensorfloat(inputData, new[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(_inputName, inputTensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // 解析输出... }这里有个性能关键点每次Run都会重新分配输出缓冲区。如果实时性要求高可以考虑复用输入输出缓冲区用Run的重载方法传入预分配的内存。不过这个优化带来的复杂度比较高建议先把基础流程跑通性能不够再优化。3.4 输出解析坐标还原与置信度筛选YOLOv8的输出解析是这份代码里最容易出错的部分。输出张量的形状是[1, 84, 8400]你需要转成[8400, 84]再处理。每行84个元素里前4个是目标框的中心点坐标和宽高(cx, cy, w, h)后面是各类别的置信度。烟雾检测如果是单类别模型那就简单了只看最后一个置信度大于阈值就认为检测到烟雾。如果是多类别模型要遍历所有类别找到最大置信度再看类别索引。我封装了一个Detection类来存最终结果public class Detection { public float X1, Y1, X2, Y2; // 原始图像坐标 public float Confidence; public int ClassId; }从模型输出还原到原始图像坐标需要把LetterBox的缩放和偏移还原回去。这里千万别忘模型输出的坐标是基于640x640输入图像的不是原图。要乘回缩放比例再减去填充区域的偏移量。坐标解析完成后还需要做NMS非极大值抑制。ONNX Runtime本身不内置NMS算子要么自己写一个简化版要么在导出模型时加nmsTrue参数让Ultralytics帮你集成。我建议自己写原因后面讲。4. 核心环节实现NMS、绘制与触发式检测4.1 手写NMS算法原理与C#实现NMS做的事情很直观同一片区域可能有好几个重叠的候选框都指向同一个烟雾目标只保留置信度最高的那个去掉重复的框。实现步骤就是按置信度降序排序逐个拿当前最高置信度的框把所有IoU超过阈值的框都删掉重复直到遍历完。IoU就是两个框的交集面积除以并集面积。private static ListDetection Nms(ListDetection detections, float iouThreshold) { var result new ListDetection(); var sorted detections .OrderByDescending(d d.Confidence) .ToList(); while (sorted.Count 0) { var best sorted[0]; result.Add(best); sorted.RemoveAt(0); sorted.RemoveAll(d CalcIou(best, d) iouThreshold); } return result; }NMS阈值一般取0.45到0.5。烟雾目标边界模糊框和框之间重叠面积比较大阈值太严会把相邻的重复框全删掉容易漏检太松又会保留一堆多余的框。我试下来0.45对于烟雾检测来说比较均衡。4.2 把检测结果可视化并叠加到图像上显示检测结果时建议直接用OpenCvSharp在原图上画框和标签而不是自己用GDI画。OpenCvSharp画出来的框在存储和视频流处理时兼容性更好而且代码简短很多Cv2.Rectangle(mat, new OpenCvSharp.Rect((int)x1, (int)y1, (int)(x2 - x1), (int)(y2 - y1)), Scalar.Red, 2); Cv2.PutText(mat, $Smoke {conf:P0}, new OpenCvSharp.Point(x1, y1 - 5), HersheyFonts.HersheySimplex, 0.6, Scalar.Red, 1);这里注意坐标类型要转成intOpenCvSharp的绘图API不直接接受float坐标。另外标签文字如果被截断可以调整一下绘制起点保证文字在图像范围内。4.3 触发式检测结合扫码枪与外部信号的架构思路烟雾检测和常规的视频流检测不太一样很多场景下不需要7x24小时连续跑推理。比如某个工位在特定工序才产生烟雾或者检测窗口只在扫码枪触发后才打开。这种触发式检测的设计可以让工控机CPU占用率低很多。具体做法是把检测流程封装成异步方法用扫码枪触发事件作为入口。扫码枪一般是串口或HID设备数据到达时会触发事件。事件响应函数里做两件事一是不做任何大计算量操作立即把任务丢到后台线程池二是置一个isBusy标志位防止上一次推理没结束又来新任务导致排队堆积。private async Task OnScanTriggered(object sender, string barcode) { if (_isBusy) return; _isBusy true; try { await Task.Run(() { var detections DetectFromCamera(); if (detections.Count 0) { RaiseAlarm(detections); } }); } finally { _isBusy false; } }这种架构的另一个好处是后期如果要集成PLC信号触发、定时轮询、告警联动都只需要在事件入口处做扩展不需要改动推理核心代码。5. 工程化经验性能优化、常见坑与排查思路5.1 推理卡顿与UI假死的根治办法这是上位机开发里最常见的问题把推理放到UI线程执行界面直接假死。尤其是OpenCvSharp的ImRead和Resize在图片较大时本来就耗时再加上模型推理UI线程根本扛不住。规范做法是所有耗时操作都放到后台线程用async/await或System.Threading.Tasks.Task.Run包裹。UI线程只负责显示结果。如果你需要实时预览摄像头画面可以用一个独立的摄像头采集线程专门抓帧另一个推理线程从缓冲区取帧做检测两个线程之间用队列或BlockingCollection解耦。另外提醒一点不要在UI线程里反复创建Mat对象也别在对性能敏感的循环里用Bitmap.Save写文件。实测这会让内存碎片化GC频繁触发表现就是过一会儿卡一下。5.2 模型加载失败与DLL缺失排查ONNX Runtime部署最典型的坑就是DLL问题。程序在本机跑得好好的拷到现场工控机上就报System.DllNotFoundException或者Failed to load onnxruntime.dll。原因通常是本机装了Visual C Redistributable而现场机器没有。解决办法有两个一是打包时把Microsoft.ML.OnnxRuntime目录下的原生DLL都带上专门整理成一个runtimes文件夹随程序一起发布二是在目标机器上安装对应版本的VC运行库。另外如果用了GPU版本还需要对方机器装CUDA和cuDNN版本必须和ONNX Runtime要求的严格对应这点最容易踩坑建议CPU版本打天下除非现场确认有独立显卡。还有一点ONNX Runtime 1.16以上版本对CPU指令集有要求老旧的工控机CPU可能不支持AVX指令集会直接崩溃。如果你要部署到老旧设备建议用旧版ONNX Runtime或者用DnnloneDNN执行提供程序测试兼容性。5.3 检测准确率不佳的常用排查方向模型检测不到烟雾或者误报率高先别急着怀疑模型。按这个顺序排查预处理是否正确RGB/BGR是否转换、归一化是否正确、输入尺寸是否和模型匹配640还是其他尺寸、置信度阈值是否合理烟雾建议0.25~0.35不要用默认的0.5、NMS阈值是否合理烟雾建议0.4~0.5、检查测试图片本身是否清晰。烟雾和其他检测目标有个明显区别烟雾是半透明且无固定形态的边界很模糊标准IoU计算本身就容易出错。所以有时降低置信度阈值到0.2反而能抓到很多肉眼可见的烟雾虽然也会带来一些误报但配合帧间连续判定连续3帧都检测到才报警会可靠很多。5.4 我个人实测的性能数据参考最后给一组我实际跑过的数据供参考。机器是i5-95006核6线程无独立显卡CPU推理YOLOv8n烟雾模型输入640x640单帧推理耗时大约35~45毫秒折合帧率22~28 FPS。换成YOLOv8s大约70~90毫秒一帧。在烟雾检测这种对实时性要求不是极端的场景里完全够用了。如果采用触发式检测实际CPU占用率还可以压到10%以下。如果要做到实时视频流25FPS以上有两个方向一是用YOLOv8n配合IntraOpNumThreads调优二是封装GPU推理。但GPU方案要考虑现场硬件条件、CUDA版本、显存大小维护复杂度高不少。我个人的建议是烟雾检测对帧率需求没那么高5~10 FPS足够用了CPU方案简单可靠部署成本低客户现场不会因为你用CPU版而卡顿报警反而会因为你用了GPU版导致驱动冲突而投诉你。从部署到验收这套方案我已经在三个项目里跑过了。从最早的Python原型到后面纯C#上位机集成最大的体会是模型训练只是整个项目的一小部分部署链路里的坑预处理、后处理、线程模型、DLL依赖、现场环境差异才是真正花时间的部分。如果你也在做类似的烟雾检测集成建议先把本文的流程跑通再根据自己的现场情况调整。最后提醒一句现场部署前务必在目标机器上做一次完整的冒烟测试尤其是DLL依赖和权限问题提前暴露别等验收时出事。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →