YOLOv8打电话抽烟检测实战:2037张数据集格式转换与训练调优

📅 发布时间:2026/10/10 2:33:13
YOLOv8打电话抽烟检测实战:2037张数据集格式转换与训练调优
简介这是一份面向目标检测实战的YOLO数据集聚焦驾驶员打电话和抽烟两类危险行为识别适用于安全监控、驾驶行为分析、算法竞赛与毕设实验等场景。数据全部来自实际项目采集未做数据增强保留原始拍摄分布方便自行叠加旋转、饱和度、曝光量等增强策略。资源压缩包约291MB内含约2000个图像文件实际为2037张JPG原图每张均配套VOC格式XML标签与YOLO格式TXT标签XML记录类别与边界框坐标TXT使用归一化坐标两种主流标注规范可满足不同训练框架的直接导入。目录结构清晰下载后按需划分训练/验证集即可投入训练。目前已有5680人学习使用。整体标注精确无误背景与姿态多样能有效提升模型对真实场景的泛化能力适合需要高质量电话/抽烟样本的开发者快速上手也适合用于YOLO系列模型的效果对比与调参练习。1. 2037 张打电话抽烟数据集先解决“没数据”再谈调参做目标检测项目最折磨人的环节往往不是调参而是数据本身。接了一个驾驶行为识别的需求客户点名要识别打电话和抽烟两个动作翻遍公开数据集要么样本量小得可怜要么标注框把手机和手糊在一起完全没法直接用。后来拿到这份 yolo 实际项目数据-打电话抽烟数据集总共 2037 张自采自标图片带 VOC 与 YOLO 两种格式标签解压之后基本可以直接进训练管线。它主要适合安防巡检、驾驶舱行为分析、工地安全检测这类场景的开发者能省掉采集、清洗、格式转换三块耗时。下面我把数据格式、训练参数、踩坑记录和评估调优挨个拆开讲照着能复现。2. 数据集结构拆解图片、XML、TXT 三件套怎么协同拿到数据集先别急着喂给模型。先把目录和标签结构过一遍确认图片和标注能对上号再跑训练脚本。这一章讲三件事目录怎么组织、VOC 的 XML 标注包含哪些信息、VOC 与 YOLO 两种格式之间怎么转换和校验。2.1 目录组织与文件齐整性检查一份整理到位的数据集目录结构一般是这样的dataset_root/ ├── images/ │ ├── img_0001.jpg │ ├── img_0002.jpg │ └── ... ├── Annotations/ │ ├── img_0001.xml │ ├── img_0002.xml │ └── ... └── labels/ ├── img_0001.txt ├── img_0002.txt └── ...images 目录放原始图片Annotations 目录放 PASCAL VOC 标准的 XML 标注labels 目录放 YOLO 风格的 TXT 标注。三个目录里文件同名同前缀只是扩展名不同这是格式能正常工作的前提。拿到数据集的第一步不是看标注画得好不好而是核对三件套齐不齐。我用一段 Python 脚本把图片、XML、TXT 全对一遍from pathlib import Path img_dir Path(images) xml_dir Path(Annotations) txt_dir Path(labels) miss_xml, miss_txt [], [] img_count 0 for img in sorted(img_dir.iterdir()): if img.suffix.lower() not in (.jpg, .jpeg, .png): continue img_count 1 stem img.stem if not (xml_dir / f{stem}.xml).exists(): miss_xml.append(img.name) if not (txt_dir / f{stem}.txt).exists(): miss_txt.append(img.name) print(f图片总数: {img_count}) print(f缺失 XML: {len(miss_xml)} {miss_xml[:5]}) print(f缺失 TXT: {len(miss_txt)} {miss_txt[:5]})这段脚本里最关键的是 stem 属性它拿掉扩展名只留文件主体再拼接 XML 和 TXT 的文件名规避大小写和后缀不一致带来的假报错。缺失数量不为零就把文件补齐很多训练中随机报错的根源就在这一步。还有一点容易被忽略文件后缀大小写。同一批数据里既有 .jpg 也有 .JPG在 Windows 上没事在 Linux 服务器上会被识别成两个不同文件。我先批量统一后缀find images -type f -name *.JPG -exec mv {} {}.tmp \; -exec rename s/\.JPG\.tmp$/.jpg/ {} \;这手只处理 JPG 转 jpg不动其他内容。做完以后重新跑一遍齐整性脚本确认 2037 张图片都有对应的 xml 和 txt再继续下一步。2.2 VOC 标注的五要素类别、坐标框、difficult 与图片尺寸VOC 格式的每个 XML 文件对应一张图片典型结构如下annotation folderimages/folder filenameimg_0017.jpg/filename size width1920/width height1080/height depth3/depth /size object namephone/name truncated0/truncated difficult0/difficult bndbox xmin720/xmin ymin400/ymin xmax810/xmax ymax470/ymax /bndbox /object object namesmoke/name truncated0/truncated difficult0/difficult bndbox xmin1100/xmin ymin520/ymin xmax1160/xmax ymax580/ymax /bndbox /object /annotation读这个 XML 要看五个字段。filename 决定标签和哪张图配对。size 里的 width 和 height 是坐标归一化的基准YOLO 格式转换全靠它。bndbox 标出目标的左上角和右下角像素坐标。name 是类别名。difficult 标记这个目标是否难以识别。difficult 位要单独拎出来说因为很多训练代码会跳过 difficult1 的样本。如果一张图里有三个 difficult1 的目标实际参与训练的只有剩余目标样本量比看起来小。要把数据集里 difficult 位用了多少摸清楚这直接影响训练配置。这套数据集的类别一般是两类phone打电话和 smoke抽烟。打电话的框通常圈住手机和手持手机的手抽烟的框圈住烟和叼烟区域。标注规范不同模型学到的东西天差地别。如果框只圈了手机没圈手模型学到的更偏向“手机”而不是“打电话动作”部署时人手机没贴脸、只是攥在手里也会被误报成打电话。反过来把整个头都圈进去模型又会把正常摸脸动作当成抽烟。所以标注框的边界很重要这也是自采数据和公开数据最大的差别你知道这些框是谁画的也就知道它能学到什么程度的语义。2.3 VOC 与 YOLO 格式的转换公式及校验脚本YOLO 格式的标签是纯文本每行一条目标五个浮点数class_id x_center y_center width height后四个值全是归一化浮点数取值范围在 0 到 1。归一化基准就是 XML 里 size 的 width 和 height。从 VOC 像素坐标转成 YOLO 归一化坐标公式如下x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height这里常见的错误是忘掉最后一次除法。算出中心点的像素坐标之后如果不除以图片宽高写进 txt 的数值远大于 1训练时 YOLO 直接报数据非法。批量转换我一般写成一个函数import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, txt_path, class_map): tree ET.parse(xml_path) 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): name obj.find(name).text.strip() cls_id class_map.get(name) if cls_id is None: print(f跳过未知类别: {name} in {xml_path.name}) 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{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_path.write_text(\n.join(lines)) class_map {phone: 0, smoke: 1} xml_dir Path(Annotations) txt_dir Path(labels) for xml_path in xml_dir.glob(*.xml): txt_path txt_dir / f{xml_path.stem}.txt voc_to_yolo(xml_path, txt_path, class_map)class_map 的顺序一旦定下训练用的 YAML 里 names 必须保持一致。最常见的翻车现场就是这里 phone 是 0、smoke 是 1训练配置文件里写反了模型把 phone 预测成 smoke指标全乱。转换完别直接开工画框验证越早做越省钱import cv2 from pathlib import Path def draw_boxes(image_path, label_path, out_path, class_names): img cv2.imread(str(image_path)) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.split() if len(parts) ! 5: continue cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 255, 0) if cls_id 0 else (0, 0, 255) label class_names[cls_id] cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(out_path), img) draw_boxes( Path(images/img_0017.jpg), Path(labels/img_0017.txt), Path(check.jpg), [phone, smoke] )这个脚本把中心点横纵坐标反算回左上角像素坐标再画框和类别名。框和原图中人和物是否吻合一目了然。重点查三类问题坐标越界、类别错位、框只画了半个目标。这三类问题在自采数据里都出现过越早发现越省钱。提示VOC 转 YOLO 之后框面积低于 8x8 像素的小目标训练时很容易学不到特征后面会单独讨论处理方式。格式对齐是数据能不能用的分水岭这一步做扎实后面的训练才有意义。3. 从零把训练跑起来数据划分、YAML 配置与命令参数数据结构和格式没问题接下来就是把 2037 张图片送进训练管线。这一章覆盖三个动作怎么划分 train/val、怎么写数据集 YAML、训练命令里哪些参数必须动以及训练起来后盯哪几个指标。3.1 数据划分train/val 比例与相关图片隔离先定划分比例。2037 张算小规模数据集常规用 8:2 或者 9:1。但如果某个类别的目标样本偏少就不能简单随机切。先统计两个类别各自的目标数量和图片数量from pathlib import Path label_dir Path(labels) stats {phone_target: 0, smoke_target: 0, phone_images: set(), smoke_images: set()} for txt_path in label_dir.glob(*.txt): with open(txt_path) as f: lines [l.strip() for l in f if l.strip()] for line in lines: cls_id line.split()[0] if cls_id 0: stats[phone_target] 1 stats[phone_images].add(txt_path.stem) elif cls_id 1: stats[smoke_target] 1 stats[smoke_images].add(txt_path.stem) print(phone 目标数:, stats[phone_target], 涉及图片:, len(stats[phone_images])) print(smoke 目标数:, stats[smoke_target], 涉及图片:, len(stats[smoke_images]))统计完会发现一个普遍现象抽烟目标往往比打电话目标少。因为很多人抽着烟同时在看手机但单独打电话的行为更常见。如果验证集里抽烟样本只有十几个某次随机划分出来的 mAP 波动会很大甚至从 80% 跌到 40%。遇到这种情况可以做分层划分按图片里包含的类别把样本分层train 和 val 都保证两个类别有覆盖。还有一个更容易被忽视的坑相关图片隔离。自采数据很多是从视频片段里截帧得到的同一段视频相邻帧高度相似。随机划分时这些相似帧很容易一个进 train、一个进 val最后验证集指标虚高部署到现场完全不同。解决方法是按视频片段或场景分组划分先从文件名提取场景编号再按场景整体分配import random scenes {} for txt_path in label_dir.glob(*.txt): scene_id txt_path.stem.split(_)[0] # 假设文件名前缀是场景号 scenes.setdefault(scene_id, []).append(txt_path.stem) all_scenes list(scenes.keys()) random.seed(42) random.shuffle(all_scenes) split int(len(all_scenes) * 0.8) train_scenes set(all_scenes[:split]) val_scenes set(all_scenes[split:]) train_stems [s for sc in train_scenes for s in scenes[sc]] val_stems [s for sc in val_scenes for s in scenes[sc]] print(len(train_stems), len(val_stems))按场景编号切分比按单张图片随机切分更贴近真实评估需求。前提是文件名里保留场景信息如果原始数据没保留可以按图片的采集时间分组效果类似。3.2 数据集 YAML 配置路径、类别与目录对应划分完写一个 YAML 配置文件给 YOLO 用# smoke_phone.yaml path: /data/smoke_phone train: images/train val: images/val nc: 2 names: 0: phone 1: smokepath 是数据集根目录train 和 val 填相对 path 的图片目录。YOLO 会自动去找与 images 目录平级的 labels 目录所以目录命名最好保持 images / labels 这种对仗关系。nc 必须和 names 长度一致names 的顺序必须和标签文件里 class_id 的顺序一致。几点细节值得注意路径如果带空格YAML 解析会出问题用引号包住。类别名不要用中文否则后续可视化、导出 ONNX 时容易出编码问题。path 用绝对路径避免不同机器跑训练时目录对不上。3.3 YOLOv8 训练命令与关键参数配置就位可以开始训练。YOLOv8 的命令长这样yolo detect train \ datasmoke_phone.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ device0 \ patience15逐项拆解参数含义。modelyolov8n.pt 指定预训练权重n 是 nano 最轻量版本显存小、跑得快适合先把流程跑通。imgsz640 是训练输入分辨率手机小目标多时后续提到 1280。epochs100 是最大轮数配合 patience15连续 15 个 epoch 验证指标不升就提前停止省时间。batch16 受显存限制8GB 显存跑 640 分辨率勉强能带再大就显存溢出。device0 指定第一张显卡。如果显存不够可以降低 batchyolo detect train \ datasmoke_phone.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch8 \ device0 \ patience15batch8 的梯度噪声更大但小数据集上训练反而更稳。注意 yolov8n.pt 会从预训练权重开始训练如果连预训练权重都不加载可以用 modelyolov8n.yaml 从零开始但小数据集不建议这样收敛慢还容易过拟合。3.4 训练过程监控盯 loss 还是盯 mAP训练启动后控制台会按每个 epoch 输出训练 loss 和验证指标。盯三个地方train 的 box_loss 和 cls_loss、验证集 mAP50、每个类别的 AP。有一个常见误区只看总 loss。box_loss 降但 cls_loss 升的情况经常出现最终分类错误一堆。训练脚本跑完后打开 runs/detect/train 目录下的 results.png里面单列了 box_loss、cls_loss、mAP50、mAP50-95 几条曲线逐条看才准。如果 mAP50 高但 mAP50-95 低说明模型能检出目标但框不够准常见于小目标多的电话和烟。如果两类 AP 差距大比如 smoke 的 AP 比 phone 低 20 个点先怀疑标注质量而不是模型结构。数据集的类别分布和标注边界往往才是影响 AP 的第一因素。4. 打电话抽烟检测避坑指南标注、训练与推理三大环节的踩坑记录以下四条是这一类小目标数据集里极高发的坑我全都遇到过按「现象 → 原因 → 解决」逐一列出来可以直接对照自己的训练日志。4.1 坐标越界标签文件里出现负数和大于 1 的框现象训练中途报错 data is not valid或者打开 txt 发现 x_center 或 width 是 -0.02、1.15 这种越界值模型没法算 IoU。原因两个来源。一是 VOC 转 YOLO 时忘了除图片宽高导致归一化后数值大于 1二是标注过程中标注工具允许把框拖到图片边缘外侧xmin 或 ymin 变成负数转换脚本没做修剪。我排查过的一个案例整批数据里有 37 张图的框越界全部集中在图片左边缘附近。解决给转换脚本加边界限制把非法值限制在 [0,1] 区间def clip_value(v): return max(0.0, min(1.0, v)) x_center clip_value((xmin xmax) / 2 / img_w) y_center clip_value((ymin ymax) / 2 / img_h) w clip_value((xmax - xmin) / img_w) h clip_value((ymax - ymin) / img_h)但 clip 只是治标不治本。根源是原始 XML 里坐标本身越界顺手再跑一遍检查把 xmin0 或 ymin0 的 XML 找出来回标注工具修正。如果不修即使 clip 后训练不报错框位置也是偏的影响精度。4.2 小目标漏检手机和烟在 640 分辨率下只有几个像素现象整体 mAP 还行但单独看 phone 类 AP 不高smoke 尤其低。查看推理结果小框的烟基本没检出或者框偏移很大。原因打电话和抽烟的目标主体太小。一张 1080p 的监控图人脸区域可能只有 60x60 像素烟头区域只有 20x30 像素。640 分辨率输入下缩放后烟头只剩几个像素特征完全丢失。mosaic 增强会把目标缩得更小等于雪上加霜。解决先提高输入分辨率到 1280再调低或关闭 mosaic 对目标的影响。YOLOv8 训练时用 mosaic0.5 或直接把 mosaic 关闭效果变化明显。再把 imgsz 提到 1280batch 对应减半。这一步之后 smoke 目标的 AP 往往能提升 10 个点以上。4.3 类别混淆phone 和 smoke 的框互相渗透现象推理时把拿着烟的手识别成 phone或者手靠近耳朵时被识别成 smoke。混淆矩阵里 phone 行和 smoke 列有不容忽视的值。原因两个类别在视觉上高度相似——手、脸、嘴部区域都在画面中央且打电话和抽烟动作都发生在头部附近。如果标注规范不统一有的框圈手、有的框圈手机、有的框圈整张脸模型学习的边界就乱了。解决先统一标注规范再考虑模型。确定“打电话”的框是圈手机还是圈手加手机“抽烟”的框是圈烟本身还是圈叼烟的嘴部区域。统一后用脚本统计两类的 bbox 宽高分布看是否有大量框重叠。如果重叠过多引入 hard negative或者把类别定义改成互斥的也有效果。4.4 验证集指标虚高相似图片被分进 train 和 val现象训练 mAP50 到 0.95现场实测马上掉到 0.7 以下差得离谱。反复调参也没用最后发现是数据划分问题。原因从视频抽帧采集时同一场景的多帧相似随机划分会让 train 和 val 里都有同一场景的帧模型实际上“见过”验证集图片。解决用第 3 章里按场景分组划分的方法按视频片段把相似帧分到同一个集合。划分完做一次相似度抽查从 val 里随机挑 5 张、train 里挑 5 张人工对比确认没有同一场景串组。这一步检查虽然土但能救回整个评估的可信度。5. 训练结果评估读 mAP 之外还要看两类 AP 差和 PR 曲线训练跑完先别急着部署。这一章讲三件事怎么从结果文件里读有效指标、置信度和 NMS 阈值怎么选、误检漏检怎么定位到具体环节。5.1 两类目标的 AP 分开看phone 与 smoke 不该用同一个标准YOLO 训练完会在 runs/detect/train 目录生成一批文件其中 confusion_matrix.png 和 results.png 是重点。results.png 里能看到每个类别的 PR 曲线。我之前跑过一个模型整体 mAP50 有 0.88但拆开看 phone 的 AP 是 0.92smoke 只有 0.71。如果只报一个整体 mAP实际部署时烟检测就会翻车。所以评估动作第一步是拆开读cat runs/detect/train/results.csvresults.csv 里每一列对应一个指标整体 mAP50 在单类别的 AP 也会根据 YAML 里 names 自动拼接出来。单类 AP 差距如果超过 15 个点优先怀疑类别样本量不平衡或标注边界不一致而不是模型结构问题。5.2 置信度阈值与 NMS IoU 的选择逻辑推理时的两个参数直接决定输出质量conf_thres 和 iou_thres。yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_video.mp4 \ conf0.25 \ iou0.7默认 conf0.25、iou0.7 适合一般检测场景但对打电话抽烟这类小目标默认值并不合适。小目标由于特征少置信度天然偏低conf 设太高会把一堆真实目标滤掉。反过来conf 设太低又会把背景误报成对象尤其人脸手部区域极易误检。我用下面这个组合跑过一轮效果相对稳参数推荐值说明conf_thres0.3-0.4压住误报必然牺牲少部分低置信度小目标iou_thres0.5比默认 0.7 更严格减少密集目标重叠框imgsz1280推理分辨率提到 1280小目标召回率显著提升注意推理分辨率和训练分辨率保持一致或者成整数倍关系否则有尺度偏移问题模型性能会打折扣。5.3 从混淆矩阵定位误检漏检而不是瞎调参误检漏检出现后正确做法是看 confusion_matrix_normalized.png。这个图会把预测结果按类别和背景组织成矩阵对角线越亮越好。重点看两个角落背景被预测成 phone 的数量、phone 被预测成 smoke 的数量。矩阵数值高但找不到原因时跑一轮可视化推理yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_images/ \ saveTrue \ conf0.3推理结束后逐个检查 runs/detect/predict 目录下每张图的框。和原图标注对比看漏检的是小目标还是遮挡目标误报的是背景区域还是人物附近。如果背景误报集中在人脸或手部附近说明类别特征没学干净回到训练数据里加 hard negative 图片这类图片本身没有任何待检目标但包含大量人物和手部背景样本。最后一招是调整学习率。小数据集上常见的问题是收敛不够把 lr0 从默认 0.01 降到 0.005跑完后模型对模糊目标的响应会稳定一些。但这一招只能微调数据本身有问题的话调学习率只是拖延时间。6. 进阶技巧小目标增强、迁移学习与部署后处理前面几步解决“能跑”这一步解决“跑得好”。针对打电话抽烟这种小目标为主的数据集有三个性价比很高的进阶手段。6.1 训练输入分辨率与 mosaic 增强协同小目标场景下先把 imgsz 从 640 提到 1280配合调低 mosaicyolo detect train \ datasmoke_phone.yaml \ modelyolov8n.pt \ imgsz1280 \ epochs100 \ batch8 \ mosaic0.51280 输入会让小目标的像素面积变成原来的四倍模型更容易学。mosaic0.5 表示一半训练样本走 mosaic 增强另一半走原图避免小目标被裁掉。6.2 迁移学习与权重选择2037 张图的数据量不足以从零训练。从 yolov8n.pt 开始训练是最稳的组合。想要更高精度换 yolov8s.pt 或 yolov8m.pt 也可以但显存需求会翻倍。训练完用 best.pt 进行推理而不是 last.pt。best.pt 是验证指标最好的权重last.pt 是最后一轮权重两者在早停后可能差距不小。6.3 部署后处理置信度与关键帧策略推理完成后我习惯在业务侧再做一次过滤把置信度低于 0.35 的框过滤掉输出结果只保留两类框。视频流场景加关键帧策略每 5 帧处理一次而不是逐帧检测。打电话和抽烟都是持续动作关键帧对召回影响很小却能把推断耗时降到原来的五分之一。这种场景的数据集很小方案选型上不用追求最重的模型yolov8n 定型和微调就够用。重要的是数据质量和推理侧的策略数据达标才是这 2037 张图片真正发挥价值的地方。如果你也在做类似项目建议第一次跑通前一定把格式检查脚本跑一遍。从那以后我每次换新数据集都强制走一遍文件核对和画框校验再进训练管线。希望帮到你。本文还有配套的精品资源点击获取