☰
玩手机检测YOLOv8实战:593张图训练全流程与避坑指南
2026/10/5 3:49:38 网站建设 项目流程

简介:这是一份面向目标检测入门与实战的YOLO系列算法数据集,聚焦“手机、玩手机、打电话”三类目标识别场景,适用于yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流版本,可直接用于模型训练、验证与测试。数据集已按常规比例划分,并提供yolo格式与voc格式两套标签,分别保存在不同文件夹中。yolo标签采用“类别索引、归一化中心坐标、宽高比例”的txt文件记录,坐标值均位于0到1之间,使用起来直观便捷;voc格式的xml文件则便于在标注工具或传统流程中查看与转换。压缩包共1780个文件,包含593张jpg图像、593个txt标签、593个xml标签及1个yaml配置文件,整体大小约20.46MB,轻量易下载,适合快速跑通算法流程或进行多版本对比实验。目前已有191人学习下载,可用于课堂案例、毕业设计或工程预研,省去自行采集、标注与整理数据的环节,拿到后即可开始训练。

1. 玩手机/打电话检测数据集:为什么 593 张图够启动一个 YOLO 项目

做行为检测的人拿到这个数据包时,最先注意到的是它把「yolo 算法」「手机 / 玩手机 / 打电话」「593 张图像带标签」三个关键词放在了一起。也就是说,解压之后你得到的不是一堆零散的照片,而是一套已经标好框、可以直接进入训练流程的 YOLO 格式数据集,目标只有一类事:把“人手里拿着手机”和“人把手机贴在耳边”这两种动作从画面里揪出来。这类需求在实际项目里非常常见:课堂里学生低头刷手机、仓库里员工边走边看屏幕、驾驶室司机单手打电话,都是要靠检测模型去盯的场景。用公开的 COCO 预训练权重直接跑,模型认的是“手机”这个物体,不是“玩手机”这个行为,所以你需要一份标注粒度正好卡在动作上的数据,这份 593 张的压缩包干的就是这个活。

很多人会怀疑 593 张图是不是太少。说实话,如果目标是做一个要扛住千万级流量的通用检测服务,这点数据连热身都不够;但如果你的目标是跑通一套行为检测原型、验证算法选型、或者给领导做一个能演示的 Demo,593 张带标签图像配合 YOLOv8 的迁移学习,已经足够启动项目。刚入门 YOLO 的新手可以把它当练习集,把数据配置、训练、验证、导出整条链路跑一遍;有经验的工程师则可以用它快速确认“玩手机/打电话”这个检测任务在自己业务场景下的可行性。后面所有章节都围绕这 593 张图展开,从解压后的目录结构讲起,一路讲到参数怎么设、坑在哪里。

2. 解压后的第一步:先从目录结构读懂数据集的标注约定

2.1 images 与 labels:YOLO 格式的坐标到底怎么存

拿到压缩包解压后,常见做法是先看顶层目录,多数这类数据集的目录结构会分成 images 和 labels 两个目录,图像放一边,标注放另一边。YOLO 格式的标注不是用一个 JSON 或 XML 文件装下所有框,而是一张图像对应一个同名的 .txt 文件,比如 IMG_0001.jpg 旁边一定有个 IMG_0001.txt。打开这个 txt,里面每行代表一个目标,格式固定为五列:class_id、x_center、y_center、width、height,其中后四列都是相对图像宽高的归一化数值,范围在 0 到 1 之间。

这里有个容易忽略的点:YOLO 的坐标是“中心点 + 宽高”的归一化表达,不是左上角和右下角的像素坐标。比如图像宽 1920、高 1080,一个手机框的中心点落在 (960, 540),宽 200、高 400,那 txt 里存的就是0 0.5 0.5 0.1042 0.3704。这个设计是为了跟输入尺寸解耦——训练时无论把图缩放到 640 还是 1280,标签都不用改。如果你解压后发现标注是 VOC 风格的 XML 文件,也就是每个框存的 xmin、ymin、xmax、ymax,那需要先做个转换,我一般会写一个一次性转换脚本,把 XML 里的绝对坐标换算成 YOLO 的归一化中心坐标。

