简介:面向需要将YOLOv8目标检测与ByteTrack多目标追踪能力落地到.NET环境的开发者,这套演示源码包提供了完整的C#调用TensorRT加速的工程方案,覆盖从模型推理、后处理到多目标跟踪的完整链路。包内包含C#主工程、TensorRtSharp封装层、C++底层扩展项目以及OpenCvSharp相关依赖,共78个文件,涵盖cs源码、sln工程文件、dll运行库、h头文件、xml注释与pdb调试信息,压缩后约325MB。资源附带了经过验证的测试环境说明,包括Win10 x64、VS2019、CUDA11.7、TensorRT8.6、.NET Framework4.7.2等,并已集成全部所需DLL,可省去自行编译TensorRT接口和匹配OpenCV版本的繁琐配置。目录按TensorRtSharp、Custom、common等模块划分,便于拆解模型推理、目标检测与追踪框架的具体实现;同时提供了C++底层工程和C#互操作层代码,适合想深入研究原理或直接复用工程的人。目前已有820人学习下载,可作为C#开发者在真实项目中快速集成YOLOv8+ByteTrack的实用参考。
1. 上位机方案里的 C# + YOLOv8 + TensorRT + ByteTrack,解决的不只是“检测”
C# 上位机跑 YOLOv8 目标检测不稀奇,稀奇的是把 TensorRT 加速和 ByteTrack 目标追踪同时装进一个 WinForm 工程,还能直接编译运行——这就是这套演示源码的价值。单纯 YOLOv8 逐帧检测,目标一被遮挡、一变速转弯就丢;接上 ByteTrack 之后,检测框变成有稳定 ID 的轨迹,能持续盯住一个目标。这套方案解决的核心问题正是“实时性 + 身份连续性”,适合工业视觉、安防监控、巡检机器人这类“检测完还要持续跟踪”的上位机场景。对新手,它是把模型、推理库、追踪算法串起来的最小可行路径;对老手,它是可以拆开改造成自己工程底子的完整样板。
2. 从 PT 权重到 TensorRT Engine:模型转换与 C# 工程的 DLL 依赖准备
2.1 导出 ONNX:用 ultralytics 命令行输出 8400 个候选框
拿到一个训练好的 YOLOv8 权重(官方 yolov8s.pt 或自己训练的数据集产物),第一步不是直接转 TensorRT,而是先导出 ONNX。YOLOv8 的导出链路是 PT → ONNX → Engine,中间尽量不要跳步,因为 TensorRT 直接读 PT 不现实,而 ONNX 是调试中间层的最佳形态,出了问题可以用 onnxruntime 先验证一遍。
常见做法是在虚拟环境里装好 ultralytics,然后用它的命令行导出:
pip install ultralytics onnx onnxruntime yolo export model=yolov8s.pt format=onnx dynamic=True opset=12导出完成会生成yolov8s.onnx。dynamic=True表示输入尺寸动态,后续在 TensorRT 里可以限定最小、最优、最大 batch;opset=12是老版本兼容性最稳的算子集,如果你用的 TensorRT 版本很新,也可以试 opset 13 或 14,但 12 踩坑最少。
这个 ONNX 的输出节点形状一般是(1, 84, 8400)。8400 是 YOLOv8 在 640 输入下三个尺度特征图(80×80、40×40、20×20)的锚框总数,84 是 4 个框坐标 + 80 个 COCO 类别分数。注意 YOLOv8 是解耦头,输出里没有 objectness 这一维,后处理时不要按 YOLOv5 的习惯去乘置信度,否则分数会整体偏低。
提示:用
onnxruntime跑一遍 ONNX,先确认输出数值和 PyTorch 推理结果一致,再做 TensorRT 转换。这一步能省掉后面至少一半的“推理结果全是零”的排查时间。
2.2 用 trtexec 生成 Engine:FP16、动态 batch 和 workspace 取舍
TensorRT 安装教程网上很多,装完之后真正干活的是trtexec.exe,它在 TensorRT 的bin目录下。转 Engine 的核心命令是这样:
trtexec --onnx=yolov8s.onnx --saveEngine=yolov8s_fp16.engine \ --fp16 \ --minShapes=images:1x3x640x640 \ --optShapes=images:4x3x640x640 \ --maxShapes=images:8x3x640x640 \ --workspace=4096--fp16开启半精度,GTX 1660 Ti 这类图灵卡能明显提速;如果显卡是 GTX 1070 这种 Pascal 老卡,FP16 加速收益没那么大,可以先跑 FP32 版本对比一下。--minShapes / --optShapes / --maxShapes只在动态输入时才需要,如果你的 C# 端固定用 640×640,直接写死--shapes=images:1x3x640x640更省显存。--workspace是构建时允许 TensorRT 使用的显存上限,单位 MB,4096 对 YOLOv8s 足够。
转换完的yolov8s_fp16.engine是二进制文件,和你的 C# 工程没有直接关系,推理时只需要这个文件,不需要再依赖 ONNX。一个容易忽略的点:Engine 和 TensorRT 版本强绑定,TensorRT 8.x 生成的 engine 在 10.x 环境里可能直接加载失败。换版本就要重新生成,这属于“生成一次就别乱动”的资产。
2.3 C# 工程要备齐的 DLL:TensorRT、CUDA、cuDNN 依赖链
标题里写了“所有 dll 文件”,实际跑起来你会看到一条完整的原生依赖链。C# 侧通过 P/Invoke 或者 C++/CLI 调用一个封装 DLL,这个封装 DLL 又依赖 TensorRT 的nvinfer.dll、nvinfer_plugin.dll,再往下是 CUDA 的cudart64_12.dll、cublas64_12.dll、cublasLt64_12.dll,以及 cuDNN 的cudnn64_9.dll、cudnn_ops_infer64_9.dll。
一个健康演示包的 DLL 分布应该是这样的:
| DLL 类别 | 典型文件名 | 作用 |
|---|---|---|
| 封装层 | YoloTensorRT.dll | C++ 写的导出函数,C# 直接调用 |
| TensorRT | nvinfer.dll、nvinfer_plugin.dll | Engine 加载与推理 |
| CUDA Runtime | cudart64_12.dll、cublas64_12.dll、cublasLt64_12.dll | 显存操作、张量运算 |
| cuDNN | cudnn64_9.dll、cudnn_ops_infer64_9.dll | 部分层加速 |
把这些 DLL 全部丢到 C# 项目的输出目录(bin\Debug 或 bin\Release)是最省事的做法,因为DllImport默认会从当前程序集目录找依赖。别只把封装 DLL 放进去、TensorRT 或 CUDA 不放,运行时报错会从“找不到封装 DLL”一路变成“找不到 cudart64_12.dll”,这种问题最耗时间。
还要注意运行机器必须装 VC++ 2015-2022 Redistributable。封装 DLL 是 MSVC 编译的,缺了这个运行时,C# 调用时会报“无法加载 DLL,找不到指定的模块”,而且这个错误提示里不会告诉你是缺 VC 运行库,只能靠经验定位。
注意:所有 DLL 必须是 x64 版本。TensorRT 没有 32 位版,C# 项目平台目标必须设为 x64,AnyCPU 在 64 位系统上默认也走 x64 没问题,但一旦勾选了“首选 32 位”就会直接 BadImageFormatException。
3. C# 侧推理封装:TensorRT 加载、Letterbox 前处理与 NMS 后处理
3.1 选哪种封装方式:C++/CLI、原生 DLL + P/Invoke,还是 C# 绑定
把 TensorRT 接进 C# 有三种常见路线:C++/CLI 写托管封装、原生 C++ DLL 加 P/Invoke、用现成的 C# 绑定库。演示源码里最常见的是第二种——原生 DLL 导出几个 C 风格接口,C# 侧用DllImport声明即可,这套方式对 .NET Framework 4.7.2 和 .NET 6/8 都通用,项目里也最好移植。
接口设计一般是这样的:
public static class YoloNative { private const string DllName = "YoloTensorRT.dll"; [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr CreateEngine( [MarshalAs(UnmanagedType.LPStr)] string enginePath, [MarshalAs(UnmanagedType.LPStr)] string pluginPath); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern int Detect( IntPtr handle, [In] byte[] bgrData, int width, int height, out IntPtr resultPtr, out int resultCount); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern void FreeResults(IntPtr resultPtr); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern void DestroyEngine(IntPtr handle); }CreateEngine只调用一次,在窗体启动时初始化;Detect每帧调用一次,传入 BGR 原始图像字节流,返回的是非托管内存里的一组结果结构体,用完必须FreeResults释放。很多人在这一步踩 Access Violation(c0000005),十有八九是结构体布局和 C++ 端不一致,或者 byte 数组没有固定住导致 GC 移动了内存。解法是确认[StructLayout(LayoutKind.Sequential)]、提前 pin 住 byte 数组,或者干脆把结果拷贝到托管数组后再释放指针。
C++/CLI 只适合 .NET Framework 老工程,到了 .NET Core 时代混合模式程序集支持不完整,不太建议新项目跳这个坑。第三方 C# 绑定库能跑通最小 demo 的不少,但一旦要调进 ByteTrack,还是自己包一层原生 DLL 最可控。
3.2 Letterbox 前处理与坐标逆映射:BGR、RGB 和 114 灰边
YOLOv8 训练时输入是 640×640 等比例缩放 + 灰色填充,推理端必须复现这套预处理。用 OpenCvSharp 实现 Letterbox 是很顺手的做法:
private static Mat Letterbox(Mat src, int targetSize, out float scale, out int padX, out int padY) { int h = src.Height; int w = src.Width; scale = Math.Min((float)targetSize / w, (float)targetSize / h); int newW = (int)Math.Round(w * scale); int newH = (int)Math.Round(h * scale); padX = (targetSize - newW) / 2; padY = (targetSize - newH) / 2; Mat resized = new Mat(); Cv2.Resize(src, resized, new Size(newW, newH), 0, 0, InterpolationFlags.Linear); Mat canvas = new Mat(new Size(targetSize, targetSize), MatType.CV_8UC3, Scalar.All(114)); resized.CopyTo(canvas[new OpenCvSharp.Rect(padX, padY, newW, newH)]); return canvas; }scale、padX、padY这三个值必须原样传出去,因为追踪框画回原图时要做逆变换。YOLOv8 的 ONNX 输入要求 RGB 顺序且归一化到 0~1,而摄像头和视频解码出来是 BGR,所以 C++ 封装层里要先做颜色通道翻转,再除以 255。有些封装 DLL 在内部已经处理好了这一步,C# 端只要保证传进去的是原始 BGR buffer 就行。
补边值用 114 是训练时默认的,和模型训练参数不一致会导致检测精度下降。还有一个容易翻车的细节:OpenCvSharp 的Mat.CopyTo在 ROI 区域尺寸不一致时会报错,所以 resize 时newW / newH不要四舍五入舍掉太多,否则padX + newW可能超出 canvas 宽度一两个像素。稳妥做法是往下取整并确保不超过 targetSize。
3.3 解析 YOLOv8 输出:84 维向量与 NMS 的 C# 实现
前处理做完,推理拿到的是一个连续内存的 float 数组,布局是[8400, 84]还是[84, 8400]取决于 ONNX 导出和 TensorRT 的优化,实际调试时先打印首地址附近的值,确认排列方向再做解析。通常按[8400, 84]逐行读取是最好理解的方式:
private static List<Detection> DecodeYolov8Output(float[] output, float confThres, float iouThres) { const int numAnchors = 8400; const int stride = 84; var candidates = new List<Detection>(numAnchors); for (int i = 0; i < numAnchors; i++) { int baseIdx = i * stride; float cx = output[baseIdx + 0]; float cy = output[baseIdx + 1]; float w = output[baseIdx + 2]; float h = output[baseIdx + 3]; float bestScore = 0f; int bestClass = 0; for (int c = 4; c < stride; c++) { float score = output[baseIdx + c]; if (score > bestScore) { bestScore = score; bestClass = c - 4; } } if (bestScore >= confThres) { candidates.Add(new Detection { X = cx, Y = cy, Width = w, Height = h, Confidence = bestScore, ClassId = bestClass }); } } return Nms(candidates, iouThres); }这一步最关键的是类别索引,c - 4是从第 5 个分量开始才是第一个类别分数。NMS 用标准实现即可:按置信度降序排序,依次取框,把 IoU 大于阈值的框全部剔除。iouThres一般取 0.45 或 0.5,别设太高,否则重叠的同类物体会输出一堆重复框,ByteTrack 会把这些重复框当成不同目标,ID 数量瞬间爆炸。
推理结果从非托管内存拷回托管数组后,处理期间不要再访问原来的指针,释放操作放在这一帧完全结束之后。如果检测画框和 ByteTrack 追踪要共用同一份结果,建议先把坐标从 640×640 的模型空间逆映射回原图,再喂给追踪器,而不是让追踪器也在模型空间工作,省得后面画线时到处换算。
4. 接入 ByteTrack:让检测框变成有 ID 的稳定目标轨迹
4.1 ByteTrack 的匹配策略:高分框优先,低分框补充召回
ByteTrack 的核心思想和 YOLOv5 时代常用的 DeepSORT 不一样,它不做表观特征提取,也不用 ReID 模型,纯粹靠两个检测阈值来做关联:高阈值检测框负责“确定性高”的匹配,低阈值检测框负责“召回可能被漏掉的遮挡目标”。匹配过程分两步:高分框和已有轨迹做 IoU 匈牙利匹配,匹配不上的高分框再和“丢失状态”的轨迹匹配一次;剩下的高分框作为新轨迹候选,低分框则只在遮挡场景中用来续上老轨迹。
这个策略对工业场景非常友好,因为 DeepSORT 需要额外加载一个 ReID 模型,在 C# 上位机里又难看又占显存。ByteTrack 只需要检测框坐标和 IoU,完全可以在 C# 侧自己实现,不依赖 Python 生态。代价是它假设目标是刚性运动、检测框稳定,目标快速形变或相机大幅抖动时轨迹容易断,但这不是框架的错,是 IoU 关联的天然边界。
4.2 C# 端追踪器结构:卡尔曼滤波、Track 状态与参数落位
在 C# 里实现 ByteTrack 追踪器,核心是定义轨迹状态类和一个管理类。轨迹状态大致分三层:未激活候选、已激活轨迹、丢失轨迹。已激活轨迹有一个永久的 TrackId、累计命中帧数、丢失帧数,以及卡尔曼滤波预测的下一帧位置。
public class TrackTarget { public int TrackId { get; set; } public float CenterX { get; set; } public float CenterY { get; set; } public float Width { get; set; } public float Height { get; set; } public int Hits { get; set; } public int TimeSinceUpdate { get; set; } public bool IsActivated { get; set; } public float PredictX { get; set; } public float PredictY { get; set; } } public class ByteTracker { private readonly List<TrackTarget> _tracks = new List<TrackTarget>(); private int _nextTrackId = 1; public float HighThreshold = 0.5f; public float LowThreshold = 0.1f; public int MaxAge = 30; public int MinHits = 3; }管理轨迹集合时,List<TrackTarget>是自然选择,因为追踪器要频繁增删轨迹、按状态过滤;但做 IoU 矩阵计算时我会把坐标抽到数组里一次性算完,避免频繁访问 List 属性引发边界检查和缓存抖动。这和 C# 中数组和集合的经典取舍一致:动态生命周期用集合,批量数值计算用数组。
卡尔曼滤波可以直接用 OpenCvSharp 自带的KalmanFilter类,状态量设为 4 维(中心 x、中心 y、宽、高),观测也是 4 维,detection 的框坐标作为观测输入。追踪器每一步执行流程是:用卡尔曼预测所有已激活轨迹的位置,计算预测框和当前检测框的 IoU 矩阵,调用匈牙利匹配,更新命中的轨迹、删除超龄丢失轨迹、初始化新轨迹候选。MinHits=3意味着连续三帧命中才给正式 ID,否则一直以候选状态存在,这样能过滤掉单帧误检。
4.3 主循环管线:视频/摄像头输入 → 检测 → 追踪 → UI 绘制
整套演示源码的运行时管线只有一条主循环,但必须拆成两段,否则界面会卡死。拉流和推理放到后台线程,UI 只负责显示结果。结果从后台线程传递到 UI 线程,最常见的做法是用 C# 委托加事件回调触发布局绘制。
private void WorkerLoop() { using var capture = new VideoCapture(videoPath); using var tracker = new ByteTracker(); var frameBuffer = new byte[640 * 640 * 3]; while (_running) { using var frame = new Mat(); if (!capture.Read(frame)) break; _lastFrameStopwatch.Restart(); // 1. 推理 IntPtr ptr = YoloNative.Detect(_engine, frame.Data, frame.Width, frame.Height, out IntPtr results, out int count); // 2. 解析结果到 List<Detection> var detections = ParseDetections(results, count); YoloNative.FreeResults(results); // 3. ByteTrack 更新 var tracked = tracker.Update(detections); // 4. 回 UI OnFrameProcessed?.Invoke(this, new FrameEventArgs(frame, tracked)); } }OnFrameProcessed是一个EventHandler委托,WinForm 端用BeginInvoke把绘制逻辑切回主线程。注意不要在整个循环里频繁创建 Mat 和 byte 数组,frameBuffer复用固定的 640×640×3 缓冲能显著减少 GC 压力。检测结果坐标从模型空间逆映射回原图之后,ByteTrack 的预测框和绘制框都在原图坐标系里工作,画框时直接画目标中心点和 ID 文本即可。
如果摄像头走的是 DirectShow UVC 回调,回调线程里不要做推理,把原始帧塞进BlockingCollection<Mat>,后台线程消费这个队列。拉流线程和推理线程之间用队列解耦,才能让检测 FPS 不跟着采集帧率上下抖动。
5. 避坑排查:DLL 加载失败、显存泄漏与追踪 ID 抖动的处理
5.1 DllNotFoundException / BadImageFormatException:平台与依赖链排查
现象:程序启动时报“无法加载 DLL‘YoloTensorRT.dll’:找不到指定的模块”,或者直接抛 BadImageFormatException。
原因:分三种情况。第一种是项目平台目标没有设为 x64;第二种是封装 DLL 找到了,但它依赖的nvinfer.dll或cudart64_12.dll不在运行目录;第三种是目标机器没装 VC++ Redistributable,系统返回的错误信息也是“找不到指定的模块”。
解决:先把 Visual Studio 的解决方案平台全部改为 x64,关闭“首选 32 位”;再用 Dependencies 工具打开 YoloTensorRT.dll,看它缺哪些依赖,缺什么就从 TensorRT 安装目录或 CUDA 安装目录补什么;最后确认所有 DLL 都在 exe 所在目录。一个土办法是任务管理器里查看进程已加载的模块列表,TensorRT 相关模块如果没有出现在列表里,就说明还没加载到。
C# 调用 C++ 封装 DLL 出现 Access Violation(c0000005)时,不要慌,先检查结构体定义。C++ 端返回的检测结果如果用struct Detection { float x, y, w, h, score; int classId; },C# 侧必须按float x, y, w, h, score; int classId顺序布局,并且用[StructLayout(LayoutKind.Sequential)]标注。字段顺序只要错一个,后面的数据全部错位,直接访问非法内存地址。
5.2 GPU 显存只增不减:引擎生命周期与 Mat 缓冲复用
现象:程序刚启动时显存占用 800MB,跑十分钟后涨到 2GB,最后报CUDA error: out of memory。
原因:多数情况下不是推理引擎泄漏,而是 C# 侧每帧新建 Mat、byte 数组,托管对象虽然会被 GC 回收,但非托管显存不会随 GC 立即释放。另一个高频原因是推理封装 DLL 内部每一帧都重新分配 CUDA buffer,没有在引擎创建时就预分配。
解决:推理引擎全局只创建一次,窗口关闭时调用DestroyEngine释放。每帧的输入 buffer 用ArrayPool<byte>.Shared租用,用完归还。如果是自己的封装 DLL,建议在CreateEngine阶段把输入输出 CUDA buffer 全部固定,Detect函数只做 memcpy 和 kernel 启动,不要在推理路径里cudaMalloc。运行中用nvidia-smi盯显存曲线,稳定跑半小时不涨,才算解决泄漏。
5.3 追踪 ID 频繁跳变:阈值、min_hits 与多线程竞态
现象:画面里明明只有两个人,追踪 ID 却从 1 跳到 20,而且 A 的 ID 会突然变成 B 的 ID。
原因:两种情况最常见。检测阈值设太低,大量低置信度误检框进入 ByteTrack 成为新轨迹,MinHits=3虽然能过滤一部分,但误检连续几帧出现时照样激活新 ID;另外如果检测结果解析线程和追踪器 Update 线程是同一个还好,一旦把检测和追踪拆到不同线程又没有加锁,追踪状态可能在匹配过程中被改写,产生半初始化轨迹。
解决:检测置信度从 0.25 提到 0.45 以上,ByteTrack 的HighThreshold和检测置信度对齐,LowThreshold保持在 0.1 左右;MaxAge从默认 30 帧适当调低到 20,让消失的目标尽快退出轨迹集合;多线程场景把检测结果先拷贝成快照再喂给追踪器,追踪器内部加上锁。还有一个容易被忽略的场景:固定摄像头下静止目标本来就会因为轻微抖动产生 ID 跳变,这不是 bug,是 IoU 匹配的边界,后续可以用目标中心点位移阈值做平滑。
5.4 FPS 卡在个位数:瓶颈在解码、前处理还是推理
现象:TensorRT 推理明明只要 5ms,整体却只有 8 FPS。
原因:管线是串行的,读帧、解码、颜色转换、Letterbox、推理、绘制全部挤在一个线程,任何一段慢都会拖低整体 FPS。1080p 视频用 OpenCV 的VideoCapture.Read本身就要十几毫秒,再来一次完整尺寸转 640×640 的 Mat 拷贝,又吃掉一截。GTX 1660 Ti 这类卡跑 YOLOv8s TensorRT FP16 推理延迟通常 5~12ms,可前提是输入已经是 640×640,而没有人提过这一步 CPU 拷贝有多贵。
解决:把拉流队列化,用独立线程抓帧;前处理在推理线程做,但不每次new Mat,而是复用一块 640×640 的 buffer;绘制只画框和 ID,不在 UI 线程做任何 resize 和颜色转换。真CPU 卡在解码时,直接用硬件解码或降低采集分辨率,比优化算法有效得多。
5.5 TensorRT 10.x 在老显卡(GTX1070)上的兼容性处理
现象:用 TensorRT 10.x 生成的 engine 在 GTX 1070 上能转换成功,但推理速度反而不如预期,或者直接报driver version is insufficient。
原因:GTX 1070 是 Pascal 架构,FP16 算力比 20 系之后的图灵、安培卡弱一大截,FP16 engine 的加速收益有限。TensorRT 10.x 默认绑定 CUDA 12 runtime,如果机器显卡驱动停留在 2019 年左右的版本,驱动里的 CUDA 兼容层不够新,加载就会失败。
解决:要么把驱动更新到支持 CUDA 12 的版本,要么退回 TensorRT 8.6 + CUDA 11.8 的组合,这两个版本搭配老卡最稳。更换 TensorRT 版本时,engine 必须重新生成,C# 工程里的 DLL 也要整套替换,不能混搭 TensorRT 8 的 nvinfer.dll 和 TensorRT 10 的插件 DLL。这个版本匹配问题看起来像玄学,本质是 engine 文件和运行库版本强绑定,生产环境一定要把 TensorRT 版本写进部署文档。
6. 把演示源码改成你的追踪模块:基准测试、参数表与回归验证
6.1 用 nvidia-smi 和代码计时做性能基准
拿到演示源码跑通后,别急着加功能,先建立一套性能基线。命令行窗口开一个nvidia-smi -l 2盯显存和 GPU 利用率,代码里用Stopwatch分别统计读帧耗时、推理耗时、追踪耗时、绘制耗时,每一段单独记录。建议用控制台输出而不是界面显示,因为 WinForm 的绘制本身就会干扰帧率数据。
6.2 按场景调整检测与追踪参数
| 参数 | 推荐值 | 调整方向 |
|---|---|---|
| 检测置信度 | 0.45 | 漏检多就降低,ID 跳变多就提高 |
| NMS IoU | 0.45 | 重叠目标多可降到 0.4 |
| ByteTrack HighThreshold | 0.5 | 和检测置信度保持接近 |
| ByteTrack LowThreshold | 0.1 | 遮挡严重场景可升到 0.2 |
| MaxAge | 20~30 | 目标频繁消失可调大,ID 恢复更快 |
| MinHits | 3 | 误检多就调到 4~5 |
6.3 回归验证:用固定视频片段对比 ID 切换次数
我自己的习惯是准备一段 30 秒固定视频,跑完自动统计 ID 切换次数和漏检帧数。任何参数调整都要重跑这段视频对比,不能让现场上线后才发现这个参数改了另一个场景崩了。追踪效果评估别看“感觉变好了”,要看 ID 切换次数是否下降、目标重入画面后 ID 是否保持。
如果你刚接手这类工程,第一步就是从这段视频回归开始上手。先让演示源码在自己机器上跑通,再换自己的模型和视频源,最后才调参数。希望这个方案能帮到你。
本文还有配套的精品资源,点击获取