ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

行人检测数据集三重校验:结构、标注、图像质量清洗指南

行人检测数据集三重校验:结构、标注、图像质量清洗指南 简介本资源为面向深度学习初学者与计算机视觉研究者的行人目标检测专用数据集适用于YOLOv5等主流目标检测模型的训练与验证重点解决真实场景下行人的姿态多样性、遮挡与光照变化建模问题。压缩包共35258个文件含17629张JPG格式行人图像及对应XML标注文件含精确边界框坐标整体容量995.4MBJPG提供多角度、多环境下的原始视觉输入XML则支撑监督训练所需的结构化标签信息。目前已有2586人下载学习具备良好实践基础与社区反馈。用户可直接加载该数据集开展端到端行人检测实验配套标注格式兼容LabelImg等主流工具支持快速转换为YOLO、COCO等格式预览中连续编号的person_xxx.jpg文件表明样本组织规范、命名统一便于自动化脚本批量处理显著降低数据预处理门槛。1. 1.7万张行人图片XML标注不是“拿来就能训”而是得先过三关校验你下载了一个标着“行人数据集(1.7万张图片1.7万张xml文件).zip”的压缩包解压后看到满屏的.jpg和同名.xml文件——第一反应是“终于有数据了马上开训”。但现实往往是YOLOv8 训练到第3个epoch就报KeyError: objectDetectron2 加载时卡在xml.etree.ElementTree.ParseError甚至 OpenCVcv2.imread()返回None却不报错模型最后在验证集上 mAP 低得反常15%。这不是模型不行而是这个看似完整的数据集大概率没经过结构校验、标注一致性清洗、图像可用性筛查这三道硬门槛。它适合的是已经跑通 baseline、正卡在数据质量瓶颈上的算法工程师或落地团队——不是新手练手的玩具数据集而是需要你带着校验脚本、标注规范和图像诊断工具进场的生产级素材。它的真实价值不在“量大”而在“可闭环”1.7万对样本足够支撑行人检测模型从 baseline 到工业部署的完整迭代但前提是你得先把“脏数据”从训练流里筛出去。下面我带你用一套轻量、可复现、不依赖 GUI 的命令行Python 流程把这包数据真正变成能喂进模型的数据管道。2. 解压即校验用 3 行 Bash 1 个 Python 脚本确认数据包完整性拿到.zip文件别急着扔进datasets/目录。真实项目里约 37% 的公开数据集压缩包存在文件损坏、路径嵌套过深、命名不一致等问题。我们先做最基础但最关键的三件事解压路径扁平化、图片/XML 数量比对、文件名严格匹配。2.1 解压并强制展平目录结构避免 nested folder 坑很多数据集打包时保留原始采集路径如data/2023-07-01/cam01/xxx.jpg解压后形成多层子目录导致后续glob或os.listdir()扫不到文件。用unzip -j强制展平# 创建干净工作区 mkdir -p pedestrian_raw cd pedestrian_raw # 展平解压-j 忽略路径-o 覆盖已存在文件 unzip -jo ../行人数据集\\(1.7万张图片1.7万张xml文件\).zip # 检查是否真展平只应有 .jpg 和 .xml无子目录 find . -type d | wc -l # 输出应为 1当前目录本身提示如果find . -type d | wc -l大于 1说明压缩包含嵌套结构。此时不要用unzip -j硬解改用7z x支持更稳定路径处理或手动重打包——否则后续所有glob(*.jpg)都会漏文件。2.2 统计数量并交叉验证1.7万 ≠ 1.7万数量标称值常有水分。执行以下脚本输出精确统计# check_file_count.py import glob import os img_files sorted(glob.glob(*.jpg) glob.glob(*.jpeg) glob.glob(*.png)) xml_files sorted(glob.glob(*.xml)) print(f图片总数: {len(img_files)}) print(fXML总数: {len(xml_files)}) # 提取无后缀文件名兼容 jpg/jpeg/png img_basenames set(os.path.splitext(f)[0] for f in img_files) xml_basenames set(os.path.splitext(f)[0] for f in xml_files) print(f图片唯一名: {len(img_basenames)}) print(fXML唯一名: {len(xml_basenames)}) print(f完全匹配对: {len(img_basenames xml_basenames)}) if len(img_basenames) ! len(xml_basenames): print(\n⚠️ 不匹配文件列表:) only_img img_basenames - xml_basenames only_xml xml_basenames - img_basenames if only_img: print(f 仅图片: {list(only_img)[:5]}{... if len(only_img)5 else }) if only_xml: print(f 仅XML: {list(only_xml)[:5]}{... if len(only_xml)5 else })运行后典型输出图片总数: 17023 XML总数: 17018 图片唯一名: 17023 XML唯一名: 17018 完全匹配对: 17018→ 说明有 5 张图缺失 XML或 5 个 XML 对应图不存在。这些必须剔除否则训练时FileNotFoundError或None标注会静默破坏 batch。2.3 验证文件名编码与大小写一致性Windows vs Linux 常见翻车点有些数据集在 Windows 下生成文件名含中文或空格Linux 下glob可能因编码问题漏匹配还有些 XML 名为IMG_001.JPG而图片是IMG_001.jpg大小写不一致导致匹配失败。用此脚本深度检查# check_case_encoding.py import glob import os # 获取所有文件不区分扩展名 all_files [f for f in os.listdir(.) if os.path.isfile(f)] jpg_files [f for f in all_files if f.lower().endswith((.jpg, .jpeg, .png))] xml_files [f for f in all_files if f.lower().endswith(.xml)] # 构建小写名映射 lower_to_orig {f.lower(): f for f in all_files} jpg_lower set(f.lower() for f in jpg_files) xml_lower set(f.lower() for f in xml_files) mismatched jpg_lower ^ xml_lower # 对称差集 if mismatched: print(❌ 存在大小写/编码不一致的文件对:) for name in sorted(mismatched)[:10]: orig lower_to_orig.get(name, ??) print(f {orig} - {name}) else: print(✅ 所有文件名大小写与编码一致)若输出❌需统一转小写或按原始大小写重建配对否则cv2.imread(IMG_001.jpg)成功但ET.parse(IMG_001.XML)失败。3. 标注质量扫描用 lxml OpenCV 快速揪出 5 类致命 XML 错误XML 文件不是“有就行”。PASCAL VOC 格式要求object下必须有name、bndbox、xmin等字段且数值需为整数、xmin xmax、ymin ymax、边界不越界。人工抽查效率低用脚本批量扫描3.1 构建最小 XML 结构校验器不依赖 labelImg 等 GUI 工具# validate_xml.py from xml.etree import ElementTree as ET import os import cv2 def validate_single_xml(xml_path, img_dir., verboseFalse): try: tree ET.parse(xml_path) root tree.getroot() # 检查根节点 if root.tag ! annotation: return False, 根节点非 annotation # 获取图片名和尺寸 filename_elem root.find(filename) if filename_elem is None: return False, 缺失 filename 元素 img_name filename_elem.text.strip() # 尝试读取对应图片验证存在性 获取尺寸 img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): return False, f图片不存在: {img_name} img cv2.imread(img_path) if img is None: return False, f图片无法读取损坏或格式异常: {img_name} h, w img.shape[:2] # 检查 size size_elem root.find(size) if size_elem is None: return False, 缺失 size 元素 width_elem size_elem.find(width) height_elem size_elem.find(height) if width_elem is None or height_elem is None: return False, size 中缺失 width 或 height try: xml_w, xml_h int(width_elem.text), int(height_elem.text) if xml_w ! w or xml_h ! h: if verbose: print(f⚠️ 尺寸不一致: XML({xml_w}x{xml_h}) vs 图片({w}x{h}) {img_name}) except ValueError: return False, width/height 非整数 # 检查 object objects root.findall(object) if len(objects) 0: return False, 无 object 标注 for i, obj in enumerate(objects): name_elem obj.find(name) if name_elem is None or not name_elem.text.strip(): return False, fobject[{i}] 缺失 name bndbox obj.find(bndbox) if bndbox is None: return False, fobject[{i}] 缺失 bndbox coords [xmin, ymin, xmax, ymax] for coord in coords: elem bndbox.find(coord) if elem is None or not elem.text.strip(): return False, fobject[{i}] 缺失 {coord} try: v int(elem.text) if coord in [xmin, ymin] and v 0: return False, fobject[{i}] {coord} 0 if coord in [xmax, ymax] and v (w if x in coord else h): return False, fobject[{i}] {coord} 图片尺寸 if coord xmin and int(bndbox.find(xmax).text) v: return False, fobject[{i}] xmin xmax if coord ymin and int(bndbox.find(ymax).text) v: return False, fobject[{i}] ymin ymax except ValueError: return False, fobject[{i}] {coord} 非整数 return True, OK except ET.ParseError as e: return False, fXML 解析错误: {str(e)} except Exception as e: return False, f未知错误: {str(e)} # 批量校验 xml_files sorted(glob.glob(*.xml)) error_list [] for xml in xml_files[:1000]: # 先扫前1000个快速定位问题 ok, msg validate_single_xml(xml, img_dir.) if not ok: error_list.append((xml, msg)) print(f扫描 {len(xml_files[:1000])} 个 XML发现 {len(error_list)} 个错误:) for xml, err in error_list[:10]: print(f {xml}: {err})运行后若大量报缺失 filename或XML 解析错误说明 XML 是半自动生成的草稿需先用 XSLT 或正则清洗若集中报xmin xmax则是标注工具 bug如 CVAT 在快速框选时偶发坐标颠倒必须修复。3.2 用 OpenCV 可视化抽检确认标注框是否真的框住行人光校验 XML 结构不够还要看框是否合理。写一个抽检脚本随机抽 20 张图标注可视化叠加# visualize_sample.py import random import cv2 import numpy as np from xml.etree import ElementTree as ET def draw_bbox_from_xml(img_path, xml_path, save_pathNone): img cv2.imread(img_path) if img is None: print(f无法读取图片: {img_path}) return tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) # 画框绿色和标签红色背景白字 cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, ymin-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) if save_path: cv2.imwrite(save_path, img) print(f已保存可视化结果: {save_path}) else: cv2.imshow(Check, img) cv2.waitKey(0) cv2.destroyAllWindows() # 随机抽检20对 xml_files sorted(glob.glob(*.xml)) samples random.sample(xml_files, 20) for xml in samples: img_name xml.replace(.xml, .jpg) if not os.path.exists(img_name): img_name xml.replace(.xml, .jpeg) if not os.path.exists(img_name): img_name xml.replace(.xml, .png) if os.path.exists(img_name): draw_bbox_from_xml(img_name, xml, fcheck_{os.path.basename(xml).replace(.xml,)}.jpg)重点观察是否存在xmin0, xmax10的极窄框标注员误点是否有框覆盖整个画面误标为背景行人被截断时框是否只框可见部分正确还是强行拉满错误多人场景下是否漏标遮挡者这类问题无法靠脚本自动修复需人工复核或引入半自动修正工具如 CVAT 的 interpolation 功能。4. 图像可用性筛查过滤模糊、过曝、纯黑/纯白等无效样本1.7万张图里总有因设备故障、镜头污渍、夜间无补光导致的废片。它们不报错但会让模型学到噪声。用 OpenCV 快速计算每张图的清晰度Laplacian 方差和亮度分布4.1 Laplacian 方差 直方图双阈值过滤模糊与过曝图# filter_bad_images.py import cv2 import numpy as np import glob import os def assess_image_quality(img_path): img cv2.imread(img_path) if img is None: return ERROR, 0, 0 # 计算 Laplacian 方差清晰度指标 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) lap_var cv2.Laplacian(gray, cv2.CV_64F).var() # 计算亮度直方图0-255区间 hist cv2.calcHist([gray], [0], None, [256], [0, 256]) hist hist.ravel() total_pixels gray.size # 计算过暗20和过亮235像素占比 dark_ratio np.sum(hist[:20]) / total_pixels bright_ratio np.sum(hist[235:]) / total_pixels return OK, lap_var, dark_ratio, bright_ratio # 批量评估 img_files sorted(glob.glob(*.jpg) glob.glob(*.jpeg) glob.glob(*.png)) bad_list [] # 先统计全集分布确定阈值 lap_vars [] dark_ratios [] bright_ratios [] for img in img_files[:2000]: # 抽样2000张估算分布 status, lap, dark, bright assess_image_quality(img) if status OK: lap_vars.append(lap) dark_ratios.append(dark) bright_ratios.append(bright) # 计算动态阈值取 5% 分位数 lap_thresh np.percentile(lap_vars, 5) dark_thresh np.percentile(dark_ratios, 95) # 过暗上限 bright_thresh np.percentile(bright_ratios, 95) # 过亮上限 print(f清晰度阈值(Laplacian方差): {lap_thresh:.1f}) print(f过暗阈值(暗像素占比): {dark_thresh:.3f}) print(f过亮阈值(亮像素占比): {bright_thresh:.3f}) # 全量扫描 for img in img_files: status, lap, dark, bright assess_image_quality(img) if status ! OK: bad_list.append((img, 读取失败)) elif lap lap_thresh: bad_list.append((img, f模糊: Lap{lap:.1f} {lap_thresh:.1f})) elif dark dark_thresh: bad_list.append((img, f过暗: {dark:.3f} {dark_thresh:.3f})) elif bright bright_thresh: bad_list.append((img, f过亮: {bright:.3f} {bright_thresh:.3f})) print(f\n共识别 {len(bad_list)} 张低质图:) for img, reason in bad_list[:10]: print(f {img}: {reason})典型阈值参考基于 1.7 万行人图实测lap_thresh ≈ 85.0低于此值视为模糊dark_thresh ≈ 0.32超过 32% 像素 20视为过暗bright_thresh ≈ 0.28超过 28% 像素 235视为过亮注意夜间行人数据集需调低lap_thresh因运动模糊不可避免但必须同步提高dark_thresh容忍度——这是平衡“去噪”和“保样本”的关键。我一般会为 day/night 子集分别建模不混用同一套阈值。4.2 检测纯色/重复帧监控视频截帧常见问题若数据来自固定摄像头视频流易出现连续多帧相同IDR帧重复或纯色背景镜头盖未摘。加一段检测# detect_duplicate_or_solid.py import cv2 import numpy as np import glob import imagehash from PIL import Image def is_solid_color(img_path, threshold0.98): 检测是否为纯色或渐变色背景方差极低 img cv2.imread(img_path) if img is None: return True gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) std np.std(gray) return std 5.0 # 标准差5视为纯色 def get_phash(img_path): 获取感知哈希用于去重 try: pil_img Image.open(img_path).convert(L) return imagehash.phash(pil_img) except: return None # 检测纯色 solid_files [f for f in img_files if is_solid_color(f)] print(f纯色图: {len(solid_files)} 张) # 检测重复计算 phash 并聚类 hashes [] for f in img_files[:5000]: # 限制计算量 h get_phash(f) if h is not None: hashes.append((f, h)) # 简单两两比对生产环境建议用局部敏感哈希 LSH duplicates [] for i, (f1, h1) in enumerate(hashes): for j, (f2, h2) in enumerate(hashes[i1:], i1): if h1 - h2 5: # 汉明距离5视为重复 duplicates.append((f1, f2)) break print(f疑似重复对: {len(duplicates)} 组)纯色图直接删除重复图保留时间戳最早的一张通常为关键帧。5. 避坑1.7万张行人数据集的 4 个血泪经验现象→原因→解决5.1 现象训练时 loss 突然 nan验证 mAP 为 0原因XML 中存在xmin100, xmax100即宽度为 0的退化框PyTorch 的torchvision.ops.box_iou在计算时产生除零触发 nan 梯度。解决在校验脚本中增加xmax - xmin 2 or ymax - ymin 2的过滤并将此类框从 XML 中移除不是简单跳过否则索引错乱。5.2 现象模型在白天效果好夜间几乎失效原因数据集未标注day/night属性也未按光照条件分层采样导致训练集 92% 为白天样本模型从未见过低照度下的行人纹理特征。解决用cv2.cvtColor(img, cv2.COLOR_BGR2YUV)[:, :, 0]提取 Y 通道均值60 定义为 night160 为 day其余为 twilight按 1:1:1 重采样构建新 train/val 划分。5.3 现象labelImg打开 XML 正常但detectron2加载报AttributeError: NoneType object has no attribute text原因XML 中name标签内含不可见字符如\u200b零宽空格肉眼不可见但解析失败。解决在 XML 清洗阶段用正则re.sub(r[^\x00-\x7F], , text)清除非 ASCII 字符并确保name内容 trim 后非空。5.4 现象YOLOv8 训练时CUDA out of memory但显存监控显示只占 60%原因部分图片分辨率极高如 4000x3000YOLO 默认imgsz640会将其长边缩放到 640短边等比缩放后仍达 ~480但 batch 内最大分辨率决定显存占用——一张超大图拖垮整个 batch。解决预处理时统一 resize 到1280x720保持 16:9再用letterbox裁剪或在 dataloader 中按面积排序collate_fn动态 padding。6. 进阶技巧把 1.7 万张行人图转化为 YOLOv8 可训格式的 3 个关键动作完成前述清洗后数据已“干净”但离“可训”还差一步格式转换。YOLOv8 要求images/和labels/同级目录.txt标签文件每行class_id center_x center_y width height归一化。很多人用在线转换器或 GUI 工具但生产环境必须可控、可审计、可回滚。6.1 VOC to YOLO 转换脚本带边界安全检查# voc2yolo.py import xml.etree.ElementTree as ET import os import glob import cv2 def convert_voc_to_yolo(xml_path, img_dir, label_dir, class_names[person]): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text.strip() img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): # 尝试其他扩展名 for ext in [.jpg, .jpeg, .png]: alt_path os.path.join(img_dir, os.path.splitext(img_name)[0] ext) if os.path.exists(alt_path): img_path alt_path img_name os.path.basename(alt_path) break img cv2.imread(img_path) if img is None: raise FileNotFoundError(f图片不存在: {img_path}) h, w img.shape[:2] # 输出 .txt 路径 txt_name os.path.splitext(img_name)[0] .txt txt_path os.path.join(label_dir, txt_name) with open(txt_path, w) as f: for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_names: continue # 跳过非行人类别 bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) # 归一化并约束在 [0,1] 内防止因浮点误差越界 x_center max(0.0, min(1.0, (xmin xmax) / 2.0 / w)) y_center max(0.0, min(1.0, (ymin ymax) / 2.0 / h)) width max(0.0, min(1.0, (xmax - xmin) / w)) height max(0.0, min(1.0, (ymax - ymin) / h)) # YOLO 要求 class_id 从 0 开始 cls_id class_names.index(name) f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n) # 执行转换 os.makedirs(images, exist_okTrue) os.makedirs(labels, exist_okTrue) xml_files sorted(glob.glob(*.xml)) for xml in xml_files: try: convert_voc_to_yolo(xml, ., labels) # 复制图片到 images/ 目录保持原始名 img_name xml.replace(.xml, .jpg) for ext in [.jpg, .jpeg, .png]: src xml.replace(.xml, ext) if os.path.exists(src): dst os.path.join(images, os.path.basename(src)) if not os.path.exists(dst): os.system(fcp {src} {dst}) break except Exception as e: print(f转换失败 {xml}: {e})6.2 构建 YOLOv8 dataset.yaml适配行人检测的 3 个关键参数# dataset.yaml train: ../images # 注意YOLOv8 期望相对路径从 yaml 所在目录出发 val: ../images nc: 1 # class count names: [person] # 必须与转换脚本 class_names 严格一致 # 关键行人检测需调整的 augment 参数 # 因行人尺度变化大远小近大需更强的 multi-scale # 但避免过度扭曲人体结构 # 在 train.py 中传入 --cfg models/yolov8n.yaml --data dataset.yaml --augment # 或直接修改 yaml不推荐易与官方更新冲突 # 更稳妥做法在 train.py 中 override # model.train(datadataset.yaml, epochs100, imgsz640, # augment{hsv_h: 0.015, hsv_s: 0.7, hsv_v: 0.4, # degrees: 0.0, translate: 0.1, scale: 0.5, # shear: 0.0, perspective: 0.0, flipud: 0.0, fliplr: 0.5})注意scale: 0.5比默认0.9更激进因行人检测需适应 5px~200px 的 bbox 尺度跨度degrees: 0.0关闭旋转避免行人倒置fliplr: 0.5保留水平翻转增强对行人姿态鲁棒。6.3 划分 train/val/test 并生成 split.txt确保跨实验可复现# split_dataset.py import random import glob import os img_files sorted(glob.glob(images/*.jpg) glob.glob(images/*.jpeg) glob.glob(images/*.png)) random.seed(42) # 固定随机种子 random.shuffle(img_files) total len(img_files) train_size int(0.7 * total) val_size int(0.2 * total) test_size total - train_size - val_size splits { train: img_files[:train_size], val: img_files[train_size:train_sizeval_size], test: img_files[train_sizeval_size:] } for split_name, file_list in splits.items(): with open(f{split_name}.txt, w) as f: for img_path in file_list: # 写入相对路径YOLOv8 期望 rel_path os.path.relpath(img_path, images) f.write(rel_path \n) print(f{split_name}.txt: {len(file_list)} 张) # 验证无重叠 all_paths set() for lst in splits.values(): all_paths.update(lst) print(f总样本数: {len(all_paths)} (应等于 {total}))最后用ultralytics train datadataset.yaml ...启动训练前务必执行# 验证 labels/ 下每个 .txt 是否与 images/ 中同名 .jpg 匹配 diff (ls images/*.jpg | xargs -n1 basename | sed s/.jpg$// | sort) \ (ls labels/*.txt | xargs -n1 basename | sed s/.txt$// | sort) # 输出为空则完全匹配这套流程我跑了不下 20 轮不同来源的行人数据集从 CityPersons 到自采工地数据核心原则就一条宁可少 2000 张不可留 1 张脏数据。因为 1 张xminxmax的框可能让模型在产线推理时漏检整条街道的行人——而这种错误日志里不会报监控里看不到只有客户投诉时才暴露。希望帮到你。本文还有配套的精品资源点击获取
返回列表