import os import xml.etree.ElementTree as ET xml_dir = "Annotations" txt_dir = "labels" os.makedirs(txt_dir, exist_ok=True) class_map = {"phone": 0, "play": 1, "call": 2} for xml_file in os.listdir(xml_dir): tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): cls = obj.find("name").text if cls not in class_map: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_map[cls]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(txt_dir, xml_file.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines))

这段脚本做了三件事:解析 XML 里的图像宽高、读取每个目标的类别和 bndbox 坐标、最后把绝对坐标转成归一化中心坐标写入 txt。转换时最常翻车的是漏了归一化,直接把像素值写进 txt,训练时 loss 会异常大甚至直接 NaN。另一个翻车点是 class_map 的对应关系,XML 里的类别名和你 data.yaml 里的类别序号必须严格一致,顺序错了模型不会报错,只是把“玩手机”学成了“打电话”。跑完脚本后建议随机抽几张图,用后面 2.3 的可视化脚本画框确认,而不是直接开训。

2.2 用脚本核对 593 张图的标签分布与类别平衡

拿到 txt 标签后,我习惯先做一次全量统计:每张图有几个目标、每个类别出现多少次、哪些图像是空标签。593 张图的量级不大,但手工一个个看依然不现实,写个小脚本最靠谱。这个脚本要回答三个问题:类别 id 的取值是不是从 0 开始连续排列、各类别的样本数量差距是否悬殊、有没有图像完全没有标签。

import os from collections import Counter, defaultdict label_dir = "labels" class_counter = Counter() per_image_counter = defaultdict(int) for f in os.listdir(label_dir): if not f.endswith(".txt"): continue path = os.path.join(label_dir, f) num_objects = 0 with open(path) as fp: for line in fp: line = line.strip() if not line: continue parts = line.split() cls_id = int(parts[0]) class_counter[cls_id] += 1 num_objects += 1 per_image_counter[num_objects] += 1 print("每个类别的目标数量:", dict(class_counter)) print("每张图像的目标数分布:", dict(sorted(per_image_counter.items())))

这段代码的逻辑很简单,但输出信息量不小。class_counter 告诉你每个类别 id 总共有多少个框,如果发现只出现了 0 和 2 而没有 1,说明类别 id 有跳号,data.yaml 里如果硬写 0、1、2 三个类,训练时类别映射就对不上。per_image_counter 统计每张图里目标数量的分布,如果大量图像只有 1 个框,而少量图像有 5 个以上框,说明这个数据集以单人单动作场景为主,训练出来的模型在多人场景下可能漏检。593 张图像、每类几百个框的规模,做行为检测只能算“看得见”,还谈不上“看得稳”,后续数据增强必不可少。

2.3 画框验证:标签和图像真的对得上吗

统计标签分布只是第一步,标签坐标和图像内容是否真的匹配,必须通过可视化确认。这步不能省,我见过太多次因为标签坐标的宽高顺序写反、或者归一化时除以了错误的图像尺寸,导致训练时 mAP 看起来还行、实际推理时框全部偏移的案例。最简单的做法是用 OpenCV 把 txt 里的坐标画回原图。

import cv2 import os import random img_dir = "images" label_dir = "labels" output_dir = "check_vis" os.makedirs(output_dir, exist_ok=True) imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] random.seed(42) sample = random.sample(imgs, 20) class_names = ["phone", "play", "call"] colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0)] for img_name in sample: img_path = os.path.join(img_dir, img_name) label_path = os.path.join(label_dir, img_name.replace(".jpg", ".txt")) img = cv2.imread(img_path) h, w = img.shape[:2] if not os.path.exists(label_path): continue with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls_id], 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) cv2.imwrite(os.path.join(output_dir, img_name), img)

这段脚本随机抽 20 张图,把标签框和类别名画上去。需要注意坐标还原时是反归一化:中心点坐标乘图像宽、宽高乘图像宽高,换算回像素后再算左上角和右下角。跑完后打开 check_vis 目录逐张翻一遍,重点看两类问题:一是框是不是准确包住了手机和手,有没有只框一半或者把脸框进去;二是类别是否合理,同一个动作是不是被标成了不同类别。画框验证看起来是笨办法,但对小数据集来说,这一步比任何自动化评估都更能发现问题,593 张图多翻几遍也就一顿饭的功夫。

3. 用 YOLOv8 训练玩手机检测:从 data.yaml 到跑通最小训练

