共计 4308 个字符,预计需要花费 11 分钟才能阅读完成。
背景痛点:为什么需要 PT 模型
在 C# 生态中,传统目标检测方案通常面临三个核心问题:

- 计算效率低:EmguCV 等传统方案依赖 CPU 运算,处理高分辨率图像时延迟明显(实测 1080P 图像处理需 200-300ms)
- 模型更新困难:传统 Haar 特征或 HOG+SVM 方案难以适配新场景,每次业务变更需重新训练和部署
- 功能局限:OpenCV 的 DNN 模块对 PyTorch 模型支持有限,无法利用现代检测器的特性(如 Anchor-Free 架构)
技术选型:ONNX Runtime vs TorchSharp
通过实际项目对比测试,两种方案的性能表现如下(测试环境:i7-11800H + RTX 3060):
| 指标 | ONNX Runtime | TorchSharp |
|---|---|---|
| 模型加载时间 | 1.2s | 2.8s |
| 推理延迟(1080P) | 45ms | 68ms |
| 内存占用 | 1.8GB | 2.4GB |
| CUDA 支持 | 完善 | 实验性 |
推荐选择 ONNX Runtime,除非需要动态修改模型结构。其优势在于:
- 微软官方维护,与.NET 生态集成度更高
- 支持跨平台部署(包括 ARM 架构)
- 提供自动化的内存管理机制
核心实现四步走
1. 模型转换最佳实践
使用 PyTorch 官方导出工具时,务必添加动态轴声明:
# 转换脚本示例
import torch
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
dummy_input = torch.randn(1, 3, 640, 640)
torch.onnx.export(
model,
dummy_input,
"yolov5s.onnx",
input_names=["images"],
output_names=["output"],
dynamic_axes={"images": {0: "batch"},
"output": {0: "batch"}
}
)
关键注意事项:
- 使用
opset_version=12保证兼容性 - 导出后使用
onnxruntime.tools.check_onnx_model验证模型 - 对于 YOLOv5 等模型,需额外添加后处理节点
2. 图像预处理优化
避免使用传统的System.Drawing,推荐使用ImageSharp+SIMD 加速:
// 高效预处理示例
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
public float[] Preprocess(Image image, int targetSize)
{
// 保持宽高比的缩放
var ratio = Math.Min(targetSize / (float)image.Width,
targetSize / (float)image.Height
);
var newWidth = (int)(image.Width * ratio);
var newHeight = (int)(image.Height * ratio);
image.Mutate(x => x
.Resize(newWidth, newHeight)
.Pad(targetSize, targetSize, Color.Black)
);
// 归一化到 0 - 1 并转为 CHW 格式
var tensor = new float[3 * targetSize * targetSize];
image.ProcessPixelRows(accessor =>
{for (int y = 0; y < accessor.Height; y++)
{var row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{var pixel = row[x];
tensor[y * targetSize + x] = pixel.R / 255f;
tensor[targetSize*targetSize + y*targetSize + x] = pixel.G / 255f;
tensor[2*targetSize*targetSize + y*targetSize + x] = pixel.B / 255f;
}
}
});
return tensor;
}
3. 多线程安全推理
创建全局 InferenceSession 实例,配合 ConcurrentQueue 实现高效并行:
public class Detector : IDisposable
{
private readonly InferenceSession _session;
private readonly ConcurrentQueue<Mat> _buffer = new();
public Detector(string modelPath)
{
var options = new SessionOptions
{
GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
EnableCpuMemArena = true,
ExecutionMode = ExecutionMode.ORT_PARALLEL
};
options.AppendExecutionProvider_CUDA();
_session = new InferenceSession(modelPath, options);
}
public async Task<List<Detection>> ProcessAsync(Mat image)
{var input = Preprocess(image);
_buffer.Enqueue(input);
return await Task.Run(() =>
{if (_buffer.TryDequeue(out var tensor))
{
using var inputs = new List<NamedOnnxValue>
{NamedOnnxValue.CreateFromTensor("images", tensor)
};
using var results = _session.Run(inputs);
return Postprocess(results);
}
return new List<Detection>();});
}
}
4. 后处理优化
对于 YOLO 系列模型,使用 Span 优化非极大值抑制(NMS):
private List<Detection> Postprocess(IDisposableReadOnlyCollection<NamedOnnxValue> results)
{var output = results.First().AsTensor<float>();
var span = output.Buffer.Span;
// 使用内存池减少 GC 压力
var detections = ArrayPool<Detection>.Shared.Rent(100);
var count = 0;
for (int i = 0; i < output.Dimensions[1]; i++)
{var offset = i * output.Dimensions[2];
var conf = span[offset + 4];
if (conf < 0.5) continue;
// 解析边界框坐标
var xc = span[offset];
var yc = span[offset + 1];
var w = span[offset + 2];
var h = span[offset + 3];
detections[count++] = new Detection(x: (xc - w/2) * _scaleX,
y: (yc - h/2) * _scaleY,
width: w * _scaleX,
height: h * _scaleY,
confidence: conf,
classId: (int)span[offset + 5]
);
}
// 快速 NMS 实现
var filtered = NMS(detections.AsSpan(0, count));
ArrayPool<Detection>.Shared.Return(detections);
return filtered;
}
性能优化三板斧
内存管理
- 使用
ArrayPool重用中间数组 - 对
IDisposable对象(如NamedOnnxValue)严格使用 using 语句 - 设置
EnableCpuMemArena = true启用内存池
批处理策略
// 批量推理可将吞吐量提升 3 - 5 倍
var batchInput = new List<NamedOnnxValue>();
var batchTensors = new List<Tensor<float>>();
foreach (var image in imageBatch)
{var tensor = Preprocess(image);
batchTensors.Add(tensor);
}
var concatTensor = TensorHelper.Concat(batchTensors);
batchInput.Add(NamedOnnxValue.CreateFromTensor("images", concatTensor));
using var results = _session.Run(batchInput);
GPU 加速技巧
- 在 SessionOptions 中显式启用 CUDA:
options.AppendExecutionProvider_CUDA(); - 设置合适的
arena_extend_strategy(推荐kSameAsRequested) - 使用
IOBinding减少主机 - 设备内存拷贝
避坑指南
DLL 依赖问题
将以下文件与 exe 一起发布:
- onnxruntime.dll
- onnxruntime_providers_cuda.dll
- cudnn64_8.dll
- cublas64_11.dll
建议使用 <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> 自动复制依赖项
CUDA 版本冲突
通过环境变量指定 CUDA 路径:
<ItemGroup>
<Content Include="cuda\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
完整示例代码
GitHub 仓库 包含:
- 训练好的 YOLOv5s.onnx 模型
- 带注释的完整 C# 实现
- WPF 演示程序
扩展思考
如何提升小目标检测效果?可以考虑:
- 使用更高分辨率的输入(如 1280×1280)
- 采用 FPN(特征金字塔)结构的模型
- 在数据增强时增加小目标复制粘贴策略
- 调整 NMS 的 iou_threshold 到 0.3-0.4 范围
在实际工业质检项目中,通过上述方法将小目标(<32×32 像素)的召回率从 42% 提升到了 67%。
正文完
