多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

徘徊检测数据集实战:VOC/YOLO/JSON三格式解析与YOLOv8训练指南

徘徊检测数据集实战:VOC/YOLO/JSON三格式解析与YOLOv8训练指南 简介这份人员徘徊检测数据集面向计算机视觉方向的科研人员、课程设计学生及算法竞赛选手聚焦「正常行走」与「观望」两类行为的识别任务可支撑人员徘徊检测、观望行为分析等实际项目落地。数据集共2926张手机实拍图像手工标注精准目标大小分布均匀拍摄角度与背景多样算法拟合效果较好。压缩包约803.81MB含2000余个文件以jpg原图、xmlVOC、txtYOLO和json三种标签格式为主覆盖主流目标检测框架的数据读取需求可直接投入训练与实验。目前已有234人学习下载。资源源自比赛项目标签完整、类别清晰适合作为科研实验、课程作业与竞赛方案的可靠数据基础帮助读者省去采集与标注成本快速验证模型效果。1. 从 2926 张手机实拍里拆出「行走 vs 观望」这份徘徊检测数据集到底能干什么商场防损、园区周界、地铁站台这些场景里最让人头疼的往往不是快速奔跑的可疑目标而是那种慢悠悠来回踱步、时不时停下来张望的人。快速移动的目标用普通运动检测就能抓个大概但「徘徊」这个行为本身没有剧烈位移帧间差分几乎失效必须靠目标检测模型先把人框出来再结合轨迹和姿态做二次判断。问题来了公开数据集里专门标注「正常行走」和「观望」这两类细粒度行为的资源少得可怜COCO、VOC 这类通用集只有 person 一个大类根本区分不了行走和驻足观望。这份 2926 张的人员徘徊检测数据集恰好补的就是这个缺口——两类标签正常行走和观望手机实拍手工标注同时给了 VOC XML、YOLO TXT、JSON 三种格式意味着你拿到手就能直接喂给 YOLO 系列、Faster R-CNN、Detectron2 或者任何吃 COCO JSON 的框架不用再写格式转换脚本。适合做课程设计、比赛 baseline、行为识别预研以及需要快速验证徘徊检测 pipeline 的从业者。2. 三种标签格式怎么选VOC、YOLO、JSON 的适用边界与转换逻辑拿到一个压缩包里面躺着annotations.xml、labels/*.txt、instances.json三套标注第一反应不该是「全都要」而是先想清楚你的训练框架到底吃哪一套。选错了格式轻则多写两百行转换代码重则坐标对不上导致模型学了个寂寞。这一章把三种格式的坐标体系、字段含义和典型消费场景拆开讲再给一套可复现的校验脚本。2.1 三种格式的坐标体系差异VOC XML 用的是绝对像素坐标bndbox里xmin/ymin/xmax/ymax直接对应原图像素位置图像尺寸记录在size节点里。这种格式的好处是可读性强用文本编辑器打开就能核对框的位置缺点是文件体积大2926 张图就是 2926 个 XML磁盘 IO 在训练时容易成为瓶颈。YOLO TXT 用的是归一化中心点坐标每行class_id cx cy w h五个值全部除以图像宽高落在 0 到 1 之间。这种格式体积小、读取快是 Ultralytics YOLOv5/v8/v11 系列的标准输入。但要注意YOLO 的class_id是从 0 开始的整数而这份数据集的类别顺序需要你确认——常见做法是0对应「正常行走」1对应「观望」但不同标注工具导出的顺序可能相反训练前必须用脚本统计一遍每个 id 的框数量和 XML 里的类别名做交叉验证。JSON 格式这里大概率是 COCO 风格的instances.json包含images、annotations、categories三个顶层字段。annotations里的bbox是[x, y, width, height]绝对坐标注意是左上角加宽高不是右下角。COCO 格式被 Detectron2、MMDetection、YOLO 的部分分支以及很多多模态框架直接支持如果你打算做「检测 轨迹分析」的联合 pipelineJSON 是最省事的中间格式。格式坐标类型类别字段典型消费框架体积VOC XML绝对像素 xmin/ymin/xmax/ymaxname文本Faster R-CNN、SSD、Pascal VOC 工具链大YOLO TXT归一化 cx/cy/w/h行首整数 idUltralytics YOLO 全系小COCO JSON绝对 x/y/w/hcategory_id整数Detectron2、MMDetection、多模态中2.2 用脚本校验三套标签是否对齐在正式训练前我一般会跑一个交叉校验脚本确认同一张图在三套标注里的框数量和类别一致。下面这段 Python 逻辑说明遍历 XML 目录解析每张图的框再读对应的 YOLO TXT把归一化坐标反算回像素最后和 XML 的绝对坐标做容差比对。容差设 2 像素因为归一化再反算会有浮点误差。import os import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes def parse_yolo(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid, cx, cy, bw, bh map(float, parts) # 反算回绝对像素 xmin (cx - bw / 2) * img_w ymin (cy - bh / 2) * img_h xmax (cx bw / 2) * img_w ymax (cy bh / 2) * img_h boxes.append((int(cid), xmin, ymin, xmax, ymax)) return boxes # 遍历校验容差 2 像素 xml_dir annotations txt_dir labels mismatch 0 for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem xml_file[:-4] txt_file os.path.join(txt_dir, stem .txt) if not os.path.exists(txt_file): print(f缺失 TXT: {stem}) mismatch 1 continue w, h, voc_boxes parse_voc(os.path.join(xml_dir, xml_file)) yolo_boxes parse_yolo(txt_file, w, h) if len(voc_boxes) ! len(yolo_boxes): print(f框数量不一致: {stem}, VOC{len(voc_boxes)}, YOLO{len(yolo_boxes)}) mismatch 1 continue for vb, yb in zip(voc_boxes, yolo_boxes): if abs(vb[1] - yb[1]) 2 or abs(vb[2] - yb[2]) 2: print(f坐标偏差过大: {stem}) mismatch 1 break print(f校验完成异常样本数: {mismatch})这段脚本的关键参数是容差2如果你发现大量样本偏差在 3 到 5 像素之间可能是标注工具在导出 YOLO 时做了取整属于正常范围把容差放宽到 5 即可。如果偏差超过 10 像素那就要怀疑是不是图像被 resize 过但 XML 没同步更新这种样本建议直接剔除否则训练时回归头会学歪。2.3 从 VOC 转 YOLO 的批量脚本与类别映射虽然数据集已经给了 YOLO TXT但如果你要合并自己的数据或者调整类别顺序还是得会写转换脚本。下面这段把 VOC XML 批量转成 YOLO TXT同时支持自定义类别映射表。import os import xml.etree.ElementTree as ET # 类别名到 id 的映射根据你的训练配置调整 class_map {正常行走: 0, 观望: 1} def voc_to_yolo(xml_path, out_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue cid class_map[name] bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 转归一化中心点 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) for xml_file in os.listdir(annotations): if xml_file.endswith(.xml): stem xml_file[:-4] voc_to_yolo(os.path.join(annotations, xml_file), os.path.join(labels, stem .txt))class_map这个字典是唯一需要你手动确认的地方。如果训练时发现模型把「观望」预测成「正常行走」先别急着调学习率回头检查这个映射是不是反了。我见过太多人在这行翻车训练一下午 loss 不降最后发现类别 id 写反了。3. 用 YOLOv8 跑通第一版徘徊检测数据配置、训练参数与指标解读格式校验通过之后下一步就是让模型真正跑起来。这一章以 Ultralytics YOLOv8 为例从目录结构、data.yaml 配置、训练命令到指标解读给一条能直接抄的路径。选 YOLOv8 不是因为它是唯一选择而是它对新手最友好pip install ultralytics之后三行代码就能开训同时熟手也能通过参数文件精细控制。3.1 目录结构与 data.yaml 的正确写法YOLO 训练对目录结构有硬性要求图像和标签必须分开放且文件名不含扩展名一一对应。推荐的结构如下dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml的内容path: /absolute/path/to/dataset train: images/train val: images/val nc: 2 names: 0: 正常行走 1: 观望这里有几个容易出错的点。path必须是绝对路径用相对路径在部分版本会报找不到文件。names的顺序必须和 TXT 里的class_id严格对应写反了模型学出来的语义就是错的。nc是类别数这份数据集是 2别照抄网上模板的 80。另外中文类别名在部分终端会乱码如果训练日志里看到names显示成问号不影响训练但建议在最终部署时换成英文别名避免推理后处理时编码问题。3.2 训练命令与关键参数含义yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/walk_watch \ nameexp1逐参数说明modelyolov8n.pt用 nano 版做 baseline参数量小、训练快适合先验证数据质量如果 mAP 上不去再换yolov8s.pt或yolov8m.pt。imgsz640是输入分辨率手机拍摄的图像长边通常在 1000 到 2000 像素之间缩到 640 后小目标可能丢细节如果「观望」类目标普遍偏小可以提到 960 或 1280但显存占用会翻倍。batch16在 8GB 显存上比较稳显存不够就降到 8 并配合accumulate做梯度累积。patience20表示 20 个 epoch 指标不提升就早停防止过拟合。lr00.01是初始学习率YOLOv8 默认用 SGD 时这个值比较合适如果你换成 AdamW建议降到 0.001。训练启动后终端会打印每个 epoch 的 box_loss、cls_loss、mAP50、mAP50-95。box_loss 下降说明框回归在收敛cls_loss 下降说明分类在收敛。如果 box_loss 一直震荡不降优先检查标注框有没有越界或者宽高为负的异常样本。3.3 指标解读mAP50 和混淆矩阵怎么看训练结束后runs/walk_watch/exp1/下会生成results.csv、confusion_matrix.png、val_batch0_pred.jpg等文件。results.csv里最该关注的是metrics/mAP50(B)和metrics/mAP50-95(B)。mAP50 是 IoU 阈值 0.5 时的平均精度反映「框得准不准」mAP50-95 是 IoU 从 0.5 到 0.95 每隔 0.05 取一次的平均反映「框得有多精细」。徘徊检测场景里如果 mAP50 能到 0.85 以上mAP50-95 在 0.55 左右基本可用。混淆矩阵更直观。打开confusion_matrix.png看「观望」这一行被误判成「正常行走」的比例。如果误判率超过 20%说明两类在视觉特征上重叠严重——手机拍摄的行走和观望如果都是背面或者侧面模型确实难分。这时候可以考虑加姿态关键点做辅助特征或者把「观望」的定义收窄到「正面/侧面且停留超过 N 帧」的样本重新标注。提示验证集划分建议按拍摄场景分层抽样不要随机切。同一段视频里连续帧的相似度极高随机切会导致验证集和训练集高度相似mAP 虚高实际部署时掉点严重。4. 避坑与排查标注、格式、训练里最容易翻车的五个点这一章不讲新东西专门收那些我踩过或者看别人踩过的坑。每条按「现象 → 原因 → 解决」写你训练卡住的时候可以逐条对照。4.1 现象训练 loss 正常下降但 mAP 始终为 0原因data.yaml里的names顺序和 TXT 里的class_id对不上或者nc写成了 1。模型在学但验证时类别匹配全错mAP 自然为 0。解决用第 2 章的校验脚本统计每个class_id的框数量再打开一张 XML 确认类别名。如果class_id0的框数量远多于class_id1而你的names里 0 是「观望」那大概率反了。4.2 现象训练报错「No labels found in ...」原因YOLO 在labels/train/下找不到和图像同名的 TXT 文件或者 TXT 文件为空。常见于从 VOC 转换时类别名没匹配上class_map导致所有框被跳过生成了空文件。解决检查labels/train/下随便一个 TXT 是否有内容。如果为空回到转换脚本打印一下 XML 里的name文本确认和class_map的 key 完全一致注意有没有多余空格。4.3 现象模型把「观望」框得很大把背景也包进去原因标注时「观望」的人如果靠近货架或墙壁标注员容易把背景一起框进去导致框的宽高比异常。模型学到的是「大框 观望」这种错误关联。解决统计所有「观望」框的宽高比分布把宽高比超过 3:1 或者面积占比超过图像 50% 的样本挑出来人工复核。这类样本要么重标要么直接剔除。我一般会写个脚本按面积排序看最大的 20 个框基本能揪出问题样本。4.4 现象验证集 mAP 很高但拿手机拍一段新视频推理漏检严重原因数据集是手机拍摄但拍摄角度、光照、背景和你的实际场景差异大。模型过拟合到了训练集的背景纹理上换场景就失效。解决做一轮困难样本挖掘。用训练好的模型在你的实际场景视频上推理把漏检和误检的帧截出来人工标注后加入训练集再微调 20 到 30 个 epoch。这比盲目加数据增强有效得多。4.5 现象JSON 格式用json.load读取时报编码错误原因标注工具导出的 JSON 可能带 BOM 头或者用了 GBK 编码。Python 的json.load默认按 UTF-8 读遇到 BOM 会抛json.decoder.JSONDecodeError。解决用open(path, encodingutf-8-sig)打开utf-8-sig会自动跳过 BOM。如果还不行用chardet检测编码后指定。import json import chardet with open(instances.json, rb) as f: raw f.read() encoding chardet.detect(raw)[encoding] data json.loads(raw.decode(encoding)) print(data.keys())5. 从检测框到徘徊判定用轨迹缓存做二次过滤的实战技巧模型能框出「正常行走」和「观望」只是第一步真正要输出「某人正在徘徊」这个结论还得在检测结果之上加一层时序逻辑。这一章给一个轻量级的轨迹缓存方案不依赖复杂的跟踪算法用检测框的 IOU 匹配加位置历史就能跑出可用的徘徊判定。5.1 为什么单帧检测不够单帧检测只能告诉你「这一帧里有人处于观望状态」但观望 1 秒和观望 30 秒的意义完全不同。徘徊的核心特征是「在有限区域内反复移动且伴随停留」这必须靠跨帧的轨迹来判断。常见做法是接一个 ByteTrack 或 DeepSORT 做多目标跟踪但这两个库的依赖较重如果你只是想快速验证可以用一个简化版的 IOU 轨迹缓存。思路是维护一个轨迹列表每条轨迹记录最近 N 帧的检测框中心点和类别。新一帧的检测框先和已有轨迹做 IOU 匹配匹配上就更新轨迹匹配不上就新建轨迹。然后对每条轨迹计算两个指标位移范围最近 N 帧中心点的包围盒面积和观望帧占比类别为「观望」的帧数除以总帧数。位移范围小且观望帧占比高就判定为徘徊。5.2 轨迹缓存与徘徊判定的代码实现import numpy as np from collections import deque class Track: def __init__(self, tid, box, cls_id, maxlen30): self.tid tid self.boxes deque(maxlenmaxlen) self.classes deque(maxlenmaxlen) self.boxes.append(box) self.classes.append(cls_id) def update(self, box, cls_id): self.boxes.append(box) self.classes.append(cls_id) def is_loitering(self, area_thresh80, watch_ratio0.5): if len(self.boxes) 10: return False centers np.array([[(b[0]b[2])/2, (b[1]b[3])/2] for b in self.boxes]) # 位移范围中心点包围盒面积 w centers[:, 0].max() - centers[:, 0].min() h centers[:, 1].max() - centers[:, 1].min() area w * h # 观望帧占比 watch_count sum(1 for c in self.classes if c 1) ratio watch_count / len(self.classes) return area area_thresh and ratio watch_ratio def iou(box1, box2): x1 max(box1[0], box2[0]) y1 max(box1[1], box2[1]) x2 min(box1[2], box2[2]) y2 min(box1[3], box2[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area1 (box1[2]-box1[0]) * (box1[3]-box1[1]) area2 (box2[2]-box2[0]) * (box2[3]-box2[1]) return inter / (area1 area2 - inter 1e-6) # 主循环伪代码 tracks [] next_id 0 for frame_dets in video_stream: matched set() for det in frame_dets: box, cls_id det[:4], det[5] best_iou, best_tid 0, -1 for t in tracks: if t.tid in matched: continue score iou(box, t.boxes[-1]) if score best_iou: best_iou, best_tid score, t.tid if best_iou 0.3: tracks[best_tid].update(box, cls_id) matched.add(best_tid) else: tracks.append(Track(next_id, box, cls_id)) next_id 1 for t in tracks: if t.is_loitering(): print(f轨迹 {t.tid} 判定为徘徊)area_thresh80是位移范围阈值单位是像素平方意思是中心点活动范围不超过约 9x9 像素。这个值要根据你的视频分辨率和实际场景调整分辨率 1080p 的走廊场景可以设到 200 到 300如果是 720p 的门口80 到 150 比较合适。watch_ratio0.5表示最近 30 帧里至少一半被判定为「观望」才算徘徊调高到 0.7 会更严格误报少但漏报多。maxlen30是轨迹缓存长度按 25 FPS 算大约 1.2 秒如果你要检测更长时间的徘徊把这个值提到 75 或 100。5.3 参数调优的验证方法调参不能靠感觉。我一般会录一段包含「正常走过」「短暂停留」「来回踱步」三种行为的测试视频人工标出每种行为的起止时间然后跑判定逻辑统计误报和漏报。重点看两个边界一个正常走路的人如果走得很慢会不会被误判成徘徊一个真正徘徊的人如果移动范围稍大会不会被漏掉。根据这两类错误的比例微调area_thresh和watch_ratio直到误报和漏报都在可接受范围内。注意这套简化方案在目标交叉、遮挡时会丢轨迹导致同一人被分配多个 tid。如果场景里人流密集还是建议上 ByteTrack它的低分检测框二次匹配能显著减少 ID switch。6. 把数据集用出复利增量标注、模型蒸馏与部署前的一致性检查数据集的价值不在于跑一次 baseline而在于能不能持续迭代。这一章讲三个进阶习惯都是我在实际项目里反复用到的。第一个习惯是增量标注闭环。每次模型在实际场景里漏检或误检把那些帧截出来用 LabelImg 或 CVAT 补标然后只在这些新样本上微调 10 到 20 个 epoch。不要全量重训全量重训既慢又容易把之前学好的特征带偏。微调时把学习率降到初始值的十分之一比如lr00.001让模型在新样本上小步调整。这个闭环跑上三四轮模型在你自己的场景里的表现会明显好过直接拿原始数据集训出来的版本。第二个习惯是模型蒸馏。如果你最终要部署到边缘设备YOLOv8n 可能还是偏大这时候可以用原始数据集训一个 YOLOv8m 或 YOLOv8l 作为教师模型再用教师模型的软标签去蒸馏一个更小的学生模型。蒸馏的关键是温度参数T常见取值 3 到 5T越大软标签越平滑学生模型学到的类别间关系越丰富。Ultralytics 本身不直接支持蒸馏需要自己写训练循环但如果你只是想把模型压小更省事的做法是用 YOLOv8n 训完后导出 ONNX再用 ONNX Runtime 或 TensorRT 做量化INT8 量化后模型体积能压到原来的四分之一推理速度翻倍精度掉点通常在 1 到 2 个 mAP 以内。第三个习惯是部署前的一致性检查。训练时图像经过 resize、归一化、通道转换推理时如果预处理不一致结果会差很多。我一般会写一个最小验证脚本取一张训练集里的图分别用训练时的预处理和部署时的预处理各跑一遍对比输出的框坐标。如果偏差超过 5 像素就说明预处理链路有问题。这个检查花不了十分钟但能省掉部署后排查一整天的痛苦。检查项训练侧部署侧容差输入尺寸640x640 letterbox640x640 letterbox0归一化/255/2550通道顺序RGBRGB0输出坐标归一化 cx/cy/w/h绝对像素反算后比对NMS 阈值0.450.450最后说一个我自己的教训。早期做徘徊检测时我图省事直接用 JSON 格式训了 Detectron2效果不错但后来要换到 YOLO 部署发现 JSON 转 YOLO 时类别 id 映射写错了模型把「观望」全预测成了「正常行走」上线前一天才发现。从那以后我每次换格式都强制走一遍第 2 章那个交叉校验脚本确认三套标签的框数量和类别完全对齐才敢往下走。希望这份数据集和这套流程能帮你少走点弯路把徘徊检测这个场景真正落地。本文还有配套的精品资源点击获取
返回列表