3. 数据集构建:安防场景数据采集规范,LabelImg标注工具使用,数据集划分与格式转换(COCO转YOLO)
做安防项目,数据就是命根子。模型再牛,喂进去的数据是垃圾,出来的结果也是垃圾。这一章,我带你走一遍从采集到标注再到格式转换的完整流程。说白了,就是教你怎么造出一份能打的安防数据集。
核心要点:安防场景的数据集构建,不是简单的拍拍照、拉拉框。它需要你理解场景的复杂性,规范采集流程,并掌握工具链的每一个细节。数据质量直接决定了模型的上限。
3.1 安防场景数据采集规范
采集数据前,先想清楚你的模型要部署在什么环境。室内?室外?白天?黑夜?固定摄像头还是移动设备?这些因素决定了你的数据分布。
我个人习惯,在项目启动阶段会先做一份《数据采集规范文档》。这份文档会明确以下几点:
- 场景覆盖:至少包含晴天、阴天、雨天、夜间(红外/微光)四种光照条件。室内场景还要考虑不同灯光色温。
- 视角多样性:同一个目标,要有俯视、平视、侧视多个角度。安防摄像头通常安装在高处,俯视角度是重点。
- 目标尺度:行人、车辆在画面中的大小要覆盖大、中、小三个尺度。小目标(比如远处的行人)是安防场景的难点。
- 遮挡与截断:目标被树木、车辆、建筑物遮挡的情况必须包含。我记得有一次项目,模型在遮挡场景下漏检率高达30%,就是因为训练数据里遮挡样本太少。
我的经验:采集时,每个场景至少录制5-10分钟的视频,然后抽帧。不要只拍静态照片,动态模糊也是安防场景的常见问题。
下面这张图,是我总结的安防数据采集核心要素。你可以把它当成一个检查清单。
3.2 LabelImg标注工具使用
标注工具我推荐LabelImg。它轻量、开源、跨平台,而且支持YOLO和Pascal VOC两种格式。你想想看,一个工具能搞定两种主流格式,省了多少事。
安装很简单,直接pip搞定:
pip install labelImg
labelImg
启动后,界面很直观。左边是图片列表,中间是画布,右边是标注框和标签列表。我个人习惯的标注流程是这样的:
- 设置自动保存:在View菜单里勾选Auto Save mode。不然标注到一半崩溃了,哭都来不及。
- 选择输出格式:点击Change Save Dir,选择YOLO格式。LabelImg会为每张图片生成一个同名的.txt文件。
- 创建标签列表:在项目根目录下创建一个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张图片,人工查看标注框 | 框过大/过小、漏标、错标 |
自动化检查脚本:我写了一个小脚本,遍历所有图片和标签,检查文件名是否匹配、坐标是否合法。跑一遍只需要几秒钟,能避免很多低级错误。