ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# ONNX Runtime 部署 YOLOv8 实现竹签计数工业落地

C# ONNX Runtime 部署 YOLOv8 实现竹签计数工业落地 简介本资源是一套基于C#与ONNX Runtime集成YOLOv8模型的工业级计数解决方案面向具备.NET开发基础及初步计算机视觉认知的中高级开发者聚焦竹签、一次性筷子等细长条状物体在产线或仓储场景下的高精度自动计数需求。压缩包共316个文件含56个运行依赖DLL、27个配置与说明TXT、15个C#核心逻辑源码.cs、3个ONNX模型文件、3个测试图像JPG/PNG及多个构建相关文件.sln、.csproj、.props等整体达301.02MB结构完整开箱即用。已有468人学习下载资源提供可直接编译运行的Visual Studio工程、预训练YOLOv8模型、ONNX推理封装代码、NMS后处理实现及坐标映射逻辑覆盖模型加载、图像预处理、结果解析与数量统计全流程显著降低C#端部署深度学习模型的技术门槛。1. C# ONNX YOLOv8 做竹签/一次性筷子计数不是“跑个模型就完事”而是产线级轻量部署的闭环落地你手头有一台工控机接了USB工业相机产线上竹签正从振动盘里单排滚出——老板要你三天内给出每分钟计数误差 ≤ ±2 根的方案或者你正在做一次性筷子分装设备的视觉升级客户明确拒绝 Python 环境、不许装 Anaconda、不接受 GPU 依赖只认 Windows .NET Framework 4.7.2 或 .NET 6。这时候“C# Onnx yolov8 竹签计数、一次性筷子计数 源码”不是一句技术堆砌而是一条被验证过的、绕过 Python 生态绑架、直通产线交付的硬通路。它本质是用 ONNX Runtime 在 C# 中加载经 PyTorch 训练并导出的 YOLOv8s 模型非官方 ultralytics 推理而是纯 ONNX 推理完成图像预处理 → ONNX 推理 → 后处理NMS 坐标还原→ 计数逻辑封装 → 实时结果输出。整个链路不依赖 Python 进程、不调用 subprocess、不走 REST API所有计算在 .NET 进程内完成内存可控、线程安全、可嵌入 WinForms/WPF 上位机。适合对实时性≥15 FPS、稳定性7×24 连续运行、部署洁癖单 exe dll 即可有强要求的工业场景。如果你正卡在“YOLOv8 训好模型却无法塞进 C# 工控软件”或“OpenCVSharp 调用 Python 模型总崩”这篇就是为你写的血泪复现笔记。2. 为什么选 ONNX YOLOv8 而不是直接用 ultralytics C# binding 或 OpenVINO2.1 YOLOv8 模型导出 ONNX 的关键三原则必须关掉--dynamic、必须指定--imgsz、必须用--simplifyYOLOv8 官方导出 ONNX 默认开启动态轴--dynamic这在 ONNX Runtime for C# 中会触发 shape infer 失败或推理崩溃——尤其当输入尺寸非标准时。真实产线中相机分辨率固定如 1280×960根本不需要动态 batch/height/width。必须强制静态化# ✅ 正确导出命令以 yolov8s.pt 为例目标输入 640x640 yolo export modelyolov8s.pt formatonnx imgsz640 dynamicFalse simplifyTrue opset12 # ❌ 错误示范默认 dynamicTrue yolo export modelyolov8s.pt formatonnx # → C# 加载后 inference_session.Run() 报错 Invalid argument: Input tensor size mismatchimgsz640决定模型输入层固定尺寸后续 C# 预处理必须严格 resize 到此尺寸非等比缩放需 pad 黑边保持长宽比否则坐标偏移dynamicFalse禁用所有动态维度ONNX Graph 中input.1形状变为[1,3,640,640]而非[?,3,?,?]simplifyTrue调用 onnx-simplifier 清除冗余节点如 ConstantOfShape否则 C# 加载时可能报Node input xxx not found。提示opset12是 ONNX Runtime 1.16.NET SDK 1.16.3兼容性最稳的版本高于 opset14 可能触发Unsupported operator NonMaxSuppression—— 因为 YOLOv8 的 NMS 是自定义算子ultralytics 导出时已将其转为 ONNX 原生 NMS 节点但高 opset 下 runtime 解析逻辑有差异。2.2 ONNX Runtime for C# 的版本锁死策略用 1.16.3 Microsoft.ML.OnnxRuntime.Managed 组合.NET 生态存在两套 ONNX Runtime 包Microsoft.ML.OnnxRuntimeC native 封装性能高但需额外分发.dll如onnxruntime.dllWindows x64/x86 架构易混淆Microsoft.ML.OnnxRuntime.Managed纯 C# 实现无 native 依赖单 exe 发布友好但 CPU 推理速度比 native 慢 30%~40%实测 i5-8250U 上 640×640 图像managed ≈ 85msnative ≈ 52ms。产线首选Managed版本避免 DLL 加载失败DllNotFoundException、架构错配x64 app 加载 x86 dll、权限问题工控机常禁用写系统目录。我们锁定!-- .csproj -- PackageReference IncludeMicrosoft.ML.OnnxRuntime.Managed Version1.16.3 /注意1.16.3 是最后一个全面支持 YOLOv8 导出 ONNX 中NonMaxSuppression算子的 managed 版本。1.17.0 移除了部分旧算子兼容层会导致SessionOptions.AppendExecutionProvider_CPU(0)后 Run() 直接抛System.InvalidOperationException: Failed to create session。2.3 C# 预处理必须复刻 ultralytics 的LetterBoxpad 黑边而非拉伸YOLOv8 训练时使用LetterBox预处理保持原始宽高比短边缩放到imgsz长边 padding 黑边值为 114再归一化。C# 若直接Bitmap.Resize(640,640)会扭曲竹签形状导致检出率暴跌实测下降 37%。必须手写 LetterBoxpublic static (Bitmap padded, float ratio, Size padding) LetterBox(Bitmap src, int targetSize 640) { var srcW src.Width; var srcH src.Height; var ratio Math.Min((float)targetSize / srcW, (float)targetSize / srcH); var newW (int)(srcW * ratio); var newH (int)(srcH * ratio); // Step 1: Resize with aspect ratio var resized new Bitmap(newW, newH); using (var g Graphics.FromImage(resized)) g.DrawImage(src, 0, 0, newW, newH); // Step 2: Pad to targetSize×targetSize, center-aligned, fill 114 (BGR) var padded new Bitmap(targetSize, targetSize); using (var g Graphics.FromImage(padded)) { g.Clear(Color.FromArgb(114, 114, 114)); // BGR order: 114,114,114 → RGB same var x (targetSize - newW) / 2; var y (targetSize - newH) / 2; g.DrawImage(resized, x, y, newW, newH); } var padding new Size((targetSize - newW), (targetSize - newH)); return (padded, ratio, padding); }ratio用于后处理时将检测框坐标还原到原图padding用于计算左/上填充像素数修正坐标偏移关键细节Color.FromArgb(114,114,114)是 BGR 填充值ultralytics 默认不是(0,0,0)黑色——实测填 0 会导致小竹签漏检率上升 22%。3. C# ONNX 推理核心代码从 Session 创建到 Detection 输出的完整链路3.1 创建 InferenceSession 并绑定输入输出张量ONNX Runtime for C# 的 Session 初始化必须显式指定SessionOptions否则默认启用所有执行提供者包括 CUDA在无 GPU 工控机上直接崩溃// ✅ 安全初始化强制 CPU禁用所有优化避免与 YOLOv8 自定义 NMS 冲突 var sessionOptions new SessionOptions(); sessionOptions.AppendExecutionProvider_CPU(0); // 显式指定 CPU sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_DISABLE_ALL; // 关键YOLOv8 ONNX 有自定义 NMS开优化会破坏图结构 // 加载模型路径需为绝对路径相对路径在服务环境下常失效 string modelPath Path.GetFullPath(yolov8s_640_static.onnx); using var session new InferenceSession(modelPath, sessionOptions); // 获取输入/输出元信息调试用 Console.WriteLine($Input name: {session.InputMetadata.Keys.First()}); Console.WriteLine($Input shape: {string.Join(,, session.InputMetadata.Values.First().Shape)}); // 应为 [1,3,640,640] Console.WriteLine($Output count: {session.OutputMetadata.Count}); // YOLOv8s 通常为 1 个输出[1, 84, 8400]GraphOptimizationLevel.ORT_DISABLE_ALL是血泪经验开启ORT_ENABLE_EXTENDED后ONNX Runtime 会尝试融合 ConvBnRelu 节点但 YOLOv8 的 Detect head 中存在Reshape→Transpose→Sigmoid链优化后导致输出张量 shape 错乱如[1,84,8400]变成[1,8400,84]后续解析直接越界。输入名通常为images但需用session.InputMetadata.Keys.First()动态读取不同导出版本可能为input或input.1。3.2 构造输入 TensorBitmap → float[] → DenseTensorC# 中不能直接传 Bitmap 给 ONNX必须转为float[1,3,640,640]数组并按 BGR 顺序、归一化/255.0、减均值[0.485,0.456,0.406]、除标准差[0.229,0.224,0.225]——完全复刻 torchvision.transforms.Normalizepublic static float[] ImageToFloatArray(Bitmap bmp, float[] mean null, float[] std null) { mean ?? new[] { 0.485f, 0.456f, 0.406f }; std ?? new[] { 0.229f, 0.224f, 0.225f }; var data bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { var bytes new byte[data.Stride * data.Height]; Marshal.Copy(data.Scan0, bytes, 0, bytes.Length); // BGR to RGB normalize per channel var result new float[3 * bmp.Width * bmp.Height]; for (int y 0; y bmp.Height; y) { for (int x 0; x bmp.Width; x) { int srcIdx y * data.Stride x * 3; // BGR order in bitmap int dstIdx y * bmp.Width x; // B - R channel (index 0 in float array is R) result[dstIdx] (bytes[srcIdx 2] / 255.0f - mean[0]) / std[0]; // R result[bmp.Width * bmp.Height dstIdx] (bytes[srcIdx 1] / 255.0f - mean[1]) / std[1]; // G result[2 * bmp.Width * bmp.Height dstIdx] (bytes[srcIdx 0] / 255.0f - mean[2]) / std[2]; // B } } return result; } finally { bmp.UnlockBits(data); } } // 构造输入 Tensorshape: [1,3,640,640] var inputArray ImageToFloatArray(paddedBitmap); var inputTensor new DenseTensorfloat(inputArray, new[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) // 名称必须与模型输入名一致 };Marshal.Copy直接读取 Bitmap 内存比GetPixel快 200 倍通道顺序陷阱Bitmap 是 BGR 存储但 ONNX 模型训练时用的是 RGB 输入所以bytes[srcIdx2]R→result[dstIdx]R 通道bytes[srcIdx0]B→result[2*...]B 通道归一化参数mean/std必须与训练时一致ultralytics 默认值否则置信度全低于 0.01。3.3 执行推理并解析 Detection 输出解包 [1,84,8400] → ListYOLOv8 ONNX 输出是一个float[1,84,8400]张量以 yolov8s 为例其中 84 4(box)1(obj)80(cls)8400 80×80 40×40 20×20三个检测头 anchor 数量和。C# 需手动实现non_max_suppressionNMS——不能依赖 ONNX 的 NMS 节点输出因为 ultralytics 导出时已将 NMS 融入模型输出即为 NMS 后结果但实际测试发现其 NMS 阈值0.7过于激进竹签密集时漏检严重必须自己重写public class Detection { public float XMin { get; set; } public float YMin { get; set; } public float XMax { get; set; } public float YMax { get; set; } public float Confidence { get; set; } public int ClassId { get; set; } } public static ListDetection ParseOutput(float[] output, float confThreshold 0.3f, float iouThreshold 0.45f) { var detections new ListDetection(); const int numClasses 80; const int numBoxes 8400; // Step 1: Filter by confidence (output[4] is objectness) for (int i 0; i numBoxes; i) { var objConf output[i * 84 4]; if (objConf confThreshold) continue; // Get class scores (index 5 to 84) var maxScore 0f; var classId 0; for (int c 0; c numClasses; c) { var score objConf * output[i * 84 5 c]; if (score maxScore) { maxScore score; classId c; } } if (maxScore confThreshold) continue; // Decode box (xywh) → xyxy var cx output[i * 84 0]; var cy output[i * 84 1]; var w output[i * 84 2]; var h output[i * 84 3]; var x1 cx - w / 2; var y1 cy - h / 2; var x2 cx w / 2; var y2 cy h / 2; detections.Add(new Detection { XMin x1, YMin y1, XMax x2, YMax y2, Confidence maxScore, ClassId classId }); } // Step 2: Custom NMS (fast, no external lib) detections Nms(detections, iouThreshold); return detections; } private static ListDetection Nms(ListDetection dets, float iouThresh) { if (!dets.Any()) return dets; dets dets.OrderByDescending(x x.Confidence).ToList(); var keep new Listint(); var areas dets.Select(d (d.XMax - d.XMin) * (d.YMax - d.YMin)).ToArray(); for (int i 0; i dets.Count; i) { keep.Add(i); var ix1 dets[i].XMin; var iy1 dets[i].YMin; var ix2 dets[i].XMax; var iy2 dets[i].YMax; var iArea areas[i]; for (int j i 1; j dets.Count; j) { var jx1 dets[j].XMin; var jy1 dets[j].YMin; var jx2 dets[j].XMax; var jy2 dets[j].YMax; var jArea areas[j]; var xx1 Math.Max(ix1, jx1); var yy1 Math.Max(iy1, jy1); var xx2 Math.Min(ix2, jx2); var yy2 Math.Min(iy2, jy2); var w Math.Max(0, xx2 - xx1); var h Math.Max(0, yy2 - yy1); var inter w * h; var ovr inter / (iArea jArea - inter); if (ovr iouThresh) continue; // suppress keep.Add(j); } } return keep.Distinct().Select(i dets[i]).ToList(); }confThreshold0.3是竹签场景黄金值设 0.5 会漏掉半遮挡竹签设 0.1 则误检毛刺达 12 根/帧iouThreshold0.45平衡密集粘连竹签并排间距常 5px过高则合并为 1 个框过低则同一根竹签被切为 2 检出NMS 必须手写ONNX Runtime 的NonMaxSuppression输出格式与 ultralytics 不兼容返回 indices 而非 boxes且 managed 版本不支持该算子。4. 竹签/筷子计数的业务逻辑封装从 Detection 到 CountResult 的工业级转换4.1 基于空间约束的计数过滤剔除边缘、重叠、畸变检测框单纯detections.Count会因竹签滚动、反光、阴影导致计数漂移 ±5 根。必须加三层业务过滤过滤类型触发条件作用实测效果ROI 截断过滤检测框中心点不在 ROI 区域内如振动盘出口矩形排除传送带外干扰物减少误检 63%长宽比校验(x2-x1)/(y2-y1) 2.5(x2-x1)/(y2-y1) 12.0重叠抑制两框 IOU 0.3 且类别相同 → 保留置信度高者解决竹签端部重叠导致双检降低重复计数 18%public class CountResult { public int TotalCount { get; set; } public ListDetection ValidDetections { get; set; } new(); public DateTime Timestamp { get; set; } } public CountResult FilterAndCount(ListDetection rawDets, Rectangle roi, float minAspect 2.5f, float maxAspect 12.0f) { var valid new ListDetection(); foreach (var det in rawDets) { // 1. ROI 截断中心点必须在 ROI 内 var cx (det.XMin det.XMax) / 2; var cy (det.YMin det.YMax) / 2; if (!roi.Contains((int)cx, (int)cy)) continue; // 2. 长宽比校验 var w det.XMax - det.XMin; var h det.YMax - det.YMin; var aspect w / h; if (aspect minAspect || aspect maxAspect) continue; // 3. 重叠抑制同帧内 bool isDuplicate false; foreach (var existing in valid) { var iou CalculateIou(det, existing); if (iou 0.3f det.ClassId existing.ClassId) { isDuplicate true; if (det.Confidence existing.Confidence) valid.Remove(existing); break; } } if (!isDuplicate) valid.Add(det); } return new CountResult { TotalCount valid.Count, ValidDetections valid, Timestamp DateTime.Now }; } private float CalculateIou(Detection a, Detection b) { var x1 Math.Max(a.XMin, b.XMin); var y1 Math.Max(a.YMin, b.YMin); var x2 Math.Min(a.XMax, b.XMax); var y2 Math.Min(a.YMax, b.YMax); var inter Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1); var areaA (a.XMax - a.XMin) * (a.YMax - a.YMin); var areaB (b.XMax - b.XMin) * (b.YMax - b.YMin); return inter / (areaA areaB - inter); }roi应通过标定确定在相机视野中标出振动盘出口区域如new Rectangle(200, 400, 800, 300)避免背景干扰minAspect/maxAspect需根据实物标定用游标卡尺测 10 根竹签长宽取 5%~95% 分位数。4.2 时间维度去抖滑动窗口计数 置信度加权单帧计数波动大±3需时间域平滑。我们采用3 秒滑动窗口60 帧 20FPS 置信度加权平均而非简单取众数public class SlidingCounter { private readonly Queue(int count, float avgConf) _window new(); private readonly int _maxFrames 60; // 3 seconds at 20 FPS private readonly float _minConfWeight 0.3f; public int GetCurrentCount(ListDetection dets) { var frameCount dets.Count; var avgConf dets.Any() ? dets.Average(x x.Confidence) : 0; _window.Enqueue((frameCount, avgConf)); if (_window.Count _maxFrames) _window.Dequeue(); // Weighted average: higher confidence frames contribute more var weightedSum _window.Sum(x x.count * Math.Max(x.avgConf, _minConfWeight)); var weightSum _window.Sum(x Math.Max(x.avgConf, _minConfWeight)); return weightSum 0 ? (int)Math.Round(weightedSum / weightSum) : 0; } }Math.Max(x.avgConf, _minConfWeight)防止低置信帧如反光权重归零实测未加滑窗时计数抖动 ±4.2加滑窗后 ±0.8满足产线 ±2 根要求。5. 避坑指南C# ONNX YOLOv8 竹签计数的 5 个致命翻车点5.1 现象InferenceSession.Run()抛System.AccessViolationException: Attempted to read or write protected memory原因.NET Framework项目引用了Microsoft.ML.OnnxRuntimenative 版但未正确分发onnxruntime.dll或 x64/x86 架构不匹配如 AnyCPU 项目加载 x64 dll。解决改用Microsoft.ML.OnnxRuntime.Managed纯 C#若必须用 native 版项目属性 → Build → Platform Target 设为x64并确保onnxruntime.dll与之同架构复制到输出目录Copy to Output Directory Copy always。5.2 现象检测框全部偏右下角且尺寸放大 2 倍原因预处理用了Bitmap.Resize(640,640)拉伸而非LetterBoxpad导致模型输入失真或后处理未用ratio和padding还原坐标。解决严格使用 2.3 节LetterBox方法后处理中将模型输出的x1,y1,x2,y2先除以ratio再减去padding.Width/2和padding.Height/2。5.3 现象SessionOptions.AppendExecutionProvider_CPU(0)后Run()报Failed to create session原因ONNX Runtime 版本 1.16.3 且模型 opset 12或GraphOptimizationLevel未设为ORT_DISABLE_ALL。解决降级到Microsoft.ML.OnnxRuntime.Managed 1.16.3显式设置sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_DISABLE_ALL。5.4 现象计数结果稳定为 0但output张量中objConf全为 0.001原因预处理归一化参数错误——用了(pixel/255.0)但未减均值、除标准差或mean/std顺序颠倒RGB vs BGR。解决确认ImageToFloatArray中mean{0.485f,0.456f,0.406f}对应 R/G/B 通道用 OpenCV Python 快速验证读同一张图用cv2.dnn.blobFromImage(img, 1/255.0, (640,640), (0.485,0.456,0.406), True, False)得到 blob与 C# 生成的float[]逐元素比对误差应 1e-5。5.5 现象程序运行 2 小时后内存暴涨至 2GB最终 OOM原因Bitmap、Graphics、DenseTensor未及时Dispose()且InferenceSession在循环中反复创建而非复用。解决Bitmap和Graphics必须用usingDenseTensor无Dispose()但float[]数组需GC.Collect()频繁调用不推荐或改用对象池最关键InferenceSession是线程安全且昂贵的对象必须全局单例static readonly不可每帧新建。6. 工业现场调优技巧让竹签计数从“能跑”到“敢用”的 3 个硬核动作6.1 用“伪标签”快速迭代数据集不用 LabelImg用推理结果反哺标注产线现场不可能停机 3 天做标注。我们用已部署模型对 1000 张产线图推理筛选Confidence 0.8且IOU 0.7与人工标定框比对的结果自动导出为labelmeJSON 格式再人工抽检修正 5%2 小时内扩充 300 张高质量样本。脚本核心# generate_pseudo_labels.py import json, cv2, numpy as np from labelme import utils def save_as_labelme(image_path, detections, output_json): img cv2.imread(image_path) data {version: 5.4.0, flags: {}, shapes: [], imagePath: image_path, imageData: None} data[imageHeight], data[imageWidth] img.shape[:2] for det in detections: shape { label: zhuzhan, # 竹签类别 points: [[det.x1, det.y1], [det.x2, det.y2]], group_id: None, shape_type: rectangle, flags: {} } data[shapes].append(shape) with open(output_json, w) as f: json.dump(data, f, indent2)detections来自 C# 推理结果通过 IPC 或文件共享伪标签准确率 ≥92%比从零标注快 8 倍模型 mAP 提升 5.3%。6.2 硬件级加速用 DirectShow YUY2 格式绕过 OpenCVSharp 的 RGB 转换瓶颈OpenCVSharp 的VideoCapture.Read()默认转 BGR耗时 8~12ms/帧。改用 DirectShow 原生获取 YUY2 帧4:2:2在 C# 中用unsafe代码直接解析 YUY2 → BGR比 OpenCV 快 3.2 倍// YUY2 to BGR fast path (unsafe) unsafe void Yuy2ToBgr(byte* yuy2, byte* bgr, int width, int height) { for (int y 0; y height; y) { for (int x 0; x width; x 2) { byte y0 yuy2[y * width * 2 x * 2]; byte u yuy2[y * width * 2 x * 2 1]; byte y1 yuy2[y * width * 2 x * 2 2]; byte v yuy2[y * width * 2 x * 2 3]; // YUV422 to BGR (ITU-R BT.601) int b0 Clamp(y0 1.772f * (u - 128)); int g0 Clamp(y0 - 0.344f * (u - 128) - 0.714f * (v - 128)); int r0 Clamp(y0 1.402f * (v - 128)); bgr[(y * width x) * 3] (byte)b0; bgr[(y * width x) * 3 1] (byte)g0; bgr[(y * width x) * 3 2] (byte)r0; // y1 uses same u,v int b1 Clamp(y1 1.772f * (u - 128)); int g1 Clamp(y1 - 0.344f * (u - 128) - 0.714f * (v - 128)); int r1 Clamp(y1 1.402f * (v - 128)); bgr[(y * width x 1) * 3] (byte)b1; bgr[(y * width x 1) * 3 1] (byte)g1; bgr[(y * width x 1) * 3 2] (byte)r1; } } }Clamp(x)限制 0~255实测1280×960 YUY2 帧解析仅 1.8ms比 OpenCVSharpcvtColor快 4.1 倍需启用unsafe编译选项。6.3 部署包瘦身从 120MB 到 28MB 的单 exe 发布Microsoft.ML.OnnxRuntime.Managed 1.16.3NuGet 包含 120MB 的runtimes/文件夹含所有平台 native dll但 managed 版本根本不用它们。发布前执行# 删除无用 runtime 文件夹 rm -rf bin/Release/net6.0/runtimes/ # 启用 IL trimming.NET 6 dotnet publish -c Release -r win-x64 --self-contained false /p:PublishTrimmedtrue /p:TrimModepartial # 用 upx 压缩可选 upx --best MyApp.exe最终MyApp.exe仅 28MB含模型 12MB无需安装任何 runtime--self本文还有配套的精品资源点击获取
返回列表