简介:从WIDER Face中筛选出的B子集大目标人脸检测数据集,包含8188张jpg图片及对应标注,仅face一个类别,总标注框14649个。所有目标的像素面积均大于3500,专门面向近距离、大脸检测场景,可显著减少远距离小目标带来的误检,适合刚接触目标检测的学习者快速上手,也便于中高级开发者做数据增强与模型调优。压缩包共2000个文件,含1999个VOC格式xml标注文件和1个txt使用说明,xml可直接转为YOLO等格式,配合原图即可接入主流检测框架;包体862.16MB,已有468人学习下载。标注由labelImg完成,矩形框规范,并附使用前必读说明,便于快速了解目录与格式。对需要补充大目标人脸样本、验证检测精度的项目,这份筛选后的数据能节省大量清洗与标注时间,让研究者更专注于模型训练和效果优化。
人脸检测数据集从零搭建:WIDER FACE大目标子集8188张的VOC转YOLO全流程
1. 先聊聊这个数据集能帮你解决什么问题
做人脸检测的工程师几乎都绕不开WIDER FACE,但真正动手时你会发现,原版数据集32203张图像里大量都是小目标——街拍路人、密集人群,一个脸可能只有十几个像素。训练出来的模型在监控、门禁、闸机这种人脸占比大的场景下反而表现平平。标题里这个8188张大目标子集,就是把WIDER FACE里人脸面积占比较大的样本筛出来,统一转成VOC和YOLO两种标注格式,省掉你自己写脚本筛数据、转格式的时间。这套东西适合三类人:一是刚入门目标检测、想拿人脸数据练手YOLOv5/v8的;二是被小目标折磨、需要一个纯大目标数据集做对比实验的;三是公司项目急着上线、标注预算有限想白嫖公开数据的。我拿到这个.7z解压后第一反应是:训练集、验证集、测试集分好了没有、标注坐标是绝对坐标还是归一化坐标、类别名是face还是person——这三个问题直接决定你要不要额外写脚本。下面从数据构成、格式转换、训练验证到踩坑排查,一条线讲完。
2. WIDER FACE大目标子集的数据构成:先搞清8188张和类别1的真面目
2.1 原版WIDER FACE与这个子集的差异
原版WIDER FACE按事件类型分成61个场景,训练集、验证集、测试集比例大约是40%训练、10%验证、50%测试。但原版测试集的标注是不公开的,你只能通过提交到评测服务器拿结果。这个8188张的大目标子集应该是从原版训练集和验证集里筛出来的——注意,如果作者公开了测试集标注,那另说,但大多数第三方处理版本只动train和val。我做目标检测数据集处理时,第一件事永远是数图像张数和标注框数是否对得上。8188张"1类别"意味着标注里只有face这一类,不像VOC原始数据集有person、car、dog等20类。你要是有强迫症,解压后跑一条命令核对:
find . -name "*.xml" | wc -l find . -name "*.txt" | wc -l如果XML文件数和TXT文件数不一致,说明转换脚本漏了某些样本或标注为空被跳过了。我遇到过转换时丢掉空标注样本导致总数对不上的情况——不是bug,是脚本逻辑里刻意忽略了无目标的图像,这在训练时其实合理,因为YOLO不支持负样本图像。但你要是拿验证集做mAP评估,丢了空标注图像会虚高——硬编码往每张图塞一个背景框才是错上加错。
2.2 训练集、验证集、测试集怎么分
大多数这类打包好的数据集,目录结构是images/train、images/val、images/test、labels/train、labels/val、labels/test这样的对位结构。YOLO训练时你需要的是images和labels两个分支下的对应子目录一致。我自己习惯的做法是拿8188张按8:1:1切,但前提是作者没有已经切好。如果已经有划分,就用它的划分,不要自己重新随机打乱——除非你要做跨数据集对比,必须保证划分方式和原论文一致。这里有个关键点:WIDER FACE原版的验证集有3226张,压根不是8188张按比例切出来的。这个子集如果是从原版val里筛大目标,那验证集就小得多。你训练完模型如果想在原版WIDER FACE验证集上对比SOTA,得找原版标注。我一般把两个验证集都留一份:一个用于日常迭代,一个用于最终对比。
2.3 图像分辨率分布和目标的"大"是怎么定义的
大目标的定义通常有三种口径:一是目标框面积占图像总面积的比例,二是目标框短边像素数,三是目标框面积绝对值。WIDER FACE原版有个有趣现象:高度在30像素以下的人脸占了相当比例,很多脸在20×20以下。我自己做行人检测时习惯用短边大于等于32像素作为小目标和大目标的分界线。这个子集如果要叫"大目标",作者很可能是按面积比例筛的——比如人脸框面积占图像面积10%以上。解压后你可以自己验证一下,用Python快速统计:
import os from PIL import Image img_dir = "images/train" min_face_ratio = 0.05 stats = {"total": 0, "big": 0} for name in os.listdir(img_dir): img = Image.open(os.path.join(img_dir, name)) w, h = img.size # 这里根据自己的标注格式读取 GT # 统计人脸框面积 / 图像面积 的分布 stats["total"] += 1 print(stats)这个统计有两个用途:确认数据符合"大目标"预期;后面训练时如果发现模型收敛异常,回头查这个分布。很多人拿到数据集不检查分布就直接train,结果训练集里混了一些极小目标,导致Anchor尺寸设置失配。你要真图省事,记住一条结论:大目标数据集用默认Anchor基本没问题,但如果你要把这个数据集和其他小目标数据混合训练,Anchor就得重新聚类。
3. 从WIDER FACE原格式到VOC再到YOLO:转换脚本与格式边界
3.1 WIDER FACE原始标注的存储方式
WIDER FACE官方给的标注是一个txt文件,格式是:图像文件名、人脸框数量、然后每个框一行(x1, y1, w, h, blur, expression, illumination, invalid, occlusion, pose)。其中x1、y1是左上角坐标,w、h是框宽高。blur、occlusion这些属性在做人脸检测时大部分工程师直接忽略——你是做检测不是做属性分类,只有invalid标记值得注意:invalid=True的框说明这张脸几乎看不清或严重遮挡,严格做评测时应该剔掉。标题里的8188张大目标子集如果已经帮你过滤了invalid框,那就省事了。如果没过滤,你转换时必须决定:全保留会造成模型学到脏标注;全部过滤可能把难例都丢了,模型泛化变差。我的经验是训练集保留部分困难样本,验证集过滤掉invalid,不然mAP会有虚低。
3.2 VOC标注格式的XML结构
VOC格式就是把标注写成XML,每个object节点包含name、pose、truncated、difficult、bndbox(xmin、ymin、xmax、ymax)。注意bndbox是左上角右下角(绝对值),不是中心点。WIDER FACE给的x1,y1,w,h转换时要用xmax = x1 + w,ymax = y1 + h。好多人在这一步翻车,把w直接当xmax用,结果训练时损失爆炸。我自己的习惯是转换脚本里打印几个样本的坐标做人工核对,解压一张图画框看看。
3.3 从VOC转YOLO格式的Python脚本
假设你已经有了VOC格式的XML,转YOLO就是把bndbox归一化成中心点坐标和宽高。YOLO的标签文件每行是:class_id x_center y_center width height,其中x_center、y_center、width、height都是除以图像宽高后的浮点数。核心代码就这几行:
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_path, out_label_path, class_list): tree = ET.parse(xml_path) root = tree.getroot() img = Image.open(img_path) img_w, img_h = img.size with open(out_label_path, "w") as f: for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in class_list: continue cls_id = class_list.index(cls_name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 越界裁剪:防止标注超出图像边界 xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) box_w = xmax - xmin box_h = ymax - ymin if box_w <= 0 or box_h <= 0: continue x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h norm_w = box_w / img_w norm_h = box_h / img_h f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}\n") # 使用示例 class_list = ["face"] voc_to_yolo("000001.xml", "000001.jpg", "000001.txt", class_list)这段脚本做了三件容易被忽略的事:第一,越界裁剪——WIDER FACE原版标注偶尔有框超出图像边界的情况,不裁剪YOLO训练时会报错或产生错误anchor;第二,过滤掉宽高非正的框——某些标注文件里会出现width=0的脏数据;第三,归一化输出保留6位小数——精度太高文件变大,太低两个目标坐标太接近会合并。class_list只放face一个类,也就对应标题里的"1类别"。
3.4 批量转换脚本:目录结构与文件命名对齐
单张图转换没问题后,批量转换要注意保持文件名一一对应。常见目录结构是这样:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── voc_xmls/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/批量脚本加上进度输出和日志,跑完检查零字节文件:
import os from glob import glob def batch_voc_to_yolo(xml_root, img_root, out_root, class_list): xml_files = sorted(glob(os.path.join(xml_root, "*.xml"))) total, skipped = 0, 0 for xml_path in xml_files: base = os.path.splitext(os.path.basename(xml_path))[0] img_candidates = [".jpg", ".jpeg", ".png"] img_path = None for ext in img_candidates: cand = os.path.join(img_root, base + ext) if os.path.exists(cand): img_path = cand break if img_path is None: skipped += 1 print(f"[SKIP] no image for {xml_path}") continue voc_to_yolo(xml_path, img_path, os.path.join(out_root, base + ".txt"), class_list) total += 1 print(f"converted: {total}, skipped: {skipped}")注意我特意加了找不到对应图像时的跳过分支——在实际数据集中,偶尔会有标注文件存在但原图丢失的情况。这类坏样本如果直接灌给YOLO训练,会在读取图像时报FileNotFoundError,打断整个训练流程,别问我怎么知道的,跑过一次半夜的分布式训练你就懂了。
4. 用YOLOv5/v8训练大目标人脸检测:data.yaml与超参数调整
4.1 数据集配置文件data.yaml怎么写
有了images和labels目录,YOLO训练还需要一个data.yaml文件指定路径和类别信息:
# data.yaml train: /path/to/dataset/images/train val: /path/to/dataset/images/val test: /path/to/dataset/images/test nc: 1 names: ['face']三个关键细节:路径用绝对路径还是相对路径——我一般用绝对路径,避免工作目录切换导致相对路径失效;train和val不能指向同一个目录,否则验证结果没有参考意义;nc必须和names列表长度一致,这里nc=1、names=['face']。有些人从别人项目里复制data.yaml没改路径,训练半天发现迭代了0张图,就是这个原因。
4.2 Anchor尺寸:大目标数据集要不要重新聚类
YOLO默认anchor是针对COCO数据集聚类的,里面从小到大的目标都有。这个大目标数据集人脸占比大,默认anchor可能偏小。YOLOv5训练时如果用了--autoanchor参数,会自动用K-Means重新聚类。YOLOv8不暴露anchor参数,它在Decoder里用Anchor-Free的方式,对anchor的依赖小得多。如果你用YOLOv5,我建议开--autoanchor,省得手动改anchor。聚类前先想想你的"大目标"到底多大——如果人脸占比大,通常聚出来的anchor会偏大,模型收敛更快。如果你在训练日志里看到指标震荡,可以试试固定anchor,看看是不是聚类结果导致训练不稳定。
4.3 训练命令和参数调优
训练命令用YOLOv5举例:
cd yolov5 python train.py --data /path/to/data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --autoanchor这里有几个参数值得解释。--img 640:输入分辨率,大目标数据集用640够用,没必要上1280,除非你的部署设备性能余量大且目标对细节要求高。--batch 16:按你的显存来,16G显存跑yolov5s是够的;如果显存不足,降batch但别降分辨率,分辨率对精度影响更大。--epochs 100:这个数据量级别100轮能收敛,配合早停机制可以提前结束。数据集只有1个类别且目标较大,yolov5s这个骨干就够用,直接上yolov5x反而容易过拟合。
4.4 Loss曲线怎么看
训练过程中主要盯三件事:box_loss、obj_loss、cls_loss。因为只有face一个类别,cls_loss会很低,重点看box_loss和obj_loss是否同步下降。如果box_loss下降但obj_loss不变,大概率是正负样本不平衡——大目标数量少但面积大,模型倾向于预测背景,这时可以调大anchor阈值或检查数据增强是否翻转了目标过多次导致目标移出图像边界。还有个常见现象:train_loss下降但val_loss震荡,这是过拟合信号,可以减少epoch或加大数据增强里的flip、mosaic概率。大目标数据集的mosaic增强特别有用,因为拼接后目标相对于新图变小了,等于免费补充了一部分中等目标样本。
5. 训练与验证中的数据检查:避坑、常见问题与排查清单
5.1 标注错位:图像方向导致的人脸框偏移
现象:训练时loss一直在高位降不下来,或验证集mAP极低但loss正常。原因:某些图像样本是旋转过的(尤其是手机拍摄的竖屏照片),而标注没有跟着旋转;或者转换脚本归一化时用了错误的图像宽高。解决:训练前写脚本随机抽样100张图,把GT框画上去,人眼检查是否有明显错位。YOLOv5自带一个plot_labels工具,训练时会在runs目录生成标注可视化图,第一轮训练完先看这个再决定要不要停。
5.2 零字节标签文件
现象:labels目录里存在0字节txt文件,YOLO训练时某些实现会报错,提示corrupt label。原因:原标注为空但XML仍然生成了;或者原图本身没有目标,转换脚本没过滤。解决:批量删除空文件再训练。YOLOv5训练时遇到空标签文件不会崩,但会跳过该图像的loss计算,导致有效样本数减少。用find命令处理:
find /path/to/labels -name "*.txt" -size 0 -delete删除之后和images目录对比一下文件数量,确认图像和标签一一对应。
5.3 类别编号错误
现象:训练正常但验证时结果全是0或全是背景框。原因:data.yaml里names是['face'],而转换脚本里class_id用的0没问题;但如果数据集作者给的label是face=1,你没改class_id就开始训练,模型学的是背景。解决:打开一个txt标签文件看class_id是多少——0是YOLO默认从0开始计数的人做法;如果看到1,说明作者按VOC的1-based class标注方式做,你得批量减1或改data.yaml。这个坑我踩过不止一次,尤其是混合多个数据源时最容易犯。
5.4 图像尺寸不一致的batch训练报错
现象:训练到几个epoch时报错,类似Expected 3D tensor but got 4D。原因:数据集中混入了一通道灰度图或带Alpha通道的PNG,而YOLO默认输入是三通道RGB。WIDER FACE本身全是JPEG彩色图,但如果这个子集后来被人转存过,就可能混入变体。解决:训练前统一转换,写一条脚本把非RGB图转成RGB后另存为jpg:
import os from PIL import Image def unify_rgb(src_dir, dst_dir): os.makedirs(dst_dir, exist_ok=True) for name in os.listdir(src_dir): img = Image.open(os.path.join(src_dir, name)) if img.mode != "RGB": img = img.convert("RGB") out_name = os.path.splitext(name)[0] + ".jpg" img.save(os.path.join(dst_dir, out_name), quality=95)这里convert("RGB")会把灰度图变成三通道复制,Alpha通道会被丢弃,正好满足YOLO的输入要求。注意保存时统一成jpg格式,免得后面加载时遇到. png大小写混乱的问题。
5.5 验证标准不一致导致mAP虚高
现象:自测mAP有0.95,换到别的评测脚本只剩0.8,差距大到无法接受。原因:验证时是否过滤了difficult样本、IoU阈值用的0.5还是0.75、是否把忽略区域(ignore regions)当成了负样本,这些都直接影响mAP数值。WIDER FACE原版评测有Easy/Medium/Hard三档,Hard档大量是极小人脸,并明确说模型不该在某些区域输出检测框(对应ignore)。如果你用这个8188大目标子集做验证,事情简单了:目标都大,没有ignore区域。但如果你想对比原版WIDER FACE的论文指标,就得换原版评测脚本重新评估。最稳妥的做法是:训练用这份大目标数据,验证用同一份数据的val子集做日常迭代,最终再拿原版WIDER FACE验证集做一次横向对比。
6. 从单卡实验到部署验证:TensorRT推理与实时性检查
训练完模型后,只盯着mAP不够,还要确认推理速度能满足实际场景。视频流人脸检测项目里,动不动就要回答"640分辨率用TensorRT的YOLO能跑多少路",这里给个我自己的基准思路:T4显卡上用TensorRT FP16的YOLOv5s,单路640×640输入大概1.5到3毫秒——纯GPU推理时间,不算前后处理和图像解码。如果要求25帧每秒(单路40毫秒预算),一路大概占GPU算力10%-20%,实际能扛多路取决于图像解码方式和CPU绑定。如果每路都解H.264,CPU先爆,轮不到GPU瓶颈。建议直接软解码到BGR再喂给推理引擎,CPU核心数不够时降分辨率到416或480反而更划算——你数据集是大目标,降到416也大概率不掉点。
验证方法用TensorRT导出的Python接口批量测试:
import tensorrt as trt import numpy as np def infer_on_dir(engine_path, img_dir): logger = trt.Logger(trt.Logger.WARNING) with open(engine_path, "rb") as f: engine_data = f.read() runtime = trt.Runtime(logger) engine = runtime.deserialize_cuda_engine(engine_data) context = engine.create_execution_context() import glob from PIL import Image imgs = sorted(glob.glob(os.path.join(img_dir, "*.jpg")))[:100] for img_path in imgs: img = Image.open(img_path).resize((640, 640)) arr = np.array(img).astype(np.float32) / 255.0 arr = arr.transpose(2, 0, 1)[None] # 这里绑定输入输出 buffer # 调用 context.execute_v2 推理实际做部署时,我习惯先批量跑100张求平均耗时,再套一个多线程框架模拟多路推流,观察显存是否波动过大和是否出现偶发延迟尖峰。人脸检测这种任务,精度差两三个点通常能接受,延迟抖动反而更影响用户体验。最后的经验:做数据集转换和训练验证时,每个环节都保留一份中间产物和一行日志,跑挂了能立刻定位到是哪一步出了问题。这类WIDER FACE大目标子集的价值在于比原版更贴合实际业务中的大脸场景,但你拿它训练之前,一定要先花20分钟做标注可视化核对——数据错漏导致的返工成本远高于这20分钟。希望帮到你。
本文还有配套的精品资源,点击获取