3.1 写 data.yaml:路径、类别名与 class 序号必须一致

数据集整理完后,接下来要写一个 data.yaml 把数据喂给 YOLOv8。这份 yaml 的内容包括数据集的根目录、训练集和验证集路径、以及类别名列表,格式如下:

path: /home/user/phone_dataset train: images/train val: images/val names: 0: phone 1: play 2: call

这里的 path 是数据集的绝对路径,train 和 val 相对于 path 来写。names 的列表顺序就是类别 id 的映射关系,names 里第 0 项对应标注里 class_id 为 0 的框,第 1 项对应 class_id 为 1,依此类推。很多人喜欢把 names 写成字典形式,比如names: {0: phone, 1: play},这种写法也可以,但排序必须和标签里的 id 一致。一个常见的低级错误是先把玩手机定义为类别 0,后来在 yaml 里又把 phone 放在前面,模型本身没有任何校验机制,只会闷头按顺序学,结果就是推理时把“玩手机”的框标成了“手机”。

路径问题也是重灾区。建议 path 用绝对路径,不要用相对路径。YOLOv8 解析 train 和 val 时是拼在 path 后面的,如果你把 path 写成phone_dataset,而当前工作目录不在数据集上一级,训练会直接报数据集不存在。还有一个容易忽略的细节:yaml 文件本身的编码和缩进,不要用中文写注释之外的字符串,更不要在路径里混入中文或空格,Ultralytics 底层虽然是 Python,但 OpenCV 读取图像对中文路径支持有问题,训练中途报Unable to read image时优先查路径。

3.2 训练命令与关键参数:imgsz / epochs / batch 怎么定

写好了 data.yaml,就可以跑训练了。 train 命令是这套流程的核心,用的训练脚本是自带的yolo detect train子命令,因为它可以把配置、训练、评估一条龙串起来。

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0

这里每个参数都有讲究。model 我一般推荐先用yolov8n.pt,也就是 nano 版本,它是八个模型里参数量最小的一个,官方权重已经用 COCO 预训练过,对于 593 张图的小数据集,用 nano 起步收敛最快,显存占用也最低。epochs 设 100,配合早停机制,大多数情况下模型会在 50 到 70 个 epoch 内找到最佳权重。imgsz 是输入分辨率,640 是 YOLOv8 的默认值,如果你的验证集里手机框占比特别小,可以尝试提到 960,代价是训练速度下降约一半。batch 大小主要看显存,16 是一个比较保守的默认值,如果显卡只有 8GB 显存,别硬上,直接把 batch 降到 8 或者 4。

训练过程中会在当前目录生成 runs/detect/train 目录,里面有 weights/best.pt、weights/last.pt、以及各种曲线图。best.pt 是按照在验证集上的 mAP 指标自动挑选的最优权重,last.pt 是最后一个 epoch 的权重。对小数据集来说,last.pt 经常已经过拟合,所以后续做推理和导出都用 best.pt。训练时还有一个细节:如果显存不够,不要只调 batch,可以同时把 imgsz 降到 480,效果比单纯降 batch 更明显。跑之前确认一下 device 参数,CPU 机器上把device=0删掉,但 593 张图用 CPU 训练会非常痛苦,建议至少用一块支持 CUDA 的 GPU。

3.3 训练日志怎么看:box_loss、cls_loss 和 dfl_loss 在说什么

YOLOv8 训练时终端每秒刷一行指标:box_loss、cls_loss、dfl_loss,后面对应的是验证集上的各种 mAP。很多新手只看 mAP 涨得快就开心,却忽略了这几个 loss 的含义。box_loss 是回归损失,衡量预测框和真实框的位置偏移,它降不下去说明框不准;cls_loss 是分类损失,衡量类别判断的错误,它高就说明分类混淆严重;dfl_loss 是分布聚焦损失,YOLOv8 用来让框的边更贴合目标,这个数对手机这种小目标格外敏感。三个 loss 在训练前 20 个 epoch 会快速下降,之后开始震荡收窄,如果某个 loss 持续上升,比如 cls_loss 在 30 个 epoch 后反弹,基本可以断定模型开始过拟合。

