基于C#和OpenCV的目标检测计数实战:从算法选型到性能优化

1次阅读
没有评论

共计 2943 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

背景痛点:为什么传统计数方法总翻车

在工厂流水线或仓储盘点场景中,传统计数方法主要有三大硬伤:

基于 C# 和 OpenCV 的目标检测计数实战:从算法选型到性能优化

  • 背景差分法 对光照变化极度敏感,早上和下午的摄像头画面能差出 20% 的误判率
  • 帧间差分法 遇到传送带上的静止物体直接失效,而动态调整阈值又会导致计数结果像心电图一样波动
  • 轮廓检测 在物体重叠时(比如堆叠的纸箱)会合并成单个 blob,实测重叠 3 个以上物体时准确率跌破 60%

去年帮某电子厂改造 SMT 贴片元件计数系统时,就遇到过传送带反光导致每天要人工复核 10% 的批次。这就是我们转向深度学习方案的直接原因。

技术选型:C# 生态下的三大候选

方案对比表

框架 模型支持 部署复杂度 推理速度(1080p)
TensorFlow.NET 所有 TF 模型 45ms/ 帧
ML.NET 自定义轻型模型 120ms/ 帧
OpenCV+ONNX YOLO/SSD 等 28ms/ 帧

选择 OpenCV 有四个现实理由:

  1. 产线工控机多是 Windows 系统,OpenCV 的 C ++ 原生库在 DNN 模块有深度优化
  2. ONNX 运行时内存占用比 TensorFlow 少 30%,在 4GB 内存设备上也能跑 YOLOv5s
  3. 我们的视频处理管线本来就用了 OpenCV 做预处理,减少数据跨框架传递开销
  4. 产线设备通常禁用 Python 环境,而 OpenCVSharp 可以直接 NuGet 安装

核心实现:从单帧检测到连续追踪

1. ONNX 模型加载与推理

关键点在于正确处理 NHWC 到 NCHW 的转换,YOLOv5 的官方 PyTorch 模型输出需要额外处理:

// 模型加载
var net = CvDnn.ReadNetFromONNX("yolov5s.onnx");
net.SetPreferableBackend(Backend.OPENCV);
net.SetPreferableTarget(Target.CPU);

// 预处理(注意保持与训练时相同的归一化方式)var blob = CvDnn.BlobFromImage(frame, 1/255.0, 
    new Size(640, 640), 
    new Scalar(0,0,0), 
    true, false);

// 推理
net.SetInput(blob);
var output = net.Forward();

// 后处理(输出是 1x25200x85 的矩阵)var rawData = new float[output.Total()];
output.GetArray(out rawData);
var detections = ParseYoloOutput(rawData, frame.Width, frame.Height);

2. 多线程流水线设计

采用生产者 - 消费者模式时容易忽略的细节:

graph LR
    A[视频采集线程] -->|Queue| B[检测线程池]
    B -->|ConcurrentBag| C[追踪计数线程]
  • 使用 BlockingCollection 实现有界队列防止内存暴涨
  • 每个检测线程独立维护 Net 对象避免 OpenCV 的线程安全问题
  • 计数线程采用双缓冲策略:当前帧结果直接输出,同时异步更新追踪状态

3. 目标去重算法

当两个检测框 IOU>0.7 时,用匈牙利算法解决 ID 分配问题:

// 代价矩阵计算
private double[,] BuildCostMatrix(List<Detection> prev, List<Detection> current)
{var matrix = new double[prev.Count, current.Count];
    for(int i=0; i<prev.Count; i++)
        for(int j=0; j<current.Count; j++)
            matrix[i,j] = 1 - IoU(prev[i].Rect, current[j].Rect);
    return matrix;
}

// 使用 HungarianAlgorithm 库求解
var assignments = HungarianAlgorithm.Solve(costMatrix);

性能优化:从 28ms 压榨到 9ms

SIMD 加速实战

OpenCV 的 Mat 对象原生支持 SIMD,但需要手动开启:

// 在 app.config 中添加运行时指令
<runtime>
    <gcAllowVeryLargeObjects enabled="true" />
    <Thread_UseAllCpuGroups enabled="true" />
</runtime>

// 矩阵运算前检查 SIMD 状态
if (Cv2.HardwareConcurrency() >= 8)
{Cv2.SetUseOptimized(true);
    Cv2.SetNumThreads(4);  // 实测 4 线程性价比最高
}

内存池技巧

检测过程中会频繁创建 640×640 的临时 Mat,采用对象池后 GC 暂停从 200ms/ 分钟降到 5ms:

public class MatPool : IDisposable
{private ConcurrentBag<Mat> _pool = new();

    public Mat Rent(Size size, MatType type)
    {if(!_pool.TryTake(out var mat))
            return new Mat(size, type);

        if(mat.Size() != size || mat.Type() != type)
        {mat.Dispose();
            return new Mat(size, type);
        }
        return mat;
    }

    public void Return(Mat mat) => _pool.Add(mat);
}

避坑指南:血泪教训总结

ONNX 维度陷阱

YOLOv5 的 onnx 模型输出 shape 是(1,25200,85),但某些导出工具会变成(1,3,80,80,85)。遇到维度不匹配时用 Netron 可视化模型:

python -m pip install netron
netron yolov5s.onnx

死锁预防三原则

  1. 永远不要在锁内调用第三方库(OpenCV 某些方法会内部加锁)
  2. 使用 Monitor.TryEnter 设置超时,超过 100ms 直接放弃当前帧
  3. 线程间共享的 Mat 对象要用 Mat.Clone() 深拷贝

计数漂移处理

对连续 10 帧的计数结果做滑动窗口滤波:

private Queue<int> _countHistory = new(10);

public int StableCount 
{
    get {if(_countHistory.Count < 3) return CurrentCount;
        return (int)_countHistory.Average();}
}

延伸思考:通向边缘计算

这套方案可以无缝迁移到 Azure Functions:

  1. 将 OpenCV 编译为 Azure Custom Handler 所需的 WebAssembly 版本
  2. 使用 Durable Functions 编排检测流水线
  3. 用 Azure Video Analyzer 替代本地视频采集

在最近的一次压力测试中,4 核 ARM 架构的边缘节点能稳定处理 8 路 720p 视频流,平均延时控制在 150ms 以内。这证明 C# 在边缘计算场景同样具备竞争力。

最后说两句

实际部署时发现,车间的粉尘环境会导致摄像头镜片每周衰减约 5% 的清晰度。后来我们加了个简单的清晰度检测逻辑,当 SSIM 指数低于 0.8 时就触发清洁报警。技术方案再完美,也得适应真实的物理世界。

正文完
 0
评论(没有评论)