3. 数据集构建:安防场景数据采集规范,LabelImg标注工具使用,数据集划分与格式转换(COCO转YOLO)

做安防项目,数据就是命根子。模型再牛,喂进去的数据是垃圾,出来的结果也是垃圾。这一章,我带你走一遍从采集到标注再到格式转换的完整流程。说白了,就是教你怎么造出一份能打的安防数据集。

核心要点:安防场景的数据集构建,不是简单的拍拍照、拉拉框。它需要你理解场景的复杂性,规范采集流程,并掌握工具链的每一个细节。数据质量直接决定了模型的上限。

3.1 安防场景数据采集规范

采集数据前,先想清楚你的模型要部署在什么环境。室内?室外?白天?黑夜?固定摄像头还是移动设备?这些因素决定了你的数据分布。

我个人习惯,在项目启动阶段会先做一份《数据采集规范文档》。这份文档会明确以下几点:

  • 场景覆盖:至少包含晴天、阴天、雨天、夜间(红外/微光)四种光照条件。室内场景还要考虑不同灯光色温。
  • 视角多样性:同一个目标,要有俯视、平视、侧视多个角度。安防摄像头通常安装在高处,俯视角度是重点。
  • 目标尺度:行人、车辆在画面中的大小要覆盖大、中、小三个尺度。小目标(比如远处的行人)是安防场景的难点。
  • 遮挡与截断:目标被树木、车辆、建筑物遮挡的情况必须包含。我记得有一次项目,模型在遮挡场景下漏检率高达30%,就是因为训练数据里遮挡样本太少。

我的经验:采集时,每个场景至少录制5-10分钟的视频,然后抽帧。不要只拍静态照片,动态模糊也是安防场景的常见问题。

下面这张图,是我总结的安防数据采集核心要素。你可以把它当成一个检查清单。

安防数据采集核心要素 数据采集规范 场景覆盖 视角多样性 目标尺度 遮挡与截断 晴天/阴天/雨天 俯视/平视/侧视 大/中/小目标 树木/车辆/建筑 夜间/红外/微光 不同安装高度 远/中/近距离 部分/严重遮挡 💡 避坑指南 我曾经在一个项目中,只采集了白天的数据,结果模型在夜间场景下几乎完全失效。 后来补采了红外和微光数据,mAP才从0.35提升到0.78。

3.2 LabelImg标注工具使用

标注工具我推荐LabelImg。它轻量、开源、跨平台,而且支持YOLO和Pascal VOC两种格式。你想想看,一个工具能搞定两种主流格式,省了多少事。

安装很简单,直接pip搞定:

pip install labelImg
labelImg

启动后,界面很直观。左边是图片列表,中间是画布,右边是标注框和标签列表。我个人习惯的标注流程是这样的:

  1. 设置自动保存:在View菜单里勾选Auto Save mode。不然标注到一半崩溃了,哭都来不及。
  2. 选择输出格式:点击Change Save Dir,选择YOLO格式。LabelImg会为每张图片生成一个同名的.txt文件。
  3. 创建标签列表:在项目根目录下创建一个classes.txt文件,里面按行写入所有类别名称。比如:
person
car
bicycle
motorcycle
bus
truck

然后在LabelImg里加载这个文件:View → Change Saved Classes → 选择classes.txt。

注意:YOLO格式的类别索引是从0开始的。classes.txt里第一行对应索引0,第二行对应索引1,以此类推。这个顺序一旦确定,训练时就不能改。

标注时,有几个快捷键能大幅提升效率:

快捷键 功能 我的使用频率
W 创建标注框 极高
A / D 上一张 / 下一张图片 极高
Ctrl + S 保存当前标注 高
Ctrl + R 重置当前图片的标注 中
Delete 删除选中的标注框 中
Space 标记为已检查 低(我习惯批量检查)

标注完成后,你会得到这样的目录结构:

dataset/
├── images/
│   ├── img_0001.jpg
│   ├── img_0002.jpg
│   └── ...
└── labels/
    ├── img_0001.txt
    ├── img_0002.txt
    └── ...

每个.txt文件的内容长这样:

0 0.523 0.412 0.156 0.234
1 0.789 0.345 0.112 0.189

格式是:class_id center_x center_y width height。注意,这些坐标都是归一化到[0,1]的,计算方式是:归一化值 = 像素值 / 图片宽或高。

一个小技巧:标注时,框要尽量贴合目标边缘,但不要切掉目标的关键部位。比如行人,框的下边缘要贴着脚底,上边缘到头顶。我曾经见过有人把框画得特别大,把背景也包进去了,结果模型学了一堆背景特征。

3.3 数据集划分与格式转换(COCO转YOLO)

标注完数据,下一步就是划分和格式转换。这一步看似简单,但坑不少。

3.4.1 数据集划分

标准划分是训练集70%、验证集20%、测试集10%。但安防场景有个特殊情况:同一个场景的连续帧,不能同时出现在训练集和验证集里。否则模型会“记住”场景,导致验证指标虚高。

我一般按视频片段来划分。比如有10个视频片段,取7个做训练,2个做验证,1个做测试。这样能保证数据独立性。

下面是一个简单的划分脚本:

import os
import random
import shutil

# 设置随机种子,保证可复现
random.seed(42)

# 图片和标签路径
image_dir = 'dataset/images'
label_dir = 'dataset/labels'

# 获取所有图片文件名
images = [f for f in os.listdir(image_dir) if f.endswith('.jpg')]
random.shuffle(images)

