第四章 数据集构建:安防场景数据采集、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?原因有三:

  1. 统一标准:COCO格式是业界通用标准,很多预训练模型都是用COCO数据集训练的
  2. 便于管理:所有标注信息都在一个JSON文件里,不像VOC那样每个图一个XML
  3. 支持多任务: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项目的基石,这一步做扎实了,后面的训练和部署才能顺风顺水。下一章我们聊聊数据增强——怎么用有限的数据,训练出更鲁棒的模型。