1. 项目概述:一套完整的目标检测与追踪工作流
先说结论:这套基于YOLOv8的工作流,解决的是从零开始训练自己的目标检测模型,并在视频流中实现目标追踪的完整链路问题。如果你手上有一批图片数据,想训练出一个能识别特定目标的模型,还希望模型能在连续视频帧里稳定跟踪目标而不是一帧一帧地“丢目标”,那么这个笔记正好覆盖了这三件事:labelme标注、ultralytics训练、ByteTrack/BoT-SORT追踪。
我最初接触YOLOv8是在GTX 1660 Ti这张6GB显存的显卡上跑的。说实话,这张卡放在今天已经不算主流,但跑YOLOv8n和YOLOv8s的推理完全够用,甚至训练小模型也没太大压力。很多朋友一听到“深度学习模型训练”就觉得非要RTX 4090不可,实际上对于单目标或者少量类别的检测任务,YOLOv8n的参数量只有3.2M,在6GB显存上训练COCO级别的数据集也不是什么大问题。这个点后面会专门展开讲。
这个项目适合谁来参考?我觉得有三类人最合适:一是正在做毕业设计,需要训练自定义检测模型的大学生;二是刚进入算法岗、需要在业务场景里快速验证检测效果的工程师;三是在嵌入式设备(比如RK3588)上部署模型,需要先在上位机把模型训练好的硬件开发者。前两类人最需要的是“跑通流程”,第三类人最需要的是“模型导出和压缩”。无论哪一类,这篇笔记都会尽量把每一环节的“为什么”讲清楚,而不只是甩给你一条命令让你照抄。
2. 环境搭建:真正踩过坑才知道的版本匹配问题
2.1 显卡驱动、CUDA、PyTorch三者到底怎么匹配
先说一个最容易被忽略的问题:很多人装完ultralytics之后,跑model.pt时发现GPU利用率是0%,或者直接报错说CUDA不可用。绝大多数情况不是代码写错了,而是PyTorch装的CPU版本,或者PyTorch与显卡驱动的CUDA版本不对应。
# 查看显卡驱动支持的CUDA版本 nvidia-sminvidia-smi输出右上角的CUDA Version是驱动程序支持的最高CUDA版本,并不代表你机器里装了对应版本的CUDA Toolkit。PyTorch是自带CUDA运行时的,所以只需要满足一个条件:PyTorch内置的CUDA版本 ≤ 显卡驱动的CUDA版本。
以GTX 1660 Ti为例,我当时的驱动是545.xx,显示CUDA Version: 12.3,那么装PyTorch 2.1.0(对应CUDA 11.8或12.1)都没问题。如果驱动版本比较老,比如只有CUDA 11.4,那就得装PyTorch 1.12或更早的版本,否则PyTorch会直接报错。
# 创建一个干净的虚拟环境,避免把base环境搞坏 conda create -n yolov8 python=3.9 -y conda activate yolov8 # 安装PyTorch,我的建议是直接用国内镜像站的命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里有个小技巧:直接用默认PyPI源装PyTorch,下载速度会因为包体积太大(约2GB)而让人崩溃。建议先配好清华或阿里云的pip源,再执行安装命令。
2.2 ultralytics安装:不是装完就完事了
安装ultralytics其实特别简单,一条pip命令就够了:
pip install ultralytics但问题往往出现在依赖项冲突上。ultralytics会自动依赖opencv-python、matplotlib、pillow、pyyaml等库,如果之前的项目里已经装了老版本的opencv,可能会出现读图异常或者显示异常的诡异错误。我的经验是:在干净的虚拟环境里装ultralytics,永远不要在原来跑过其他深度学习项目的环境里直接升级。
验证环境是否正常,有两种方式。第一种是纯命令验证:
import torch print(torch.cuda.is_available()) # 输出 True 说明GPU可用 print(torch.cuda.get_device_name(0)) # 输出你的显卡型号 from ultralytics import YOLO model = YOLO("yolov8n.pt") print(model)第二种是跑一个最小的推理脚本,确认整个流程顺畅:
yolo predict model=yolov8n.pt source="https://ultralytics.com/images/bus.jpg"如果能正常运行,说明环境没有问题。注意第一次运行ultralytics会自动下载yolov8n.pt权重文件,如果下载比较慢或者失败,可以手动下载后放到当前目录。
2.3 labelme安装:它不依赖GPU,但依赖Python版本
labelme是一个经典的图像标注工具,纯Python写的,安装很简单:
pip install labelme不过新版labelme对Python版本有要求。Python 3.10以下装labelme 5.x没问题,Python 3.10及以上建议直接装最新版,避免一些Qt相关的兼容性问题。如果装完启动后界面空白或者打不开,大概率是PyQt5没装好:
pip install pyqt5启动方式也很简单,在终端里敲labelme就会弹出一个图形界面。
3. Labelme标注:这一步的质量直接决定模型上限
3.1 标注前的数据准备和标注规范
拿到原始图片之后,不要急着打开labelme就开始框,先做三件事:
- 把图片统一命名,建议用数字编号(如
0001.jpg),避免中文名和特殊字符,这在后续生成索引文件(txt文件)时省去很多麻烦。 - 对所有图片做一次快速检查,删除严重模糊、完全不相关或者无法判断目标的图片。
- 确认标注类别名称,用英文且不要有空格。比如要检测“黄色安全帽”,类别名就叫
yellow_helmet,不要在类名里写空格,否则后续解析标签文件时类名会被切碎。
打开labelme后,界面比较简单,核心操作就是:打开图片目录、点击Create Polygons(创建多边形)、沿着目标边缘描点、输入类别名、保存。保存后每张图片对应生成一个同名的JSON文件,里面记录了多边形坐标。
3.2 标注细节决定了训练效果
标注的质量比标注的数量更重要。这个道理很多人都听说过,但具体到操作层面有几个关键细节值得强调:
- 贴合边缘,不要求绝对像素级精度。
YOLO系列训练时会把标注框转换为矩形边框,并重新计算中心点、宽度和高度。所以标注时多边形贴得越紧,最终的矩形框就越准确。但也没必要一个像素一个像素地描,标注凹多边形时有些边缘点可以适当放宽。
- 遮挡目标怎么标?
这是最常见的问题。我的经验是:目标主体被遮挡超过50%就不标,被遮挡小于50%就正常标。另外,如果目标只露出一条边或者被人完全挡住,就不要标了。这类标注会被模型当成“噪声”,直接影响mAP。
- 小目标要有策略。
如果你的任务里有大量小目标(比如远距离的行人、小零件),标注的时候要把这类目标单独重视起来。YOLOv8在训练时会对小目标分配更少的正样本,所以标注小目标时格外要注意框的完整性,不要随手拉一个大概位置就完事。
- 类别不平衡的处理。
假设4个类别中有1个类别的样本数量只有其他类别的1/10,训练出来的模型基本会对这个类别“视而不见”。两个选择:一是收集更多该类别的样本,二是每个类别至少保证500个实例。如果实在收集不到,就用数据增强(旋转、缩放、光照变化)来扩充。
3.3 从JSON到YOLO格式:一站式脚本讲解
Labelme的标注格式是JSON,里面保存的是多边形坐标。而YOLOv8需要的标签格式是每个图片对应一个TXT文件,每一行格式是:class_id x_center y_center width height(归一化坐标)。
每次都要手动转换太痛苦了,我把这个脚本写出来,用的直接是JSON解析加上坐标归一化处理:
import json import os import glob def convert_labelme_json_to_yolo(json_file, output_dir, class_names): """ json_file: labelme生成的单张标注JSON文件路径 output_dir: 输出的TXT标签目录 class_names: 类别名列表,如 ["person", "helmet"] """ with open(json_file, "r", encoding="utf-8") as f: data = json.load(f) img_width = data["imageWidth"] img_height = data["imageHeight"] txt_name = os.path.basename(json_file).replace(".json", ".txt") txt_path = os.path.join(output_dir, txt_name) lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_names: continue # 跳过不在类别列表里的标注 class_id = class_names.index(label) points = shape["points"] # 多边形顶点列表 # 计算多边形的最小外接矩形 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # 转成YOLO需要的归一化格式 x_center = (x_min + x_max) / 2.0 / img_width y_center = (y_min + y_max) / 2.0 / img_height box_width = (x_max - x_min) / img_width box_height = (y_max - y_min) / img_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) # 使用示例 class_names = ["person", "helmet", "car"] # 按你的标注顺序写 json_files = glob.glob("labelme_jsons/*.json") for json_file in json_files: convert_labelme_json_to_yolo(json_file, "yolo_labels", class_names)这个脚本核心思路是把多边形所有顶点的横纵坐标分别取最小值和最大值,得到一个轴对齐的矩形,这就是YOLO训练时的边界框。注意:如果你在labelme里用的是矩形标注工具(Create Rectangle),points数组里只会保存两个对角点,取min/max完全不受影响。
4. 模型训练:从数据集目录到损失曲线的完整拆解
4.1 数据集目录结构:不是你想怎么放就怎么放
YOLOv8要求数据集按以下目录结构组织:
dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ ├── 0100.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ │ └── 0100.txttrain和val的划分比例建议8:2或者9:1。有一个常被忽略的点:train和val的图片是不能有重合的,而且最好是同一个场景下的图片不要全部塞进train里,否则模型在训练集上见过这类场景,val的评估结果会虚高,部署到现场后效果直线下降。
图像和标签必须严格同名(后缀不同),否则训练时会报错或者该图片被自动跳过。
4.2 编写data.yaml配置
在数据集根目录下创建一个data.yaml文件:
path: /home/user/dataset # 数据集根目录的绝对路径 train: images/train # 相对根目录的训练图片路径 val: images/val # 相对根目录的验证图片路径 nc: 3 # 类别数量 names: ["person", "helmet", "car"] # 类别名,与class_names顺序一致有一个特别容易踩的坑:nc填错了,或者names顺序和TXT标签里的class_id对应不上。TXT文件里第一个数字如果是0,那对应就是names[0],以此类推。转换脚本里class_id是通过class_names.index(label)生成的,所以顺序必须和data.yaml里完全一致。
4.3 训练参数说明:哪些值得调,哪些别动
yolo train model=yolov8n.pt data=data.yaml epochs=100 imgsz=640 batch=8 device=0这条命令已经能跑通。但每个参数背后的含义值得说透:
model=yolov8n.pt:使用预训练权重继续训练,这叫“迁移学习”。用COCO上已经学好的特征来初始化模型参数,比自己从零随机初始化收敛快得多,精度也更高。epochs=100:训练轮数。小数据集(几百张)50-100轮足够,大数据集(几千上万张)100-200轮。判断收敛的指标看损失曲线即可。imgsz=640:训练时图片会被缩放到640x640。imgsz越大,小目标检测效果越好,但显存消耗也越大。GTX 1660 Ti这种6GB显存的卡,建议用640。batch=8:批大小。如果显存不够,报错OOM(Out of Memory),优先把batch调成4,再不行就调成2。device=0:使用第一个GPU。CPU训练则改成device=cpu。除非你的CPU非常有实力,否则强烈不推荐CPU训练。
另外两个隐藏参数也很有用:patience=20表示连续20轮验证损失没有下降就提前停止训练;workers=4是数据加载线程数,调大点能缩短训练时间,但Windows系统下建议设为0避免死锁。
训练过程中终端输出的信息包含box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95等指标。mAP50是我们最常看的指标,表示IoU阈值为0.5时的平均精度。
4.4 损失函数曲线怎么看:训练是否健康的四个信号
训练结束后,在runs/train/exp目录下会生成results.png。这张图画了多个曲线的变化趋势。我建议重点看四个:
- box_loss(边界框回归损失):正常情况下应该单调下降或波动下降,如果后期不降反升,说明模型开始过拟合了。
- cls_loss(分类损失):下降速度慢很正常,但总体趋势向下就没问题。
- mAP50(验证集上的平均精度):应该是波动向上爬,曲线上的凸起和凹陷都很正常。
- val box_loss(验证集的box_loss):如果训练集loss下降但验证集loss一直不降,就是过拟合,需要增加数据量或调大正则化参数。
如果你的mAP50在你的验证集上达到了0.8以上,对于绝大多数业务场景来说已经算可用了。
5. 模型推理:别看这个环节简单,其实坑也不少
训练完成后,模型权重保存在runs/train/exp/weights/best.pt。这个文件就是性能最好的权重。推理脚本如下:
from ultralytics import YOLO # 加载模型 model = YOLO("runs/train/exp/weights/best.pt") # 预测单张图片 results = model.predict( source="test_images/0001.jpg", conf=0.25, # 置信度阈值,低于该值的目标会被过滤 iou=0.45, # NMS的IoU阈值,用于抑制重叠框 save=True, # 保存标注后的图片 imgsz=640 ) # 查看检测结果 for r in results: boxes = r.boxes for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别: {model.names[cls_id]}, 置信度: {conf:.2f}, 坐标: {xyxy}")运行后会输出每个检测框的类别、置信度和坐标,并把标注后的图片保存到runs/detect/predict目录下。
5.1 推理参数调优:两个关键参数帮你处理错误检测
conf和iou这两个参数,都要根据场景去调。
conf设得越低,检测出的目标越多,但误检率也越高。实测下来,0.25是一个比较通用的值,如果现场漏检多,把conf降到0.1;如果误检多,就提到0.5。iou控制NMS时两个框的重合程度超过多少就被合并。场景中有大量密集小目标时,建议把iou调高到0.7,避免邻近目标被错误合并。
还有一个容易被忽略的问题:预测时的imgsz最好和训练时保持一致。如果你训练用的640,预测却用1280,模型不会崩,但小目标的检测精度会有变化。以我实测的经验来看,绝大多数场景下保持同尺寸效果最稳定。
5.2 视频与摄像头推理:实时检测时如何提速
如果需要对视频文件或者摄像头实时流做检测:
from ultralytics import YOLO model = YOLO("best.pt") # 视频文件推理 model.predict(source="test_video.mp4", save=True) # 摄像头实时检测 model.predict(source="0", show=True) # 0 表示第一个摄像头实时场景中最头疼的是帧率不够。如果你的视频帧率只有个位数,优先尝试以下优化:
- 把模型从yolov8s换成yolov8n,速度能提升30%-50%左右。
- 降低输入尺寸:
imgsz=416或者imgsz=320。每降低一档尺寸,推理速度都会明显加快。 - 开启半精度推理:
model.predict(..., half=True),有TensorRT基础的话可以继续用TensorRT导出加速。
6. 目标追踪:从单帧检测到连续跟踪的关键一步
6.1 为什么单帧检测不够,需要追踪
单帧检测有一个致命弱点:它没有“记忆”。视频第10帧检测到目标,第11帧如果目标被遮挡或者模糊,检测就断了。目标追踪就是为了解决这个问题。
YOLOv8的追踪基于两个主流的追踪器:ByteTrack和BoT-SORT。ByteTrack的核心思路是:利用检测框和预测框的IoU匹配来关联目标,并且把低置信度的检测框也纳入匹配过程(这就是Byte这个名字的由来),能有效应对目标短时间遮挡后的重识别问题。BoT-SORT则在此基础上引入了ReID(重识别)特征,对长时遮挡的记忆能力更强,但计算量也更高。
6.2 用ultralytics接口实现目标追踪
使用ultralytics的追踪非常简洁,直接调用model.track()即可。下面是一个完整的视频追踪脚本:
from ultralytics import YOLO import cv2 model = YOLO("best.pt") video_path = "test_video.mp4" cap = cv2.VideoCapture(video_path) # 获取视频信息 fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 输出视频 fourcc = cv2.VideoWriter_fourcc(*"mp4v") out = cv2.VideoWriter("track_result.mp4", fourcc, fps, (width, height)) while cap.isOpened(): ret, frame = cap.read() if not ret: break # 追踪:persist=True 表示持续追踪,tracker=bytetrack.yaml 选择追踪器 results = model.track(frame, persist=True, tracker="bytetrack.yaml", conf=0.25, iou=0.45) if results[0].boxes is not None and results[0].boxes.id is not None: # 每个目标都有唯一的ID,boxes.id就是追踪ID boxes = results[0].boxes.xyxy.cpu().numpy() track_ids = results[0].boxes.id.cpu().numpy().astype(int) class_ids = results[0].boxes.cls.cpu().numpy().astype(int) for box, track_id, cls_id in zip(boxes, track_ids, class_ids): x1, y1, x2, y2 = map(int, box) label = f"ID:{track_id} {model.names[cls_id]}" cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) out.write(frame) cv2.imshow("Tracking", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() out.release() cv2.destroyAllWindows()这里有几个容易踩的坑,值得单独说明:
persist=True不能丢。不加这个参数时,追踪器不会在帧间保持状态,每帧都是重置状态,追踪等于白做。boxes.id可能为None。当画面里没有检测到目标时,boxes是空的,直接访问id会报错,所以必须有判断。- 颜色分配。代码里所有目标都画成绿色是为了简单,实际项目中建议用不同的颜色区分不同ID,特别是多个目标同时出现时,绿色框很容易混淆。
6.3 追踪器选择:ByteTrack还是BoT-SORT
| 追踪器 | 速度 | 长时间遮挡鲁棒性 | 适用场景 |
|---|---|---|---|
| ByteTrack | 快 | 较弱 | 目标密度高、遮挡不严重的实时场景 |
| BoT-SORT | 慢 | 更强 | 需要长时间跟踪、场景中遮挡较多的场景 |
如果你是刚接触追踪,先用ByteTrack就好。等发现目标ID频繁跳变(同一个目标被反复分配新ID)时,再换BoT-SORT试试,一般能让ID切换次数减少不少。
6.4 计数、特定区域检测的常见做法
追踪最常用的扩展功能之一是计数。比如统计画面中进入某个区域的目标数量,基本思路是:判断目标追踪框中心点是否经过设定的线段(越线计数),把通过的目标ID记录下来,同一个ID不要重复计数:
# 简单越线计数示例 line_y = height // 2 # 水平线的y坐标 crossed_ids = set() count = 0 # 在追踪循环内部: for box in boxes: cy = (box[1] + box[3]) / 2 if int(track_id) not in crossed_ids and cy > line_y: crossed_ids.add(int(track_id)) count += 1这类功能在很多项目中都是刚需,比如人员进出统计、车流统计、施工区域闯入检测等。
7. 模型部署到边缘设备:从best.pt到RK3588的完整路线
7.1 模型导出:ONNX、TensorRT的不同选择
很多项目最终都要部署到嵌入式设备上,比如RK3588。这时候best.pt这个PyTorch权重文件就不好用了,需要转换成边缘设备支持的格式。
转换的顺序是:.pt -> ONNX -> RKNN(瑞芯微平台的格式)。ultralytics直接支持导出ONNX:
yolo export model=best.pt format=onnx imgsz=640 dynamic=True导出ONNX时值得注意几点:
dynamic=True允许输入尺寸不固定,适合实际部署时输入分辨率可能变化的场景。- 导出后建议先用
onnxruntime验证一下ONNX模型的推理结果和PyTorch模型的结果是否接近,差异应该在小数点后两三位以内。 - 如果后续要利用TensorRT加速,需要把模型导出的精度设为FP16,体积小一半,推理速度在NVIDIA设备上快很多。
7.2 模型压缩:剪枝、蒸馏和量化
部署到资源受限设备,除了换更小的模型外,还有几个方案:
- 模型剪枝:把权重中接近0的连接去掉,模型体积变小。在YOLOv8上做剪枝通常需要借助第三方库(如
torch-pruning),有一定上手门槛。 - 知识蒸馏:用小模型(学生)去学习大模型(教师)的输出,学生模型能在更小的参数量下逼近教师的精度。一个简单的蒸馏想法是:训练时同时计算学生模型预测和教师模型预测之间的损失,叠加到正常损失上。
- 量化:把FP32的权重变成INT8,模型体积缩小为原来的1/4。同样的模型在量化后推理速度通常能提升2-3倍,但mAP会有1-3个百分点的轻微下降,属于正常现象。
7.3 实测效果:GTX 1660 Ti上的表现
我自己在GTX 1660 Ti上实测了几组数据,用的是YOLOv8n:
| 输入尺寸 | 推理耗时(ms) | FPS | 显存占用(GB) |
|---|---|---|---|
| 640 | 约15-20 | 50-60 | 约1.2 |
| 416 | 约10-12 | 80-90 | 约0.9 |
| 320 | 约7-9 | 110-130 | 约0.7 |
如果要做实时摄像头检测,6GB显存完全够用,还能同时跑一个PP-OCR之类的小模型做文字识别。
8. 常见问题与排查技巧实录
8.1 问题速查表
| 症状 | 可能就是这3个原因 | 解决办法 |
|---|---|---|
| CUDA不可用 / GPU利用率0% | 1. PyTorch是CPU版 2. 驱动版本太老 3. 环境混用了 | 先torch.cuda.is_available()检查,再卸载重装GPU版PyTorch |
| 训练时OOM | 1. batch太大 2. imgsz太大 3. 显存被其他进程占用 | batch调小、imgsz调小、关闭无关进程 |
| 训练结束了但mAP特别低 | 1. 标注质量差 2. 数据集太小 3. 学习率不合适 | 抽查标注质量,扩充数据,调整lr |
| 追踪ID频繁切换 | 1. 追踪器不适合场景 2. 目标遮挡严重 3. 检测质量差 | 换BoT-SORT,提高conf阈值 |
| 部署在RK3588上速度慢 | 1. 还在跑FP32 2. 未经过RKNN优化 | 导出FP16或INT8,用RKNN Toolkit转换 |
8.2 标签错位排查方法
训练时如果发现loss无法下降,第一件事不是调参,而是检查图片和标签是否对齐。快速验证方式:
import cv2 import numpy as np def visualize_label(image_path, label_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() cls_id = int(parts[0]) x_center = float(parts[1]) * w y_center = float(parts[2]) * h box_w = float(parts[3]) * w box_h = float(parts[4]) * h x1 = int(x_center - box_w / 2) y1 = int(y_center - box_h / 2) x2 = int(x_center + box_w / 2) y2 = int(y_center + box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("Check", img) cv2.waitKey(0)随便抽5张训练图和5张验证图,把标注框可视化出来,肉眼扫一遍,能筛出90%以上因标签错位导致的训练问题。
8.3 训练速度慢的排查
除了显卡性能,还有一个容易忽视的瓶颈是数据加载。如果数据集放在机械硬盘上,训练时读取速度可能跟不上。解决方法是先把数据集拷到SSD上,同时把workers参数调大。还有一个技巧:如果图片很大,可以先统一缩小到训练尺寸再存盘,能大幅缩短数据读取时间。
9. 写在最后的一点体会
从labelme标注到ultralytics训练,再到ByteTrack追踪以及RK3588部署,这条完整链路其实并没有想象中那么遥不可及。你在跑通流程的过程中会发现,最耗时间的往往不是训练本身,而是数据标注的规范和排错的经验。希望这份笔记能帮你绕开我刚入坑时踩过的几个大坑,尤其是环境匹配和标签格式这两块,真的是“一看就会,一跑就废”的经典环节。
最后分享一个实际项目中特别实用的小技巧:训练完成后不要只盯着mAP,最好把模型放到实际业务场景里的视频上进行推理测试,观察错检和漏检的case,再决定是否要补数据或者调conf阈值。很多在验证集上看起来效果很好的模型,一到真实场景就会暴露出各种问题。数据、训练、追踪、部署这四步之间,一定要反复来回迭代,模型才能真正在你自己的场景里站稳脚跟。