COCO转YOLOv8训练商店偷窃检测:从标注到部署全指南
发布时间:2026/9/24 23:17:12来源:尧图网络
简介面向安防与零售场景的商店偷窃行为识别数据集资源主要服务于计算机视觉算法工程师、研究人员及安防应用开发者用于解决监控视频中偷窃行为的自动检测与识别问题。数据集中包含从原始视频中抽取的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, encodingutf-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 超过图片宽度 12 个像素的情况。检测阶段这类框影响不大训练阶段却会产生错误的 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, encodingutf-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_okTrue) os.makedirs(yolo_dataset/labels/train, exist_okTrue) os.makedirs(yolo_dataset/images/val, exist_okTrue) os.makedirs(yolo_dataset/labels/val, exist_okTrue) 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 fyolo_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(ftrain images: {len(train_ids)}, val images: {len(val_ids)})这里我没有直接把图片复制进对应目录而是先只生成标签。实际上 YOLO 训练时需要图片路径正确所以你应该在转换前就把图片文件按 train/val 组织好或者在写 txt 的同时执行 shutil.copy。上面这种划分方式的关键是按视频划分而不是按图片随机划分。商店监控视频的相邻帧往往只差几十毫秒画面几乎一样随机划分会让验证集变成训练集的“复读机”指标虚高得离谱部署到真实视频流时立刻现原形。类别 ID 重映射也千万别省略COCO 原始类别 ID 可能是 1、3、5YOLO 要求从 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( datadata.yaml, imgsz1280, # 监控画面中小目标多统一放大输入 epochs100, batch16, lr00.001, augmentTrue, mosaic0.5, # 前 50% epoch 用 mosaic后面关掉 close_mosaic10, # 最后 10 个 epoch 关闭 mosaic稳定收敛 patience20, seed42, projectshoplifting_exps, namerun_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( datadata.yaml, splitval, imgsz1280, conf0.001, iou0.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 才是真实水平。如果你用 conf0.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_dirFalse 只返回结果不落盘加速 results model.predict(img_path, conf0.1, imgsz1280, save_dirFalse) # 需要拿到 gt 做比对这里省略详细匹配代码 # 如果 gt 框与预测框最大 IOU 0.5就加入 hard_set hard_set.append(img_path) # 用 hard_set 上的 yaml 进行二次微调 model.train(datadata_hard.yaml, epochs30, lr00.0001, resumeFalse)难例挖掘本质上是让模型把注意力集中在它最容易犯错的地方。我在实际项目中遇到过的典型情况是模型把所有推购物车的顾客都识别成了 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 APperson 类 AP 95%shoplifting 类 AP 只有 65%整体 mAP 被拉高但实际需要的行为识别能力不达标。原因店铺监控里人的边界框往往每帧都有而“偷窃行为”相关的标注框只在动作发生时出现两类数量可能相差 10 倍以上。模型为了降低整体 loss会偏向学样本量大的类别导致行为类别的召回率很低。加上 shoplifting 这种动作类目标往往没有稳定的视觉外观不像“人”有统一的形状特征学起来更困难。解决不要用最简单的 oversample 复制少数类图片那样容易过拟合。我一般会用两阶段方法第一阶段只训练 person 检测器把所有人框出来第二阶段对每个 person 框做动作分类输入的裁剪区域包含历史和未来几帧的堆叠。这样就把“在整图中找目标”和“判断动作”两个难度拆开了。如果坚持单阶段检测需要给 shoplifting 类别更高的 loss 权重Ultralytics 支持在 loss 层面配置类别权重实际项目中我一般把少数类权重设为 23 倍同时配合 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, window15, min_votes10): self.window window self.min_votes min_votes # key: (class_id, normalized_center_x, normalized_center_y) self.history deque(maxlenwindow) 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 秒窗口看手的位置并没有靠近携带物。从那以后我每次做动作类检测项目都会强制走一遍“单帧检测 滑动窗口投票 告警延迟”的链路先把图像模型的可信度边界摸清再谈准确率。这个数据集能让你快速跑通前面的所有步骤真正的业务挑战永远在模型之外。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网