简介:这是一份面向计算机视觉开发者与目标检测学习者的厨师帽检测数据集,采用Pascal VOC标注格式,适用于餐饮后厨场景下的人员头部与厨师帽识别模型训练。数据集共2090张jpg图片,每张均配有同名xml标注文件,标注类别为chef hat与head两类,其中厨师帽标注框4174个、头部标注框1553个,使用labelImg工具按矩形框规则完成标注,可直接用于YOLO、Faster R-CNN等主流检测框架的数据加载与训练。压缩包内共约2000个文件,以2090个xml标注和2090个jpg图像为主,另附一份使用说明txt,整体大小174.79MB,目录结构简洁便于快速接入训练流程。目前已有1534人学习下载,作者还提供了基于yolov5训练模型的演示视频链接,方便读者直观了解标注效果与检测表现,适合需要快速获取高质量厨师帽检测数据的研究者与工程人员参考使用。
1. 厨师帽检测数据集VOC-2090张:后厨合规抓拍到底能不能直接训
后厨监控画面里,一顶白色厨师帽在蒸汽和油烟里时隐时现,这种场景做视觉检测的人多半都碰过。厨师帽检测数据集VOC-2090张,说的就是一份按Pascal VOC格式标注、总量2090张、专门用来训练“有没有戴帽子”这类二分类或单类目标检测模型的数据集。它解决的不是通用目标检测问题,而是后厨、食品加工车间、中央厨房这类场景里,对人员头部着装合规性的自动识别。适合谁用?做智慧食堂、明厨亮灶、餐饮连锁巡检系统的算法工程师,以及手头只有少量现场图、想先拿一份现成标注数据跑通baseline的开发者。但先泼一盆冷水:2090张在目标检测里属于小体量,直接拿来训一个从零开始的网络,大概率会翻车。它真正的价值在于快速验证流程、做迁移学习的起点,或者作为你自采数据前的格式参照。下面按“先看清数据长什么样、再动手跑通、最后避开标注和训练里的坑”这条线,把这份VOC厨师帽数据集讲透。
2. 拆开VOC-2090张:标注结构、类别分布与选型判断
2.1 VOC格式的目录骨架和XML字段含义
拿到一份标着VOC的数据集,第一件事不是急着写训练脚本,而是把目录结构和XML内容看清楚。Pascal VOC的标准结构是Annotations放XML、JPEGImages放原图、ImageSets/Main放划分文件。厨师帽检测这类单类别任务,XML里通常只有一个<object>,<name>字段写的是hat或者chef_hat这类标签名。你需要重点确认三件事:<size>里的宽高是否和实际图片一致、<bndbox>的坐标是不是1-based(VOC传统是1-based,转YOLO时要减1)、有没有<difficult>或<truncated>标记。很多网上流传的VOC数据集,XML是从别的工具导出的,坐标基准不统一,直接转格式会整体偏移一个像素,小目标上这个偏差很致命。
# 先看目录骨架,确认三件套是否齐全 find ./ChefHat_VOC2090 -maxdepth 2 -type d # 预期输出: # ./ChefHat_VOC2090/Annotations # ./ChefHat_VOC2090/JPEGImages # ./ChefHat_VOC2090/ImageSets/Main # 统计图片数量和XML数量是否对得上 ls ./ChefHat_VOC2090/JPEGImages | wc -l ls ./ChefHat_VOC2090/Annotations | wc -l上面两条命令是每次拿到新数据集的固定动作。图片数和XML数不一致,说明有图没标或者有标没图,训练时DataLoader会直接报文件找不到。find的-maxdepth 2限制只往下看两层,避免在大目录里刷屏。如果两个wc -l结果都是2090,说明这份数据在完整性上过关。
2.2 类别分布与图像尺寸的统计脚本
单类别数据集看似简单,但“帽子”这个目标在画面里的尺度差异极大:远景监控里可能只有20×15像素,近景抓拍能到200×180。不先统计边界框尺寸分布,你没法决定anchor尺寸或者输入分辨率。下面这段脚本遍历所有XML,输出宽高分布和每张图的目标数量。
import os import xml.etree.ElementTree as ET import numpy as np ann_dir = "./ChefHat_VOC2090/Annotations" widths, heights, per_img = [], [], [] for fname in os.listdir(ann_dir): if not fname.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, fname)) root = tree.getroot() objs = root.findall("object") per_img.append(len(objs)) for obj in objs: bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) xmax = float(bbox.find("xmax").text) ymin = float(bbox.find("ymin").text) ymax = float(bbox.find("ymax").text) widths.append(xmax - xmin) heights.append(ymax - ymin) widths = np.array(widths) heights = np.array(heights) print(f"总目标数: {len(widths)}") print(f"宽 min/median/max: {widths.min():.0f}/{np.median(widths):.0f}/{widths.max():.0f}") print(f"高 min/median/max: {heights.min():.0f}/{np.median(heights):.0f}/{heights.max():.0f}") print(f"每图目标数 均值: {np.mean(per_img):.2f}, 最大: {max(per_img)}")这段代码的关键在最后三行输出。如果中位数宽高小于40像素,说明小目标占比高,训练时输入分辨率不能低于640,否则特征图上下采样几次目标就没了。如果每图目标数均值接近1,说明基本是一图一人,那数据增强里的Mosaic要慎用,拼接后容易出现目标被裁一半的情况。ET.parse对格式错误的XML会直接抛异常,跑的时候如果中断,就说明有XML损坏,需要单独挑出来修。
2.3 2090张够不够:和自采数据的成本对比
很多人纠结2090张能不能训出可用模型。我的经验是:单类别、场景相对固定(都是后厨)、目标外观一致(白色厨师帽),2090张做迁移学习是够的;但如果你要从零训一个backbone,至少需要5000张以上才能压住过拟合。这里给一个选型判断表,帮你决定是直接用还是先扩充。
| 条件 | 2090张直接训 | 需要先扩充 |
|---|---|---|
| 预训练权重 | 有COCO或ImageNet预训练 | 从零开始 |
| 场景一致性 | 全部为后厨/食堂 | 混杂室内外多场景 |
| 目标尺度 | 中位数>50像素 | 大量小于30像素 |
| 类别数 | 单类(hat) | 多类(hat+口罩+手套) |
| 验收指标 | mAP@0.5>0.85 | 要求mAP@0.5>0.95 |
如果你的场景落在右列任意两项以上,建议先拿这份数据跑通流程,再用它训出的模型去半自动标注新数据,把量堆到5000以上。直接硬训不是不行,是调参成本会高到让你怀疑人生。
3. 从VOC到YOLO:转换脚本、训练命令与参数怎么设
3.1 VOC转YOLO格式的完整脚本与坐标陷阱
现在主流训练框架里,YOLO系列对VOC的支持要么靠内置转换,要么得自己写。我一般直接转成YOLO的txt格式,每行class_id cx cy w h,全部归一化到0~1。这里有个血泪经验:VOC的xmin/ymin是1-based,转YOLO必须先减1再算中心点,否则整体偏右下。下面脚本把转换和划分一次性做完。
import os import random import xml.etree.ElementTree as ET ann_dir = "./ChefHat_VOC2090/Annotations" img_dir = "./ChefHat_VOC2090/JPEGImages" out_dir = "./chefhat_yolo" os.makedirs(f"{out_dir}/images/train", exist_ok=True) os.makedirs(f"{out_dir}/images/val", exist_ok=True) os.makedirs(f"{out_dir}/labels/train", exist_ok=True) os.makedirs(f"{out_dir}/labels/val", exist_ok=True) CLASS_MAP = {"hat": 0, "chef_hat": 0} # 兼容两种标签名 files = [f for f in os.listdir(ann_dir) if f.endswith(".xml")] random.seed(42) random.shuffle(files) split = int(len(files) * 0.8) for idx, fname in enumerate(files): tree = ET.parse(os.path.join(ann_dir, fname)) root = tree.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 bbox = obj.find("bndbox") # VOC坐标是1-based,减1转0-based xmin = float(bbox.find("xmin").text) - 1 ymin = float(bbox.find("ymin").text) - 1 xmax = float(bbox.find("xmax").text) - 1 ymax = float(bbox.find("ymax").text) - 1 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if not lines: continue subset = "train" if idx < split else "val" stem = os.path.splitext(fname)[0] with open(f"{out_dir}/labels/{subset}/{stem}.txt", "w") as f: f.write("\n".join(lines)) # 图片用软链接省空间,实际训练时按需改成copy src_img = os.path.join(img_dir, stem + ".jpg") dst_img = f"{out_dir}/images/{subset}/{stem}.jpg" if os.path.exists(src_img) and not os.path.exists(dst_img): os.symlink(os.path.abspath(src_img), dst_img)脚本里CLASS_MAP同时兼容hat和chef_hat,是因为不同标注批次可能改了标签名,不统一会导致部分目标被丢弃。random.seed(42)保证每次划分一致,方便复现。软链接那步在Windows上可能不生效,换成shutil.copy即可。转换完一定要抽查几个txt,确认数值都在0~1之间,出现大于1的就是坐标没减1或者宽高算反了。
3.2 YOLOv8训练命令与三个必调参数
转换完直接上YOLOv8,因为它的命令行最省事,配置文件也清晰。下面这条命令是我在单卡24G显存上跑这份数据集的常用配置。
yolo detect train \ data=./chefhat_yolo/data.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ mosaic=0.5 \ close_mosaic=20 \ patience=30 \ project=chefhat_runs \ name=exp01data.yaml里写train: ./images/train、val: ./images/val、nc: 1、names: ['hat']。三个必调参数:imgsz决定小目标能不能被看到,2090张里如果小目标多,建议上到800;mosaic在单类别一图一人的数据上别开太高,0.5足够,开1.0容易拼出半个帽子;close_mosaic设20,意思是最后20个epoch关掉Mosaic,让模型在真实分布上收尾,这个技巧对mAP提升很明显。lr0用0.01配合lrf=0.01是YOLOv8的常规组合,如果loss震荡就降到0.005。
3.3 训练过程看什么:loss曲线与验证指标
跑起来之后别只盯着最终mAP。前10个epoch看box_loss是否稳定下降,如果来回跳,多半是学习率太大或者标注里有异常框。cls_loss在单类别任务里应该很快降到很低,如果一直不降,检查data.yaml的nc是不是写成了别的数。验证阶段重点看metrics/mAP50-95和metrics/precision,厨师帽检测这种合规场景,召回率比精确率更重要——漏检一个没戴帽子的,比误报一个戴了帽子的后果严重。如果recall明显低于precision,把置信度阈值从0.25降到0.15再试。
4. 避坑与排查:标注、转换、训练里最容易翻车的五件事
4.1 现象:训练loss正常但验证mAP一直是0
原因:data.yaml里的names顺序和txt里的class_id对不上,或者验证集图片路径写错导致加载不到图。解决:先用yolo detect val单独跑一次验证,看输出里val的图片数量是不是0。如果是0,检查val:路径是相对data.yaml所在目录还是当前工作目录,YOLOv8按前者解析。再抽查一个val的txt,确认class_id是0而不是1。
4.2 现象:预测框整体偏右下或偏左上
原因:VOC坐标基准没统一。有的XML是0-based,有的是1-based,转换脚本里统一减1就会让0-based的数据偏左上。解决:写个检查脚本,随机抽10个XML,用xmin和图片实际像素对比,如果xmin=0存在,说明是0-based,转换时不要减1。更稳妥的做法是判断xmin最小值,如果为0则按0-based处理。
4.3 现象:小目标帽子完全检不出
原因:输入分辨率太低,或者anchor尺寸不匹配。2090张里如果中位宽高只有30像素,640输入下经过32倍下采样只剩不到1个像素。解决:把imgsz提到960或1280,同时把mosaic关掉,避免小目标被进一步缩小。如果显存不够,改用yolov8n或yolov8s,别硬上大模型。
4.4 现象:模型把白色餐盘、白色口罩误检成厨师帽
原因:单类别数据集里负样本不足,模型只学到了“白色圆形物体”这个特征。解决:在训练集里加入不含帽子的后厨负样本图,或者用yolov8的rect模式训练减少拼接带来的干扰。另一个办法是提高box损失的权重,让模型更关注形状而不是颜色。
4.5 现象:训练到一半显存爆了
原因:batch设太大,或者mosaic开启时单张图拼接后尺寸波动。解决:把batch降到8,开启amp=True混合精度,cache=False关掉内存缓存。如果还爆,把imgsz从640降到512,但要注意小目标召回会掉,需要重新评估。
5. 把2090张用出5000张的效果:半自动标注与增量训练
这份数据集最大的价值不是直接训一个上线模型,而是当“种子”。我一般会先用它训一个baseline,然后用这个模型去推理新采集的后厨视频帧,把置信度高于0.7的预测框直接转成VOC XML,人工只做删改,标注效率能提升三到五倍。下面这段脚本把YOLO预测结果写回VOC格式,方便你继续用LabelImg微调。
from ultralytics import YOLO import xml.etree.ElementTree as ET import cv2 import os model = YOLO("chefhat_runs/exp01/weights/best.pt") img_dir = "./new_frames" out_ann = "./new_annotations" os.makedirs(out_ann, exist_ok=True) for img_name in os.listdir(img_dir): if not img_name.endswith(".jpg"): continue img_path = os.path.join(img_dir, img_name) img = cv2.imread(img_path) h, w = img.shape[:2] results = model.predict(img_path, conf=0.7, iou=0.5, verbose=False) root = ET.Element("annotation") ET.SubElement(root, "folder").text = "new_frames" ET.SubElement(root, "filename").text = img_name size = ET.SubElement(root, "size") ET.SubElement(size, "width").text = str(w) ET.SubElement(size, "height").text = str(h) ET.SubElement(size, "depth").text = "3" for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() obj = ET.SubElement(root, "object") ET.SubElement(obj, "name").text = "hat" bbox = ET.SubElement(obj, "bndbox") # 转回1-based VOC坐标 ET.SubElement(bbox, "xmin").text = str(int(x1) + 1) ET.SubElement(bbox, "ymin").text = str(int(y1) + 1) ET.SubElement(bbox, "xmax").text = str(int(x2) + 1) ET.SubElement(bbox, "ymax").text = str(int(y2) + 1) tree = ET.ElementTree(root) tree.write(os.path.join(out_ann, img_name.replace(".jpg", ".xml")))conf=0.7是保守阈值,宁可少标也不要引入太多错标,人工复核时删比改快。iou=0.5控制重叠框合并。写回的坐标加1还原成VOC的1-based,这样和原始2090张的格式完全一致,可以直接合并进Annotations目录重新训练。增量训练时,把新老数据混在一起,学习率降到0.001,epoch设50左右,避免把旧知识冲掉。这个流程跑两三轮,2090张种子数据就能滚到5000张以上,而且标注质量比纯人工高,因为模型已经帮你过滤了一遍明显误检。
最后说个我自己的习惯:每次拿到一份VOC数据集,先跑一遍统计脚本,把宽高分布和每图目标数记在README里,再开始转格式。这个动作花不到五分钟,但能省掉后面调参时反复猜“是不是小目标问题”的时间。厨师帽检测这个方向,数据比模型重要,2090张够你起步,但别指望它一步到位。希望帮到你。
本文还有配套的精品资源,点击获取