ARTICLE DETAIL

资讯详情

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

C# Onnx Yolov8-OBB 旋转目标检测实战:从模型输出到C#解码

C# Onnx Yolov8-OBB 旋转目标检测实战:从模型输出到C#解码 简介这份源码面向具备一定C#基础的计算机视觉开发者与深度学习部署工程师提供在.NET环境中借助ONNX Runtime运行Yolov8-OBB模型、实现旋转目标检测的完整方案。相比常规水平框检测OBB旋转边界框能更精准地框定车辆、文字、航拍目标等倾斜物体适合工业质检、遥感识别等场景落地。压缩包共300个文件约411.7MB包含dll动态库、xml配置、cs源码、onnx模型、nupkg依赖包及多平台so、dylib等运行时文件并附Visual Studio解决方案与示例工程结构完整、依赖齐全。目前已有1522人学习下载。读者可据此掌握C#调用ONNX模型、图像预处理、旋转框解码与结果可视化的完整链路理解OBB检测的工程化封装思路并直接复用示例代码快速搭建自己的检测应用对深入计算机视觉与C#应用开发具有较高参考价值。1. 从一张歪着的标签说起C# Onnx Yolov8-OBB 旋转目标检测源码能解决什么做工业视觉和遥感标注的同行大概率都遇到过这种场景传送带上的轴承、遥感图里的舰船、票据上的印章目标本身是斜的用普通 YOLO 的水平框去套框里一半是背景NMS 一压还容易把相邻目标吞掉。C# Onnx Yolov8-OBB 旋转目标检测源码讲的就是用 C# 加载 ONNX 格式的 YOLOv8-OBB 模型在 .NET 环境里跑出带角度信息的旋转框Oriented Bounding Box。它解决的核心问题是让上位机、桌面端、工控机这类 C# 主战场不依赖 Python 运行时也能做带角度的目标检测。适合谁一是手里已经有 C# 上位机、想加视觉能力的工控开发者二是模型在 Python 训好了、要往 Windows 端交付的算法同学三是想搞懂 onnx 怎么运行、pytorch 转 onnx 之后 C# 侧到底怎么接的工程人员。这篇不空谈从模型输出结构一路讲到 C# 里怎么解码、怎么画框、坑在哪。2. YOLOv8-OBB 的输出到底长什么样先搞懂张量再写代码2.1 旋转框和水平框的本质差别普通 YOLOv8 检测头输出的是[x_center, y_center, w, h, class_scores...]而 OBB 检测头多了一个角度维度典型输出是[x_center, y_center, w, h, angle, class_scores...]。这个 angle 是旋转框相对水平轴的夹角不同训练框架的角度定义范围不一样常见有[-90, 0)、[-45, 45)、[0, 90)几种约定。这一点必须先确认否则 C# 侧解码出来的框会整体转错方向而且错得很隐蔽——框的位置对就是角度反了。YOLOv8-OBB 用的是 ProbIoU 或类似的角度回归方式导出 ONNX 后检测头输出的原始张量形状通常是[1, 4 num_classes 1, num_anchors]其中前 4 个是cx, cy, w, h第 5 个是角度后面是各类别分数。注意这里没有独立的 objectness 分支YOLOv8 系列是 anchor-free 且类别分数直接作为置信度这点和 YOLOv5 不一样照搬 v5 的解码逻辑会翻车。2.2 用 Python 先验证 ONNX 输出结构在写 C# 之前强烈建议先用 Python 把 ONNX 跑一遍把输出形状和数值范围打印出来这是最省时间的后悔药。下面这段脚本只依赖 onnxruntime 和 numpyimport onnxruntime as ort import numpy as np # 加载导出的 OBB 模型输入尺寸按训练时的 imgsz 来 sess ort.InferenceSession(yolov8n-obb.onnx, providers[CPUExecutionProvider]) # 打印输入输出信息确认输入名、输出名和形状 for i in sess.get_inputs(): print(input:, i.name, i.shape, i.type) for o in sess.get_outputs(): print(output:, o.name, o.shape, o.type) # 构造一个假输入NCHWfloat32归一化到 0-1 dummy np.random.rand(1, 3, 1024, 1024).astype(np.float32) outs sess.run(None, {sess.get_inputs()[0].name: dummy}) for idx, o in enumerate(outs): print(fout[{idx}] shape{o.shape} dtype{o.dtype}) print( min/max:, o.min(), o.max())逻辑说明get_inputs/get_outputs拿到的是模型元信息能直接告诉你输出是单输出还是多输出、维度顺序是什么。参数说明imgsz必须和导出时的尺寸一致OBB 模型常见 1024 或 640providers里 CPU 最稳GPU 需要装 onnxruntime-gpu 并匹配 CUDA 版本。跑完这一步你会看到类似[1, 8, 21504]的输出8 4 1 角度 3 类21504 是三个尺度特征图 anchor 数之和。把这个数字记下来C# 侧解码要按它来。2.3 从 PyTorch 到 ONNX 的导出要点模型不是凭空来的导出这一步的参数直接决定 C# 侧好不好接。常见做法是yolo export modelyolov8n-obb.pt formatonnx imgsz1024 opset12 simplifyTrueopset建议 11 或 12太高有些老版本 onnxruntime 不认simplifyTrue会调用 onnx-simplifier 去掉冗余节点能显著减少 C# 侧加载时间和推理耗时。导出后务必用上面的 Python 脚本验证一遍确认输出维度没有多余的 reshape 或 transpose 残留。有些版本导出后输出是[1, 21504, 8]而不是[1, 8, 21504]这个转置关系要在 C# 里处理别想当然。3. 在 C# 里跑通 ONNX 推理环境、张量和最小可运行代码3.1 选 Microsoft.ML.OnnxRuntime 而不是自己封装C# 侧跑 ONNX主流就两条路一是 Microsoft.ML.OnnxRuntime官方 NuGet 包二是自己 P/Invoke onnxruntime 的 C API。后者是血泪经验的重灾区c#调用c出现access violation c0000005这类崩溃十有八九是内存生命周期没管好。直接用官方包它把 Session、Tensor、内存都封装好了跨平台也支持 Windows/Linux/macOS。安装很简单在项目里加 NuGet 包dotnet add package Microsoft.ML.OnnxRuntime # GPU 版本换成 Microsoft.ML.OnnxRuntime.Gpu并确保 CUDA/cuDNN 版本匹配CPU 版开箱即用GPU 版要注意 onnxruntime 版本和 CUDA 版本的对应关系装错了会在创建 Session 时直接抛异常而不是推理时才报错。3.2 图像预处理letterbox 不能省OBB 模型训练时用的是 letterbox 缩放保持长宽比、灰边填充推理时如果直接 resize 会引入形变角度回归会偏。C# 里没有现成的 letterbox得自己写。核心是算缩放比例、算 padding、把原图贴到画布中央// 输入原图 Bitmap输出 NCHW float 数组和缩放/填充参数 public static (float[] tensor, float ratio, int padW, int padH) Letterbox(Bitmap src, int targetSize) { float ratio Math.Min((float)targetSize / src.Width, (float)targetSize / src.Height); int newW (int)(src.Width * ratio); int newH (int)(src.Height * ratio); int padW (targetSize - newW) / 2; int padH (targetSize - newH) / 2; // 用 114 灰填充和 ultralytics 默认一致 using var canvas new Bitmap(targetSize, targetSize); using (var g Graphics.FromImage(canvas)) { g.Clear(Color.FromArgb(114, 114, 114)); g.DrawImage(src, new Rectangle(padW, padH, newW, newH)); } // 转 NCHW归一化到 0-1注意 BGR/RGB 顺序要和训练一致 float[] data new float[3 * targetSize * targetSize]; for (int y 0; y targetSize; y) for (int x 0; x targetSize; x) { var c canvas.GetPixel(x, y); int idx y * targetSize x; data[idx] c.R / 255f; // R 通道 data[targetSize * targetSize idx] c.G / 255f; // G 通道 data[2 * targetSize * targetSize idx] c.B / 255f; // B 通道 } return (data, ratio, padW, padH); }逻辑说明ratio和padW/padH必须返回因为后处理要把检测框坐标映射回原图。参数说明填充值 114 是 ultralytics 的默认值训练时如果改过就要同步改通道顺序 RGB 还是 BGR 取决于训练配置ultralytics 默认 RGB但 OpenCV 读进来是 BGR这里用Bitmap.GetPixel拿到的是 RGB所以直接填即可。注意GetPixel性能很差生产环境建议用LockBits或unsafe指针1024 尺寸下能快十倍以上。3.3 创建 Session 并推理拿到张量后喂给 onnxruntimeusing Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var options new SessionOptions(); options.IntraOpNumThreads Environment.ProcessorCount / 2; // 线程数别拉满留点给 UI using var session new InferenceSession(yolov8n-obb.onnx, options); var (tensor, ratio, padW, padH) Letterbox(bitmap, 1024); var inputTensor new DenseTensorfloat(tensor, new[] { 1, 3, 1024, 1024 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(session.InputMetadata.Keys.First(), inputTensor) }; using var results session.Run(inputs); var output results.First().AsTensorfloat(); // 形状 [1, 8, 21504]逻辑说明InputMetadata.Keys.First()拿到输入节点名避免硬编码。参数说明IntraOpNumThreads控制算子内并行线程数设太大在工控机上反而因为线程切换变慢一般设物理核数的一半。session.Run返回的results用完要 Dispose否则长时间运行会内存泄漏这是 C# 侧最常见的坑之一。4. 后处理才是重头戏解码、旋转 NMS 和坐标还原4.1 从原始张量解出候选框输出[1, 8, 21504]里8 个通道的含义是cx, cy, w, h, angle, cls0, cls1, cls2。解码时先按列遍历 21504 个 anchor取类别分数最大值超过阈值才保留int numAnchors output.Dimensions[2]; // 21504 int numChannels output.Dimensions[1]; // 8 int numClasses numChannels - 5; // 3 float confThreshold 0.25f; var candidates new List(float cx, float cy, float w, float h, float angle, float score, int cls)(); for (int i 0; i numAnchors; i) { float bestScore 0; int bestCls -1; for (int c 0; c numClasses; c) { float s output[0, 5 c, i]; if (s bestScore) { bestScore s; bestCls c; } } if (bestScore confThreshold) continue; candidates.Add(( output[0, 0, i], output[0, 1, i], output[0, 2, i], output[0, 3, i], output[0, 4, i], bestScore, bestCls)); }逻辑说明这里没有 objectness 乘法因为 YOLOv8 的类别分数本身就是置信度。参数说明confThreshold是置信度阈值OBB 任务建议从 0.25 起调太低会出一堆碎框太高会漏斜目标。注意output[0, 4, i]是角度单位是弧度还是角度取决于导出配置ultralytics 导出后通常是弧度画框前要转成角度。4.2 旋转框的 NMS 怎么算 IoU水平框的 NMS 用矩形交并比就行旋转框不行两个斜框的相交面积要用多边形裁剪算。C# 里没有现成的旋转 IoU常见做法是用 Sutherland-Hodgman 算法做多边形裁剪或者用近似方法。下面给一个基于多边形裁剪的旋转 IoU 核心// 把旋转框转成四个角点 static PointF[] ToCorners(float cx, float cy, float w, float h, float angleRad) { float cos MathF.Cos(angleRad), sin MathF.Sin(angleRad); float dx w / 2, dy h / 2; // 四个角相对中心再旋转 var pts new[] { (-dx, -dy), (dx, -dy), (dx, dy), (-dx, dy) }; return pts.Select(p new PointF( cx p.Item1 * cos - p.Item2 * sin, cy p.Item1 * sin p.Item2 * cos)).ToArray(); } // 用多边形裁剪算交面积再算 IoU static float RotatedIoU(PointF[] a, PointF[] b) { var inter PolygonClip(a, b); // Sutherland-Hodgman 裁剪返回交集多边形 float interArea PolygonArea(inter); float areaA PolygonArea(a), areaB PolygonArea(b); return interArea / (areaA areaB - interArea 1e-6f); }逻辑说明ToCorners把中心点宽高角度转成四个角点这是旋转框一切几何运算的基础。PolygonClip和PolygonArea需要自己实现前者是经典的凸多边形裁剪后者用鞋带公式。参数说明角度单位要统一如果模型输出是弧度这里就传弧度1e-6f是防止除零。NMS 阈值 OBB 任务一般设 0.4 到 0.5比水平框略低因为斜框重叠判定更严格。4.3 坐标还原到原图候选框是在 1024 的 letterbox 画布上的要映射回原图float x (cx - padW) / ratio; float y (cy - padH) / ratio; float w0 w / ratio; float h0 h / ratio; // 角度不变因为 letterbox 是等比缩放逻辑说明减 padding 再除缩放比例角度不受等比缩放影响。参数说明padW/padH/ratio就是 3.2 节 Letterbox 返回的那三个值一定要一路传下来丢了就还原不回去。这一步做完框就落在原图坐标系里了可以直接画。5. 避坑与排查OBB 在 C# 里最容易翻车的五个点5.1 框整体转了 90 度或方向反了现象检测框位置对但角度明显不对长边短边互换或者整体偏 90 度。原因角度定义范围不一致训练时是[-90, 0)解码时按[0, 90)处理了或者 w/h 和角度的对应关系搞反。解决回到 2.2 的 Python 脚本把某个已知目标的原始 angle 值打印出来对照训练时的定义确认范围在 C# 解码后统一做一次角度归一化。5.2 推理结果全是空或者分数极低现象C# 跑出来一个框都没有但同一张图 Python 跑正常。原因预处理不一致最常见是通道顺序RGB/BGR反了或者归一化没做像素值还是 0-255。解决把 C# 预处理后的张量存成 npy 或二进制用 Python 加载对比逐像素比对差异通常几分钟就能定位。5.3 长时间运行内存持续上涨现象跑几小时后内存爆掉。原因InferenceSession、NamedOnnxValue、IDisposableResult没释放或者每帧都 new 一个 Session。解决Session 全局只创建一次session.Run的返回值用using包住输入张量复用缓冲区而不是每帧新分配。这是 C# 上位机最常见的资源管理问题。5.4 GPU 版加载就崩或报 DLL 找不到现象创建 Session 时抛DllNotFoundException或 CUDA 相关异常。原因onnxruntime-gpu 版本和本机 CUDA/cuDNN 版本不匹配或者缺onnxruntime_providers_cuda.dll。解决查官方版本对应表装匹配的 CUDA 和 cuDNN把相关 DLL 放到输出目录。工控机上如果没独显老老实实用 CPU 版别折腾。5.5 小目标斜框漏检严重现象大目标正常小斜目标基本检不到。原因letterbox 后小目标被缩得更小或者 confThreshold 设太高。解决把输入尺寸从 640 提到 1024 甚至 1280confThreshold 降到 0.15 试同时确认训练时小目标样本够不够。OBB 对小目标本身就比水平框敏感这是任务特性不是代码 bug。6. 进阶把 OBB 推理封装成可复用的 C# 服务走到这一步单张图能跑通了接下来要考虑的是怎么把它变成能长期用的东西。我一般会把整个流程封装成一个类对外只暴露Detect(Bitmap) - ListObbResult内部管好 Session 生命周期、张量缓冲区和后处理。缓冲区复用是关键优化1024 尺寸的输入张量是3*1024*1024*4字节约 12MB每帧新分配会疯狂触发 GC工控机上表现为周期性卡顿。做法是预分配一个float[]每帧只覆盖数据。另一个值得做的进阶是批量推理。ONNX 模型支持动态 batch导出时把 batch 维度设成动态C# 侧一次喂多张图吞吐能提升 2 到 3 倍。但要注意 letterbox 后每张图的 padding 可能不同批量时要么统一 padding 策略要么按比例分组。下面是一个批量输入的张量构造示意// batchSize 张图统一 letterbox 到同一尺寸 var batchTensor new DenseTensorfloat(new[] { batchSize, 3, 1024, 1024 }); for (int b 0; b batchSize; b) { var (data, _, _, _) Letterbox(bitmaps[b], 1024); for (int i 0; i data.Length; i) batchTensor[b, i / (1024 * 1024), (i / 1024) % 1024, i % 1024] data[i]; }逻辑说明把多张图的张量填进一个四维张量batch 维度在第一维。参数说明batchSize受显存/内存限制CPU 上一般 4 到 8GPU 上可以更大。注意每张图的ratio/padW/padH要单独存后处理时按 batch 索引取对应的还原参数。验证方面我习惯用一个笨办法拿同一张图Python 和 C# 各跑一遍把解码后的候选框NMS 之前按分数排序逐行对比 cx/cy/w/h/angle 的数值。如果前 10 个框的误差都在 1e-3 以内说明预处理和解码完全对齐如果差很多问题一定在预处理或张量布局。这个对比能省掉大量瞎猜的时间。最后说个习惯每次换模型或换 onnxruntime 版本我都会重新跑一遍这个对比流程而不是假设「上次好的这次也好」。OBB 的角度回归对数值精度比水平框敏感得多opset 变一下、simplify 开关变一下输出都可能微妙地漂移。把验证脚本固化成项目里的一部分比事后救火划算得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表