简介:厨师帽检测数据集是面向yolo系列目标检测算法整理的图像标注资源,适合算法研究者、深度学习开发者及工业质检场景应用人员。1620张真实图像已划好训练集、验证集与测试集,配套data.yaml配置,覆盖yolov5、yolov7、yolov8、yolov9、yolov10及yolo11等主流版本,解压即可直接训练与验证。包内标签体系完整,同时提供yolo格式(txt)与VOC格式(xml)两套标注,共1289个xml与711个txt,合计2000个文件,压缩包约26.17MB。yolo格式以归一化中心点与宽高比例记录目标框,便于原生训练;VOC格式保留结构化信息,方便标签转换或多框架复用。两套标签分开存放,按需调用即可。已有78人参与学习,适用于厨师帽佩戴检测、安全规范监控等项目。压缩包内含已划分目录、完整标注与可直接引用的数据集配置,节省预处理时间,帮助快速完成算法效果验证。
1. 为什么把1620张带标签的厨师帽图像喂给YOLO,是明厨亮灶项目的正确起点
做明厨亮灶改造的同行都清楚,最躲不开的检测需求就是“后厨员工有没有戴好厨师帽和发罩”。我接过一个食安监管试点项目,客户开口就要自动识别谁没戴帽子,翻遍公开数据集,要么是COCO里根本没有这个类别,要么就是场景对不上、标注稀烂。后来自己整理了一个厨师帽数据集:1620张图像全部带标签,打包成一个zip,用YOLO算法做训练、验证、部署,一条链路直接跑通。这篇就顺着这个链路讲:解压后如何确认标注质量、怎么组织目录结构、训练配置与参数怎么定、最常见的几个坑在哪,最后落到真实监控场景里的验证和部署。适合两类人——想用YOLO做穿戴合规检测的小团队,以及已经在做餐饮数字化方案、要把模型真正装到摄像头后面的工程师。
2. 拆开zip先看懂数据:标签文件与目录结构的三个门道
拿到zip先别急着双击解压。一个人工标注的数据集,真正要检查的不是图像数量,而是标签文件质量与目录组织方式。这个厨师帽数据集虽然1620张图像都带标签,但只有标签格式、类别编号、目录结构与YOLO算法预期一致,训练才能一次跑通。否则你很可能遇到后面第4章讲的“All labels empty”或者mAP一直为0的情况。
2.1 YOLO标签的一行五个数字:从标注文件里能看出什么
YOLO格式中,每张图像对应一个同名的txt标签文件,每一行表示图中的一个目标,包含五个数字:类别ID、目标中心点x坐标、中心点y坐标、目标宽度、目标高度。坐标不是像素值,而是相对图像宽高的归一化值,范围在0到1之间。
打开一个厨师帽数据的标签文件,应该看到类似这样的内容:
0 0.489843 0.372357 0.351562 0.478323含义是:画面中心附近有一个类别为0的目标,其宽度约为整张图宽的35%,高度约为图高的47%。如果发现某行的坐标值大于1,或者宽度高度出现负数,说明标注异常,常见原因包括标注工具导出精度问题、坐标越界等。
建议先跑一遍下面的Python脚本,检查缺标签文件、空标签文件和类别分布,这三项在YOLO训练中分别对应不同的问题:
import os from collections import Counter # 解压后的目录,按你的实际路径修改 img_dir = "chef_hat_dataset/images" label_dir = "chef_hat_dataset/labels" total = 0 missing_label = [] empty_labels = [] class_hist = Counter() for file in os.listdir(img_dir): if not file.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): continue total += 1 stem = os.path.splitext(file)[0] label_path = os.path.join(label_dir, stem + ".txt") if not os.path.exists(label_path): missing_label.append(file) continue with open(label_path, "r", encoding="utf-8") as f: lines = [l.strip() for l in f.readlines() if l.strip()] if not lines: empty_labels.append(file) for line in lines: parts = line.split() if len(parts) != 5: print(f"异常行: {label_path}: {line}") continue class_hist[int(parts[0])] += 1 print(f"图像总数: {total}") print(f"缺标签文件: {len(missing_label)}") print(f"空标签文件: {len(empty_labels)}") print(f"类别分布: {dict(class_hist)}")这段代码做了四件事:统计图像总数、查找缺失标签、识别空标签、统计每类目标数量。我最关注“缺标签文件”和“空标签文件”这两个数字。YOLO训练时缺标签的图像被当作负样本参与损失计算,数量多了会拉低召回率;空标签在验证阶段还会干扰mAP统计,所以越早发现越好。zip解压后先跑这个检查,比直接丢进训练脚本要省事得多。
2.2 目录必须拆成train和val,你的训练命令才不用反复改
YOLO训练命令要求明确指定train和val两个数据子集。如果所有图像都堆在同一个images目录里,训练前还得临时写拆分脚本,而且每次拆分的随机结果可能不同,后面的实验对比就乱了。
标准目录结构是下面这样,图像和标签按train/val分开,一一对应:
chef_hat_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml图像和标签的对应关系靠文件名保持一致,跟目录位置无关。拆分时要把同名文件同时移到train或val,不能只动图像不动标签。假设解压后是images和labels两个平铺目录,可以用这段代码拆分:
import os import random import shutil src = "chef_hat_dataset" # 解压后的目录 dst = "chef_yolo" # 整理后的目录 random.seed(42) os.makedirs(f"{dst}/images/train", exist_ok=True) os.makedirs(f"{dst}/images/val", exist_ok=True) os.makedirs(f"{dst}/labels/train", exist_ok=True) os.makedirs(f"{dst}/labels/val", exist_ok=True) files = [f for f in os.listdir(f"{src}/images") if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(files) split_idx = int(len(files) * 0.9) train_set = files[:split_idx] val_set = files[split_idx:] def move_pair(file_list, subset): for f in file_list: stem = os.path.splitext(f)[0] shutil.copy(f"{src}/images/{f}", f"{dst}/images/{subset}/{f}") label_src = f"{src}/labels/{stem}.txt" if os.path.exists(label_src): shutil.copy(label_src, f"{dst}/labels/{subset}/{stem}.txt") else: print(f"警告: 找不到 {label_src}") move_pair(train_set, "train") move_pair(val_set, "val") print(f"train={len(train_set)}, val={len(val_set)}")随机种子固定成42,保证拆分可复现。这里要提一个数据泄漏的坑:如果数据集里的图像是连续监控帧,同一个厨师在相邻帧反复出现,随机拆分必然会让同一个人同时出现在train和val里。表面指标会虚高,真实场景一测就露馅。常见做法是先按文件名里的时间戳或人员字段分组,再把组拆到不同集合。这个数据集如果拍摄来源是单段视频,建议至少按视频片段切分。
2.3 类别设计先想清楚:厨师帽和发罩合并还是分开
数据集命名里同时出现了“厨师帽”和“发罩”,说明标注时可能两类目标都标了。这里有一个关键决策:标签要分成chef_hat、hair_cover两个类别,还是合并成一个。
按我的实施经验,如果业务目标只是“判断员工是否佩戴合规帽具”,合并成1类最稳。模型只需要学会区分“头上有没有帽子/发罩”,不需要区分具体是哪种。餐饮后厨里发罩、网帽、白色厨师帽视觉差异不大,拆成两类容易在低分辨率下互相混淆,反而降低精度。只有当监管要求明确区分“佩戴白色厨师帽”和“仅佩戴发罩”时,才有拆分的必要。
| 类别方案 | nc | 训练要求 | 适合场景 |
|---|---|---|---|
| 合并为chef_hat | 1 | 1620张足够 | 判断是否佩戴帽具,只看合规与否 |
| 拆为chef_hat、hair_cover | 2 | 每类最好各1200张以上 | 要区分帽型,风控规则更细 |
等训练出第一版基线指标后,再根据误检情况决定是否拆类,也是一种常见的迭代路径。但第一版我强烈建议走单类。
3. 从yaml到跑完第一个epoch:训练配置与四个必调参数
目录整理完,进入训练环节。写data.yaml、准备训练命令、选预训练权重,三步看着简单,但每一步都有细节能决定训练成败。
3.1 data.yaml怎么写:路径、nc和names
YOLO训练的第一步是定义数据集描述文件data.yaml。内容很短,但字段含义容易写错。
# chef_hat.yaml path: /data/chef_yolo # 指向整理后的根目录 train: images/train # 相对path的训练图片目录 val: images/val # 相对path的验证图片目录 nc: 1 # 类别数量 names: 0: chef_hat # 类别0,厨师帽两个常见的坑:第一,YOLO新版用path加相对路径的方式,不要只在train和val里写绝对路径。第二,names建议用显式键值对写清类别ID,尤其类别数超过1时。如果你在2.3中选了合并方案,nc写1,names对应0为chef_hat。names字符串会出现在训练日志、指标图和验证输出里,写清楚方便后面分析。
如果选拆分方案,nc写2:
nc: 2 names: 0: chef_hat 1: hair_cover拆分方案对标注质量要求更高,两个类别在视觉上必须能区分。如果标注时把发罩和厨师帽标混了,模型就会陷入自我矛盾,结果很可能就是mAP卡在0.6以下。
3.2 一条能跑通的训练命令与epochs、batch、imgsz、patience
数据集文件就绪后,下面是常用的一组训练命令:
yolo detect train \ data=chef_hat.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ workers=4四个参数值得展开讲,它们直接决定训练时间、显存占用和最终精度:
| 参数 | 常见范围 | 作用 | 建议 |
|---|---|---|---|
| imgsz | 320~1280 | 输入分辨率 | 640是起点;帽子在画面中较小时需要提高到768或960 |
| batch | 4~64 | 每批图像数 | 先看显存,16G显存配imgsz=640可以跑batch=16 |
| epochs | 100~300 | 最大训练轮数 | 设大一点没坏处,实际靠patience提前停止 |
| patience | 10~50 | 验证指标多轮不升则停止 | 20是稳妥值,设太大会拖长无效训练时间 |
epochs设150不代表一定跑满。YOLO训练过程中每个epoch都会在验证集上算一次mAP,如果连续patience轮没有提升,训练自动停止,最后生成best.pt和last.pt两个权重,前者是验证集最优结果。
关于yolo损失函数,训练日志里会看到三部分:box_loss是目标框回归损失,cls_loss是分类损失,dfl_loss是分布焦点损失,用来优化框的边缘。单类检测场景下cls_loss本身很低,不要盯着它判断训练好坏。我一般看box_loss和dfl_loss是否持续下降并趋于平缓,再结合验证集mAP判断。loss降到某个平台后继续强行多跑epoch,往往只是过拟合的开始。
3.3 预训练权重怎么选:按显卡和部署目标决定
模型名里的n、s、m、l、x代表YOLO系列的不同规模。对yolo预训练模型下载这件事,很多人上来就选最大号的yolov8x,结果训练时间翻几倍,还容易在小数据集上过拟合。1620张图像的数据规模,本身就不适合过大的模型。
按经验给出一个选型建议:
| 模型 | 权重文件 | 训练显存要求 | 适用场景 |
|---|---|---|---|
| YOLOv8n | yolov8n.pt | 小 | 边缘设备、CPU推理,追求速度 |
| YOLOv8s | yolov8s.pt | 中等 | 通用项目,精度和速度的平衡 |
| YOLOv8m | yolov8m.pt | 较大 | 标注质量高,想再提精度 |
| YOLOv8l/x | yolov8l.pt / yolov8x.pt | 大显存 | 数据量很大时,否则容易过拟合 |
如果目标设备是Jetson或普通边缘盒子,yolov8s以下比较合适。只有在V100、A100这类GPU上训练,且数据量充足时,才考虑m以上。YOLO系列对比选型有一条朴素标准:数据量小选小模型,数据量大才值得上大模型。
4. 避坑实战:1620张的厨师帽数据集训练时常见的5个翻车现场
这一章记录的五个问题都是实际项目里反复遇到的,按“现象、原因、解决”写,遇到类似问题直接对号入座。
4.1 loss不降或mAP一直在0附近
现象:训练日志里loss在下降,但每个epoch的验证mAP始终是0,或者更糟,从头到尾loss基本不动。
原因:最常见的是标签里的类别ID和data.yaml的names对不上。例如标签里写1,但names只定义了0,模型不知道这个类别是什么;还有一种是标注框严重越界,坐标出现负值或大于1,YOLO预处理时会把这些框直接过滤掉,导致标签实际为空。
解决:先做越界检查,再可视化标注,把标注框画到原图上。这一步最直观,脚本如下:
import cv2 def visualize_label(image_path, label_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) 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.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img img = visualize_label( "chef_hat_dataset/images/00123.jpg", "chef_hat_dataset/labels/00123.txt", ["chef_hat"] ) cv2.imwrite("check_label.jpg", img)脚本把归一化坐标还原成像素坐标并画框。如果画出来的框不在帽子上,或者框的大小明显失真,说明标注有问题,这时候去调模型参数没有意义。
4.2 训练中断后续跑,结果反而更差
现象:训练跑到第80个epoch时断电或掉线,重新启动后运行同样的训练命令,发现mAP比中断前还低,loss从高位重新开始。
原因:重新执行完整训练命令会从预训练权重重新初始化,相当于从零开始。要续跑必须用resume机制加载last.pt,它保存了模型权重、优化器状态和当前epoch数。如果直接换batch或imgsz,优化器里的学习率状态和梯度动量对不上,loss就会反弹。
解决:按下面的命令续跑,且不要改batch和imgsz:
yolo detect train resume=true \ model=runs/detect/train/weights/last.pt \ data=chef_hat.yaml如果确实需要调整batch,建议不resume,从头开始训练,否则玄学问题会很难定位。
4.3 验证集mAP不错,真实视频漏检一堆
现象:验证集mAP已经到了0.95,把best.pt扔进一段真实监控视频,帽子稍微远一点或者人侧身站着就漏了。
原因:训练集和真实场景分布不一致。这个数据集里的图像即使来源是后厨,测试视频的光线、机位、距离仍可能有差异。更直接的问题是:小目标在640分辨率下只占几十个像素,模型根本没学会这种尺度。
解决:先对比mAP50和mAP50-95两个指标。如果mAP50很高,mAP50-95却明显偏低,说明检测框位置和大小不够准,此时最有效的办法是提高训练分辨率。imgsz从640提到960,小目标AP往往能提升3到5个点,代价是训练时间和显存上升。推理时conf阈值也可以试着从0.5降到0.3,配合在输出端加一个“连续多帧命中”的判定,能压掉不少漏检。
4.4 训练时提示All labels empty
现象:训练刚开始或中途,终端反复出现WARNING: All labels empty in dataset/xxx.jpg。
原因:这张图的标签文件是空文件,或者标签里的坐标在预处理时被过滤掉了。空标签在数据集中通常表示标注员漏标了这一帧,也可能这一帧确实没有目标。训练不会中断,但空标签图像相当于强行给模型提供负样本,数量多了会明显拉低召回率。
解决:回头跑一遍2.1节的检查脚本,把empty_labels列表打印出来。逐个判断:如果图像中确实没有厨师帽,可以保留;如果漏标了,就补标注或者直接删掉这张图。我的习惯是直接删除,因为漏标图像数量少,不值得为它单独开一轮标注工具。
4.5 模型把白色物体误检成厨师帽
现象:推理时,白色操作台、塑料箱、一摞白碗全被框成chef_hat,提高conf后会漏检真目标,降低conf又误检一堆。
原因:明厨亮灶场景里白色和高光物体太多,厨师帽的视觉特征就是白色鼓起的一团。如果训练集里缺少这类白色负样本,模型学到的语义就太窄,把“白色圆顶”等同于帽子。
解决:通用数据增强解决不了这个问题,需要靠推理侧策略补救。我的做法是加一个ROI过滤:在后厨场景里划定门禁、操作台等有效区域,只保留落在区域内的检测框。另一个思路是引入“连续N帧确认”逻辑,单帧误检不触发报警,多帧一致命中才输出。这个数据集的标注内容没法帮你解决负样本问题,但配合规则层可以让误检率大幅下降。
5. 训练完之后:怎么确认这个数据集真的够用
训练结束后的验证阶段,目标是确认指标没有骗你。只看train日志不够,需要检查混淆矩阵、真实视频测试和过拟合迹象。
5.1 混淆矩阵和PR曲线怎么读
训练输出目录runs/detect/train里有confusion_matrix.png和PR_curve.png。混淆矩阵对角线越亮越好。对于单类模型,重点看两处:真实chef_hat预测为chef_hat的比率,以及background被误判为chef_hat的比率。
背景误检率超过10%,就要回到4.5的问题做处理。PR曲线反映的是不同置信度下精度和召回率的权衡,曲线越靠近右上角越好。看两个数字:mAP50是IoU阈值0.5下的平均精度,mAP50-95是把IoU阈值从0.5逐步加到0.95再取平均。如果顾客要求“帽子被完整框住才算数”,mAP50-95才是有意义的指标。
5.2 用一段真实监控片段做冒烟测试
建议准备一段10分钟左右的真实厨房监控,覆盖这些情况:员工在操作台前、员工背对摄像头、员工戴深色发罩、白帽和白墙同框、员工从画面远处走近。这段视频不应该出现在训练数据里。
跑一次推理:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=kitchen_test.mp4 \ conf=0.4 \ imgsz=640 \ save=True生成的输出视频里重点看两件事:漏检出现在什么位置,误检出现在什么位置。如果漏检集中在人刚进门、帽子面积小的时候,把imgsz提到768或960再训一轮。如果误检集中在强光反射区,用后面第6章的hsv增强参数加大色彩扰动。
5.3 过拟合与欠拟合的快速判断
1620张是中等偏小的数据量,过拟合是主要风险。打开results.png看train_box_loss和val_box_loss两条曲线:训练loss持续下降到很低,验证loss在中段就进入平台甚至回升,这就是典型过拟合信号。
解决方式按优先级排序:先换更小的模型,比如m改s、s改n;然后加大增强强度,把mosaic和hsv扰动调高;最后才考虑增加数据。反过来,如果train和val的loss同时停留在高位不降,是欠拟合,这时应该提高epoch上限、增加imgsz或换大一号模型。比较实用的是在训练过程中记录过拟合出现的epoch位置,把patience调小一点节省时间。
6. 1620张图像还能怎么榨出价值:增强与部署
训出best.pt只是第一步。数据量有限的情况下,增强和部署策略决定模型真实场景中的可用性。
6.1 用YOLO内置增强提升泛化能力
针对后厨环境的强反光、不同机位和光线,建议显式调整增强参数,而不是一直用默认值:
yolo detect train \ data=chef_hat.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ degrees=10 \ translate=0.1 \ scale=0.5 \ fliplr=0.5 \ mosaic=1.0hsv_h、hsv_s、hsv_v控制色调、饱和度、明度的随机扰动,模拟不同灯光下的色彩偏移。后厨常见的白色LED灯会让画面偏冷,暖光灯会让帽子偏黄,色彩扰动能让模型不依赖固定色温。degrees=10表示训练时图像随机旋转10度以内,适应摄像头安装角度不水平的情况;如果是俯视角度较大的机位,可以加到15。mosaic=1.0启用的马赛克增强把多张图拼成一张,变相扩大batch容量,对小数据集帮助很大。
6.2 从best.pt到部署:用ONNX导出和边缘推断的取舍
导出模型到ONNX格式是常见做法,之后可以接onnxruntime或TensorRT推理:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640用onnxruntime做一次简单验证,确认输出正确:
import cv2 import onnxruntime as ort session = ort.InferenceSession("best.onnx") input_name = session.get_inputs()[0].name img = cv2.imread("test.jpg") img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) img = img.astype("float32") / 255.0 img = img[None, ...] outputs = session.run(None, {input_name: img}) print(outputs[0].shape)边缘设备部署有三件事值得留意:帧率是否满足实时要求、INT8量化后精度损失多少、以及接入实际业务规则。如果摄像头固定对着后厨入口,建议加一个ROI区域过滤,只保留门口和通道附近的检测框,CPU占用能降不少。我在做这类合规检测时养成了一个习惯:检测结果不做单帧判定,连续三帧以上命中才确认报警,单帧误检不触发告警,这个习惯避免了很多次因为反光或人转身造成的误报。
这套流程跑下来,最深的体会是:数据集的标签质量和业务规则往往比模型结构更决定项目成败。希望这篇笔记能帮你把从zip解压到部署的整条路走通,少花几个晚上的调试时间。
本文还有配套的精品资源,点击获取