人员徘徊检测数据集解析:三种标注格式转换与YOLO训练避坑指南
发布时间:2026/9/28 15:47:20来源:尧图网络
简介面向目标检测与行人行为分析任务这份人员徘徊行走观望检测数据集共2926张实拍图片覆盖“正常行走”与“观望”两类目标手动标注精准目标尺度与场景背景多样适用于课程设计、科研实验及比赛项目。资源包约804MB共8780个文件包含2926张JPG图片、2927个YOLO格式TXT标签、2926个VOC格式XML标签并附带含JSON标注的ZIP文件标签格式覆盖主流检测框架可直接用于YOLO、Faster R-CNN等算法训练。目前已有234人学习下载。该数据集可支撑人员徘徊预警系统、观望行为识别等实际应用三种标注格式让预处理环节大幅简化帮助开发者快速聚焦模型调优与效果验证。1. 人员徘徊检测(行走-观望)数据集2926张三种格式标签到底在解决哪类需求先还原一个监控画面一个人从右侧走进来在闸机前停住退回两步又上前朝摄像头张望了一下然后离开。这种行走-观望的行为普通YOLO逐帧检测每一帧都能框住人但产生不出任何可用的报警事件。人员徘徊检测(行走-观望)数据集要补的恰好就是这中间断了的一环——把哪个目标在徘徊做成带标注的检测任务让模型先在每一帧里把人稳定检出来再把2926张连续画面的框串成逗留事件。这套数据集同时给了VOC(xml)、YOLO(txt)、json三种标签是我平时比较认可的一种打包方式不管你的训练管线吃的是哪套格式都能找到对应文件不用从零开始转。适合的场景是安防人流量统计、重点区域防逗留、智慧门店里顾客只逛不买这类行为分析项目。无论你手里是YOLOv5还是YOLOv8的检测流程还是留着标注工具做二次清洗这套数据都能直接落到现有管线里而不是再花一周去重新标注。2. 把VOC(xml)、YOLO(txt)、json三种标签放到一张桌上坐标体系与转换脚本很多新手拿到zip第一件事就是解压、把图片塞进训练脚本然后发现loss飞了也不知道为什么。这个方向多数翻车不是模型问题是标签格式没对齐。VOC、YOLO、json三种格式看起来都是框一个目标但坐标基准完全不同一个是绝对像素一个是归一化中心点一个是带行为语义的事件记录。先花十分钟把坐标体系理清楚比后面调试一百次都管用。2.1 VOC(xml)标注存的是绝对像素bndbox四个值先读对再用VOC格式是目标检测标注里的老面孔一个xml对应一张图。xml里的核心信息分两块size记录图像宽度和高度object里放着类别名和bndbox。bndbox里的xmin、ymin、xmax、ymax四个值单位是原始图像里的绝对像素左上角为原点。annotation filenameframe_001203.jpg/filename size width1920/width height1080/height depth3/depth /size object nameperson/name bndbox xmin720/xmin ymin521/ymin xmax901/xmax ymax1024/ymax /bndbox /object /annotation这段xml说明得很直白这张1920×1080的图里编号为1的目标类别是person框从像素(720, 521)延伸到(901, 1024)。拿到徘徊检测数据集的zip后我一般会先写个小脚本把全部xml里的name字段去重打印一遍确认类别到底叫person还是walking、looking这类行为名。别嫌这一步啰嗦行为类标注经常出现同类目标重复标注的情况后面做class_map时全靠这份清单。提示xml里的width和height不一定等于图片真实分辨率尤其经过二次压缩的数据集。转换前抽三张图核对一下避免全局错位。2.2 YOLO(txt)要的是归一化中心坐标换算公式与一个voc2yolo函数YOLO的txt标签每一行代表一个目标格式是五段class_id、cx、cy、w、h。和VOC最大的区别是cx和cy是目标中心点的归一化坐标w和h是框宽高的归一化长度四个浮点数的值域都在0到1之间跟图像分辨率解耦。这样1080p的监控图和720p的图才能共用一套训练标签。换算关系并不复杂只是方向容易搞反cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h对应的Python函数写出来大概是这样的def voc_to_yolo_lines(xml_path: str, class_map: dict) - list: import xml.etree.ElementTree as ET root ET.parse(xml_path).getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_map: continue cls_id class_map[name] 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) # 边界保护坐标先裁剪到图像范围内 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines这里有两个容易忽略的点一是坐标裁剪标注偶尔会给出超出图像边界的值不裁会导致训练时loss异常跳动二是保留六位小数不要四舍五入成三位徘徊检测这类小目标场景对框的精度比常规检测更敏感。2.3 json标签在徘徊检测项目里通常有几副面孔先判断再解析json是三种格式里最容易让人误判的。它不像xml和txt那么统一取决于数据是用什么工具产出的结构可能完全不一样。拿到json先别急着解析先判断它属于哪一种。第一种是COCO检测标注顶层是一个dict包含images、annotations、categories三个数组。这是mmdetection、detectron2这类框架直接能吃的格式bbox字段是[x, y, width, height]的绝对像素表示从左上角开始。{ images: [ {id: 1, file_name: frame_001203.jpg, width: 1920, height: 1080} ], annotations: [ {id: 1, image_id: 1, category_id: 1, bbox: [720, 521, 181, 503], area: 91043} ], categories: [ {id: 1, name: person} ] }第二种是行为事件json常见于徘徊检测或动作识别的后处理结果顶层是list每个元素是一个事件字段里会出现track_id、start_frame、end_frame、action这类时序信息。这种json不适合直接训练检测器而是用来验证你后续的跟踪和判定逻辑是否正确。import json with open(labels.json, encodingutf-8) as f: data json.load(f) if isinstance(data, dict): keys set(data.keys()) if {images, annotations, categories} keys: print(COCO检测标注格式) elif events in keys or tracks in keys: print(时序行为事件格式) elif isinstance(data, list): print(列表结构看首元素字段再定向)我拿到这类zip的处理习惯是先跑这段判断代码确认json的“身份”之后再决定走哪条转换路径。把这三种格式混为一谈是后面一切坐标错乱的根源。2.4 xml批量转txt的可运行脚本class_map、空标注和边界坐标三个注意点当你确定xml里的类别名之后批量转换就可以放心跑了。这里给一份我常用的批处理脚本直接放在zip解压目录下执行即可from pathlib import Path def batch_convert(xml_dir: Path, txt_dir: Path, class_map: dict): txt_dir.mkdir(parentsTrue, exist_okTrue) for xml_path in xml_dir.glob(*.xml): lines voc_to_yolo_lines(str(xml_path), class_map) txt_path txt_dir / f{xml_path.stem}.txt if lines: txt_path.write_text(\n.join(lines), encodingutf-8) else: # 空标注也生成空txt否则YOLO训练时会跳过这张图 txt_path.touch()class_map定义在脚本入口处例如{person: 0}但必须以你第一步打印出的实际类别名为准。如果xml里混着Person和person先统一大小写再做映射否则很多目标会被静默跳过而检测训练最难发现的错误就是这么来的。坐标边界也要顺手处理。VOC标注偶发出现xmax小于xmin的情况多半是标注工具拖动时手抖。转换函数里应该在裁边之前做一个min/max修正否则归一化后w是负值训练loss直接变成NaN。最后空标注文件必须生成空的txt而不是跳过。YOLO训练时通过图片找到同名txt没有txt会默认忽略这张图但空txt存在会告诉框架“这张图里没有目标”两者对loss和验证指标的影响完全不同。3. 用2926张徘徊样本跑通YOLO训练目录、data.yaml与保底超参数标签格式全部对齐之后下一步就是把这个数据集真正喂进YOLO训练管线。2926张图单独看数量不算多但对徘徊检测这种类别单一、场景固定的任务来说是足够的关键是目录结构别搞乱。训练前花五分钟摆好目录会省掉之后无数次因为路径问题导致的报错。3.1 目录结构用8:1:1把图片和标签拆进train/val/testYOLO训练要求的标签目录和图片目录必须平行对应图片放images下同名txt放labels下。先建好四个目录mkdir -p dataset/images/train dataset/images/val dataset/images/test mkdir -p dataset/labels/train dataset/labels/val dataset/labels/test然后是划分脚本。注意这里每个split里同步拷贝图片和同名txt错一个配对训练时就会报空标签警告import random from pathlib import Path from shutil import copyfile src_img Path(images) src_txt Path(labels_yolo) dst Path(dataset) random.seed(42) imgs sorted(src_img.glob(*.jpg)) sorted(src_img.glob(*.png)) random.shuffle(imgs) n len(imgs) train_imgs imgs[:int(n * 0.8)] val_imgs imgs[int(n * 0.8):int(n * 0.9)] test_imgs imgs[int(n * 0.9):] splits {train: train_imgs, val: val_imgs, test: test_imgs} for split, items in splits.items(): (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) for img in items: copyfile(img, dst / images / split / img.name) txt src_txt / f{img.stem}.txt if txt.exists(): copyfile(txt, dst / labels / split / f{img.stem}.txt)随机种子固定成42是为了让你和别人复现时得到一模一样的划分结果这在调试阶段很重要。如果原始zip里已经带了train和val目录就不必重复划分直接用原结构。另外要特别警惕纯随机划分监控场景的图像往往按点位连续采集同一个点位里前后几帧画面极其相似如果同时落入train和val验证分数会虚高后面单独测一个陌生点位时再原形毕露。3.2 data.yaml四行配置path、train、val、names与ncYOLOv5和YOLOv8的训练入口都吃data.yaml四行核心配置缺一不可path: /home/user/dataset train: images/train val: images/val nc: 1 names: 0: personpath建议写绝对路径相对路径在换机器部署时经常出问题。train和val字段指向相对于path的子目录不要写成绝对路径因为YOLO会拿它们和path拼接。nc是类别数量这里只有person一类就写1names的索引0必须和txt第一列的class_id一一对应。如果names写错训练不会报错但混淆矩阵和PR曲线会莫名对不上属于隐蔽性最强的一类错误。3.3 训练命令与三个必调参数imgsz、batch、patience数据和配置文件就绪后我习惯用YOLOv8来跑这条基线命令如下yolo detect train \ data/home/user/dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ batch16 \ epochs100 \ patience30 \ device0三个参数值得单独说明。第一是imgsz监控画面里的行人通常只占图像的一小块1080p原图下一个人可能只有120×300像素缩到640后变成几十像素的小目标所以如果发现recall始终上不去优先把imgsz调到960或1280试试。第二是batch12G显存的卡跑yolov8nimgsz640时batch16已经接近上限报CUDA OOM就降到8不要硬撑。第三是patience徘徊检测样本单一模型容易很快收敛设30表示30个epoch内mAP没提升就自动停省时间也防过拟合。模型权重复用方面直接用yolov8n.pt作为预训练即可28百万张图的COCO基座对行人这类通用目标已经够用。2926张图属于小样本不建议一上来就换yolov8x很容易过拟合到当场景的墙面纹理和光线条件。还有一点针对徘徊检测的特殊观察这类项目最终关心召回率远大于精确率——漏掉一个正在观望的人比多框一个过路行人严重得多。所以训练完看验证集时重点看recall列而不是mAP50。mAP是综合指标对徘徊业务没有直接帮助。4. 徘徊检测训练避坑三种标签转换与行为判定的五个高频翻车点这部分写的是我实际跑数据时踩过、也帮人排查过的坑。每一条都按“现象、原因、解决”的顺序记录你可以直接对照排查。4.1 坑一txt框画回原图整体错位左上角对不上现象训练前可视化检查把转换好的txt框画到原图上框的位置偏了半个身位有的甚至跑到图像外面。原因是转换公式里w和h的分母写错了——用了绝对像素差值但没有除以图像宽高或者xml里的size字段和图片实际分辨率不一致。解决方式是用正确公式重转并加一段可视化脚本随机抽查十张import cv2 img cv2.imread(frame_001203.jpg) h, w img.shape[:2] with open(frame_001203.txt, encodingutf-8) as f: for line in f: _, cx, cy, bw, bh map(float, line.split()) 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), (0, 255, 0), 2) cv2.imwrite(check.jpg, img)这里的关键是以img.shape[:2]取到的真实分辨率为准不要直接信xml里存的width和height。如果抽查十张里有两张偏移大概率是size字段写错了。4.2 坑二json的bbox被当成归一化坐标二次处理现象从json读取标签后转出来的yolo txt数值经常大于1训练时坐标越界。原因是把COCO json的bbox当成yolo格式解析了。COCO的bbox四元组是[x, y, width, height]单位是绝对像素且从左上角开始而yolo需要的是中心点加归一化尺寸两者不能直接互换。正确公式是cx (bbox[0] bbox[2]/2) / img_w注意不是bbox[0] bbox[2]除以二再除以宽。如果json里的标注已经转了两次解决起来更麻烦因为错误被叠加了最稳妥的办法是抽三张图分别用xml、txt、json画框三组框重合度应该在99%以上差异超过2个像素就要回头查某一步转换。4.3 坑三类别名大小写和空格不一致class_map漏掉大量目标现象训练时类别数正常但recall特别低画出图片可视化后发现很多行人都没被框上。原因多半是某个xml里的name写成了Person带大写或者person 带尾部空格class_map里只有一个小写的person转换时这些目标被continue静默跳过。解决的唯一可靠方式是转换前先做一次类别名普查import glob import xml.etree.ElementTree as ET names set() for xml_path in glob.glob(xml/*.xml): root ET.parse(xml_path).getroot() for obj in root.findall(object): names.add(obj.find(name).text.strip()) print(names)打印出来的集合就是真实类别集合把它整理成class_map不要用肉眼猜。这个坑在多人标注的数据集里很常见一个人习惯写person另一个人习惯写Pedestrian不改完后面全部白训练。4.4 坑四随机切训练集验证集同一个监控点位同时出现在两边现象训练和验证的mAP都很高但把模型部署到新的监控点位效果断崖式下跌。原因是2926张图如果来自几条固定监控点位随机8:1:1切分会把同一点位的相邻帧同时分给训练集和验证集模型相当于提前看到了“标准答案”。解决方法是按点位或视频片段分组切分同一个点位下的所有帧进同一侧验证集必须完全由模型没见过的点位组成。这样验证分数会掉一些但掉完之后的数字才接近真实上线表现。4.5 坑五把“行走-观望”的行为类别硬塞给单帧检测器现象训练目标里除了person还有一个walking_looking类别loss正常收敛但实际跑视频时行为类别预测极不稳定同一目标前一帧是walking后一帧就变looking。原因很本质在单帧图像里行走和观望的姿态差异可能很小甚至看不出区别单帧检测器拿到的信息根本不足以判断行为状态强行分类只会学到场景先验。解决方法是检测层只输出person框把walking和looking的判定放到后续跟踪加时间窗口的逻辑里。所以拿到这样的数据集时xml或json里如果出现了行为类别名把它当评估标签用别当检测训练目标。5. 从单帧检测跨越到徘徊事件用IoU跟踪加时间窗口完成最终判定5.1 轻量级IoU跟踪给每一个person框分配稳定track_id训练完模型最后一公里是把单帧检测框串成轨迹。如果项目不想引入ByteTrack这类重跟踪器我一般先用IoU贪心匹配搭一个最轻量的跟踪def iou(a, b): ax1, ay1, ax2, ay2 a bx1, by1, bx2, by2 b ix1, iy1 max(ax1, bx1), max(ay1, by1) ix2, iy2 min(ax2, bx2), min(ay2, by2) iw, ih max(0, ix2 - ix1), max(0, iy2 - iy1) inter iw * ih union (ax2 - ax1) * (ay2 - ay1) (bx2 - bx1) * (by2 - by1) - inter return inter / union if union 0 else 0 tracks {} for frame_id, dets in enumerate(frame_detections): for det in dets: best_id, best_score None, 0.3 for tid, prev_boxes in tracks.items(): score iou(det[box], prev_boxes[-1]) if score best_score: best_id, best_score tid, score if best_id is None: tracks[id(det)] [det[box]] else: tracks[best_id].append(det[box])IoU阈值取0.3是经验值因为行人走动时相邻帧框一般有70%以上重叠如果摄像头帧率较低或画面里人较多可以降到0.25但太低会把不同目标串成同一条轨迹。这段代码只用于说明匹配顺序实际工程里我会用ByteTrack替换但首帧关联的思路一致。5.2 用驻留帧数判定徘徊min_frames按摄像头帧率换算单帧检测框串成了track之后徘徊判定的核心参数只有一个——min_frames。目标连续出现超过一定帧数且没有离开画面就触发“行走-观望”事件min_frames 75 for tid, boxes in tracks.items(): if len(boxes) min_frames: continue events.append({ track_id: tid, start_frame: first_frame_of_track, end_frame: last_frame_of_track, action: walking_looking, duration_sec: len(boxes) / fps })min_frames必须按摄像头帧率换算。25fps下75帧是3秒符合“短暂逗留”的直觉如果是15fps的老监控设备同样3秒只需要45帧。这个参数和你画的ROI区域大小强相关——闸机前的小区域10秒不离开才算徘徊大厅开阔区域3秒就该报警。数据集本身只能帮你把检测做稳这个阈值需要根据现场业务场景去定是徘徊检测里最说不清但最关键的玄学参数。5.3 用json验证事件判定逻辑我之前做过一个卡壳的点检测和跟踪都正常但报警事件总是提前或延后触发。后来把事件的开始帧和结束帧导出成json和原数据集json里的行为事件字段做逐一对齐问题一目了然。验证方法也很简单——检查你的start_frame和json标注的start_frame差值连续样本差在正负10帧以内就算命中。json在这个阶段的价值不再是为检测训练提供坐标而是成为评估徘徊判定逻辑的基准。我在这类项目上交过几回学费最贵的一回是过度迷信单帧mAP忽略了对track连续性的验证结果上线后一条轨迹断成三段误报漏报齐飞。现在的习惯是先把“检测召回率做足”再把“行为判定交给时间”不指望单帧分类器输出行为语义。这套思路配合这类三种标签的数据集会走得更顺。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网