共计 2710 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在工业自动化、智慧零售等场景中,目标检测计数是一个常见的需求。传统的 OpenCV 方案虽然简单易用,但在复杂场景下往往会出现漏检问题。例如,在密集物体或遮挡情况下,OpenCV 基于轮廓检测的方法很难准确区分和计数各个目标。

另一个常见的解决方案是采用 Python+C# 混合架构,即用 Python 训练和运行模型,再通过某种方式(如 gRPC、REST API)与 C# 应用通信。这种方案虽然功能强大,但部署成本高,且增加了系统复杂性。对于需要实时处理的应用(如视频监控),这种架构的性能往往难以满足要求。
技术选型
在选择目标检测框架时,我们主要考虑了 YOLOv5 和 YOLOv8。两者都是优秀的实时目标检测模型,但 YOLOv5 的社区支持和文档更为丰富,特别是在 C# 生态中的集成案例更多。
对于推理引擎,我们对比了 TensorRT 和 ONNX Runtime。TensorRT 虽然性能优异,但在 C# 中的支持相对有限,且配置复杂。ONNX Runtime 则提供了官方的 C# API,支持跨平台运行,并且可以充分利用 GPU 加速。综合考虑后,我们选择了 ONNX Runtime 作为推理引擎。
核心实现
1. 导出 ONNX 模型
使用 Ultralytics 导出 YOLOv5 模型到 ONNX 格式时,有几个关键注意事项:
# 导出命令示例
yolo export model=yolov5s.pt format=onnx imgsz=640 dynamic=True
imgsz参数必须与训练时保持一致dynamic=True允许模型接受不同尺寸的输入,但会增加推理时的开销- 需要检查输出的 shape 是否符合预期,特别是 batch 维度
2. C# 加载 ONNX 模型
以下是使用 Microsoft.ML.OnnxRuntime 加载模型的完整代码:
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
// 初始化推理会话
var sessionOptions = new SessionOptions();
sessionOptions.AppendExecutionProvider_CUDA(); // 启用 GPU 加速
var session = new InferenceSession("yolov5s.onnx", sessionOptions);
// 准备输入数据
var inputTensor = new DenseTensor<float>(new[] {1, 3, 640, 640});
// 填充数据...
// 创建输入容器
var inputs = new List<NamedOnnxValue>
{NamedOnnxValue.CreateFromTensor(session.InputNames[0], inputTensor)
};
// 执行推理
using var results = session.Run(inputs);
3. NMS 的 C# 实现
在目标检测后处理中,非极大值抑制 (NMS) 是一个关键步骤。以下是 C# 原生实现:
public static List<Detection> ApplyNMS(List<Detection> detections, float iouThreshold)
{var results = new List<Detection>();
detections = detections.OrderByDescending(d => d.Confidence).ToList();
while (detections.Count > 0)
{var current = detections[0];
results.Add(current);
detections.RemoveAt(0);
for (int i = detections.Count - 1; i >= 0; i--)
{var iou = CalculateIoU(current.Box, detections[i].Box);
if (iou > iouThreshold)
detections.RemoveAt(i);
}
}
return results;
}
性能优化
1. 使用 Span优化数据处理
在预处理和后处理阶段,大量时间花费在数据拷贝和转换上。使用 Span
unsafe void PreprocessImage(byte[] imageData, Span<float> outputTensor)
{fixed (byte* pImage = imageData)
fixed (float* pOutput = outputTensor)
{// 高性能像素处理...}
}
2. 异步流水线设计
为了避免 GPU 阻塞,我们实现了异步处理流水线:
public async Task ProcessFrameAsync(Mat frame)
{
// 异步预处理
var inputTensor = await Task.Run(() => Preprocess(frame));
// 异步推理
var results = await Task.Run(() => session.Run(new[] {inputTensor}));
// 异步后处理
return await Task.Run(() => Postprocess(results));
}
3. 性能对比数据
使用 BenchmarkDotNet 进行量化对比,优化前后的性能差异:
| 优化项 | FPS (1080p) | 内存使用 |
|---|---|---|
| 原始实现 | 15.2 | 1.2GB |
| Span 优化 | 18.7 | 0.9GB |
| 异步流水线 | 24.3 | 0.8GB |
| GPU 加速 | 32.1 | 1.1GB |
避坑指南
-
模型输入输出验证:使用 Netron 工具可视化 ONNX 模型,确认输入输出维度
-
颜色格式冲突:ONNX 模型通常期望 RGB 输入,而 OpenCV 使用 BGR 格式。需要在预处理时进行转换:
Cv2.CvtColor(input, input, ColorConversionCodes.BGR2RGB);
- 多线程资源管理:ONNX Runtime 的 Session 对象不是线程安全的,建议每个线程创建独立的 Session,或使用会话池。
延伸思考
对于需要更高性能的场景,可以考虑以下优化方向:
- 模型量化:将 FP32 模型转换为 INT8,可以显著减少模型大小并提高推理速度
- 云端协同:结合 Azure Custom Vision 服务,将部分计算卸载到云端
- 模型剪枝:移除模型中冗余的层或通道,减少计算量
通过以上优化,我们在实际项目中实现了每秒 30 帧以上的实时检测性能,同时保持了较高的准确率。这套方案完全基于 C# 生态,避免了 Python 环境的依赖,大大简化了部署流程。
希望这篇实战经验能帮助你在 C# 中高效实现目标检测计数功能。如果你有任何问题或更好的优化建议,欢迎交流讨论。
