
简介面向目标检测入门与算法实战的食品图像数据集包含鸡蛋、冰箱、米饭等常见物品共273张标注图像。资源采用YOLO标准格式每个jpg对应一个txt标签文件另附yaml配置文件标签字段为类别索引及归一化后的中心点坐标与宽高可直接用于YOLOv5、YOLOv8、YOLOv9、YOLOv10等系列模型训练与评估。压缩包内共547个文件其中273张jpg图像、273个txt标签及1个yaml配置整体大小约13.7MB数据集已划分好训练集与验证集打开即可开始训练。目前已有56人学习使用适合需要快速获取规范标注数据集进行模型验证或迁移学习的开发者。借助该数据集可省去自行采集与标注的耗时工作专注算法调参与效果对比同时标签文件也可按需转换为VOC格式便于在不同框架间切换。1. 273张图的food-items数据集能用来做什么拿到food-items-gldps.zip的人多半是想用 yolo 算法做食品检测数一下餐盘里还剩多少米饭、识别冰箱里的鸡蛋和食材或者给自动结算台做视觉方案。273张图像带标签量不算大但足够把“数据集 → 训练 → 评估 → 格式转换”这条 yolo 落地的完整链路走一遍。这篇文章按我处理这类小样本食品数据集的实际顺序来写先拆包体检标签再跑通训练再讲格式转换和避坑最后聊验证。适合刚接触 yolo 数据集、想找一个可复现的食品检测小项目的人。273张图不多但作为第一次完整跑通 yolo 食品检测的练习数据它比那些几万张的大数据集更合适——正因为小每一步的问题都能暴露得很明显。2. 拆开food-items-gldps.zip目录结构、标签类别与一个解析脚本2.1 解压后的目录一般长这样拿到手先别急着训练。解压先看目录结构。常见做法是像这样unzip food-items-gldps.zip -d ./datasets/food-items find ./datasets/food-items -maxdepth 2 -type d | sortfood-items-gldps这个名字里的gldps我翻了不少打包习惯没有统一答案大概率是标注平台或打包人加的后缀和数据内容无关不用在这上面浪费时间。解压后你大概率会看到两种布局之一一种是images/和labels/平铺两张目录下文件名一一对应另一种是train/和val/嵌套各自带images/和labels/。这两种处理逻辑差不多只是后面写路径时来源不同。先确认一件事labels目录里是不是每个.jpg都对应一个同名.txt。缺标签文件、空标签文件都会在训练时被 YOLO 静默跳过最后你发现“训练完了但某类完全没学到”回头查才发现是标签漏了。这一步用一条命令就能查ls images | sed s/\.jpg$/.txt/ | sort /tmp/img_list.txt ls labels | sed s/\.txt$/.txt/ | sort /tmp/lbl_list.txt diff /tmp/img_list.txt /tmp/lbl_list.txtdiff有输出就是图像和标签对不上先把对不上的文件筛出来再说。小数据集最忌讳刚拿到手就开训先花十分钟体检能省后面一整天的排查时间。2.2 YOLO标签txt里那五列数字到底怎么读YOLO 的标签文件是纯文本每一行代表一个目标框格式固定为五列类别id 中心点x 中心点y 宽度w 高度h。注意是归一化坐标所有数值都在 0 到 1 之间相对于图像宽度和高度的比例。举个例子一行1 0.5234 0.4412 0.3120 0.2655表示类别 id 为 1框中心在图像横向 52.34%、纵向 44.12% 的位置框宽占图像宽 31.20%高占图像高 26.55%。这套格式解析起来非常简单但有一个容易忽略的点图像尺寸决定了坐标的实际像素值。同样一个0.3120宽度在 640x640 和 1920x1080 的图上像素宽度差出好几倍。后面做格式转换、数据可视化时先拿到图像的宽高再换算像素坐标别凭感觉估。我一般会写一个小脚本把整个数据集的标签情况一次性统计出来包括每个类别的框数量、图像数量、有没有格式异常import os from collections import Counter def inspect_yolo_labels(label_dir, img_dir): cls_counter Counter() img_counter Counter() errors [] for txt_name in sorted(os.listdir(label_dir)): if not txt_name.endswith(.txt): continue txt_path os.path.join(label_dir, txt_name) img_path os.path.join(img_dir, txt_name.replace(.txt, .jpg)) # 图像缺失是最常见的问题先拦下来 if not os.path.exists(img_path): errors.append(fmissing image: {img_path}) continue with open(txt_path, r) as f: lines f.read().strip().splitlines() # 空标签文件会让该图在训练时被跳过 if not lines or all(not l.strip() for l in lines): errors.append(fempty label: {txt_path}) continue for line in lines: parts line.split() if len(parts) ! 5: errors.append(fbad format {txt_path}: {line}) continue cls, xc, yc, w, h parts cls_counter[cls] 1 img_counter[cls] 1 print(class box distribution:, dict(cls_counter)) print(class image distribution:, dict(img_counter)) print(total boxes:, sum(cls_counter.values())) print(errors:, errors[:20]) inspect_yolo_labels(./datasets/food-items/labels, ./datasets/food-items/images)这段脚本的核心是三点检查图像是否缺失、检查标签文件是否为空、检查每行是否恰好五列。类别分布统计可以让你一眼看出这 273 张图里鸡蛋占多少、米饭占多少。如果某一个类别的框数只有个位数后面训练时这个类别基本学不出来要有心理预期。2.3 训练前先做数据体检类别分布与异常标签统计完分布还有一个更隐蔽的问题标签坐标越界。YOLO 格式要求坐标归一化到 0~1但人工标注偶尔会出现框的中心点或宽高略超边界尤其是冰箱场景里食材紧贴图像边缘时。绝大多数训练代码能容忍极小范围的越界但越界太多的框会让损失函数不稳定。用一段脚本把所有异常坐标揪出来import glob for txt in sorted(glob.glob(./datasets/food-items/labels/*.txt)): for line in open(txt): parts line.split() if len(parts) ! 5: continue c, xc, yc, w, h parts xc, yc, w, h map(float, [xc, yc, w, h]) # 归一化坐标应当落在 0~1宽高为正 if not (0 xc 1 and 0 yc 1) or w 0 or h 0: print(fanomaly - {txt}: {line.strip()}) # 中心点 ± 半宽超出图像边界超过 2%说明这个框明显画歪了 if xc w / 2 1.02 or xc - w / 2 -0.02: print(fboundary overflow - {txt}: {line.strip()})我的判断标准是允许 2% 的容差因为标注时手抖一点很正常超过 2% 就说明这个框可能是复制粘贴错了或者坐标归一化基准不同。发现异常框后用 LabelImg 或 Roboflow 打开原图人工确认别急着自动裁掉——自动裁剪有可能把原本正确的框改坏。提示labels目录里如果有classes.txt它只是类别名对照文件不是标签数据。训练前把它移出labels目录否则 YOLO 会尝试把它当作一个图像的标签来读白白浪费一条训练样本。3. 用YOLO在本地跑通食品检测数据划分、yaml与训练命令3.1 273张不做随机划分按场景分训练验证数据只有 273 张验证集的划分方式直接决定你看到的 mAP 是真是假。我见过最典型的翻车操作直接用train_test_split随机抽 20% 做验证结果同一个冰箱的连续帧被同时分进训练集和验证集训练时模型已经“见过”验证集了最后 mAP 虚高一上真实场景立刻现原形。正确的做法是先看文件命名。这类食品数据集的图像文件名里往往带场景前缀比如fridge_001.jpg、egg_002.jpg、rice_kitchen_003.jpg。如果有这种前缀按场景前缀分层划分import os from sklearn.model_selection import train_test_split img_files [f for f in os.listdir(./datasets/food-items/images) if f.endswith(.jpg)] # 如果文件名带场景前缀用前缀做分层没有就去掉 stratify 参数 scene_ids [f.split(_)[0] for f in img_files] train_files, val_files train_test_split( img_files, test_size0.21, # 273 张里留约 57 张做验证 random_state42, # 固定随机种子保证结果可复现 stratifyscene_ids if len(set(scene_ids)) 1 else None ) with open(./datasets/food-items/train.txt, w) as f: f.write(\n.join(train_files)) with open(./datasets/food-items/val.txt, w) as f: f.write(\n.join(val_files)) print(ftrain{len(train_files)} val{len(val_files)})stratify参数的核心价值是让每个场景在训练集和验证集里的比例保持一致避免某个场景的照片全部进了验证集导致训练数据严重缺失。如果文件名前缀看不出场景信息就退化为纯随机划分但此时你要意识到 mAP 可能会偏乐观。划分完之后验证集就锁死不要再往里加图。3.2 写数据yaml与下载预训练权重YOLO 训练前需要一个 yaml 文件描述数据路径和类别名。我一般把它放在数据集根目录这样训练命令相对简洁# food-items.yaml path: /absolute/path/to/datasets/food-items train: train.txt val: val.txt # nc 和 names 必须与 labels 里的 class id 顺序完全一致 # 以压缩包内 classes.txt 为准这里按标题可确定的类别示意 nc: 3 names: 0: egg 1: rice 2: fridgenc必须和names列表长度一致names的顺序必须和标签文件里 class id 的索引一一对应。最保险的做法是直接读压缩包里的classes.txt按它的行顺序抄进names不要自己重排。顺序一旦错了鸡蛋会被当成米饭来训练而且整个训练过程不会报任何错这是最阴的一种问题。预训练权重一定得下。273 张图从头训练的效果约等于随机初始化用 COCO 预训练权重做迁移学习才是正确姿势mkdir -p ./weights wget https://github.com/ultralytics/yolov5/releases/download/v6.0/yolov5s.pt -P ./weights/yolov5s.pt是在 COCO 上预训练过的权重COCO 里有person、bottle、bowl这些和食品检测相关的类别迁移过来的特征对食材、容器、冰箱内景都有一定的激活响应。没有预训练权重的话训练收敛会慢得多而且在小数据集上极其容易陷在局部最优里出不来。3.3 训练命令与四个关键参数假设你已经配好了 yolo 环境训练最小命令是这样cd yolov5 python train.py \ --data ../datasets/food-items/food-items.yaml \ --weights weights/yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --patience 25 \ --cache如果用 YOLOv8等价命令是yolo detect train data... modelyolov8n.pt epochs100 imgsz640 batch16参数逻辑一致。四个关键参数逐个说参数建议值说明--img640训练输入分辨率。食品检测里鸡蛋、米饭属于中小目标640 是速度和精度的平衡点后面 5.3 会说什么时候该升到 1024--batch16小样本数据集 batch 不能太小原因在第 5 章详细讲--epochs100273 张图 100 轮足够作为 baseline配合早停机制防止过拟合--patience25验证集指标连续 25 轮不提升就停止训练小数据集强烈建议开启--cache会把图像缓存到内存273 张图完全放得下能省掉训练时反复读盘的时间。训练起来后重点看两个东西train_loss和val_loss曲线。YOLO 的损失函数由分类损失、边界框回归损失和置信度损失三部分加权求和你不需要细抠每一项只需要看 val 损失是否还在下降。val 损失开始抬头而 train 损失还在降就是过拟合的经典信号这时候靠patience自动停掉就对了。4. 把YOLO标签转成VOC XML转换脚本与四个边界坑4.1 什么时候需要转换跨库合并与迁移学习YOLO 原生格式训练最快但有两个场景必须转格式。一是要把这个食品数据集和其他 VOC 格式的公开数据集合并统一成一种格式再喂给模型二是要用 MMDetection、Detectron2 这类框架做对比实验它们原生吃 COCO JSON 或 VOC XML。别嫌转换麻烦数据格式统一是数据集工程的基本功磨刀不误砍柴工。4.2 YOLO转VOC的Python脚本下面这个脚本把 YOLO 的 txt 标签转换成 VOC XML。VOC 的标注格式是绝对的左上角xmin, ymin和右下角xmax, ymax像素坐标所以核心转换逻辑是中心点坐标乘以图像宽高得到像素中心再加减半宽半高得到边界import os import glob import argparse import xml.etree.ElementTree as ET from PIL import Image def yolo_to_voc(img_dir, label_dir, out_xml_dir, classes): os.makedirs(out_xml_dir, exist_okTrue) for txt_path in glob.glob(os.path.join(label_dir, *.txt)): img_name os.path.splitext(os.path.basename(txt_path))[0] .jpg img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(fskip missing image: {img_path}) continue # 必须用 PIL 读取真实宽高不能用固定值 with Image.open(img_path) as im: W, H im.size annotation ET.Element(annotation) size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(W) ET.SubElement(size, height).text str(H) ET.SubElement(size, depth).text 3 for line in open(txt_path): parts line.split() if len(parts) ! 5: continue cls_id, xc, yc, w, h parts xc, yc, w, h map(float, [xc, yc, w, h]) # 归一化中心坐标转像素再转左上角/右下角 x1 (xc - w / 2) * W y1 (yc - h / 2) * H x2 (xc w / 2) * W y2 (yc h / 2) * H # clamp 到图像范围内防止越界框破坏训练 x1 max(0, min(W - 1, int(x1))) y1 max(0, min(H - 1, int(y1))) x2 max(0, min(W - 1, int(x2))) y2 max(0, min(H - 1, int(y2))) obj ET.SubElement(annotation, object) ET.SubElement(obj, name).text classes[int(cls_id)] bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(x1) ET.SubElement(bndbox, ymin).text str(y1) ET.SubElement(bndbox, xmax).text str(x2) ET.SubElement(bndbox, ymax).text str(y2) out_path os.path.join(out_xml_dir, os.path.splitext(os.path.basename(txt_path))[0] .xml) ET.ElementTree(annotation).write(out_path, encodingutf-8, xml_declarationTrue) print(fwritten: {out_path}) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--img-dir, requiredTrue, help图像目录) parser.add_argument(--label-dir, requiredTrue, helpYOLO txt目录) parser.add_argument(--out-dir, requiredTrue, helpXML输出目录) parser.add_argument(--classes, nargs, requiredTrue, help类别列表顺序必须与class id一致) args parser.parse_args() yolo_to_voc(args.img_dir, args.label_dir, args.out_dir, args.classes)调用方式python yolo_to_voc.py \ --img-dir ./datasets/food-items/images \ --label-dir ./datasets/food-items/labels \ --out-dir ./datasets/food-items/xmls \ --classes egg rice fridge核心逻辑就三步PIL 读宽高 → 归一化坐标乘宽高转像素 → 中心宽高式转左上右下式。classes参数的顺序必须和标签里的 class id 完全一致脚本内部是按索引取类别名的传错顺序等于所有类别集体错位。4.3 转换翻车点四个边界坑坑一中心坐标算反导致框飞出图外。现象是转换后 XML 里xmin大于xmax或者框跑到图像边缘之外。原因是PIL.Image.size返回的是(width, height)解包时写成了H, W im.size后续所有计算全部横纵颠倒。解决方法是统一写成W, H im.size并养成交互验证的习惯转换完随机抽三张图把框画回原图看一眼。坑二txt 里坐标根本不是归一化的。现象是转换出来的x1、y1动辄几千明显超出图像尺寸。原因是部分标注工具导出 YOLO 格式时默认保留像素坐标没有做归一化。判断方法很简单读 txt 时如果max(xc, yc, w, h) 1.5说明大概率是像素值。解决是在脚本开头加一个自适应判断数值超过 1.5 时不再乘以宽高。坑三类别名错位鸡蛋变米饭。现象是训练时明明数据没问题推理时模型把鸡蛋全识别成米饭。原因是转换脚本的--classes egg rice fridge顺序和 txt 里 class id 的索引对不上。这个问题在合并多个数据集时尤其容易出现——数据集 A 的0是 egg数据集 B 的0是 rice合到一起后必须重新映射索引。解决方法是永远从classes.txt生成参数不要手敲。坑四文件名带空格或中文XML 解析直接失败。现象是 XML 文件生成成功但 LabelImg 打开报错或者训练代码读到一半崩溃。原因是文件名里的空格和中文没有做转义XML 的path标签和filename内容会产生解析歧义。解决方法是转换前先批量重命名图像和标签文件统一为纯英文数字加下划线例如fridge_001.jpg。提示转换完成后用xmllint --noout *.xml批量校验一遍 XML 合法性能省掉后续训练中途崩溃的排查时间。5. 273张小样本训练避坑过拟合、崩BN与漏检排查5.1 train损失直线降、val mAP不动过拟合的两个信号现象训练日志里 train loss 前 20 轮就降到 0.02 附近看起来非常漂亮但 val mAP 始终在 0.3 以下徘徊甚至一动不动。如果你还开着 tensorboard能看到 train loss 和 val loss 在 30 轮左右开始分道扬镳。原因273 张图太少模型在 30 轮左右就把训练集“背下来”了。边界框回归损失还在降是因为模型在记忆训练样本的细节而不是在泛化“鸡蛋”这个语义概念。这也是小样本训练最容易骗到人的地方——只看 train loss 你会以为模型快收敛了。解决第一epochs 从 300 降到 100配合patience 25让早停生效第二加大数据增强YOLOv5 默认开启 Mosaic你还可以把hsv_h、hsv_s调大一个档位让模型对光照变化更鲁棒第三前 10 轮冻结 backbone用小学习率只训练检测头等损失曲线平稳后再解冻全模型第四验证集指标只看 val mAPtrain loss 只做参考。5.2 batch太小触发BN统计量崩溃loss震荡与mAP为0现象把 batch 调到 4 或 8 以适配小显存显卡训练到 20 轮左右 loss 突然剧烈震荡或者 val mAP 直接变成 0日志里偶尔出现 BN 相关的警告甚至 loss 变成 nan。这就是典型的 yolo 训练中 bn 崩溃。原因BatchNorm 层的统计量是从当前 batch 里估计的。batch 越小均值和方差的估计噪声越大。273 张小数据集本来就样本多样性不足batch 再小BN 的滑动平均就被噪声带着跑偏某一轮统计量崩了后面整个训练都跟着崩。解决batch 尽量提到 16 以上这是小样本训练的红线。显存不够时先降--img到 416 或 512把 batch 保上去而不是反过来牺牲 batch 保分辨率。如果你用的是 YOLOv8可以直接传--batch -1让框架自动按显存探测最大 batch。还有一个偏方backbone 的 BN 层冻结只训练检测头能稳定不少但会牺牲一点精度。5.3 鸡蛋小目标漏检不是调nms能解决的现象训练完在验证集上看效果大件的目标比如整盘米饭检测得很好但鸡蛋这种小目标漏检严重。有人第一反应是调 NMS 阈值或者降置信度结果误检猛增漏检没改善。原因目标小意味着它占图像的面积小、特征少。YOLO 在 640x640 输入下鸡蛋可能只有十几个像素经过下采样后特征图上的响应非常微弱。NMS 只解决“框太多该留哪个”的问题解决不了“特征根本没检测到”的问题调它属于缘木求鱼。解决最直接的一招是把--img升到 1024 训练和推理小目标的像素面积直接翻倍。代价是显存占用变大速度变慢273 张图撑得住。第二招是开启 Mosaic 增强的同时把 mosaic 概率调到 0.5 以上让模型看到更多小目标在画面中不同位置的样子。第三招是如果鸡蛋和其他类别尺寸差异过大可以考虑按目标尺度做分阶段训练但这套流程对小数据集来说过于复杂不建议一上来就上。5.4 冰箱反光和遮挡先看标签再谈模型现象冰箱场景下的食材频繁漏检或者把反光区域误检成食物。仔细看验证集结果发现漏检的目标大多处于反光带附近、被其他食材遮挡、或者紧贴冰箱边缘导致标签框只框住了一半。原因这一类问题 70% 出在标签上不是模型上。冰箱内壁是光滑塑料拍摄时灯光会在食材表面形成高光标注员如果只看缩略图很容易漏标反光区域的食材或者把标签框画得偏大偏小。模型学到的是“这些区域不是目标”而不是“目标在这里但我没检测到”。解决回到 2.3 的数据体检步骤把 val 集里漏检的图像逐个翻出来用标注工具重新校正标签。重点检查两类一是反光区域附近的食材有没有漏标二是遮挡严重的框是不是只框了可见部分。标签修正之后再把hsv_s增强饱和度范围调大让模型对高光不太敏感。这一套下来比换任何模型结构都管用。6. 用混淆矩阵和置信度阈值把关小模型验收的最后一公里6.1 先看混淆矩阵再报mAP小数据集上 mAP 数值很敏感换一个随机种子都可能波动几个点所以我验收模型的第一件事永远是看混淆矩阵而不是直接报 mAP。跑验证集时加上保存混淆矩阵的参数cd yolov5 python val.py \ --data ../datasets/food-items/food-items.yaml \ --weights runs/train/exp/weights/best.pt \ --conf-thres 0.25 \ --iou-thres 0.5 \ --save-conf \ --save-json输出目录下会生成confusion_matrix.png。读这张图有个容易踩的坑YOLO 输出的混淆矩阵通常是按行归一化的每一行代表“真实类别被预测成了什么”所以每一行的和是 100%但整张图所有格子加起来不等于 100%。很多朋友第一次看说“yolo 混淆矩阵总和怎么不唯一”其实只是没搞懂它按行归一化了。看混淆矩阵时重点盯两类格子一是鸡蛋那行里有多少被预测成了米饭这直接反映类别混淆二是背景列占比过高说明漏检严重。比 mAP 数字有意义得多。6.2 用F1曲线选置信度而不是拍脑袋定0.5280 张图训练出来的模型置信度阈值 0.5 往往不是最优解。盲调阈值是玄学用 F1 曲线选才是工程做法。YOLO 的 val.py 默认会生成F1_curve.png横轴是置信度阈值纵轴是 F1 分数取曲线峰值对应的置信度就是当前模型的最优阈值。想要更精确的数值可以写一个循环批量跑不同置信度for conf in 0.15 0.2 0.25 0.3 0.4 0.5; do python val.py --data ../datasets/food-items/food-items.yaml \ --weights runs/train/exp/weights/best.pt \ --conf-thres $conf --iou-thres 0.5 \ --save-json 2/dev/null | grep F1 done选 F1 最高的那个conf值作为最终部署阈值。小样本模型往往在低置信度下表现更好因为模型对特征不够自信但又确实学到了关键信息。我现在的习惯是拿到任何小数据集第一件事就是跑一遍验证输出混淆矩阵和 F1 曲线再谈调参。273 张图决定了模型上限就摆在那里验收的意义不是把它吹成一个完美的检测器而是准确标出“哪里能用、哪里不能用、阈值设多少能尽量避免误检”。数据标注、数据体检、验证流程这四步走完这个数据集才算真正被你掌握了。希望帮到你。本文还有配套的精品资源点击获取