语义分割-CityScapes数据集实战:从数据准备到模型训练
1. 初识CityScapes数据集:街景语义分割的黄金标准
第一次接触CityScapes数据集时,我被它的规模和质量震撼到了。这个数据集就像是用高清摄像机记录下的欧洲城市日记,5000张精细标注的街景图像,每张分辨率高达1024x2048,相当于普通高清电视的四倍清晰度。我在自动驾驶项目中最头疼的就是找不到高质量的道路场景数据,直到发现了这个宝藏。
CityScapes最特别的地方在于它的标注体系。举个例子,同样是"车辆"这个类别,它会细致区分轿车、卡车、公交车甚至自行车,总共有30多个语义类别。我在处理数据时发现,连"红绿灯"和"交通标志"这种小物体都有独立标签,这对于训练精准的识别模型太重要了。数据集按用途分为训练集(2975张)、验证集(500张)和测试集(1525张),这种划分比例在实际项目中很实用——既保证足够的训练样本,又能有效评估模型表现。
提示:下载数据集时需要注册账号,建议使用学术邮箱申请,通常审核通过率更高
2. 数据获取与解压:避开那些新手陷阱
去年带实习生时,发现十个有九个会在数据下载环节卡壳。官方下载页面像迷宫,我整理了个傻瓜式流程:先到cityscapes-dataset.com官网,找到"Downloads"选项卡,重点下载这两个压缩包:
- leftImg8bit_trainvaltest.zip(原始图像)
- gtFine_trainvaltest.zip(精细标注)
解压后目录结构特别重要,我建议保持原始文件夹命名。leftImg8bit里按train/val/test分三级,每个子目录又以城市命名。有次我手贱改了文件夹名,结果跑官方评估脚本时路径全报错,白白浪费半天时间。
标注文件比想象中复杂,每张图对应4个衍生文件:
- _color.png:可视化标注(给人类看的)
- _instanceIds.png:实例分割专用
- _labelIds.png:语义分割的核心标签
- _polygons.json:原始标注坐标
# 推荐的文件目录结构 cityscapes/ ├── leftImg8bit/ │ ├── train/ │ ├── val/ │ └── test/ └── gtFine/ ├── train/ ├── val/ └── test/3. 标签魔改实战:让数据集听你的话
真实项目中,我们往往不需要识别全部30多个类别。去年做自动驾驶感知模块时,老板突然要求只检测道路和车辆。这时候就需要修改标签映射关系,官方提供的Python脚本库cityscapesScripts就是神器。
在labels.py文件里,每个类别有6个属性:
- name:类别名称(如"road")
- id:原始ID(固定不变)
- trainId:训练使用的ID(重点修改这个)
- category:粗分类(如"vehicle")
- color:可视化颜色
# 典型的两分类修改示例(道路vs其他) labels = [ Label('road', 7, 1, 'flat', 1, (128,64,128)), Label('car', 26, 2, 'vehicle', 7, (0,0,142)), # 其他类别全部映射为0 Label('unlabeled', 0, 0, 'void', 0, (0,0,0)), ... ]改完后运行createTrainIdLabelImgs.py脚本,它会批量重生成标签图像。这里有个坑要注意:Windows用户可能会遇到路径长度限制报错,建议把数据集放在根目录(如C:/cityscapes)。我在公司服务器跑脚本时,还遇到过内存不足的问题,这时候可以分批次处理:
# 分批处理脚本示例 for city in os.listdir('gtFine/train'): os.system(f'python createTrainIdLabelImgs.py --city {city}')4. 图像预处理:小显存也能玩转大图
1024x2048的原图直接扔进模型?我的RTX 3090显卡第一个抗议。经过多次试验,我总结出三种实用预处理方案:
方案A:等比缩放(最省事)
import cv2 img = cv2.resize(img, (512, 1024), interpolation=cv2.INTER_LINEAR) label = cv2.resize(label, (512, 1024), interpolation=cv2.INTER_NEAREST)注意:标签必须用INTER_NEAREST插值,否则会产生无效的中间值
方案B:随机裁剪(数据增强)
def random_crop(img, label, size=(512,1024)): h, w = img.shape[:2] x = random.randint(0, w - size[1]) y = random.randint(0, h - size[0]) return img[y:y+size[0], x:x+size[1]], label[y:y+size[0], x:x+size[1]]方案C:滑动窗口(适合推理)我参考HRNet论文实现了重叠切片法,把大图切成512x1024的瓦片,预测后再拼接。虽然速度慢点,但能保留更多细节。有个技巧:重叠区域取预测结果的平均值,可以避免接缝处的突变。
5. 模型适配:让主流框架吃得下数据
最近在PyTorch和TensorFlow上都跑过CityScapes,分享几个关键配置。以DeepLabv3+为例,数据加载器要特别注意标签的读取方式:
# PyTorch Dataset示例 class CityscapesDataset(Dataset): def __getitem__(self, idx): img = Image.open(img_paths[idx]).convert('RGB') label = Image.open(label_paths[idx]) # 关键步骤:将PIL图像转为Tensor img = transforms.ToTensor()(img) label = torch.from_numpy(np.array(label)).long() return img, label在TensorFlow 2.x中,推荐用tf.data做数据管道:
def parse_fn(img_path, label_path): img = tf.io.read_file(img_path) img = tf.image.decode_png(img, channels=3) label = tf.io.read_file(label_path) label = tf.image.decode_png(label, channels=1) return img, label train_ds = tf.data.Dataset.from_tensor_slices((img_paths, label_paths)) train_ds = train_ds.map(parse_fn).batch(8).prefetch(2)6. 训练技巧:从baseline到SOTA的进阶之路
刚开始用CityScapes时,我的模型mIoU卡在60%上不去。后来通过这几个技巧逐步提升到75%+:
技巧1:类别权重平衡CityScapes的类别极度不均衡,道路像素占比可能是信号灯的几千倍。我采用median frequency balancing算法自动计算权重:
# 计算每个类别的出现频率 pixel_counts = np.bincount(labels.flatten()) # 计算权重 weights = np.median(pixel_counts) / (pixel_counts + 1e-5)技巧2:学习率热身大尺寸图像训练需要更谨慎的学习率控制。我采用线性热身+余弦退火策略:
# PyTorch示例 scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=0.01, steps_per_epoch=len(train_loader), epochs=50 )技巧3:多尺度训练在数据增强时随机缩放图像(0.5x-2.0x),让模型适应不同距离的物体。实测这个技巧对提升小物体识别特别有效。
7. 评估指标:看懂官方评测的玄机
CityScapes的评估服务器会返回4个关键指标:
- IoU class:标准交并比
- iIoU class:实例级交并比
- IoU category:粗类别交并比
- iIoU category:实例级粗类别交并比
最容易被忽视的是iIoU(instance-level IoU),它要求模型不仅能识别类别,还要区分不同实例。比如两辆并排的汽车,如果预测成一个连通区域,在IoU上可能得分高,但在iIoU上就会扣分。我在模型中加入条件随机场(CRF)后,iIoU提升了3个百分点。
本地验证时可以用这个简化版评估代码:
def compute_iou(pred, target, n_classes=19): ious = [] for cls in range(n_classes): pred_inds = pred == cls target_inds = target == cls intersection = (pred_inds & target_inds).sum() union = (pred_inds | target_inds).sum() ious.append(float(intersection) / max(union, 1)) return np.mean(ious)8. 实战经验:那些官方文档没告诉你的坑
内存泄漏陷阱:使用OpenCV读取图像时默认是BGR格式,如果混用PIL的RGB格式会导致内存缓慢增长。建议统一用OpenCV时加上
cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换。验证集过拟合:因为CityScapes的验证集来自3个固定城市,反复调参可能导致模型只适应这几个城市的特征。我后来养成分出10%训练集做本地验证的习惯。
标签偏移问题:当修改trainId后,某些类别可能被错误映射。有次我把"摩托车"和"自行车"合并成"二轮车"类别,结果发现模型把行人误检为二轮车。后来在labels.py里仔细检查了所有类别的父子关系才解决。
多GPU训练同步:使用
torch.nn.DataParallel时,如果验证集的样本数不是batch_size的整数倍,可能会导致某些GPU处理不到数据。解决方法是在验证时设置drop_last=False。
