尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C#结合ONNX Runtime部署P2PNet人群计数模型实战指南

C#结合ONNX Runtime部署P2PNet人群计数模型实战指南 1. 项目概述当C#遇上P2PNet实现高效人群检测与计数最近在做一个智慧安防相关的项目客户需要在本地离线环境下对摄像头视频流进行实时的人群密度分析和人数统计。考虑到部署的便捷性和性能要求我放弃了传统的YOLO系列模型转而尝试了P2PNet这个专门为人群计数设计的算法并决定用C#配合ONNX Runtime来落地。这个组合听起来可能有点“非主流”毕竟C#在深度学习部署领域不如Python那么常见但实测下来对于需要快速集成到现有Windows桌面应用或工业上位机系统的场景它有着得天独厚的优势。整个流程就是从PyTorch模型导出到ONNX再到C#端加载推理最终实现一个稳定、高效的人群检测计数模块。如果你也在寻找一种能在.NET生态中轻松部署、无需复杂环境依赖的视觉AI解决方案那么这篇基于C#、ONNX和P2PNet的实践笔记或许能给你提供一条清晰的路径。2. 技术选型与核心思路拆解2.1 为什么是P2PNet在人群计数领域主流方案大致分两类基于检测的方法如YOLO计数和基于密度图估计的方法。YOLO虽然能框出每个人但在极度拥挤、遮挡严重的场景下小目标检测效果会急剧下降并且后处理的NMS非极大值抑制步骤在人群重叠时容易误删目标。密度图方法通过回归每个人头位置的密度热图来求和得到总人数避免了检测框的冲突但对个体位置的感知不够直观。P2PNetPoint to Point Network则提供了一种更优雅的思路。它本质上是一个基于点的检测网络直接回归图像中每个人头中心点的坐标并同时为每个点预测一个尺度可以理解为该点所代表人头的尺寸范围。它没有复杂的锚框Anchor设计也不需要NMS后处理因为网络通过一套精心设计的匹配和损失函数在训练阶段就学会了为每个真实目标分配唯一的预测点。这意味着在推理时网络输出的就是一系列点每个点对应一个人点的数量就是人数。这种“点对点”的范式在拥挤场景下表现出更高的精度和更简洁的流程。对于我们的C#部署场景P2PNet的输出非常干净——就是一组坐标和可选的信度分数。这比处理YOLO复杂的输出张量需要解析成千上万个锚框的类别、坐标、置信度要简单明了得多也减少了C#端后处理的代码复杂度和计算开销。2.2 为什么是ONNX C#这是本次项目的核心部署架构。ONNXOpen Neural Network Exchange是一个开放的模型格式标准它像是一个“中间翻译”让PyTorch、TensorFlow等框架训练的模型可以在不同的推理引擎上运行。我们将P2PNet的PyTorch模型转换为ONNX格式就实现了与训练框架的解耦。而选择C#作为推理端的语言主要基于以下几点考量生态集成需求项目主体可能是一个用WinForms、WPF或ASP.NET Core编写的桌面应用或Web服务。直接在C#中调用模型避免了跨语言调用如C#调Python带来的进程间通信开销和复杂度。部署简便性ONNX Runtime是一个高性能的推理引擎对C#有原生支持通过Microsoft.ML.OnnxRuntimeNuGet包。最终程序只需要携带这个DLL和模型文件无需在目标机器上安装Python、PyTorch等一整套沉重的科学计算环境真正实现开箱即用。性能与稳定性ONNX Runtime针对不同硬件CPU/GPU做了大量优化推理效率很高。C#作为编译型语言本身执行效率也不错结合两者可以满足实时视频流处理的需求例如在主流CPU上处理一帧640x480的图像可在百毫秒内完成。工业级可靠性.NET环境在Windows系统上非常稳定对于需要7x24小时运行的安防、工业检测等场景其内存管理、异常处理机制更为成熟可靠。注意虽然ONNX理论上支持GPU推理但在C#中配置GPU版的ONNX Runtime如CUDA或DirectML后端会比Python端稍显繁琐需要确保系统有对应的驱动和运行时库。对于大多数对延迟不极端敏感的计数场景使用CPU版本已经足够且部署复杂度大大降低。2.3 整体工作流程设计我们的目标是从零开始构建一个完整的C#人群计数应用模块。流程可以清晰地分为离线准备和在线推理两个阶段离线准备阶段模型侧获取P2PNet源码与预训练模型从官方仓库获取PyTorch实现的代码和权重文件.pth。模型转换与优化编写脚本将PyTorch模型转换为ONNX格式。这一步是关键需要固定输入尺寸、处理动态维度并可能进行简化或量化如FP16/INT8以提升推理速度、减小模型体积。模型验证使用Python脚本或简单C#程序验证转换后的ONNX模型输出是否与原始PyTorch模型一致。在线推理阶段应用侧C#环境搭建创建C#项目如控制台应用、WinForms应用通过NuGet安装Microsoft.ML.OnnxRuntime和必要的图像处理库如OpenCvSharp。图像预处理流水线实现将摄像头捕获或读取的图片处理成符合模型输入要求尺寸、归一化、通道顺序等的张量Tensor。ONNX Runtime推理加载ONNX模型创建推理会话InferenceSession输入预处理后的张量获取输出点集。后处理与可视化解析模型的输出点坐标、信度根据阈值过滤低置信度的点将点坐标映射回原始图像尺寸并在图像上绘制圆点或数字进行可视化。统计点的总数即为估计人数。3. 从PyTorch到ONNX模型转换实战与陷阱规避3.1 准备PyTorch模型与转换脚本假设你已经从GitHub如TencentYoutuResearch/CrowdCounting-P2PNet克隆了P2PNet的PyTorch代码并拥有一个预训练的.pth权重文件。模型转换的核心是使用PyTorch的torch.onnx.export函数。首先你需要理解模型的输入输出。P2PNet的输入通常是一个批次的RGB图像输出是预测的点集。查看原项目代码找到模型定义如models.py中的P2PNet类和前向传播函数。一个典型的转换脚本骨架如下import torch import torchvision from models import P2PNet # 根据你的项目结构调整导入 import onnx import onnxruntime # 1. 加载模型和权重 device torch.device(cpu) # 导出时用CPU即可 model P2PNet().to(device) checkpoint torch.load(your_pretrained_model.pth, map_locationdevice) model.load_state_dict(checkpoint[model]) model.eval() # 务必设置为评估模式 # 2. 准备一个示例输入张量 # 假设模型训练时输入尺寸为 640x640批次为1 dummy_input torch.randn(1, 3, 640, 640).to(device) # [batch, channel, height, width] # 3. 指定输入输出的名称和动态维度 # ONNX需要知道哪些维度是动态的如批次、图像尺寸 input_names [input] output_names [points, scores] # 根据模型实际输出调整 dynamic_axes { input: {0: batch_size, 2: height, 3: width}, # 批次、高、宽动态 points: {0: num_points}, # 输出点的数量动态 scores: {0: num_points} } # 4. 导出模型 onnx_model_path p2pnet.onnx torch.onnx.export( model, dummy_input, onnx_model_path, export_paramsTrue, opset_version12, # 选择一个合适的opset版本建议11 do_constant_foldingTrue, input_namesinput_names, output_namesoutput_names, dynamic_axesdynamic_axes, verboseFalse ) print(fModel exported to {onnx_model_path}) # 5. (可选) 验证ONNX模型 onnx_model onnx.load(onnx_model_path) onnx.checker.check_model(onnx_model) print(ONNX model check passed.)3.2 转换过程中的关键细节与“坑”动态尺寸处理这是最大的一个坑。P2PNet作为全卷积网络理论上可以接受任意尺寸的输入。在导出时通过dynamic_axes指定动态维度可以让ONNX模型在推理时适应不同大小的图片这对于实际应用至关重要。如果不指定模型输入尺寸就被固定为示例输入如640x640的大小灵活性会大打折扣。输出结构确认务必仔细核对模型前向传播的返回值。P2PNet可能输出一个元组包含点坐标、预测分数或者还有其他的辅助输出。你需要明确output_names里每个名字对应哪个输出张量。一个常见的输出结构是points的形状为[N, 2]N个点每个点x, y坐标scores的形状为[N]每个点的置信度。在C#端解析时需要严格对应。opset版本选择opset_version定义了导出模型所使用的ONNX算子集版本。版本过低可能不支持某些算子版本过高可能目标推理引擎如特定版本的ONNX Runtime还不支持。选择12或13是一个比较稳妥且广泛兼容的版本。如果遇到不支持的算子错误可能需要尝试调整opset版本或者查看PyTorch和ONNX Runtime的版本兼容性矩阵。简化与优化导出的原始ONNX模型可能包含一些训练阶段的算子如Dropout或者结构不够精简。可以使用ONNX官方工具onnx-simplifier来优化模型pip install onnx-simplifier python -m onnxsim p2pnet.onnx p2pnet_sim.onnx简化后的模型通常会移除冗余算子有时还能提升推理速度。量化探索针对性能追求如果对推理速度有极致要求可以考虑INT8量化。这能显著减少模型体积和提升CPU推理速度但会引入轻微的精度损失。量化过程相对复杂需要在有校准数据集的情况下使用ONNX Runtime的量化工具或其它第三方库如PPQ进行。对于初次尝试可以暂不量化优先保证功能正确。实操心得在导出模型后强烈建议在Python端用ONNX Runtime跑一次推理与原始PyTorch模型的输出进行对比确保数值差异在可接受范围内如使用余弦相似度或允许微小误差。这能提前发现转换错误避免在C#端调试时问题复杂化。4. C#端推理引擎搭建与图像预处理4.1 创建项目与安装依赖打开Visual Studio 2022创建一个新的C#控制台应用或WinForms/WPF项目。然后通过NuGet包管理器安装以下核心库Microsoft.ML.OnnxRuntime这是ONNX Runtime的C# API包负责加载和运行模型。通常安装最新的稳定版即可。如果需要GPU支持可能需要安装Microsoft.ML.OnnxRuntime.GpuCUDA或Microsoft.ML.OnnxRuntime.DirectML但这会引入额外的系统依赖。OpenCvSharp4和OpenCvSharp4.runtime.win这是OpenCV的C#封装用于图像读取、缩放、色彩空间转换、绘制等操作是计算机视觉处理的瑞士军刀。runtime.win包包含了必要的本地库简化部署。安装完成后你的项目文件.csproj中应该包含类似的引用。4.2 构建图像预处理流水线模型的输入通常是一个归一化后的[1, 3, H, W]形状的浮点张量数值范围可能是[0,1]或经过特定均值和标准差归一化。而我们的输入源摄像头、图片文件通常是H x W x 3的BGR或RGB像素数组。预处理的目标就是完成这个转换。下面是一个典型的预处理类实现using OpenCvSharp; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class ImagePreprocessor { private readonly Size _targetSize; private readonly Scalar _mean; private readonly Scalar _std; private readonly bool _swapRB; // 是否交换红蓝通道 public ImagePreprocessor(Size targetSize, float[] mean, float[] std, bool swapRB false) { _targetSize targetSize; // OpenCV的Scalar接收BGR顺序 _mean new Scalar(mean[2], mean[1], mean[0]); // 假设输入mean是RGB _std new Scalar(std[2], std[1], std[0]); // 假设输入std是RGB _swapRB swapRB; } public DenseTensorfloat Process(Mat image) { // 1. 调整尺寸 (保持宽高比的resize或直接拉伸根据模型训练方式决定) Mat resized new Mat(); Cv2.Resize(image, resized, _targetSize); // 2. 转换为RGB (如果模型训练时用的是RGB) Mat rgb new Mat(); if (_swapRB) Cv2.CvtColor(resized, rgb, ColorConversionCodes.BGR2RGB); else rgb resized.Clone(); // 3. 转换为浮点型并归一化 Mat floatMat new Mat(); rgb.ConvertTo(floatMat, MatType.CV_32FC3, 1.0 / 255.0); // 先缩放到[0,1] // 4. 减去均值除以标准差 (如果模型需要) Cv2.Subtract(floatMat, _mean, floatMat); Cv2.Divide(floatMat, _std, floatMat); // 5. 将OpenCV Mat (H,W,C) 转换为 C# Tensor (N,C,H,W) // 注意OpenCV默认是BGR但上一步我们已经转换和处理现在内存布局是H,W,C int channels floatMat.Channels(); int height floatMat.Rows; int width floatMat.Cols; var tensor new DenseTensorfloat(new[] { 1, channels, height, width }); unsafe { float* ptr (float*)floatMat.Data; for (int c 0; c channels; c) { for (int h 0; h height; h) { for (int w 0; w width; w) { // 内存布局转换从 [h, w, c] 到 [batch, c, h, w] tensor[0, c, h, w] ptr[h * width * channels w * channels c]; } } } } // 释放临时Mat resized.Dispose(); rgb.Dispose(); floatMat.Dispose(); return tensor; } }关键点解析尺寸调整策略Cv2.Resize默认是直接拉伸。如果模型训练时使用了保持长宽比的填充padding那么你也需要在这里实现相同的逻辑否则会影响精度。一个常见的做法是先将图像缩放到短边符合目标尺寸再在长边两侧进行填充。颜色通道顺序OpenCV默认读取图像是BGR顺序而很多PyTorch模型训练时使用RGB。_swapRB参数和Cv2.CvtColor就是用来处理这个差异的。务必与模型训练时的预处理保持一致。归一化参数_mean和_std必须与模型训练时使用的参数完全相同。这些值通常在模型的训练代码或配置文件中能找到例如ImageNet常用的mean[0.485, 0.456, 0.406],std[0.229, 0.224, 0.225]。如果模型输入就是[0,1]则不需要减均值除标准差。内存布局转换这是预处理中最容易出错的一步。OpenCV的Mat.Data是连续存储的顺序是[height, width, channels]。而ONNX模型通常期望[batch, channels, height, width]即NCHW格式。上面的三重循环就是完成这个“转置”操作。也可以使用System.Memory和SpanT进行更高效的操作但循环方式最为清晰易懂。4.3 初始化ONNX Runtime推理会话预处理完成后我们需要加载模型并创建推理会话using Microsoft.ML.OnnxRuntime; public class P2PNetInference { private InferenceSession _session; private ImagePreprocessor _preprocessor; public P2PNetInference(string modelPath, Size inputSize) { // 创建会话选项可以配置线程数、执行器类型等 var sessionOptions new SessionOptions(); sessionOptions.ExecutionMode ExecutionMode.ORT_SEQUENTIAL; // 顺序执行 sessionOptions.InterOpNumThreads 1; sessionOptions.IntraOpNumThreads Environment.ProcessorCount; // 使用所有CPU核心 // 如果需要GPU推理确保安装了对应的包和驱动 // sessionOptions.AppendExecutionProvider_CUDA(0); // 使用第一个CUDA设备 // sessionOptions.AppendExecutionProvider_DML(0); // 使用DirectML (Windows) _session new InferenceSession(modelPath, sessionOptions); // 初始化预处理器参数需与训练时一致 float[] mean new float[] { 0.485f, 0.456f, 0.406f }; float[] std new float[] { 0.229f, 0.224f, 0.225f }; _preprocessor new ImagePreprocessor(inputSize, mean, std, true); // swapRBtrue } public (ListPoint, int) Predict(Mat image) { // 1. 预处理 var inputTensor _preprocessor.Process(image); var inputMeta _session.InputMetadata; var inputName inputMeta.Keys.First(); // 2. 准备输入容器 var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(inputName, inputTensor) }; // 3. 运行推理 using (IDisposableReadOnlyCollectionDisposableNamedOnnxValue results _session.Run(inputs)) { // 4. 解析输出 // 假设第一个输出是点坐标 [num_points, 2]第二个是分数 [num_points] var pointsTensor results[0].AsTensorfloat(); var scoresTensor results[1].AsTensorfloat(); // ... 后处理逻辑见下一节 } } public void Dispose() { _session?.Dispose(); } }注意事项InferenceSession的创建是比较耗资源的操作应该作为单例或长生命周期对象在应用中复用而不是每次推理都创建新的。SessionOptions的配置对性能有显著影响对于CPU推理将IntraOpNumThreads设置为逻辑核心数通常能获得较好的并行效果。5. 后处理、可视化与性能优化5.1 解析输出与过滤低置信度点P2PNet的输出是密集的点集其中包含大量背景或低置信度的预测。我们需要根据分数阈值进行过滤并将归一化的坐标映射回原始图像尺寸。private (ListPoint, int) ParseOutputs(Tensorfloat pointsTensor, Tensorfloat scoresTensor, Size originalImageSize, Size modelInputSize, float scoreThreshold 0.5f) { var detectedPoints new ListPoint(); int count 0; // 获取张量维度 var pointData pointsTensor.Buffer; var scoreData scoresTensor.Buffer; int numPoints pointsTensor.Dimensions[0]; // 预测的点数 // 计算缩放比例 (从模型输入尺寸缩放到原始图像尺寸) float scaleX (float)originalImageSize.Width / modelInputSize.Width; float scaleY (float)originalImageSize.Height / modelInputSize.Height; for (int i 0; i numPoints; i) { float score scoreData.Span[i]; if (score scoreThreshold) { // 点坐标在模型输入尺寸下的位置 (假设是归一化到[0,1]或直接是像素坐标) // 需要根据模型实际输出确认。假设输出是模型输入尺寸下的绝对坐标。 float x pointData.Span[i * 2]; float y pointData.Span[i * 2 1]; // 映射回原始图像坐标 int origX (int)(x * scaleX); int origY (int)(y * scaleY); // 确保坐标在图像范围内 origX Math.Clamp(origX, 0, originalImageSize.Width - 1); origY Math.Clamp(origY, 0, originalImageSize.Height - 1); detectedPoints.Add(new Point(origX, origY)); count; } } return (detectedPoints, count); }关键点解析坐标映射这是后处理的核心。你必须清楚模型输出的坐标是相对于什么尺寸的。常见情况有两种1) 输出是相对于模型输入尺寸如640x640的绝对像素坐标2) 输出是归一化到[0, 1]的相对坐标。上述代码假设是第一种情况。如果是第二种则不需要scaleX/Y直接x * originalImageSize.Width即可。务必通过模型转换时的验证步骤确认这一点。阈值选择scoreThreshold的选择需要在精度和召回率之间做权衡。阈值太高会漏检尤其是远处的小人头阈值太低会引入大量误检将背景噪点识别为人。可以通过在验证集上绘制PR曲线来选择一个合适的值通常从0.3到0.5开始尝试。性能考虑循环遍历所有预测点进行过滤是O(N)操作对于P2PNet这类输出点数量可控的模型通常几百到几千在CPU上执行也足够快。如果追求极致可以使用SIMD指令或并行循环进行优化。5.2 结果可视化与计数显示将检测到的点绘制在原始图像上可以直观地验证模型效果。使用OpenCvSharp可以轻松实现public Mat VisualizeResult(Mat originalImage, ListPoint points, int count) { Mat resultImage originalImage.Clone(); // 绘制每个检测点 foreach (var point in points) { // 画一个实心圆代表人头 Cv2.Circle(resultImage, point, radius: 3, Scalar.Red, thickness: -1); // 也可以画一个空心圆 // Cv2.Circle(resultImage, point, radius: 5, Scalar.Green, thickness: 2); } // 在图像左上角显示总人数 string countText $Count: {count}; Cv2.PutText(resultImage, countText, new Point(20, 40), HersheyFonts.HersheySimplex, 1.0, Scalar.Green, 2); return resultImage; }在WinForms或WPF应用中你可以将Mat转换为Bitmap并显示在PictureBox或Image控件中。对于实时视频流你需要建立一个循环从摄像头抓帧 - 预处理 - 推理 - 后处理 - 可视化 - 显示。注意控制推理帧率如果模型推理速度跟不上摄像头帧率如30fps可以考虑跳帧处理或使用异步流水线避免界面卡顿。5.3 性能优化实战技巧当把整个流程跑通后性能往往是下一个关注点。以下是一些针对C# ONNX Runtime部署的优化经验输入尺寸固定化虽然在导出时设置了动态尺寸但固定一个较小的输入尺寸如480x480或320x320能显著提升推理速度因为ONNX Runtime和底层计算库如MKL对固定尺寸有更好的优化。你需要评估尺寸减小对精度的损失是否在可接受范围内。会话选项调优sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL;启用所有图优化。调整线程数对于CPU推理IntraOpNumThreads操作内并行设置为物理核心数通常效果最好。InterOpNumThreads操作间并行对于P2PNet这种不算太复杂的模型设置为1即可。尝试不同的执行模式ORT_SEQUENTIAL顺序或ORT_PARALLEL并行对于简单模型顺序模式可能开销更小。内存与对象复用重用输入输出容器在循环中不要每次推理都new ListNamedOnnxValue()和new DenseTensor。可以预先创建好容器每次只更新其中的数据。对于DenseTensor如果图像尺寸固定可以只创建一个然后通过Span直接修改其底层数组。使用ArrayPool或内存池对于大规模的临时数组如图像数据转换的中间缓冲区使用System.Buffers.ArrayPoolT.Shared来租用和归还数组可以减少GC垃圾回收压力。预处理加速图像预处理Resize, Color conversion, Normalization是纯CPU操作且可能成为瓶颈。可以尝试使用OpenCV的UMat如果支持进行GPU加速。将预处理步骤特别是颜色转换和归一化合并减少循环次数。对于固定尺寸可以预先计算好缩放和归一化的查找表LUT。模型量化如前所述将FP32模型量化为INT8通常能获得2-4倍的推理速度提升模型体积减少至1/4。但这需要校准数据集并且可能会损失一些精度对于人群计数可能误差增加1-2%。这是一个用精度换速度的权衡。6. 常见问题排查与调试实录在实际集成过程中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法问题1加载ONNX模型时抛出异常提示“Failed to load model from ...”。可能原因A模型文件路径错误或文件被占用。排查检查文件路径是否包含中文字符或特殊字符尝试使用绝对路径。确保文件没有被其他进程如编辑器锁定。可能原因BONNX Runtime版本与模型opset不兼容或缺少某些自定义算子的支持。排查使用netron一个可视化工具打开你的.onnx模型文件查看顶层的ir_version和opset_import版本。确保你安装的ONNX Runtime版本支持该opset。如果模型使用了特殊算子可能需要编译支持该算子的自定义版本ONNX Runtime这通常很复杂建议回模型转换阶段尝试用更常见的算子替换或降低opset版本重新导出。问题2推理时输出结果全是零或NaN或者与Python端结果对不上。可能原因A预处理不一致。这是最常见的原因。排查这是“黄金法则”——用同一张图片分别在Python原始PyTorch模型和C#ONNX模型端运行打印出输入给模型的第一个批次、第一个通道、前几个像素的数值进行逐元素对比。确保颜色通道顺序RGB/BGR、归一化除255、减均值、除标准差、尺寸缩放算法线性插值、最近邻等完全一致。可能原因B输入张量的形状或数据类型错误。排查使用_session.InputMetadata检查模型期望的输入形状和数据类型通常是float32。确保你创建的DenseTensor的维度和类型与之匹配。特别注意那个[batch, channels, height, width]的顺序。可能原因C动态尺寸处理不当。排查如果你导出了动态尺寸模型但在C#端创建DenseTensor时使用了固定的new[] {1, 3, 640, 640}那么当你传入不同尺寸的图片时张量形状与实际数据不匹配。你需要根据每张图片的预处理后的尺寸来动态创建张量。问题3推理速度很慢无法满足实时性要求。排查步骤性能分析使用System.Diagnostics.Stopwatch分别对预处理、推理、后处理三个阶段计时找到瓶颈所在。预处理瓶颈如果预处理耗时占比高参考上一节的预处理加速技巧。推理瓶颈检查CPU占用率。如果IntraOpNumThreads设置很高但CPU占用率上不去可能是模型本身计算量不大线程创建和同步的开销反而成了负担尝试设置为1或2。确保没有其他后台程序大量占用CPU。模型层面考虑是否可以使用更小的输入尺寸或者使用量化后的INT8模型。问题4在WinForms/WPF界面中实时显示视频流时界面卡顿。原因UI线程被耗时的推理操作阻塞。解决方案使用异步编程。将摄像头抓取、预处理、推理、后处理、可视化这一系列操作放在一个独立的Task或后台线程中运行。使用Invoke或Dispatcher将最终要显示的图像数据安全地更新到UI控件上。架构上可以采用生产者-消费者模式用一个队列缓冲视频帧避免推理速度跟不上采集速度导致的堆积。问题5检测点数在视频连续帧中剧烈跳动闪烁。原因这是目标检测领域的常见问题由于单帧检测的独立性在边界情况下人的置信度在阈值上下波动会导致计数不稳定。解决方案引入简单的时序平滑滤波。例如维护一个最近N帧如5帧的计数队列当前帧的显示计数使用移动平均或中值滤波。对于点的位置也可以使用简单的跟踪算法如基于距离的匈牙利匹配进行帧间关联平滑点的出现和消失。最后调试深度学习模型部署耐心和细致的对比是关键。准备一小组有代表性的测试图片建立一个可以同时运行Python参考代码和C#推理代码的测试环境逐步比对中间结果是定位问题最高效的方法。一旦打通这个C# ONNX P2PNet的管道就会成为一个非常稳定和高效的人群计数基础组件可以方便地集成到各种.NET应用中。
返回列表