第四章 数据集构建:安防场景数据采集、LabelImg标注、COCO格式转换
做安防监控的YOLO项目,最头疼的是什么?
不是模型选型,不是调参,而是——数据。我见过太多团队,算法工程师天天催「数据不够、标注不对」,最后项目烂尾。说白了,数据集的构建质量,直接决定了你的模型能不能落地。
4.1 安防场景的数据采集:别让摄像头白装了
安防场景和普通场景不一样。你想想看,监控摄像头拍到的画面,有几个特点:
- 视角固定:不像手机拍照可以随便换角度
- 光照变化大:白天黑夜、逆光顺光、阴晴雨雪
- 目标尺度差异大:远处的人可能只有几十个像素,近处的车能占满屏幕
- 遮挡严重:行人互相遮挡、被树木遮挡、被车辆遮挡
我在一个智慧园区项目里就吃过亏。当时客户给了10万张监控截图,我一看,全是白天晴天拍的。结果模型一上线,晚上直接崩了——漏检率超过60%。后来我学乖了,数据采集必须覆盖全时段。
安防数据采集的黄金法则:
- 时间维度:凌晨、早高峰、中午、傍晚、深夜,每个时段至少20%
- 天气维度:晴天、阴天、雨天、雾天,各占一定比例
- 光照维度:顺光、逆光、侧光、强光、弱光
- 场景维度:出入口、走廊、停车场、广场、电梯间
具体怎么采?我建议直接从NVR(网络录像机)拉取历史录像,用ffmpeg按帧抽图。别用截图工具一张张截,效率太低。
# 从监控视频中按帧抽取图像
ffmpeg -i input_video.mp4 -vf "fps=1" frame_%04d.jpg
# 如果想抽关键帧(场景变化大的帧)
ffmpeg -i input_video.mp4 -vf "select='gt(scene,0.4)',setpts=N/(25*TB)" keyframe_%04d.jpg
这里有个坑:fps=1表示每秒抽一帧。但安防场景很多画面是静止的(比如深夜空荡荡的走廊),抽出来的全是重复帧。我一般会加个场景检测参数,只保留变化超过40%的帧,这样能省下大量无效数据。
4.2 LabelImg标注:别小看这个老工具
标注工具我用过不少,LabelImg、LabelMe、CVAT、Supervisely……但说实话,对于安防场景的矩形框标注,LabelImg依然是最顺手的。轻量、免费、跨平台,不需要部署服务器。
安装很简单:
# 安装LabelImg
pip install labelImg
# 启动
labelImg
启动后界面很简洁。左边是图像列表,中间是标注区域,右边是标签列表。我个人习惯把快捷键背下来,能快很多:
| 快捷键 | 功能 | 我的使用频率 |
|---|---|---|
| W | 创建矩形框 | ★★★★★ |
| A/D | 上一张/下一张 | ★★★★★ |
| Ctrl+S | 保存标注 | ★★★★★ |
| Ctrl+U | 加载下一张 | ★★★★ |
| Delete | 删除选中框 | ★★★ |
| Space | 切换标注模式 | ★★ |
我的标注小技巧:
标注安防场景时,别只框「完整的人」。如果一个人被遮挡了70%,只露出头和肩膀,也要框出来。因为模型在真实场景中遇到的就是这种情况。我曾经让标注员只标注「完整可见」的目标,结果模型对遮挡目标的召回率惨不忍睹。
LabelImg默认保存为PASCAL VOC格式(XML文件)。但YOLO训练需要的是TXT格式(归一化的中心坐标+宽高),或者COCO格式(JSON)。所以我们需要做格式转换。
4.3 COCO格式转换:为什么我推荐它?
YOLOv5/v8/v9都支持COCO格式。为什么我推荐用COCO?原因有三:
- 统一标准:COCO格式是业界通用标准,很多预训练模型都是用COCO数据集训练的
- 便于管理:所有标注信息都在一个JSON文件里,不像VOC那样每个图一个XML
- 支持多任务:COCO可以同时存储检测、分割、关键点标注
从LabelImg的XML转COCO格式,我写过一个脚本,分享给你:
import xml.etree.ElementTree as ET
import json
import os
from glob import glob
def voc_to_coco(xml_dir, output_json, categories):
"""
将LabelImg标注的VOC格式转换为COCO格式
:param xml_dir: XML文件目录
:param output_json: 输出的COCO JSON文件路径
:param categories: 类别列表,如 ['person', 'car', 'bicycle']
"""
coco = {
"images": [],
"annotations": [],
"categories": [{"id": i+1, "name": cat} for i, cat in enumerate(categories)]
}
annotation_id = 1
for xml_file in sorted(glob(os.path.join(xml_dir, "*.xml"))):
tree = ET.parse(xml_file)
root = tree.getroot()
# 解析图像信息
filename = root.find("filename").text
size = root.find("size")
width = int(size.find("width").text)
height = int(size.find("height").text)
image_id = len(coco["images"]) + 1
coco["images"].append({
"id": image_id,
"file_name": filename,
"width": width,
"height": height
})
# 解析标注信息
for obj in root.findall("object"):
name = obj.find("name").text
if name not in categories:
continue
category_id = categories.index(name) + 1
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)
# COCO格式的bbox是 [x, y, width, height]
w = xmax - xmin
h = ymax - ymin
coco["annotations"].append({
"id": annotation_id,
"image_id": image_id,
"category_id": category_id,
"bbox": [xmin, ymin, w, h],
"area": w * h,
"iscrowd": 0
})
annotation_id += 1
with open(output_json, "w") as f:
json.dump(coco, f, indent=2)
print(f"转换完成!共 {len(coco['images'])} 张图片,{len(coco['annotations'])} 个标注")
# 使用示例
categories = ["person", "car", "bicycle", "motorcycle"]
voc_to_coco("annotations/", "coco_annotations.json", categories)
注意:COCO格式的bbox坐标是绝对坐标(像素值),不是归一化的。而YOLO训练时内部会做归一化处理,所以不需要你手动归一化。但如果你用YOLO的TXT格式,就需要归一化到0-1之间。
4.4 数据集的划分与验证
标注完成后,别急着训练。先做数据集划分。我一般按这个比例:
| 数据集 | 比例 | 用途 |
|---|---|---|
| 训练集 | 70% | 模型学习 |
| 验证集 | 20% | 调参和早停 |
| 测试集 | 10% | 最终评估 |
划分时要注意:同一场景的连续帧不要分到不同集合里。比如一个监控视频抽了1000帧,前700帧做训练,中间200帧做验证,最后100帧做测试——这样不行,因为画面太相似了,模型相当于「见过」测试数据。
我建议按场景划分:A摄像头的所有数据做训练,B摄像头做验证,C摄像头做测试。这样能真正检验模型的泛化能力。
数据质量检查清单:
- ✅ 每个类别至少有1000个标注实例
- ✅ 标注框没有超出图像边界
- ✅ 没有重复标注(同一个目标被框两次)
- ✅ 类别名称没有拼写错误
- ✅ 图像格式统一(建议全部转成JPG)
嗯,到这里数据集构建的核心流程就讲完了。数据是YOLO项目的基石,这一步做扎实了,后面的训练和部署才能顺风顺水。下一章我们聊聊数据增强——怎么用有限的数据,训练出更鲁棒的模型。