共计 2815 个字符,预计需要花费 8 分钟才能阅读完成。
自动驾驶中的语义分割基石
CamVid(Cambridge-Driving Labeled Video Database)作为经典的街景语义分割数据集,包含 700 多张行车记录仪拍摄的图片,标注了道路、车辆、行人等 32 类目标。在自动驾驶系统中,这种像素级理解能力直接决定了车辆能否准确识别可行驶区域(Drivable Area)和障碍物。与 ImageNet 等分类数据集不同,CamVid 的标注采用 PNG 格式的彩色掩码图——每个像素点的颜色值对应特定类别,这种标注方式在工业界非常普遍。

数据集目录结构解密
下载解压后的 CamVid 文件夹应包含以下结构(示例路径为简化版):
CamVid/
├── train/
│ ├── 0001TP_006690.png # 原始 RGB 图像
│ └── ...
├── train_labels/
│ ├── 0001TP_006690.png # 彩色标注掩码
│ └── ...
├── val/
└── val_labels/
- 关键细节:标注 PNG 采用 24 位 RGB 色彩空间,例如 ” 道路 ” 类用 (128,64,128) 表示。需要注意的是,原始数据集中部分标注存在边缘模糊问题(标注员主观判断导致),这是后续需要数据清洗的重点区域。
PyTorch 数据管道搭建
1. 自定义 Dataset 类实现
核心任务是读取原始图片和对应的彩色掩码,并将其转换为模型可处理的张量格式。关键步骤包括:
import torch
from torch.utils.data import Dataset
import numpy as np
from PIL import Image
class CamVidDataset(Dataset):
# 预定义颜色编码表(示例仅包含部分类别)CLASS_COLORS = {'road': np.array([128, 64, 128]),
'sky': np.array([128, 128, 128]),
'building': np.array([70, 70, 70])
}
def __init__(self, img_dir, label_dir, classes):
self.img_paths = sorted(Path(img_dir).glob('*.png'))
self.label_paths = sorted(Path(label_dir).glob('*.png'))
self.classes = classes
def __getitem__(self, idx):
img = np.array(Image.open(self.img_paths[idx]))
label = np.array(Image.open(self.label_paths[idx]))
# 将彩色掩码转换为类别索引图
h, w = label.shape[:2]
class_map = np.zeros((h, w), dtype=np.uint8)
for i, (_, color) in enumerate(self.classes.items()):
matches = np.all(label == color, axis=-1)
class_map[matches] = i
return torch.FloatTensor(img).permute(2,0,1)/255., \
torch.LongTensor(class_map)
2. 数据增强策略
语义分割任务的数据增强需要同步处理图像和标注掩码:
from torchvision import transforms
train_transform = transforms.Compose([transforms.RandomHorizontalFlip(p=0.5), # 水平翻转
transforms.RandomCrop(480), # 随机裁剪到固定尺寸
# 注意:不能对标注图做归一化或颜色变换
])
# 使用时需同步应用增强
img, mask = train_transform(img), train_transform(mask)
3. 类别权重计算
解决类别不平衡的经典方法是加权交叉熵损失(Weighted CrossEntropy):
def calculate_class_weights(dataset):
pixel_counts = {i:0 for i in range(len(dataset.classes))}
# 统计每个类别的像素总数
for _, mask in dataset:
for class_idx in pixel_counts:
pixel_counts[class_idx] += (mask == class_idx).sum().item()
total_pixels = sum(pixel_counts.values())
weights = [total_pixels/(count + 1e-6) for count in pixel_counts.values()]
return torch.FloatTensor(weights)
UNet 模型训练关键代码
import torch.nn as nn
model = UNet(num_classes=len(classes))
weights = calculate_class_weights(train_dataset)
criterion = nn.CrossEntropyLoss(weight=weights)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for epoch in range(100):
for inputs, labels in train_loader:
outputs = model(inputs)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
选择加权交叉熵而非 Dice Loss 的原因:在 CamVid 这种存在明显类别不平衡(如天空占比大)但需要保证小物体(如交通标志)分割精度的场景,加权交叉熵更容易通过调整权重平衡各类别贡献。
生产环境注意事项
- 标注一致性检查:
- 使用 OpenCV 的 findContours 检测标注图中的孤立像素点
-
对比相邻帧的标注差异,排查标注员主观性导致的突变
-
显存优化技巧:
- 采用梯度累积(Gradient Accumulation):每 4 个小批次更新一次参数
-
使用混合精度训练(AMP):
torch.cuda.amp.autocast() -
指标解读陷阱:
- 不要仅关注整体准确率(Accuracy),要看各类别的 IoU
- 测试时需保持与训练相同的数据预处理流水线
延伸思考
尝试将本流程迁移到 Cityscapes 数据集时,需要处理以下新挑战:
1. 更高的图像分辨率(2048×1024 vs CamVid 的 960×720)
2. 细粒度标注(如将 ” 车辆 ” 细分为轿车、卡车等)
3. 视频序列的时序一致性要求
建议从调整数据加载策略入手,逐步解决显存消耗和计算效率问题。
