C#调用ONNX Runtime部署YOLOv8火焰识别全程实战指南

📅 发布时间:2026/10/11 12:51:02
C#调用ONNX Runtime部署YOLOv8火焰识别全程实战指南
简介一套基于C#与ONNX Runtime的YOLOv8火焰识别/火灾检测Demo内置训练好的推理模型与完整项目源码可直接编译运行。资源面向需要在桌面及工业端集成火焰报警能力的.NET开发者帮助系统掌握C#调用YOLOv8模型、OpenCvSharp图像处理、检测结果解析与界面展示的完整落地流程。资源包共69个文件、约110.57MB以20个DLL运行时依赖、12个C#源码、2个ONNX模型文件为主另含config配置、exe可执行程序、sln/csproj工程文件及resx/resources界面资源目录结构清晰便于按模块阅读和二次开发。目前已有432人学习下载适合具备基础C#知识、希望在Windows环境通过ONNX Runtime部署YOLOv8目标检测的开发者可将Demo中的工程组织、模型加载与推理、后处理逻辑迁移到相似火焰/烟雾检测项目中。1. 一个 C# 工程师把 YOLOv8 火焰识别跑上线的真实路径做过上位机的人都会遇到这类需求摄像头对着仓库、机房或者厂区甲方要求“看到火苗就弹窗报警”而且明确说不要 Python 服务、不要 Linux 容器最好直接集成在现有的 C# 客户端里。于是方案收敛成一个固定组合C# 调用 ONNX Runtime加载一个 YOLOv8 导出的火焰检测模型。这个思路能跑通但和你想的“训练好模型就完事”差得很远。我见过太多项目在演示视频上很漂亮装到现场以后被夕阳、红色灯光和监控反光打到怀疑人生。这篇内容就是把这套路径完整拆开从 C# 为什么选 Onnx Runtime、模型怎么导出到推理管线怎么写、现场哪些坑必须提前排掉最后落到验证和量化部署。适合正在做 C# 上位机、工控机部署或者刚把 YOLOv8 训练完准备往客户端里塞的人。2. 为什么 C# 侧要选 Onnx Runtime部署选型和火焰检测的模型约束2.1 上位机推理后端的三种选择为什么 Onnx Runtime 先活下来火焰检测模型训练完之后常见推理路径有三条TensorRT、OpenVINO、ONNX Runtime。如果你只做 Windows 上位机集成TensorRT 的约束最明显——它依赖 NVIDIA 显卡而工控机里大量是 Intel 核显或者老 AMD 平台完全没有 CUDA 环境。OpenVINO 在 Intel CPU 上确实快但 C# 封装基本靠 NuGet 上社区包维护版本一升级就可能出现 Native 库对不上、算子不支持的情况项目周期越短越不想踩这个坑。ONNX Runtime 赢在“官方支持”这四个字上微软出的 Microsoft.ML.OnnxRuntime 包一个引用就能跑支持 Windows/LinuxCPU/GPU 都有对应版本。这个选型决定了后续的部署路径。ONNX 格式本身就是模型交换的标准你今天在 PC 上用 C# 跑通明天如果甲方要求换到 RK3588 这类国产平台也可以把同一个 ONNX 再转成 RKNN逻辑不用推倒重来。方案硬件依赖C# 集成成本适合场景TensorRTNVIDIA GPU 限定高需封装 C 或走 NLP 接口有 3070 以上独显的集中式服务器OpenVINOIntel CPU/GPU 限定中版本兼容风险大纯 Intel 平台追求极致 CPU 吞吐ONNX Runtime无强绑定低官方 NuGet 直接引用上位机、工控机、跨平台交付2.2 火焰检测任务对 YOLOv8 的模型形状要求输出是 84 还是 5YOLOv8 导出的 ONNX 模型输出对目标检测任务来说是一个三维张量标准形状是[1, 84, 8400]。其中 1 是 batch size84 等于 4 个框坐标加 80 个 COCO 类别概率8400 是三个尺度特征图上的 anchor 点总数。但做火焰检测时绝大多数人不会直接用官方 80 类权重而是在自己的火焰数据集上微调或重新训练这时如果只标注了 fire 一个类别输出就变成[1, 5, 8400]。这个差异在 C# 端写解析代码时非常关键。84 的输出意味着要遍历 80 个类别找最大置信度而 5 类输出直接取第 4 个索引就是火焰分数。我在代码里通常直接写兼容分支不写死形状后面第 4 章会给你完整代码。火焰这个目标本身也有特殊性火苗的尺寸变化极快初始可能是几个像素几秒后就覆盖半个画面颜色和红色灯光、夕阳反光高度接近。所以模型输入分辨率不能太低常规做法是imgsz640既保证小目标不被过度压缩又控制在 CPU 能承受的算力范围内。2.3 火焰数据集的负样本决定了模型能不能用这是整个项目里最容易被低估的部分。公开的火焰数据集一般能覆盖“明火、烟雾、夜间火光”这几类正样本但真正让模型在现场翻车的全是负样本问题——夕阳、红色车灯、电焊火花、LED 红色屏幕、甚至一面红墙在某些光照下都会激活特征。我的做法是训练数据里把“红色干扰物”当成一类专门收集但不单独设标签而是作为背景图混入训练集。负样本比例至少占 30%否则模型会让你在现场付出代价。3. 让 YOLOv8 的权重变成 Onnx导出命令与 NMS 取舍3.1 导出前先确认训练时的预处理参数在用yolo export导出之前先回到训练配置里看三样东西输入尺寸、归一化方式、通道顺序。Ultralytics YOLOv8 默认训练和推理都用 640×640 输入归一化是像素值除以 255通道顺序是 BGR——注意不是 RGB因为它的数据加载链路用的是 OpenCV 的cv2.imread读进来就是 BGR 排列。这三件事如果没对齐模型导出来在 Python 里测没问题到 C# 里就概率性失灵。最典型的错误是 C# 端用ColorConversionCodes.RGB2BGR做了一次转换导致整张图的颜色被交换火焰的红橙色特征直接被破坏。我后面给的 C# 预处理代码和训练链路保持一致不做多余转换。3.2 导出 ONNX 与验证输出维度的最小命令训练完权重后导出命令如下pip install ultralytics onnx onnxruntime yolo export modelruns/detect/fire/weights/best.pt formatonnx \ imgsz640 dynamicFalse opset12 simplifyTrue导出成功后用一段 Python 快速看输入输出节点和形状这一步不能省。你会发现很多问题在导出时就埋下了比如动态轴没处理好、opset 版本过高导致 ONNX Runtime 旧版本加载不了。import onnxruntime as ort import numpy as np sess ort.InferenceSession(fire.onnx, providers[CPUExecutionProvider]) inputs sess.get_inputs() outputs sess.get_outputs() print(input:, inputs[0].name, inputs[0].shape) print(output:, outputs[0].name, outputs[0].shape) # 用随机张量验证推理可以跑通 x np.random.rand(1, 3, 640, 640).astype(np.float32) out sess.run(None, {inputs[0].name: x})[0] print(runtime output shape:, out.shape)这段脚本的核心价值有两个。一是确认输入节点名通常 YOLOv8 导出后叫images二是拿到输出张量的真实形状可能是[1, 84, 8400]也可能是[1, 5, 8400]。如果有转置版本输出为[1, 8400, 84]说明导出时带了nmsTrue参数后面解码逻辑完全不同。我建议导出时不要带nmsTrue把 NMS 留在 C# 端做原因在下一节说。3.3 为什么不在 Onnx 里内置 NMS带 NMS 的模型在 C# 端很难调Ultralytics 支持导出端到端带 NMS 的 ONNX 模型也就是把非极大值抑制打包进图里输出直接是检测框。听起来省事实际坑很多内置 NMS 的算子在不同 opset 版本上兼容性参差不齐ONNX Runtime 老版本可能直接跑不起来而且 NMS 阈值和置信度阈值被固化在导出参数里现场调参要重新导出一次模型。C# 端手写 NMS 的成本并不高。到这里有一个很关键的认知火焰检测场景中 NMS 的 IoU 阈值不能照搬通用目标检测的经验值。火焰形状不规则且边缘模糊同一个火苗在不同尺度特征图上可能预测出两个重叠度不高的框IoU 阈值设 0.45 太低会保留大量重复框。我一般用 0.5再配合置信度阈值做双门槛过滤。4. C# 调用 Onnx 模型检测火焰最小可运行的推理管线4.1 项目需要引用的 NuGet 包与引用关系C# 端实现火焰检测需要两个包Microsoft.ML.OnnxRuntime负责模型推理OpenCvSharp4负责图像读取、缩放和画框。OpenCvSharp 在 Windows 上建议直接装OpenCvSharp4.Windows它会顺带把 Native 运行库带进来。如果目标平台是 Linux 工控机则换成OpenCvSharp4加手动放置libopencv的方式。PackageReference IncludeMicrosoft.ML.OnnxRuntime Version1.19.2 / PackageReference IncludeOpenCvSharp4.Windows Version4.9.0.20240103 /版本号只是一个参考重点在于 ONNX Runtime 的版本和导出模型时的 opset 要匹配。opset 12 导出的模型用 1.19.x 加载没有任何问题如果你用最新导出的 opset 17就务必将 ONNX Runtime 升到对应支持的版本否则会出现“模型加载失败”的玄学报错。4.2 图像预处理LetterBox 与通道顺序对齐所有 YOLOv8 模型都要求固定尺寸输入。直接把一个 1920×1080 的监控画面 Resize 到 640×640 会严重破坏宽高比导致物体拉伸变形。正确做法是 LetterBox按比例缩放后在两侧填充灰色边。using OpenCvSharp; private Mat LetterBox(Mat src, int targetSize 640) { float ratio Math.Min((float)targetSize / src.Width, (float)targetSize / src.Height); int newW (int)Math.Round(src.Width * ratio); int newH (int)Math.Round(src.Height * ratio); Mat resized new Mat(); Cv2.Resize(src, resized, new Size(newW, newH), 0, 0, InterpolationFlags.Linear); Mat canvas new Mat(new Size(targetSize, targetSize), MatType.CV_8UC3, new Scalar(114, 114, 114)); int padX (targetSize - newW) / 2; int padY (targetSize - newH) / 2; resized.CopyTo(new Mat(canvas, new Rect(padX, padY, newW, newH))); return canvas; }这段代码里有两个参数值得记住。填充色用 114与 Ultralytics 训练时的默认值一致缩放差值用Linear而不是Nearest因为火焰边缘是渐变过渡的最近邻插值会把半透明的火苗边缘切成一块一块的色斑影响检测置信度。预处理里不需要做 RGB 转换OpenCvSharp的ImRead默认返回 BGR 通道和 YOLOv8 训练链路一致。4.3 推理与输出解析Box、置信度与 NMS核心推理代码写成一个检测类流程是输入 Mat → LetterBox → 填入张量 → Session.Run → 解码 → NMS。我会把输出维度兼容分支直接写进去这样不管你的模型是单类 fire 还是 80 类都能跑通。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class FireDetector : IDisposable { private readonly InferenceSession _session; private const int InputSize 640; private const float ConfThreshold 0.35f; private const float IouThreshold 0.5f; public FireDetector(string modelPath) { var options new SessionOptions(); _session new InferenceSession(modelPath, options); } public ListDetectionResult Detect(Mat bgrFrame) { using var letter LetterBox(bgrFrame, InputSize); var inputTensor new DenseTensorfloat(new[] { 1, 3, InputSize, InputSize }); // 将 BGR 图像按 HWC 转 CHW 填入张量注意不要做通道转换 for (int y 0; y InputSize; y) { for (int x 0; x InputSize; x) { var pixel letter.AtVec3b(y, x); inputTensor[0, 0, y, x] pixel[0] / 255f; inputTensor[0, 1, y, x] pixel[1] / 255f; inputTensor[0, 2, y, x] pixel[2] / 255f; } } var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // 输出形状可能是 [1, 5, 8400] 或 [1, 84, 8400]也可能是 [1, 8400, 5] var boxes DecodeOutput(output); return Nms(boxes, IouThreshold); } }这里的Session.Run是同步阻塞的。如果视频帧率要求高不要在 UI 线程里直接调用要丢到后台线程排队执行。InferenceSession本身是线程安全的但多个线程同时跑会导致 CPU 争抢检测速度反而下降。解码逻辑值得单独说。YOLOv8 的输出是[1, channel, anchors]其中前四位是框的 xywh 坐标单位是相对于 640×640 输入图像的像素值。解码后要拿这些坐标反算回原图坐标需要用到 LetterBox 的缩放比例和 padding 信息。private ListDetectionResult DecodeOutput(Tensorfloat output) { var results new ListDetectionResult(); int channels output.Dimensions[1]; int anchors output.Dimensions[2]; int classCount channels - 4; // 兼容 [1, 8400, 84] 转置输出 if (output.Dimensions[1] output.Dimensions[2]) { (channels, anchors) (anchors, channels); } for (int i 0; i anchors; i) { float score; if (classCount 1) { score output[0, 4, i]; // 只有 fire 类 } else { // 多类别时找最大分数 int bestCls 0; float bestScore 0f; for (int j 4; j channels; j) { float s output[0, j, i]; if (s bestScore) { bestScore s; bestCls j - 4; } } score bestScore; } if (score ConfThreshold) continue; float cx output[0, 0, i]; float cy output[0, 1, i]; float w output[0, 2, i]; float h output[0, 3, i]; results.Add(new DetectionResult { X cx - w / 2f, Y cy - h / 2f, Width w, Height h, Score score }); } return results; }关键参数ConfThreshold取 0.35 是现场经验值。很多人觉得 0.25 太低会引入误报但火焰检测恰恰相反——早期火苗在画面里只有几十个像素置信度天然偏低。如果分数线设太高小火苗全漏了。0.35 是一个相对平衡的点后期再通过帧间平滑和闪烁特征来过滤误报而不是靠拉高置信度硬扛。4.4 把检测结果画到画面并触发报警推理结果在 C# 端画框很容易但报警逻辑不能做成“检测到就报警”。火焰的误报多半是单帧偶发所以我会加一个“连续确认”机制同一位置连续 3 帧都出现框才触发报警。参数推荐值说明ConfThreshold0.35低于 0.35 的框直接丢弃IouThreshold0.5重叠度超过 0.5 的框合并MissFrameCount3连续几帧未检测到才取消报警最小框面积画面面积 0.1%过滤极小噪点5. 火焰识别上线后的系统排查误报、漏报与掉帧5.1 排查一真实视频上满屏闪框现象模型在测试图片上检测正常但一接摄像头画面里红色区域全在跳框每个框置信度还不低。原因这是典型的阈值设置问题。火焰检测的候选框密度比普通目标高很多因为一个火苗在不同尺度特征图上会产生大量冗余框。如果置信度阈值在 0.1 左右NMS 阈值又低就会出现满屏框。另一个原因是视频帧间干扰单帧偶发高分目标没有做时间维度平滑。解决先把ConfThreshold提到 0.35IouThreshold提到 0.5。如果还闪就在检测结果上做多点确认框的位置在连续帧之间移动不超过框宽度的 30%才认为是稳定目标。移动太大可能是强光反射或镜头晃动不该报警。5.2 排查二夕阳和红色灯光大面积误报现象下午五点左右阳光斜射进车间整个画面偏暖模型把窗户、墙面、甚至地面的反光全部识别成火焰。原因火焰的颜色特征和夕阳高度重叠模型只学到了“红色 高亮”这个粗粒度模式。绝大多数公开火焰数据集里的负样本不足模型没见过足够多的“夕阳下红色区域”照片自然就会泛化出错。解决在训练集里补齐负样本。我的做法是专门收集三个场景晴天日落、红色 LED 屏幕、红色建筑外墙。负样本不需要标注直接当背景图丢进数据集即可。如果已经来不及重新训练可以在 C# 后处理里加一条规则框内平均红色通道值大于某个阈值才保留。火焰的红色通道通常伴随着绿色通道的波动而红色灯光是稳定的纯红这个特征能过滤掉一大部分静态误报。火焰的闪烁特性可以帮大忙做帧间差分或统计红色通道方差这是现场不换模型的最快止血办法。5.3 排查三CPU 推理掉到 5 FPS 以下现象工控机 i5 处理器640×640 输入推理耗时 150ms 到 200ms加上摄像头解码和画框整个流程只有 4~6 FPS。原因没有做性能专项优化。监控画面是 1920×1080LetterBox 每次把整张原图缩放这个操作本身很耗时。另外如果流程里用了多个InferenceSession实例或者每个线程独立创建 Session内存和 CPU 开销都会翻倍。解决优先把输入尺寸降到 480×480帧率能提升 60% 以上火焰检测的精度损失不大。再一个是复用 Session 实例Detect方法里的_session不要频繁创建。最后是控制检测频率消防场景并不需要每帧都检测每 200ms 检测一次已经足够秒级响应足够应付火灾预警需求。想要更高的单帧吞吐可以考虑 ONNX Runtime 的EnableMemoryPattern和EnableCpuMemArena。5.4 排查四换一台电脑推理结果完全不一样现象开发机上检测得好好的部署到甲方电脑上同一个视频文件结果不同有的帧检测到了有的帧完全没检测到。原因两份因素叠加。一是 OpenCvSharp 的版本不一致导致像素读取方式有差异尤其是ImRead的通道顺序和颜色空间转换在不同版本上有细微差别。二是 ONNX Runtime 版本差异旧版本对某些算子的实现精度和新版本不同但通常不会导致整帧漏检。真正概率最大的原因是部署机上没有安装对方电脑的 VC 运行库Native 库加载失败后静默降级或抛异常。解决在 C# 程序启动时做一次自检打印 ONNX Runtime 版本和 OpenCvSharp 版本并把关键参数写到日志里。然后用一张固定的测试图片跑一次推理如果输出形状不对或检测框全空直接提示运行环境异常。这个自检逻辑能省掉后面大量的远程联调时间。6. 最后一公里验证火焰模型泛化能力与量化压缩6.1 自己写一个最小指标统计脚本只信误报率和漏检率很多项目验收只看一个 demo 视频这是最坑的。我的习惯是拿一段 10 分钟真实监控录像每 5 秒抽一帧逐帧标注“这帧里有没有火”。然后跑检测统计两个指标漏检率——有火但没框出来的帧占比误报率——没火但弹框的帧占比。漏检率控制在 5% 以内误报率控制在每小时不超过一次报警这两个指标比 mAP 实用得多。6.2 量化 Int8 和 RK3588 部署哪些能碰、哪些不能碰热词里的onnx量化int8和rk3588部署yolov8是这条部署链路的自然延伸。如果目标是 RK3588最终要把 ONNX 转成 RKNN 格式常见工具链是 RKNN-Toolkit2。转之前先在 C# 侧把 ONNX 验证逻辑跑通再去做格式转换能减少变量。ONNX Runtime 的 int8 量化要非常谨慎。火焰检测和通用目标检测不一样火苗的纹理和颜色是强特征int8 量化时如果校准集里火焰图像占比不够模型会直接把小火苗全部抹掉。非要量化校准集必须手动挑选火焰面积占画面 5% 以上的图像而不是随机抽几百张视频帧。我在实际项目中更愿意接受一个保守选择保持 FP32靠降输入分辨率和控制检测频率来换帧率。FP16 在支持 FP16 的平台上可以无脑开启精度损失几乎为零。6.3 我现在的落地方案与验收流程最后分享一套我现在固定使用的落地流程。训练完成后先导出 ONNX在 Python 脚本里用随机张量验证形状接着 C# 里用测试图片跑通然后接视频文件做离线回归最后才接摄像头做现场联调。现场验收时不要只看白天一定要覆盖夜间红外模式、黄昏逆光、雨天积水反光、电焊作业区域四个场景每个场景录 5 分钟视频跑完统计误报和漏检。这套流程帮我挡掉了至少三次“演示没问题进现场就废”的返工。火焰检测这个方向最大的风险从来不在模型结构本身而在于你对场景的理解是否足够。希望这篇笔记能帮你在部署之前把该踩的坑先踩一遍少走弯路。本文还有配套的精品资源点击获取