# 划分比例
train_ratio = 0.7
val_ratio = 0.2
test_ratio = 0.1

# 计算数量
total = len(images)
train_end = int(total * train_ratio)
val_end = int(total * (train_ratio + val_ratio))

# 划分
train_images = images[:train_end]
val_images = images[train_end:val_end]
test_images = images[val_end:]

# 创建目标目录
for split in ['train', 'val', 'test']:
    os.makedirs(f'dataset/{split}/images', exist_ok=True)
    os.makedirs(f'dataset/{split}/labels', exist_ok=True)

# 移动文件
def move_files(image_list, split):
    for img in image_list:
        # 移动图片
        shutil.copy(os.path.join(image_dir, img), 
                    f'dataset/{split}/images/{img}')
        # 移动对应的标签文件
        label_file = img.replace('.jpg', '.txt')
        if os.path.exists(os.path.join(label_dir, label_file)):
            shutil.copy(os.path.join(label_dir, label_file),
                        f'dataset/{split}/labels/{label_file}')

move_files(train_images, 'train')
move_files(val_images, 'val')
move_files(test_images, 'test')

print(f'训练集: {len(train_images)} 张')
print(f'验证集: {len(val_images)} 张')
print(f'测试集: {len(test_images)} 张')

3.4.2 COCO转YOLO格式

很多公开数据集(比如COCO、VisDrone)用的是COCO格式。COCO格式用JSON文件存储标注信息,而YOLO格式用单独的.txt文件。转换的核心就是解析JSON,然后为每张图片生成一个.txt文件。

COCO格式的JSON结构大致如下:

{
  "images": [
    {"id": 1, "file_name": "000001.jpg", "width": 1920, "height": 1080},
    ...
  ],
  "annotations": [
    {"id": 1, "image_id": 1, "category_id": 1, 
     "bbox": [100, 200, 300, 400], ...},
    ...
  ],
  "categories": [
    {"id": 1, "name": "person"},
    ...
  ]
}

注意,COCO的bbox格式是 [x, y, width, height],其中 (x, y) 是左上角坐标。而YOLO需要的是 [center_x, center_y, width, height],且都是归一化值。

转换公式:

center_x = (x + width / 2) / image_width
center_y = (y + height / 2) / image_height
norm_width = width / image_width
norm_height = height / image_height

下面是一个完整的转换脚本:

import json
import os

def coco_to_yolo(coco_json_path, output_dir):
    # 加载COCO JSON
    with open(coco_json_path, 'r') as f:
        coco_data = json.load(f)
    
    # 创建类别映射
    categories = {}
    for cat in coco_data['categories']:
        categories[cat['id']] = cat['name']
    
    # 创建图片信息映射
    images_info = {}
    for img in coco_data['images']:
        images_info[img['id']] = img
    
    # 按图片ID分组标注
    annotations_by_image = {}
    for ann in coco_data['annotations']:
        img_id = ann['image_id']
        if img_id not in annotations_by_image:
            annotations_by_image[img_id] = []
        annotations_by_image[img_id].append(ann)
    
    # 为每张图片生成YOLO标签
    for img_id, anns in annotations_by_image.items():
        img_info = images_info[img_id]
        img_width = img_info['width']
        img_height = img_info['height']
        img_filename = img_info['file_name']
        
        # YOLO标签文件名
        label_filename = img_filename.replace('.jpg', '.txt')
        label_path = os.path.join(output_dir, label_filename)
        
        with open(label_path, 'w') as f:
            for ann in anns:
                # COCO bbox: [x, y, width, height]
                x, y, w, h = ann['bbox']
                
                # 转换为YOLO格式
                center_x = (x + w / 2) / img_width
                center_y = (y + h / 2) / img_height
                norm_w = w / img_width
                norm_h = h / img_height
                
                # 类别ID(从0开始)
                cat_id = ann['category_id']
                # 注意:这里需要将COCO的category_id映射到YOLO的连续索引
                # 简单起见,假设category_id就是连续从1开始的
                yolo_class_id = cat_id - 1
                
                f.write(f"{yolo_class_id} {center_x:.6f} {center_y:.6f} {norm_w:.6f} {norm_h:.6f}\n")
    
    print(f"转换完成!共处理 {len(annotations_by_image)} 张图片")

# 使用示例
coco_to_yolo('annotations/instances_train2017.json', 'yolo_labels/')

常见坑点:COCO的category_id不一定是连续的。比如COCO数据集中,person的id是1,bicycle是2,但car是3。如果你的数据集类别不连续,需要自己建立一个映射表。我曾经在这个坑里爬了半小时,转换出来的标签全是错的。

3.4 数据质量检查清单

数据准备好了,别急着训练。先做一轮质量检查。我总结了一个清单,每次项目都会过一遍:

检查项 检查方法 常见问题
标签完整性 检查每张图片是否有对应的.txt文件 漏标、空标签文件
坐标合法性 检查归一化坐标是否在[0,1]范围内 坐标越界、负数
类别一致性 检查所有标签中的类别ID是否在预期范围内 类别ID错误、多类别混淆
图片-标签匹配 检查图片文件名和标签文件名是否一一对应 文件名不匹配、后缀不一致
标注质量 随机抽检100张图片,人工查看标注框 框过大/过小、漏标、错标

自动化检查脚本:我写了一个小脚本,遍历所有图片和标签,检查文件名是否匹配、坐标是否合法。跑一遍只需要几秒钟,能避免很多低级错误。