这里要给一个时间点的血泪经验:593 张图的小数据集,前 20 个 epoch 的 mAP 可能一直在 0.1 到 0.3 之间来回横跳,val loss 也不稳定,这时候不要急着停。YOLOv8 的迁移学习特性决定了它前期要先把预训练权重里的通用特征“迁移”过来,并适应你数据集的类别分布,震荡期是正常的。判断训练是否有效的标准不是单看某几个 epoch,而是看整体趋势:前 30 个 epoch 的 mAP 是否在波动中上行,以及 best.pt 和 last.pt 的差距是否越来越大。如果你的训练在 50 个 epoch 后 mAP50 还在稳定上涨,说明数据量虽然小,但模型还能从中学到东西,可以手动把 epochs 往上加,这也是小数据集上不用过于迷信默认参数的原因。

4. 验证与导出:从 best.pt 到可落地的检测权重

4.1 用 val 集评估:mAP50 和 mAP50-95 在小数据集上的合理区间

训练完成后,第一步不是急着导出,而是先用验证集评估 best.pt 的真实水平。YOLOv8 的 val 子命令会自动读取你 data.yaml 里配置的 val 路径,并在终端打印出一张指标表。

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml \ imgsz=640

输出里最重要的两个指标是 mAP50 和 mAP50-95。mAP50 是 IoU 阈值在 0.5 时的平均精度,mAP50-95 则是在 0.5 到 0.95 之间每间隔 0.05 计算一次再取平均。273 张这种量级、图像总数 593 张的数据集上,mAP50 达到 0.7 以上就可以认为模型可用,mAP50-95 通常比 mAP50 低 0.1 到 0.2。如果你看到 mAP50 还行、mAP50-95 很低,说明模型的框虽然大体位置对,但边界不够贴,这在手机这种可旋转、可部分遮挡的目标上很常见。另外一个值得关注的输出是混淆矩阵,它在 runs/detect/val 目录下,能直观看到 phone、play、call 三类之间互相误判的情况,比单看 mAP 更能定位问题。

小数据集上评估时有个容易误导人的情况:如果你只划分了一次验证集,而且随机种子固定得不合理,验证集里可能某个类别特别多,导致 mAP 虚高。我习惯在训练前用 8:2 划分训练集和验证集,同时确保两个集合里每个类别的比例基本一致。训练后再看 val 结果时,不要只盯着平均指标,把 val 目录下的预测结果图翻一遍,重点看有没有漏检的、有没有一个手机框被标成两个类别的。

4.2 导出 ONNX 与运行推理:从 PyTorch 权重到实际检测

模型验证通过后,接下来是导出和推理。导出这一步是为了让模型脱离 PyTorch 环境运行,或者部署到 TensorRT、OpenVINO 这类推理引擎上。YOLOv8 的 export 子命令一行就能完成导出:

yolo export \ model=runs/detect/train/weights/best.pt \ format=onnx \ imgsz=640

导出后会生成同名 .onnx 文件。注意 export 时指定的 imgsz 必须和训练时一致,否则导出的模型在推理时会先做输入缩放,导致检测框整体偏移。导出完成后用 predict 子命令在真实图片上跑一遍:

yolo predict \ model=runs/detect/train/weights/best.onnx \ source=test_images/ \ conf=0.25 \ iou=0.45

conf 是置信度阈值,默认 0.25,表示低于这个概率的框会被丢掉。玩手机检测场景建议保持 0.25,因为手机在画面里通常比较小,置信度天然偏低,阈值调太高容易漏检。iou 是 NMS 的 IoU 阈值,默认 0.45,如果画面里手机和人脸离得很近、框之间重叠度高,可以试着调到 0.3,允许更多重叠框被保留,再观察结果。

如果需要集成到自己的业务代码里,用 Python API 会更方便。下面这段代码从视频流读取帧并逐帧检测:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="demo.mp4", conf=0.25, iou=0.45, stream=True ) for frame_id, r in enumerate(results): boxes = r.boxes.xyxy.cpu().numpy() classes = r.boxes.cls.cpu().numpy() scores = r.boxes.conf.cpu().numpy() for box, cls, score in zip(boxes, classes, scores): x1, y1, x2, y2 = box.astype(int) print(f"帧{frame_id}: 类别{int(cls)} 置信度{score:.2f} 坐标({x1},{y1})-({x2},{y2})")

