简介:这份资源是面向零售智能化与计算机视觉方向的超市货架空置缺货检测数据集,适用于目标检测模型训练、货架陈列分析及补货预警等场景,适合具备一定深度学习基础、需要真实货架图像做实验或项目落地的开发者与研究人员。压缩包共约2000个文件,以Pascal VOC格式的xml标注文件和YOLO格式的txt标注文件为主,另附说明文档,整体约251.17MB,图片与标注一一对应,可直接用于主流检测框架。数据集包含4470张jpg图片,标注类别为Empty-Space与Reduced两类,分别对应货架空置与商品减少状态,总框数达23775个,其中Reduced框16205个、Empty-Space框7570个,采用labelImg矩形框标注,标注准确合理。目前已有380人学习下载,可为货架缺货识别、智能补货提醒等任务提供现成的数据基础与类别参考。
1. 货架空置检测:4470 张双类标签数据集到底能解决什么
超市货架上的空位,看起来是个小事,做起来才知道有多麻烦。理货员巡场靠肉眼,一圈下来十几分钟,漏检率还高;装个摄像头做自动识别,又发现公开数据集里几乎没有专门标注"空位"这个类别的。大多数检测数据集标的是商品、是人、是车,没人认真标过"这里本该有货但现在是空的"。这个 4470 张、2 类标签、VOC+YOLO 双格式的货架空置缺货检测数据集,切入的正是这个缝隙。
它适合三类人:一是做零售巡检机器人或固定摄像头方案的算法工程师,需要一个能直接训练、能快速验证 baseline 的数据起点;二是做智能货架、无人零售柜的嵌入式开发者,想评估在边缘设备上跑轻量检测模型的可行性;三是做零售数据分析的产品侧同学,想搞清楚"缺货检测"这件事在视觉层面到底能做到什么精度。数据集本身不解决业务闭环,但它把最耗时的标注环节替你省掉了——4470 张真实货架场景图,2 类标签,VOC 和 YOLO 两种格式都给你,拿到就能开跑。
2. 数据集拆开看:4470 张图、2 类标签、双格式的选型逻辑
2.1 为什么是 2 类标签而不是更多
货架场景里理论上可以标很多东西:商品、价签、货架层板、空位、错位商品。但这个数据集只保留 2 类,常见做法是empty(空位/缺货区域)和goods(有货区域),或者empty和shelf(货架结构)。2 类的好处很直接:标注一致性高,模型收敛快,推理时后处理简单。
如果你自己扩展类别,比如把不同商品品类分开标,标注成本会指数级上升,而且类别间边界模糊——一包薯片和一包饼干在货架远景里可能只差几个像素。2 类标签的本质是把问题从"识别什么商品"降维成"这里有没有货",这对缺货检测这个业务目标来说,信息量是够的。
提示:拿到数据集后先确认类别名和索引映射。VOC 的 XML 里类别是字符串,YOLO 的 txt 里类别是整数索引,两者必须对齐,否则训练时会出现"标签对但类别错"的玄学问题。
2.2 VOC 与 YOLO 格式的差异和转换验证
VOC 格式每张图对应一个 XML,标注信息是绝对坐标xmin, ymin, xmax, ymax。YOLO 格式每张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0~1。数据集同时提供两种格式,省去了自己转的麻烦,但你还是得验证一遍转换是否正确。
下面这段脚本用来检查 VOC 和 YOLO 标注是否一一对应,以及坐标是否在合理范围内:
import os import xml.etree.ElementTree as ET def parse_voc(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) boxes = [] for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes def parse_yolo(txt_path): boxes = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) == 5: cid, xc, yc, bw, bh = map(float, parts) boxes.append((int(cid), xc, yc, bw, bh)) return boxes # 遍历检查 voc_dir = 'annotations_voc' yolo_dir = 'labels_yolo' img_dir = 'images' for xml_file in os.listdir(voc_dir): if not xml_file.endswith('.xml'): continue base = xml_file.replace('.xml', '') xml_path = os.path.join(voc_dir, xml_file) txt_path = os.path.join(yolo_dir, base + '.txt') img_path = os.path.join(img_dir, base + '.jpg') if not os.path.exists(txt_path): print(f'缺失 YOLO 标签: {base}') continue if not os.path.exists(img_path): print(f'缺失图片: {base}') continue w, h, voc_boxes = parse_voc(xml_path) yolo_boxes = parse_yolo(txt_path) if len(voc_boxes) != len(yolo_boxes): print(f'数量不匹配: {base}, VOC={len(voc_boxes)}, YOLO={len(yolo_boxes)}') continue for (name, xmin, ymin, xmax, ymax), (cid, xc, yc, bw, bh) in zip(voc_boxes, yolo_boxes): # 反归一化验证 xc_abs = xc * w yc_abs = yc * h bw_abs = bw * w bh_abs = bh * h xmin_calc = xc_abs - bw_abs / 2 ymin_calc = yc_abs - bh_abs / 2 if abs(xmin_calc - xmin) > 2 or abs(ymin_calc - ymin) > 2: print(f'坐标偏差过大: {base}, VOC=({xmin},{ymin}), YOLO反算=({xmin_calc:.1f},{ymin_calc:.1f})') print('检查完成')这段脚本的逻辑是:对每张图,分别读 VOC XML 和 YOLO txt,先检查文件是否存在、标注数量是否一致,再把 YOLO 的归一化坐标反算回绝对坐标,和 VOC 的坐标做对比。偏差阈值设 2 像素,是因为标注时可能有取整误差。如果偏差超过 2 像素,说明转换脚本有问题,需要重新生成。
参数说明:voc_dir、yolo_dir、img_dir按你实际解压后的目录结构改。类别名到索引的映射需要你根据数据集自带的classes.txt或 README 确认,脚本里没硬编码,因为不同版本可能不同。
2.3 数据分布先摸清再训练
4470 张图不算多,训练前必须看分布。至少统计三件事:每类标注框的数量、每张图的平均框数、空位框的尺寸分布。空位检测有个特点:空位区域的面积差异极大,一个整层空掉可能是几千像素宽,一个单品缺位可能只有几十像素。如果小目标占比高,anchor 设置和输入分辨率都要调。
import os import matplotlib.pyplot as plt yolo_dir = 'labels_yolo' class_counts = {} box_areas = [] for txt_file in os.listdir(yolo_dir): if not txt_file.endswith('.txt'): continue with open(os.path.join(yolo_dir, txt_file), 'r') as f: for line in f: parts = line.strip().split() if len(parts) == 5: cid = int(parts[0]) bw, bh = float(parts[3]), float(parts[4]) class_counts[cid] = class_counts.get(cid, 0) + 1 box_areas.append(bw * bh) print('类别分布:', class_counts) print('框面积分位数:') for q in [0.1, 0.25, 0.5, 0.75, 0.9]: print(f' {q}: {sorted(box_areas)[int(len(box_areas)*q)]:.4f}') plt.hist(box_areas, bins=50) plt.xlabel('Normalized box area') plt.ylabel('Count') plt.title('Box area distribution') plt.show()如果中位数面积低于 0.01(归一化后),说明小目标多,训练时输入尺寸建议不低于 640,YOLO 的 anchor 可以用 k-means 重新聚类。如果大面积框占主导,可以适当降低输入分辨率换速度。
3. 用 YOLO 格式跑通第一个缺货检测 baseline
3.1 目录结构整理与 data.yaml 配置
YOLO 系列训练对目录结构有固定要求。常见做法是按images/train、images/val、labels/train、labels/val组织,然后写一个data.yaml指向这些路径。4470 张图按 8:2 划分,训练集约 3576 张,验证集约 894 张。
# 假设解压后原始目录为 raw/ mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val # 简单随机划分(实际建议按场景分层,避免同一货架出现在训练和验证集) python -c " import os, random, shutil random.seed(42) imgs = [f for f in os.listdir('raw/images') if f.endswith('.jpg')] random.shuffle(imgs) split = int(len(imgs) * 0.8) for i, img in enumerate(imgs): src_img = os.path.join('raw/images', img) src_lbl = os.path.join('raw/labels', img.replace('.jpg', '.txt')) if i < split: shutil.copy(src_img, 'dataset/images/train/') shutil.copy(src_lbl, 'dataset/labels/train/') else: shutil.copy(src_img, 'dataset/images/val/') shutil.copy(src_lbl, 'dataset/labels/val/') print('划分完成') "data.yaml内容:
path: ./dataset train: images/train val: images/val nc: 2 names: 0: empty 1: goodsnc是类别数,names的顺序必须和 YOLO txt 里的整数索引一致。如果数据集自带的类别顺序不同,以数据集为准改这里。
3.2 训练命令与关键参数怎么设
以 YOLOv8 为例,最小训练命令:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/shelf \ name=baseline参数逐个说:model选yolov8s而不是n,是因为空位检测里小目标不少,n 的容量可能不够;imgsz=640是平衡精度和速度的默认值,如果显存够可以上 1280,对小空位更友好;batch=16在 8GB 显存上比较稳,不够就降到 8;lr0=0.01是 SGD 的常见起点,如果用 AdamW 可以降到 0.001;patience=20表示验证指标 20 轮不提升就早停,避免过拟合。
训练过程中重点看mAP50和mAP50-95。缺货检测业务上更关心召回率——漏检一个空位的代价比误检一个空位大,因为漏检意味着缺货没被发现。如果 recall 偏低,可以调低置信度阈值,或者增加空位类别的样本权重。
3.3 推理与结果可视化
训练完拿验证集跑推理,看实际效果:
yolo detect predict \ model=runs/shelf/baseline/weights/best.pt \ source=dataset/images/val \ conf=0.25 \ save=True \ project=runs/shelf \ name=val_predconf=0.25是默认置信度阈值。实际部署时这个值要根据业务调:如果希望尽量不漏,降到 0.15;如果误检太多,升到 0.4。推理结果会保存在runs/shelf/val_pred下,带标注框的图可以直接看。
如果想批量统计每张图的空位数量,可以用 Python 调:
from ultralytics import YOLO model = YOLO('runs/shelf/baseline/weights/best.pt') results = model('dataset/images/val', conf=0.25, save=False) for r in results: empty_count = sum(1 for c in r.boxes.cls if int(c) == 0) print(f'{r.path}: 空位数量={empty_count}')这段代码遍历验证集,统计每张图里empty类的检测框数量。业务上可以设阈值,比如空位数量超过 3 就触发补货提醒。
4. 避坑与排查:货架空置检测训练里最容易翻车的 5 个点
4.1 空位框标到了货架层板上
现象:模型把货架层板识别成空位,推理图上层板被框出来。
原因:标注时把"层板可见区域"和"缺货区域"混为一谈。层板是结构,空位是缺货,两者视觉上都是"没有商品",但业务含义完全不同。
解决:重新检查标注规范,空位框应该只覆盖"本该有商品但缺失"的区域,层板本身不标。如果数据集里已经混了,训练前用脚本过滤掉宽高比异常的框——层板通常极扁,宽高比超过 10:1 的框大概率是层板。
4.2 训练集和验证集出现同一货架
现象:验证集 mAP 很高,但换一家超市的图推理效果断崖式下降。
原因:随机划分时同一货架的不同角度、不同时间照片被分到了训练和验证集,模型记住了货架背景而不是空位特征。
解决:按货架 ID 或拍摄批次分层划分,确保同一货架只出现在训练集或验证集之一。如果数据集没提供货架 ID,可以用图像相似度聚类后再划分。
4.3 小空位漏检严重
现象:大块空位检测很准,单品缺位的小空位几乎检不出来。
原因:小目标在 640 分辨率下特征太弱,YOLO 的 P3 特征图 stride 是 8,一个 20 像素的框在特征图上只有 2~3 个像素。
解决:输入分辨率提到 1280,或者用带 P2 层的模型变体。另外可以用 k-means 对空位框重新聚类 anchor,让小 anchor 更匹配小空位。数据层面,对小空位样本做 oversampling。
4.4 类别不平衡导致空位类被压制
现象:goods类 mAP 正常,empty类 mAP 很低,模型倾向于把空位也预测成有货。
原因:有货区域通常比空位区域多得多,类别不平衡让模型偏向多数类。
解决:在 loss 里给empty类更高权重,或者用 focal loss。YOLOv8 可以在训练时通过cls参数调分类 loss 权重,也可以简单地对含空位的图做重复采样。
4.5 推理阈值照搬默认值
现象:训练指标好看,部署后要么漏报多要么误报多。
原因:conf=0.25是通用默认值,不是为缺货检测调的。缺货检测的代价矩阵和通用检测不同。
解决:在验证集上画 precision-recall 曲线,根据业务可接受的误报率反推阈值。如果业务要求召回率不低于 95%,就找 recall=0.95 时对应的 conf 值。这个值通常比 0.25 低不少。
5. 从 baseline 到可用:提升空位检测精度的几个实操技巧
第一个技巧是用背景差分做数据增强。货架空位检测有个天然优势:同一货架在不同时间的照片,有货和缺货的差异就是空位。如果你能拿到同一货架的时序图,可以做差分增强——把有货图的商品区域替换成空位图的对应区域,生成更多空位样本。这个做法比随机裁剪、翻转更贴近真实分布。
第二个技巧是后处理里加面积过滤和 NMS 调参。空位框通常不会重叠,NMS 的 IoU 阈值可以设低一点(比如 0.3),避免相邻空位被合并。同时过滤掉面积过小的框——小于图像面积 0.1% 的框大概率是噪声。
第三个技巧是用验证集做阈值搜索。不要凭感觉设 conf,写个循环在验证集上扫:
from ultralytics import YOLO import numpy as np model = YOLO('runs/shelf/baseline/weights/best.pt') confs = np.arange(0.05, 0.55, 0.05) for c in confs: results = model('dataset/images/val', conf=c, save=False, verbose=False) tp, fp, fn = 0, 0, 0 for r in results: pred_boxes = r.boxes.xyxy.cpu().numpy() pred_cls = r.boxes.cls.cpu().numpy() # 这里需要和 GT 对比,简化写法只统计预测数量 empty_pred = sum(1 for x in pred_cls if int(x) == 0) # 实际应加载 GT 做匹配,此处省略匹配逻辑 print(f'conf={c:.2f}, 空位预测数={empty_pred}')实际使用时要把预测框和 GT 框做 IoU 匹配,算出每个阈值下的 precision 和 recall,再选最优点。这个脚本的框架给你,匹配逻辑用 pycocotools 或自己写 IoU 都行。
第四个技巧是模型集成。如果单模型精度不够,可以训两个不同 backbone 的模型(比如 YOLOv8 和 RT-DETR),推理时做加权框融合。空位检测的场景相对固定,集成带来的提升通常比通用检测更明显。
最后一个习惯:每次改完标注或增强策略,先在小验证集上跑 10 个 epoch 看趋势,不要直接上 100 epoch。我自己的血泪经验是,标注问题导致的精度瓶颈,训再久也救不回来,早发现早返工。希望帮到你。
本文还有配套的精品资源,点击获取