简介:面向安防与零售场景的商店偷窃行为识别数据集资源,主要服务于计算机视觉算法工程师、研究人员及安防应用开发者,用于解决监控视频中偷窃行为的自动检测与识别问题。数据集中包含从原始视频中抽取的8395张真实场景图像,统一采用COCO格式进行标注,可直接用于目标检测、姿态估计等任务的模型训练与效果验证,兼顾素材规模与标注质量。压缩包内共2000个文件,以1995张jpg图像为主体,图像来自视频帧截取;另外包含3个json标注文件和2个txt辅助说明文件,用于标签映射、数据划分等用途,包体总大小约455.58MB,结构清晰便于直接接入主流深度学习框架。当前资源已有545人浏览学习,具备一定的应用参考价值。数据整体在商店偷窃行为识别上的平均准确率达到98.6%,说明标注质量与场景覆盖较为可靠。对于需要训练识别模型、验证算法性能或搭建零售安防系统的开发者,这份数据集能够提供扎实的数据基础与标注支撑。
1. 商店偷窃行为识别数据集:8395张COCO标注图,从监控帧到训练闭环
如果你做过监控视频里的行为识别,一定知道最耗时间的不是调模型,而是凑数据。商店偷窃(shoplifting)这类场景尤其难搞:公开数据集少、授权敏感、真实监控画面又往往模糊、遮挡严重。这个数据集把 8395 张原始图片统一标成了 COCO 格式,等于把最脏的活先干完了。宣称的平均准确率 98.6% 听起来很漂亮,但我不建议你直接拿这个数字当交付标准,更值得关注的是它背后的标注结构和训练闭环。适合谁用?想快速验证检测方案、做行为识别落地实验的算法工程师和研究生,以及刚接触目标检测、需要一份干净 COCO 数据练手的入门者。这篇文章会把 COCO 标注拆开、给出训练和评估的完整步骤,再把帧序列、类别不平衡这些坑一个个说清楚。
2. 拆解 COCO 标注:从 JSON 结构到数据质量检查
2.1 COCO 格式核心字段与这个数据集的标注语义
COCO 格式并不是一套“文件夹里有图和 txt”的简化约定,它用单个 JSON 文件承载全部标注信息。标准的 annotations 结构包含 info、licenses、images、annotations、categories 五部分。images 里每一行是一条图片记录,annotations 里每一条对应一个目标框,categories 则定义了类别 ID 到名称的映射。很多从 Roboflow 导出的数据集会在 info 里带上导出时间、版本号,文件名里也会保留来源视频的痕迹,比如 Shoplifting034_x264-mp4_8473_jpg 这种命名,说明这张图是从 id 为 034 的视频第 8473 帧截出来的。理解这一点对后续划分训练集和验证集非常重要,因为同一个视频的相邻帧非常相似,直接随机划分会造成数据泄漏,后面我会专门讲。
这个数据集的核心用途是“商店偷窃行为识别”,但 COCO 格式本身是目标检测的通用格式,所以它做的是“检测”而非“行为分类”。在拿到数据后,第一件事不是急着训练,而是确认 categories 里到底定义了几类。常见的设计有两种:一种只框“偷窃行为发生时的关键物体”(比如顾客手部、商品),另一种会同时标注“人”和“商品”,再结合时序逻辑判断动作。从文件名和标注来源看,它倾向于把视频帧作为独立图片处理,逐帧给出边界框。无论类别设计是哪一种,都要在训练前统一类别 ID,否则后续转 YOLO 格式会乱套。
2.2 写个 Python 脚本检查标注质量:类别分布、框面积和越界框
我一般拿到 COCO 数据集后,不会先看图片,而是先跑一遍全面体检。下面这个脚本可以输出类别数量、每类目标数、边界框面积分布、以及越界框数量。它不依赖任何深度学习框架,只要装了 pycocotools 或者纯 json 解析就能跑。
import json from collections import Counter # 指向数据集里的标注文件,按实际路径改 with open('annotations/instances_train.json', 'r', encoding='utf-8') as f: coco = json.load(f) # 构建 img_id 到图片元数据的映射 id_to_img = {img['id']: img for img in coco['images']} # 构建 category_id 到名称的映射 id_to_cat = {cat['id']: cat['name'] for cat in coco['categories']} cat_counter = Counter() area_list = [] invalid_box = 0 # 越界框计数 for ann in coco['annotations']: cat_id = ann['category_id'] cat_counter[id_to_cat[cat_id]] += 1 x, y, w, h = ann['bbox'] # COCO bbox 格式是 [x, y, width, height] area_list.append(w * h) # 拿到对应图片的宽高,判断框是否超出边界 img_info = id_to_img[ann['image_id']] iw, ih = img_info['width'], img_info['height'] if x < 0 or y < 0 or x + w > iw or y + h > ih: invalid_box += 1 # 每类目标数量 print('类别统计:') for name, cnt in cat_counter.most_common(): print(f' {name}: {cnt}') print(f'平均框面积: {sum(area_list) / len(area_list):.1f} 平方像素') print(f'越界框数量: {invalid_box}')这段脚本里最值得注意的就是越界框检测。COCO 官方标注严格规定 bbox 不能超出图片边界,但半自动标注工具导出的数据经常出现 x + w 超过图片宽度 1~2 个像素的情况。检测阶段这类框影响不大,训练阶段却会产生错误的 anchor 匹配,尤其使用 YOLO 系列时可能直接报错。另外,面积分布能告诉你这个数据集是“大目标为主”还是“小目标密集”。如果平均框面积只有几百平方像素,而图片是 1280×720 的监控画面,说明后续训练必须加大输入尺寸或者使用 P2 层特征,不能用默认的 640×640。类别统计则直接暴露类别不平衡问题,如果“商品”这类框有 5 万个而“偷窃动作”只有 3000 个,那就需要考虑采样策略。
3. 把 COCO 跑进 YOLOv8:格式转换、数据划分与训练参数
3.1 COCO JSON 转 YOLO txt:坐标归一化与文件路径整理
直接用 YOLOv8 训练通常不喂 COCO JSON,而是用每张图对应一个同名 txt 的格式。转换时要注意三个细节:类别 ID 必须从 0 开始连续编号;边界框要转成归一化的中心点坐标;每个图片名要和 txt 文件完全一致。下面这段脚本可以完成转换,同时帮你按视频 ID 划分训练集和验证集,避免相邻帧泄漏。
import json import os import random from collections import defaultdict with open('annotations/instances_train.json', 'r', encoding='utf-8') as f: coco = json.load(f) # 建立 image_id -> 文件名 的映射 id_to_name = {img['id']: img['file_name'] for img in coco['images']} # 建立 image_id -> 视频编号 的映射,前提是文件名里包含帧来源 id_to_video = {} for img in coco['images']: # 示例:Shoplifting034_x264-mp4_8473_jpg.rf.jpg parts = img['file_name'].split('_') video_id = parts[0] if len(parts) > 0 else 'unknown' id_to_video[img['id']] = video_id # 把标注按图片聚合 anns_by_img = defaultdict(list) for ann in coco['annotations']: anns_by_img[ann['image_id']].append(ann) # 类别 ID 映射为连续编号 cat_ids = sorted({ann['category_id'] for ann in coco['annotations']}) cat_id_map = {old: new for new, old in enumerate(cat_ids)} img_ids = list(anns_by_img.keys()) random.shuffle(img_ids) # 按视频划分:确保同一视频的帧全部进同一侧 video_keys = set(id_to_video[i] for i in img_ids) video_keys = list(video_keys) random.shuffle(video_keys) train_videos = set(video_keys[:int(len(video_keys) * 0.8)]) train_ids = [i for i in img_ids if id_to_video[i] in train_videos] val_ids = [i for i in img_ids if id_to_video[i] not in train_videos] os.makedirs('yolo_dataset/images/train', exist_ok=True) os.makedirs('yolo_dataset/labels/train', exist_ok=True) os.makedirs('yolo_dataset/images/val', exist_ok=True) os.makedirs('yolo_dataset/labels/val', exist_ok=True) def write_yolo(img_ids, split): for img_id in img_ids: fname = id_to_name[img_id] stem = os.path.splitext(fname)[0] out_txt = f'yolo_dataset/labels/{split}/{stem}.txt' img_info = next(img for img in coco['images'] if img['id'] == img_id) iw, ih = img_info['width'], img_info['height'] with open(out_txt, 'w') as w: for ann in anns_by_img[img_id]: x, y, bw, bh = ann['bbox'] # 转归一化中心点坐标 cx = (x + bw / 2) / iw cy = (y + bh / 2) / ih nw = bw / iw nh = bh / ih cls = cat_id_map[ann['category_id']] w.write(f'{cls} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n') write_yolo(train_ids, 'train') write_yolo(val_ids, 'val') print(f'train images: {len(train_ids)}, val images: {len(val_ids)}')这里我没有直接把图片复制进对应目录,而是先只生成标签。实际上 YOLO 训练时需要图片路径正确,所以你应该在转换前就把图片文件按 train/val 组织好,或者在写 txt 的同时执行 shutil.copy。上面这种划分方式的关键是按视频划分,而不是按图片随机划分。商店监控视频的相邻帧往往只差几十毫秒,画面几乎一样,随机划分会让验证集变成训练集的“复读机”,指标虚高得离谱,部署到真实视频流时立刻现原形。类别 ID 重映射也千万别省略,COCO 原始类别 ID 可能是 1、3、5,YOLO 要求从 0 开始,不映射会导致类别错乱且不容易察觉。
3.2 训练参数怎么设:imgsz、epochs、batch 与数据增强
数据集转换好后,训练 YOLOv8 需要一份 data.yaml。下面这份配置对应单类别/多类别的场景,按你的实际类别名修改。
# data.yaml path: ./yolo_dataset train: images/train val: images/val nc: 2 names: 0: person 1: shoplifting训练命令我用的是 ultralytics 的 Python API,因为它能方便地接入后续评估脚本:
from ultralytics import YOLO # 从 YOLOv8m 预训练权重开始,比从头训练收敛快得多 model = YOLO('yolov8m.pt') model.train( data='data.yaml', imgsz=1280, # 监控画面中小目标多,统一放大输入 epochs=100, batch=16, lr0=0.001, augment=True, mosaic=0.5, # 前 50% epoch 用 mosaic,后面关掉 close_mosaic=10, # 最后 10 个 epoch 关闭 mosaic,稳定收敛 patience=20, seed=42, project='shoplifting_exps', name='run_1280', )imgsz 是这张数据集的第一个关键参数。如果原图是 1920×1080 的监控抽帧,里面的人体可能只占 80×200 像素,默认 640 输入会让小目标缩到几乎不可见。我一般会先用 960 到 1280 之间的值做一次快速实验,对比验证集 mAP。batch 大小受显存限制,但至少要保证单卡 batch 不小于 8,否则 BatchNorm 统计不稳定。mosaic 增强对这个场景是一把双刃剑:mosaic 能把四张图拼在一起,提高模型对小目标和遮挡的鲁棒性;但商店偷窃动作本身依赖完整的时序上下文,过度拼接会让模型学到“碎片化的局部特征”,所以我把 mosaic 概率降到 0.5,并且最后 10 个 epoch 完全关闭。这也是明显区别于通用目标检测调参的细节。
训练过程中要盯着两个曲线:train/cls_loss 应该平滑下降,val/box_loss 如果先降后升就是过拟合。这个数据集规模不大不小,100 个 epoch 足够。如果训练到 80 epoch 时 val mAP 还在涨,可以考虑加 50 epoch;如果 40 epoch 就不动了,优先检查前面说的数据泄漏,而不是盲目加迭代。
4. 评估与调优:98.6% 准确率怎么验证,mAP 和置信度阈值怎么配合
4.1 准确率不等于 mAP:先搞清楚数据集宣称的 98.6% 从哪来
很多人拿到数据集看到“平均准确率达到 98.6%”就以为模型部署后也有这个效果,这是最大的误解。准确率(Accuracy)在目标检测里并不是一个很自然的指标,因为检测模型输出的是“哪张图里有目标”和“目标在哪个位置”,而不是简单的二分类。98.6% 可能来自图片级的行为分类、单个类别的精确率、或者某个任务上的 top-1 准确率,只有看到评估脚本才能确认。在你没有重新跑测试集之前,这个数字只能作为数据集标注质量的参考信号,而不是模型性能承诺。
目标检测的通用评估指标是 COCO mAP,它综合了 IOU 从 0.5 到 0.95 共 10 个阈值下的平均精度。下面这段代码用训练好的权重在验证集上做评估:
from ultralytics import YOLO model = YOLO('shoplifting_exps/run_1280/weights/best.pt') # 只用 val 集评估,IOU 阈值覆盖 0.5:0.95 metrics = model.val( data='data.yaml', split='val', imgsz=1280, conf=0.001, iou=0.6, ) print('mAP50:', metrics.box.map50) print('mAP50-95:', metrics.box.map) print('每类 AP:', metrics.box.ap)评估时要注意 conf 参数。推理阶段我们通常把置信度阈值设成 0.3 或 0.5,但评估阶段为了计算完整的 PR 曲线,conf 要设得极低,比如 0.001。这样才能覆盖所有可能被误检的低置信度框,得到的 mAP 才是真实水平。如果你用 conf=0.5 去评估,mAP 会比正常值虚高很多,因为低置信度的正样本全部被提前滤掉了,PR 曲线的起点就不完整。这是很多人复现数据集指标失败的第一原因。
4.2 调优策略:从难例挖掘到损失权重
如果你发现验证集 mAP50 已经到 0.95 以上,但 mAP50-95 只有 0.6,说明框定位精度不够。此时优先做两件事:一是继续提高输入尺寸,1280 不行就试 1536,但要注意显存占用和推理延迟;二是检查验证集里是否存在大量边界框面积小于 32×32 的小目标,YOLOv8 的检测头对这类目标不敏感,需要开启 P2 层或者用 SAHI 做切片推理。
如果发现某一类 AP 特别低,比如“shoplifting”动作类的 AP 只有 40%,但“person”类有 90%,这就是典型类别不平衡。处理方法不是简单复制样本,而是先用 classifier 做难例挖掘。把训练集里被分错的样本筛出来,单独建一个“hard set”,做二次微调。下面这个策略是我常用的流程:第一轮正常训练;第二轮用 best.pt 对训练集做检测,把所有漏检的标注框对应的图片抽出来,加上随机翻转和颜色抖动,重新组成一份微调集,用更小的学习率继续训练 30 epoch。
# 难例挖掘示意:找出漏检的图片 import glob from ultralytics import YOLO model = YOLO('shoplifting_exps/run_1280/weights/best.pt') train_images = glob.glob('yolo_dataset/images/train/*.jpg') hard_set = [] for img_path in train_images: # save_dir=False 只返回结果不落盘,加速 results = model.predict(img_path, conf=0.1, imgsz=1280, save_dir=False) # 需要拿到 gt 做比对,这里省略详细匹配代码 # 如果 gt 框与预测框最大 IOU < 0.5,就加入 hard_set hard_set.append(img_path) # 用 hard_set 上的 yaml 进行二次微调 model.train(data='data_hard.yaml', epochs=30, lr0=0.0001, resume=False)难例挖掘本质上是让模型把注意力集中在它最容易犯错的地方。我在实际项目中遇到过的典型情况是:模型把所有推购物车的顾客都识别成了 shoplifting,因为购物车本身与行窃动作的空间位置高度重合。这种情况下,光加数据不够,需要在标注层面区分“推车”和“将商品放入包中/口袋”的行为语义,或者干脆把模型输出和行为分类器串联起来,用商品的位移轨迹做二次判断。这个我会在最后一章展开。
5. 避坑指南:帧泄漏、标注错位、类别不平衡的真实翻车记录
5.1 验证集 mAP 虚高,部署后效果断崖式下跌
现象:用这份数据集训练的模型,验证集 mAP50 能达到 0.98,看起来尤其优秀,但一接到真实摄像头视频流,漏检率立刻飙到 30% 以上。
原因:文件命名暴露出的视频抽帧特性没有被处理。直接随机划分训练集和验证集时,同一个视频的相邻帧大量同时出现在训练集和验证集里。相邻帧之间差异极小,模型等于 “背题” 考了高分,一到没见过的新场景就现原形。
解决:严格按视频维度划分数据集。把文件名中的视频 ID 提出来,用视频 ID 做 group split,保证同一个视频的所有帧只在训练集或验证集里出现一次。如果你拿到的是已经划分好的数据集,验证集里仍然混着同一视频的帧,建议按帧间隔重采样:每 10 帧取 1 帧进验证集,最大程度降低帧间相似度。从那以后我每次拿到监控视频数据集,第一件事就是统计“同一视频的帧是否同时出现在 train 和 val 里”,这一步不做,后面都是空中楼阁。
5.2 边界框标注与人体实际位置错位
现象:训练过程中 loss 降到一定程度后不再下降,可视化验证集预测结果时发现模型预测框总是比真实框偏左或偏上,尤其是被货架遮挡的人体。
原因:这个数据集的部分标注可能来自半自动跟踪工具,对运动速度快的目标,标注框滞后于目标实际位置。我曾在文件名里看到来自 MP4 视频的抽帧,这类工具默认按 30fps 抽帧,但对快速弯腰、伸手的动作,相邻两帧的位移可以达到几十像素,微调框跟不上就会产生系统性偏差。
解决:训练前增加一个“框对齐”检查,对同一目标在相邻帧中的位置做平滑拟合,如果某一帧的框偏离拟合轨迹超过阈值,手动修正或直接删除该帧。另一个更省事的做法是用一个先验模型对全部数据做预检测,把检测结果和标注框做比对,自动标出差异大于 20 像素的样本。这样能快速定位出错帧,而不是靠肉眼翻几千张图。标注错位不会直接导致模型不收敛,但会让边界框回归精度长期停在低水平。
5.3 “shoplifting” 类别样本远少于 “person”,模型偏向学习主体类别
现象:训练完查看 per-class AP,person 类 AP 95%,shoplifting 类 AP 只有 65%,整体 mAP 被拉高,但实际需要的行为识别能力不达标。
原因:店铺监控里人的边界框往往每帧都有,而“偷窃行为”相关的标注框只在动作发生时出现,两类数量可能相差 10 倍以上。模型为了降低整体 loss,会偏向学样本量大的类别,导致行为类别的召回率很低。加上 shoplifting 这种动作类目标往往没有稳定的视觉外观,不像“人”有统一的形状特征,学起来更困难。
解决:不要用最简单的 oversample 复制少数类图片,那样容易过拟合。我一般会用两阶段方法:第一阶段只训练 person 检测器,把所有人框出来;第二阶段对每个 person 框做动作分类,输入的裁剪区域包含历史和未来几帧的堆叠。这样就把“在整图中找目标”和“判断动作”两个难度拆开了。如果坚持单阶段检测,需要给 shoplifting 类别更高的 loss 权重,Ultralytics 支持在 loss 层面配置类别权重,实际项目中我一般把少数类权重设为 2~3 倍,同时配合 mosaic 增强。
5.4 图像尺寸不一致导致归一化标注全部偏移
现象:转换 YOLO 格式后训练,前几个 epoch loss 异常高,部分锚框与目标完全没有重叠。
原因:数据集里 8395 张图片不是同一个分辨率,一部分来自 1920×1080 的视频抽帧,另一部分可能是经过 Roboflow 预处理后的 640×480 缩略图。转换脚本里如果用的图片宽度来自 JSON,而实际图片经过外部缩放,坐标归一化后就会出现系统性偏移。
解决:转换前先扫描所有图片的实际尺寸,与 JSON 里的 width/height 逐一对比,不一致的以实际尺寸为准重新计算 bbox。更稳妥的做法是统一 resize 到同一尺寸并保持宽高比填充黑边,然后再生成标签。这一点对监控数据尤其致命,因为不同摄像头的分辨率和画幅比差异很大,模型在训练时看到不同尺度的同一目标,会无法决定应该匹配哪个 anchor。
6. 进阶技巧:把检测输出变成行为判据,用帧间投票稳住误检
到这步,你已经有了一个能稳定框出人和商品的检测模型。但“商店偷窃行为识别”在真实业务里的需求从来不是画框,而是触发告警。直接对每一帧的检测结果做判断,会出现大量抖动:顾客弯腰系鞋带被当成熟人、伸手拿货架高层商品被当成偷窃。我给自己的模型加了一层很简单的后处理,效果立竿见影。
常见做法是维护一个检测结果的滑动窗口队列,对每个目标和它的属性做“N 帧内出现 M 次”的投票。这个逻辑不需要重新训练,只改动推理脚本。下面是一个简化版:
from collections import deque class FrameVoter: def __init__(self, window=15, min_votes=10): self.window = window self.min_votes = min_votes # key: (class_id, normalized_center_x, normalized_center_y) self.history = deque(maxlen=window) def update(self, detections): current_frame = set() for det in detections: # 用归一化中心点作为目标身份近似,实际项目可以加 IoU tracker key = (int(det.boxes.cls[0]), round(float(det.boxes.xyxy[0][0]) / 1280, 2), round(float(det.boxes.xyxy[0][1]) / 720, 2)) current_frame.add(key) self.history.append(current_frame) vote_count = {} for frame_keys in self.history: for key in frame_keys: vote_count[key] = vote_count.get(key, 0) + 1 alert = False for key, cnt in vote_count.items(): if key[0] == 1 and cnt >= self.min_votes: alert = True # 行为类目标连续多帧出现,触发关注 return alert这段逻辑里,window 是时间窗口长度,按 30fps 算,15 帧就是 0.5 秒。min_votes 设置太高会漏报,太低会误报,我习惯跟客户一起调这两个值。它真正的价值是:检测模型输出的是单帧语义,行为判据依赖的是跨帧一致性。把“出现次数”作为置信度之外的第二个门槛,误报率能压下一个量级。
我在复盘这个数据集的一次测试时发现,很多“偷窃”框实际是顾客的手划过画面边缘,单帧看很像抓取动作,但拉长到 1 秒窗口看,手的位置并没有靠近携带物。从那以后我每次做动作类检测项目,都会强制走一遍“单帧检测 + 滑动窗口投票 + 告警延迟”的链路,先把图像模型的可信度边界摸清,再谈准确率。这个数据集能让你快速跑通前面的所有步骤,真正的业务挑战永远在模型之外。希望帮到你。
本文还有配套的精品资源,点击获取