资讯详情

c# + yolodotnet+opencvsharp 实现物品轨迹追踪

📅 2026/10/30 5:03:50 | 华诺云谱 👁 阅读
c# + yolodotnet+opencvsharp 实现物品轨迹追踪
前言实际项目需求是做个包裹错分检测系统通过视觉方式检测包裹流向根据追踪轨迹的方式来实现包裹定位。考虑到模型训练需要大量包裹来做样品标记所以用自己办公电脑的前置摄像头做个手机追踪的示例考虑最终可行性。技术栈C# / .NET 8 / WinForms / YOLOv8 / YoloDotNet / OpenCvSharp / ONNX Runtime项目目标调用电脑摄像头实时检测画面中的手机标记手机中心位置并绘制最近 6 秒的移动轨迹。无图言屌博客有动图大小限制看不到的自行脑补下就是跟着手机移动实时绘制手机运行轨迹一、项目效果与需求这是一个运行在 Windows 桌面端的实时视觉小工具主界面由 WinForms 实现。点击「开始检测」后程序会打开本机摄像头实时显示画面使用 YOLOv8n 检测画面中的手机用橙色框标出手机四角高亮并显示置信度用绿色十字标记手机中心点同时显示坐标绘制最近 6 秒的移动轨迹旧轨迹偏蓝新轨迹偏黄在状态栏显示实际显示 FPS、推理耗时和轨迹点数量支持切换摄像头、调整置信度、显示/隐藏轨迹、清除轨迹。最终在本机i9-13900HXCPU 推理实测指标结果采集分辨率1280×720摄像头出帧30.3 FPSMedia Foundation应用显示2531 FPS单帧推理1926 ms轨迹绘制最近 6 秒整篇文章会从技术选型、核心实现一直讲到性能排查过程中踩过的坑。二、为什么选择这套技术栈模块技术选择原因桌面界面.NET 8 WinForms开发快部署简单调用摄像头和图像控件方便摄像头采集OpenCvSharp4封装 OpenCV取帧、绘图、缩放都很直接目标检测YoloDotNet 4.2纯 C# 调用 ONNX 模型自带 YOLO 前后处理推理后端ONNX Runtime CPU Provider无 GPU 也能跑换 CUDA 只改执行提供程序检测模型YOLOv8nCOCO体积小、CPU 推理快COCO 自带cell phone类别YOLOv8n 的 COCO 模型共有 80 个类别其中手机对应索引 67 → cell phone使用官方 Ultralytics 工具导出 ONNX 模型yoloexportmodelyolov8n.ptformatonnxopset17imgsz640YoloDotNet 对 YOLOv5uYOLOv12 建议使用 opset 17模型输入为 640×640。三、整体处理流程摄像头OpenCV VideoCapture优先 MSMF ↓ BGR Mat BGR → BGRA → 固定内存 byte[] → SKImage零拷贝 ↓ YoloDotNet / ONNX RuntimeYOLOv8n ↓ ListObjectDetection 过滤 cell phone 标签 最近邻关联 ↓ One Euro 平滑中心点 X/Y 检测框宽高 ↓ OpenCV 绘制检测框、中心十字、6 秒轨迹 ↓ 按显示尺寸预缩放 → Bitmap ↓ VideoSurface 1:1 绘制UI 线程整个链路都在 C# 中完成不需要 Python 服务也没有跨进程通信。四、核心实现4.1 打开摄像头优先 Media Foundation很多示例代码会直接写varcapturenewVideoCapture(0,VideoCaptureAPIs.DSHOW);这在部分摄像头上会导致帧率异常。我在本机实测同一颗摄像头、同样 1280×720后端实际出帧率DirectShow16.9 FPSMedia Foundation30.3 FPS原因和摄像头驱动、分辨率、MJPG/YUY2 协商有关。最稳妥的方式是按顺序尝试多个后端privatestaticVideoCaptureOpenCamera(intindex){// 优先 Media Foundation同一台摄像头在 DirectShow 下可能只有 15~20 FPSforeach(varapiinnew[]{VideoCaptureAPIs.MSMF,VideoCaptureAPIs.DSHOW,VideoCaptureAPIs.ANY}){VideoCapture?cameranull;try{cameranewVideoCapture(index,api);if(camera.IsOpened()){camera.Set(VideoCaptureProperties.FrameWidth,1280);camera.Set(VideoCaptureProperties.FrameHeight,720);camera.Set(VideoCaptureProperties.Fps,30);camera.Set(VideoCaptureProperties.BufferSize,1);returncamera;}}catch{// 当前后端不可用尝试下一个}camera?.Dispose();}thrownewInvalidOperationException($无法打开摄像头{index});}BufferSize 1可以让程序尽量拿到最新帧减少内部缓冲带来的延迟。4.2 OpenCV Mat 到 Skia SKImage避免多余拷贝OpenCvSharp 取到的是 BGR 三通道Mat而 YoloDotNet 使用 SkiaSharp 的SKImage。如果每次都先转成Bitmap再交给推理会带来额外的内存分配和像素拷贝。这里的做法是用 OpenCV 把 BGR 转成 BGRA固定pin一个复用的byte[]用SKImage.FromPixels直接引用这块内存推理结束后立刻释放GCHandle。Cv2.CvtColor(frame,bgra,ColorConversionCodes.BGR2BGRA);intlengthchecked((int)bgra.Step()*bgra.Rows);if(pixelBuffer.Lengthlength)pixelBuffernewbyte[length];Marshal.Copy(bgra.Data,pixelBuffer,0,length);varhandleGCHandle.Alloc(pixelBuffer,GCHandleType.Pinned);try{varinfonewSKImageInfo(bgra.Width,bgra.Height,SKColorType.Bgra8888,SKAlphaType.Opaque);usingvarimageSKImage.FromPixels(info,handle.AddrOfPinnedObject(),(int)bgra.Step());returnyolo.RunObjectDetection(image,confidence:confidence,iou:0.7);}finally{handle.Free();}注意SKImage.FromPixels不会复制像素所以推理期间必须保证缓冲区一直有效。4.3 手机检测与多目标关联先按标签过滤结果。不同模型的标签名称可能略有差异这里兼容了几种常见写法privatestaticreadonlyHashSetstringPhoneLabelsnew(StringComparer.OrdinalIgnoreCase){cell phone,cellphone,mobile phone,phone,smartphone};同屏出现多部手机时不能每帧都重新选置信度最高的目标否则框会在几部手机之间来回跳。这里用上一帧中心点的最近邻做关联if(previousCenterisPoint2fprevious){returnphones.MinBy(pDistanceSquared(ToCenter(p.BoundingBox),previous));}returnphones.MaxBy(pp.Confidence);没有历史位置时选置信度最高的目标有历史位置时选距离上一帧最近的目标。这样在只有一部手机时能稳定跟踪多部手机时也不容易跳目标。4.4 抖动治理用 One Euro 滤波器代替简单 EMA原始检测框会持续抖动。最早我用的是指数移动平均smoothed previous 0.45 * (raw - previous)效果是平滑了但存在两个问题静止时仍然能看出细小的抖动移动时平滑参数固定跟手性和稳定性无法兼顾。后来改成了One Euro 滤波。它的核心思想是根据目标速度动态调整截止频率速度越快滤波越弱从而既能在静止时强力去抖又能在快速移动时保持跟手。publicdoubleFilter(doublevalue,doubledt){doublerawDerivative(value-_previousRawValue)/dt;_previousRawValuevalue;doublederivativeAlphaAlpha(dt,_derivativeCutoff);_filteredDerivativederivativeAlpha*rawDerivative(1-derivativeAlpha)*_filteredDerivative;doublecutoff_minCutoff_beta*Math.Abs(_filteredDerivative);doublealphaAlpha(dt,cutoff);_filteredValuealpha*value(1-alpha)*_filteredValue;return_filteredValue;}privatestaticdoubleAlpha(doubledt,doublecutoff){doubletau1.0/(2.0*Math.PI*cutoff);return1.0/(1.0tau/dt);}工程里分别对 4 个量做滤波滤波对象minCutoffbeta中心点 X1.30.02中心点 Y1.30.02检测框宽度0.90.008检测框高度0.90.008只平滑中心点是不够的如果框的宽高仍使用原始值框的四个边仍会抖动。因此这里把中心点和宽高都平滑再用平滑后的结果重新组合成检测框。4.5 漏检保持解决检测框闪烁即使模型检测率很高也难免偶尔漏掉一两帧。如果直接在漏检帧不画框画面看起来就会一闪一闪。解决方法是加一个短暂的「保持时间」漏检 ≤ 300 ms保持上一帧的检测框和中心点不新增轨迹点漏检 300 ms隐藏检测框漏检 800 ms清空轨迹并重置滤波器重新捕获目标时如果间隔超过 300 ms断开旧轨迹避免把两个位置连成一条长直线。这样短暂遮挡或模型抖动不会造成闪烁手机真正离开画面后轨迹也会自动结束。4.6 轨迹绘制Catmull-Rom 插值 分段批量绘制摄像头大约 30 FPS如果直接用相邻两个点画线段快速移动时轨迹会有明显的折线感。这里使用 Catmull-Rom 插值在每两个轨迹点之间再生成 4 个采样点privatestaticPoint2fCatmullRom(Point2fp0,Point2fp1,Point2fp2,Point2fp3,floatt){floatt2t*t;floatt3t2*t;floatx0.5f*(2f*p1.X(-p0.Xp2.X)*t(2f*p0.X-5f*p1.X4f*p2.X-p3.X)*t2(-p0.X3f*p1.X-3f*p2.Xp3.X)*t3);floaty0.5f*(2f*p1.Y(-p0.Yp2.Y)*t(2f*p0.Y-5f*p1.Y4f*p2.Y-p3.Y)*t2(-p0.Y3f*p1.Y-3f*p2.Yp3.Y)*t3);returnnewPoint2f(x,y);}轨迹颜色从旧到新渐变privatestaticScalarTrailColor(doublet)new(255*(1-t),80175*t,255*t);其中t 0偏蓝t 1偏黄。为了控制开销轨迹没有逐段调用Cv2.Line而是按颜色分成 16 个区间用Cv2.Polylines批量绘制。另外轨迹点会保留最近 6 秒如果手机几乎静止新点和上一个点的距离小于 1 像素就只更新最后一个点避免原地堆出大量重复点。4.7 UI 显示预缩放 1:1 绘制实时视频的显示链路也很容易成为瓶颈。一开始使用PictureBox的SizeMode Zoom让控件自动缩放 1280×720 的画面。后来实测发现每帧都让 GDI 做插值缩放会带来额外开销尤其是在高 DPI、窗口较大的情况下。优化方法是在采集线程按显示区域尺寸预缩放UI 线程只负责把现成的Bitmap按 1:1 画出来如果显示区域比视频大就按原始分辨率居中显示不再放大使用自定义VideoSurface开启双缓冲和AllPaintingInWmPaint。protectedoverridevoidOnPaint(PaintEventArgse){varframe_frame;if(frameisnull){e.Graphics.Clear(BackColor);return;}e.Graphics.CompositingModeCompositingMode.SourceCopy;e.Graphics.PixelOffsetModePixelOffsetMode.Half;if(frame.WidthClientSize.Widthframe.HeightClientSize.Height){intx(ClientSize.Width-frame.Width)/2;inty(ClientSize.Height-frame.Height)/2;e.Graphics.DrawImageUnscaled(frame,x,y);}else{e.Graphics.DrawImage(frame,ClientRectangle);}}UI 更新使用BeginInvoke。如果上一帧还没画完就丢弃当前帧避免消息队列越积越多、画面延迟越来越大。另外状态栏里的 FPS 统计的是实际显示帧数而不是采集循环次数。这样才能真正确认「计算了 30 帧但界面只显示了 17 帧」这类问题。五、性能优化记录优化前的现象是推理只要 23 ms 左右但画面明显不够流畅。于是对整条链路逐段计时得到下面这些数据环节优化前优化后摄像头后端DirectShow 16.9 FPSMSMF 30.3 FPS应用显示约 16.7 FPS2531 FPS单帧推理约 23 ms1926 ms预缩放 合成无约 2 msMat → Bitmap每帧缩放后转换约 1 msUI 掉帧有实测 0主要的优化点有三个换摄像头后端DirectShow 在这台机器上只有 16.9 FPS这是最大的瓶颈。改为优先 MSMF 后摄像头直接跑满 30 FPS。把缩放从 UI 线程挪到采集线程用 OpenCV 预缩放比让 GDI 每帧插值更稳定也更容易控制显示尺寸。批量绘制轨迹轨迹最多保留 6 秒、约 180 个点每个点插值 4 次后会接近 700 个线段。逐段画线会产生大量 GDI/OpenCV 绘制调用按颜色分桶后用Polylines批量画能明显降低绘制开销。六、踩坑记录坑 1帧率低不一定是模型慢一开始看到画面卡第一反应是 YOLO 推理太慢。但把推理单独计时后发现只有 20 多毫秒完全够 30 FPS。后来用 Python OpenCV 单独测摄像头capcv2.VideoCapture(0,cv2.CAP_DSHOW)# 读取 3 秒统计帧数结果 DirectShow 只有 16.9 FPS换成 MSMF 后变成 30.3 FPS。先测数据源再怀疑算法这次省了很多时间。坑 2OpenCvSharp 的Mat.Create参数顺序Mat.Create的参数是(rows, cols, type)而不是(width, height, type)。我在预缩放时写成了display.Create(targetWidth,targetHeight,MatType.CV_8UC3);结果创建出来的 Mat 宽高互换随后构造 ROI 时直接抛出0 roi.x 0 roi.width roi.x roi.width m.cols ...正确写法是display.Create(targetHeight,targetWidth,MatType.CV_8UC3);这个坑很隐蔽因为编译完全正常只在运行时才暴露。坑 3检测框和中心点必须一起平滑一开始只平滑中心点结果中心十字很稳但检测框的边缘仍然持续抖动整体观感还是不流畅。后来把宽、高也纳入 One Euro 滤波视觉上才真正稳定下来。坑 4UI 显示帧率要和采集帧率分开统计之前用采集循环的计数当作 FPS看到的是 30 FPS但实际 UI 可能在丢帧。后来在 UI 线程单独统计显示帧率才发现问题。“处理了多少帧”和“用户看到了多少帧”是两个不同的指标。七、如何运行环境Windows 10/11.NET 8 SDK可用的摄像头Visual Studio 2022/2026 或命令行主要 NuGet 包PackageReferenceIncludeOpenCvSharp4Version4.13.0.20260627/PackageReferenceIncludeOpenCvSharp4.ExtensionsVersion4.13.0.20260627/PackageReferenceIncludeOpenCvSharp4.runtime.winVersion4.13.0.20260627/PackageReferenceIncludeYoloDotNetVersion4.2.0/PackageReferenceIncludeYoloDotNet.ExecutionProvider.CpuVersion1.1.0/启动dotnet run--projectPhoneMotionTracker/PhoneMotionTracker.csproj操作选择摄像头编号默认 0调整置信度阈值默认 0.35点击「开始检测」手机进入画面观察检测框、中心标记和运动轨迹可以随时清除轨迹、显示/隐藏轨迹或停止检测。项目结构PhoneMotionTracker.sln README.md PhoneMotionTracker/ ├── Form1.cs # 采集、推理、平滑、轨迹绘制 ├── Form1.Designer.cs # WinForms 界面 ├── VideoSurface.cs # 1:1 快速视频显示控件 ├── Program.cs ├── PhoneMotionTracker.csproj └── models/yolov8n.onnx # 默认检测模型八、还可以继续优化GPU 推理把YoloDotNet.ExecutionProvider.Cpu换成 CUDA 或 TensorRT Provider推理时间可以进一步下降更小的输入尺寸把模型导出为 512 或 480 输入减少 CPU 推理耗时多目标跟踪引入 SORT、ByteTrack 等算法为每部手机分配稳定 ID轨迹导出把手机中心点坐标按时间序列导出为 CSV/JSON用于后续分析区域入侵检测结合 ROI实现“手机进入某个区域”的告警录像功能使用 OpenCVVideoWriter保存带叠加层的视频。九、总结这个项目本身不复杂但把「实时视觉」做好远不只是调用一个 YOLO 模型摄像头后端选错可能直接损失一半帧率检测框抖动需要针对中心点和宽高分别做自适应平滑漏检保持能显著改善观感轨迹插值和批量绘制决定了画面是否“丝滑”UI 线程的渲染链路也必须纳入性能统计。如果你也在用 C# 做实时摄像头 AI 检测希望这篇文章能帮你少踩几个坑。模型许可提醒Ultralytics YOLO 预训练模型通常采用 AGPL-3.0 或需要商业授权商用前请确认许可YoloDotNet 本身采用 MIT 许可最后如果你觉得这篇文章对你有帮助不妨点个赞支持一下你的支持是我继续分享只是的动力。如果有任何疑问或需要进一步的帮助欢迎随时留言。公众号会经常发一些实用的小工具和demo源码有兴趣的麻烦点个关注赠人玫瑰手留余香感谢您的关注期待和您一起探讨再会
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