这段代码的核心是 stream=True,它让模型按帧处理视频而不是一次性把所有帧加载进内存,对长视频推理非常友好。boxes 里存的是像素坐标系下的真实坐标,直接可用于在原图画框或做业务逻辑;如果要在实时告警系统里用,可以在这里接入逻辑判断,比如连续 N 帧检测到 class=2(打电话)就触发一次告警,这也是做驾驶分心检测项目时最常见的集成方式。

5. 避坑:593 张图训练玩手机检测最常见的 5 个坑

5.1 类别标签错位:class id 从 0 开始还是从 1 开始

现象:训练过程完全正常,loss 正常下降,验证集 mAP 也不低,但推理时所有框的类别都是错的,最典型的是把所有目标都识别成第一个类。原因:标注 txt 里的类别 id 从 1 开始编号,而 data.yaml 里 names 从 0 开始定义,比如标注里 class_id=1 代表“玩手机”,模型却把它当成第 0 类去学。解决:回到第 2.2 节的统计脚本,先确认 txt 文件里类别 id 的取值范围,再看 names 列表和它是否一一对应。一个小技巧是先用 50 张图跑 20 个 epoch 做冒烟测试,推理一张图看类别名对不对,再上全量训练。

5.2 小目标手机检不出:imgsz 和锚框的取舍

现象:图像里手机明明清晰可见,但模型就是不给框,或者把手机框成了人脸的一部分。原因:手机在整张图中的像素占比经常低于 5%,在 640 分辨率下,一个手机可能只有 30×30 像素,特征经过多层下采样后所剩无几。解决:优先把 imgsz 从 640 提升到 960,YOLOv8 对输入尺寸没有强制限制,只是推理耗时会上涨;其次把模型从 nano 换到 small 版,即yolov8s.pt,小目标的特征表达明显更强。还有一种两阶段方案:先用一个模型检测“人”,再把人的区域裁剪放大后输入第二个模型检测手机,这对固定摄像头的场景非常有效,缺点是做不了单张图的实时推理。

5.3 打电话类别样本太少:类别不平衡让 mAP 虚高

现象:验证集 mAP50 有 0.8,但单独看“打电话”类别的召回率不到 0.3,真实场景里打电话几乎全部漏检。原因:593 张图里打电话的样本可能只有几十张,训练时模型把“打电话”当成了难例,直接把它的置信度压低,以换取整体 loss 下降。解决:先做类别数量统计,如果打电话框不足玩手机的 20%,就不要硬训三类,把“play”和“call”合并成“hand_phone”一类,反而能提升实际可用性;如果必须区分,可以做离线过采样,把打电话的图片复制 3 到 5 份再训练,或在 loss 上给类别加权。

5.4 标注框偏离:框住手机还是框住手

现象:有些标注框框的是手机本体,有些框把整只手和手机都包进去,模型学出来的框位置时而偏上时而偏下,后处理阶段很难判断“玩手机”这个动作的覆盖范围。原因:标注人员在标注时对“目标”的定义不一致,这是 593 张图这种小数据集最常见的质量问题,因为它没有多人交叉复核。解决:在画框验证阶段就抽 20 张图看一遍,规定标注规范——我一般约定“手机全身可视时框手机,手机被手遮挡时框手和手机的最小外接矩形”,然后只保留符合这个规范的图片;如果数据集已经训练过,也可以把预测结果和标注框画在一起对比,找出偏离严重的几类图再决定是否重新标注。

5.5 过拟合:train loss 还在降,val 指标不再涨

现象:训练到第 40 个 epoch 后,train loss 持续下降,但 val mAP50 在 0.75 附近震荡不再上升,val loss 反而开始抬头,best.pt 对应的还是第 35 个 epoch 的权重。原因:593 张图的样本量太小,模型的容量超过了数据能承载的信息量,后期开始“背题”而不是“学规律”。解决:这是小数据集上必然出现的现象,不用恐慌。优先确认早停机制是否开启,Ultralytics 默认会连续 50 个 epoch 没改善就自动停;其次是改用更强的数据增强,在第 6 章会展开;最后是降低模型复杂度,从 small 换回 nano,或者从第 50 个 epoch 开始手动减小学习率,给权重更新做减速,让 loss 在极小范围内震荡而不是跳出最优区域。

6. 把 593 张图的价值榨干:数据增强与迁移学习的进阶组合

6.1 迁移学习:不要从头训练一个小数据集

对于 593 张图的数据集,从头训练一个 YOLO 模型几乎注定失败,因为你没有足够的样本让 backbone 学会“边缘”“纹理”“形状”这些基础特征。常规做法是使用官方在 COCO 上预训练好的权重,比如 yolov8n.pt,它会保留对通用目标的特征提取能力,训练时只更新检测头和部分骨干参数。这个过程相当于让模型在“已经会看图”的基础上,学会把“手机”和“打电话”这两个特定概念对上号。如果你发现训练时 loss 下降极慢,先检查是不是把 pretrained 参数写成了 False,或者误用了随机初始化权重。

6.2 针对性离线增强:比调参更能救小数据的方案

训练命令里的随机增强对 593 张图来说力度不够,我一般会做一层离线增强,把数据量扩充到 2000 张以上再进训练。下面这段代码用 albumentations 库对图片和对应的 YOLO 标签同时做变换:

import albumentations as A import cv2 import os import glob transform = A.Compose([ A.RandomBrightnessContrast(p=0.5), A.HorizontalFlip(p=0.5), A.RandomSizedBBoxSafeCrop(width=640, height=640, p=0.3), ]) image_paths = glob.glob("images/*.jpg") for i, img_path in enumerate(image_paths): img = cv2.imread(img_path) h, w = img.shape[:2] label_path = img_path.replace("images", "labels").replace(".jpg", ".txt") boxes = [] class_ids = [] with open(label_path) as f: for line in f: parts = line.strip().split() class_ids.append(int(parts[0])) cx, cy, bw, bh = map(float, parts[1:]) boxes.append([cx * w, cy * h, bw * w, bh * h]) transformed = transform( image=img, bboxes=boxes, class_labels=class_ids, format="coco" ) out_img = transformed["image"] out_boxes = transformed["bboxes"] out_labels = transformed["class_labels"] cv2.imwrite(f"aug_images/aug_{i}.jpg", out_img) with open(f"aug_labels/aug_{i}.txt", "w") as f: for box, cls in zip(out_boxes, out_labels): x, y, bw, bh = box cx = (x + bw / 2) / out_img.shape[1] cy = (y + bh / 2) / out_img.shape[0] f.write(f"{cls} {cx:.6f} {cy:.6f} {bw / out_img.shape[1]:.6f} {bh / out_img.shape[0]:.6f}\n")

这段代码的核心是 albumentations 对 bbox 的同步变换:RandomBrightnessContrast 只改图像不改框,HorizontalFlip 会同时翻转框的左右位置,RandomSizedBBoxSafeCrop 会在裁剪时保证框不被切掉,并把框坐标一同缩放。需要注意的是输入给 transform 的 boxes 是像素坐标且 format 标成 "coco",也就是 [x_min, y_min, width, height],写回文件时再转成 YOLO 的归一化中心点格式。三个变换里作用最大的是 RandomSizedBBoxSafeCrop,它会随机缩放目标在画面中的占比,等于模拟不同拍摄距离;但它只对训练集有效,验证集和测试集不能做同样的裁剪增强,否则评估指标会失真。

6.3 留出法验证:593 张图也要讲统计规则

最后一次训练和验证,我建议把增强后的数据集重新划分,而不是沿用最初的 8:2 划分。做法是按类别分层抽样:先按每个类别单独拆一个列表,再从每个列表里按相同比例抽取 20% 作为验证集,剩下的作为训练集。这一步保证了验证集里“打电话”不会只有 5 张图,否则 mAP 的波动会让你完全无法判断模型进步与否。然后把模型在初始划分和分层划分两套验证集上各跑一遍,取平均值作为最终指标。最后要留一个习惯:把验证集里预测错误的图单独放到一个目录,比如把漏检的打电话图片、误检成手机的背景图片都挑出来,翻一遍这些图比看 10 遍 mAP 曲线更能告诉你下一步该补什么数据——是补光照变化大的样本,还是补手遮挡手机的样本。这个习惯我保留了很久,每一次都是靠它找到模型真正的短板,比盲目叠增强参数有效得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询