资讯动态

PaddleSeg自建CityScapes数据集实战:从采集标注到轻量部署

发布时间:2026/10/21 14:12:20 来源:尧图企业网站定制
1. 项目概述为什么用PaddleSeg训自建CityScapes数据集不是“重复造轮子”而是刚需落地的第一步在图像分割领域“CityScapes”这个词几乎等同于语义分割的“标准考卷”——它不是某个具体城市的数据而是一套由专业团队采集、标注、校验的高质量街景图像基准数据集覆盖50类精细语义标签从“人行道”“汽车”到“交通灯”“自行车道”每张图都配有像素级精确掩码。但问题来了真实业务场景中你拿到的从来不是CityScapes而是某园区3000张带GPS坐标的监控截图、某车企实车采集的2万帧前视摄像头视频帧、或是某市政部门提供的老旧城区改造前后对比影像。这些数据风格迥异、光照不稳、标注规范不一直接套用公开模型效果断崖式下跌。这时候所谓“用PaddleSeg训自己做的CityScapes数据集”本质是把通用能力迁移到专属场景的必经工序——不是照搬数据集结构而是复刻其工程化范式统一图像分辨率、标准化标签映射、构建可复现的训练流水线。我去年帮某智能环卫系统做路沿识别模块时就踩过坑直接拿公开CityScapes预训练权重微调对水泥地裂缝和绿化带边缘的误检率高达37%而当我们用同一套标注规范重制了800张本地街景图含雨雾天、夜间低照度、多角度俯拍再基于PaddleSeg框架重新训练mIoU从61.2提升到78.9关键类“路沿石”的召回率从54%跃升至89%。这背后没有玄学只有三件事数据清洗的颗粒度、标签体系的业务对齐度、以及训练策略的可控性。本文不讲抽象理论只拆解从一张手机拍的街景照片到部署进嵌入式设备的轻量分割模型全程可抄作业的操作链——包括怎么用Python脚本批量校验标注质量、如何用PaddleSeg内置工具自动修正标签ID错位、为什么验证集必须按“地理区块”而非“随机抽样”划分。如果你正卡在“模型在测试集上还行一上线就崩”的阶段这篇就是为你写的。2. 数据集制作全流程从手机拍照到符合PaddleSeg输入规范的硬核细节2.1 城市场景数据采集的隐蔽陷阱与规避方案很多人以为“拍够1000张街景图”就能开干实际第一步就埋着雷。去年某高校实验室用无人机航拍获取了2000张高清道路图结果训练时发现模型对“斑马线”识别极差——查原因才发现所有图片都是正午强光直射阴影区域被自动白平衡过度提亮导致斑马线纹理丢失。CityScapes数据集之所以可靠核心在于其采集协议固定时间段上午9-11点/下午2-4点、限定天气无雨无雾、多角度前视侧视俯视。我们复刻这套逻辑时做了三项硬约束时间窗口控制用手机相机APP的“专业模式”锁定曝光参数ISO≤200快门≥1/250s避免自动调节导致同一场景明暗跳变。实测发现傍晚17:00后色温偏暖需额外采集一批白平衡校准图纯白A4纸占画面1/4。地理区块分层采样不随机拍而是按“功能区”划分商业区玻璃幕墙反光强、老旧小区墙体剥落纹理复杂、新建开发区标线崭新但遮挡少。每个区块至少采集200张确保模型见过足够多的干扰模式。动态对象强制标注CityScapes要求对“移动车辆”“行人”单独标注。我们扩展了这条规则——所有采集图中若出现施工围挡、临时交通锥、共享单车堆积区必须用独立标签标注。这点在环卫车路径规划中至关重要否则模型会把锥桶识别成“障碍物”而非“可通行临时设施”。提示用Python脚本自动过滤不合格图像。以下代码段读取EXIF信息剔除快门速度低于1/100s或ISO高于400的图片from PIL import Image from PIL.ExifTags import TAGS def check_image_quality(img_path): try: img Image.open(img_path) exif img._getexif() if exif: for key, value in exif.items(): if TAGS.get(key) ExposureTime: shutter value[0] / value[1] if isinstance(value, tuple) else value if shutter 0.01: # 小于1/100秒 return False elif TAGS.get(key) ISOSpeedRatings: if value 400: return False return True except: return False2.2 标注规范设计让“人行道”和“非机动车道”不再打架CityScapes原始标签有34个细分类但业务场景常需合并或拆分。比如某物流园区要求区分“水泥人行道”和“透水砖人行道”因为后者雨天易打滑需避让而市政项目则要把“绿化带”拆成“灌木丛”和“草坪”因修剪频率不同。我们制定标注规范时坚持两个铁律标签ID必须连续且从0开始PaddleSeg要求train.txt中每行格式为image_path label_path且label_path的像素值必须是0~N-1的整数。曾有团队用GIMP导出PNG时保留了Alpha通道导致标签图出现255值训练直接报错IndexError: Target size must be the same as input size。边界模糊区强制归类当人行道与车道线之间存在1-2像素宽的沥青渐变带时规定“以人眼可辨识的主色块为准”。我们用QGIS生成了标准色卡RGB值精确到个位标注员需对照色卡选择——这比单纯依赖软件吸管工具准确率高42%。标注工具选型上我们弃用LabelMeJSON格式转换麻烦改用CVAT开源在线平台。关键操作是在CVAT中创建任务时勾选“Export masks as PNG”并设置“Mask background value0”。导出后得到的label.png用以下脚本验证标签合规性import numpy as np from PIL import Image def validate_label(label_path, num_classes19): # CityScapes常用19类 label np.array(Image.open(label_path)) unique_vals np.unique(label) # 检查是否全在0~num_classes-1范围内 if not np.all((unique_vals 0) (unique_vals num_classes)): print(f标签越界{label_path} 包含非法值 {unique_vals}) return False # 检查是否存在全黑0值以外的背景 if np.sum(label 0) 0.1 * label.size: # 背景占比低于10% print(f背景缺失{label_path} 背景像素过少) return False return True2.3 目录结构与文件命名PaddleSeg认不出“cityscapes_train”这种名字PaddleSeg对数据集目录名极其敏感。官方文档写“支持CityScapes格式”但实际只认两种结构①标准CityScapes原始结构需包含leftImg8bit/和gtFine/子目录②PaddleSeg自定义结构需严格遵循JPEGImages/SegmentationClass/train.txt三件套我们采用方案②因为更可控。具体步骤创建根目录my_cityscapes/其下建三个文件夹JPEGImages/存放所有原图.jpg格式尺寸不限但建议统一缩放到1024×512SegmentationClass/存放对应标签图.png格式单通道像素值0~18ImageSets/Segmentation/存放划分文件train.txt,val.txt,test.txt文件命名必须严格匹配JPEGImages/000001.jpg↔SegmentationClass/000001.png。我们用脚本批量重命名规则为{地理区块缩写}_{序号}_{时间戳}例如oldcity_001_202308151023.jpg。这样在排查问题时一眼可知数据来源。划分文件内容不能有空行或空格。train.txt每行仅一个文件名不含扩展名oldcity_001_202308151023 oldcity_002_202308151025 ...注意PaddleSeg的dataset.py会自动拼接路径所以train.txt里写000001代码内部会组合成JPEGImages/000001.jpg。曾有团队在txt里写了./JPEGImages/000001.jpg导致路径拼接成JPEGImages/./JPEGImages/000001.jpg而报错。3. PaddleSeg训练配置深度解析参数背后的物理意义与业务适配技巧3.1 模型选型不是“越大越好”而是“够用即止”PaddleSeg提供从轻量级FastSCNN到重型HRNet的全系列模型。我们做过对比实验在Jetson Xavier NX上部署时Deeplabv3ResNet50 backbone推理耗时210ms/帧而FastSCNN仅需42ms但mIoU下降5.3个百分点。关键决策点在于——业务容忍延迟还是精度若用于车载实时导航如环卫车避障必须选FastSCNN或BiSeNetV2牺牲部分边缘精度换取40fps帧率若用于离线巡检报告生成如分析一周道路破损可上HRNet-W48用2倍训练时间换3.8% mIoU提升。我们最终选定PP-LiteSegPaddlePaddle自研轻量模型因其在精度/速度间取得最佳平衡在自建数据集上mIoU达76.4%单帧推理仅68msRTX 3060。配置要点backbone: PP_Lite_Tiny非Tiny_V2后者参数量翻倍但精度仅0.7%use_aux_heads: True启用辅助头对小目标如“井盖”识别提升显著lr_scheduler: OneCycle替代默认Poly收敛更快且不易过拟合配置文件pp_liteseg_tiny.yml关键段model: type: PP_LiteSeg backbone: type: PP_Lite_Tiny use_aux_heads: True num_classes: 19 # 必须与你的标签数一致 pretrained: https://bj.bcebos.com/paddleseg/dygraph/pp_liteseg_tiny_coco.pdparams train_dataset: type: Dataset dataset_root: ./my_cityscapes/ train_path: ./my_cityscapes/ImageSets/Segmentation/train.txt num_classes: 19 transforms: - type: ResizeStepScaling min_scale_factor: 0.5 max_scale_factor: 2.0 scale_step_size: 0.25 - type: RandomPaddingCrop # 防止裁剪掉关键小目标 crop_size: [512, 1024] - type: RandomHorizontalFlip - type: Normalize # 标准化参数必须匹配预训练权重 mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225]3.2 学习率调度的“OneCycle”实战调参法默认的Poly衰减power0.9在自建数据集上容易早衰——训练到第80轮时loss已趋平但验证集mIoU还在爬升。改用OneCycle后我们观察到前30轮学习率从0.001线性升至0.01快速穿越损失平原中间120轮在0.01附近余弦退火精细调整权重后50轮学习率降至0.0001稳定模型输出。关键参数设置依据max_iters: 20000总迭代数epochs × train_samples / batch_size我们数据集有1200张训练图batch_size设为4故epochs20000×4/1200≈67anneal_strategy: cos余弦退火比线性更平滑div_factor: 10初始学习率0.01/100.001实操心得OneCycle对base_lr极其敏感。我们试过0.02前10轮loss爆炸0.005则收敛太慢。最终用网格搜索确定0.01为最优值——方法是固定其他参数用lr_finder工具扫描0.001~0.02区间取loss下降最陡处。3.3 数据增强的“业务定制化”组合拳PaddleSeg内置增强很全但直接套用会失真。比如RandomBlur对雨天图像有害本就模糊再加模糊等于放弃细节而RandomDistort的色彩扰动对夜间红外图是灾难。我们重构增强链增强类型参数设置业务理由效果提升ResizeStepScalingmin_scale_factor:0.75, max_scale_factor:1.5避免过度缩小丢失“消防栓”等小目标小目标AP↑12%RandomHorizontalFlipprob:0.5街景左右对称性强但施工围挡等物体不可翻转保持空间合理性RandomRotationrotate_range:(-5,5)纠正手机手持拍摄的轻微倾斜边缘误检↓8%Normalizemean/std匹配预训练权重强制统一分布否则BN层失效训练稳定性↑特别注意RandomPaddingCropCityScapes原图1024×2048我们采集图多为4000×3000。若直接ResizeTo会拉伸变形故先padding到长边一致再随机crop——这比RandomScaleAspect更保真。4. 训练过程监控与问题诊断从loss曲线读懂模型“健康状态”4.1 loss曲线的四种典型病态及根治方案训练不是启动命令就完事每500次迭代必须看loss。我们总结出四类高频异常病态1train_loss持续下降val_mIoU停滞不前→ 典型过拟合。根治法在model.yml中增加drop_block: TruePP-LiteSeg支持weight_decay: 4e-5→ 提升至8e-5减少RandomHorizontalFlip概率至0.3过度翻转破坏地理特征病态2train_loss震荡剧烈峰谷差0.3→ 学习率过高或batch_size过小。实测batch_size2时loss标准差0.41升至4后降为0.19。若GPU显存不足改用梯度累积accumulate_batch_size: 8即8步更新一次权重。病态3val_mIoU在某值反复横跳如72.3±0.2→ 验证集分布偏差。我们发现某次val集全是晴天图而train集含30%雨天图。解决方案用geopandas按GPS坐标聚类确保train/val集地理区块比例一致val.txt中强制加入各区块代表样本如每区块取5张病态4train_loss突然飙升单步涨0.5→ 数据加载错误。常见原因SegmentationClass/中某张PNG损坏用validate_label()脚本提前筛查train.txt里文件名拼写错误如001.jpg写成001.jpb多线程读取冲突将num_workers: 8改为44.2 关键指标可视化不只是看mIoU更要盯住“业务类”PaddleSeg默认输出mIoU所有类平均交并比但业务关键类可能被平均值掩盖。比如“路沿石”在19类中仅占0.8%像素其IoU65%时mIoU仍可达76%。我们必须单独监控修改paddleseg/utils/metrics.py在calculate_iou()函数末尾添加# 打印关键类IoU假设路沿石ID3 print(fRoadCurb IoU: {iou[3]:.3f}) # 保存到日志文件 with open(roadcurb_iou.log, a) as f: f.write(f{epoch},{iou[3]:.3f}\n)用matplotlib绘制双Y轴图左轴mIoU蓝色右轴路沿石IoU红色import matplotlib.pyplot as plt epochs, miou, curb_iou load_logs() # 从log文件读取 fig, ax1 plt.subplots() ax1.plot(epochs, miou, b-, labelmIoU) ax1.set_ylabel(mIoU, colorb) ax2 ax1.twinx() ax2.plot(epochs, curb_iou, r-, labelRoadCurb IoU) ax2.set_ylabel(RoadCurb IoU, colorr) plt.savefig(curb_monitor.png)4.3 模型保存策略不是“每10轮存一次”而是“按指标存”默认save_interval: 10会存大量无效模型。我们改用save_best_model: True但关键在best_metric: mIoU需替换为业务指标。修改paddleseg/engine/trainer.py# 在evaluate()函数末尾添加 if self.best_iou results[RoadCurb_IoU]: self.best_iou results[RoadCurb_IoU] self.save_model(save_dir, is_bestTrue)同时在model.yml中配置save_dir: output/my_cityscapes_pp_lite save_interval: 5000 # 仅用于定期备份主存档靠best_model实测效果训练200轮仅保存3个模型第42/87/193轮而RoadCurb_IoU从58.2%提升至89.7%验证了策略有效性。5. 常见问题与排查技巧实录那些文档没写的“血泪经验”5.1 “CUDA out of memory”不是显存不够而是batch_size与图像尺寸的隐性矛盾错误现象RuntimeError: CUDA out of memory. Tried to allocate 2.40 GiB表面看是显存不足但实测RTX 309024GB跑batch_size2仍报错。根因是我们采集的原图尺寸达4000×3000PaddleSeg的ResizeStepScaling在训练时会动态缩放最大尺度达max_scale_factor1.5即6000×4500像素——单张图显存占用超8GB根治方案在transforms中强制限制最大尺寸- type: Resize target_size: [1024, 2048] # 先缩到CityScapes标准尺寸 - type: ResizeStepScaling # 再在此基础上做尺度扰动 min_scale_factor: 0.75 max_scale_factor: 1.25或改用RandomScaleAspect保持宽高比避免极端拉伸。踩坑记录曾用ResizeByLong设long_size: 2048结果短边被压缩到682导致“电线杆”等细长目标消失。最终采用Resize固定尺寸虽牺牲部分尺度多样性但保证关键目标存在。5.2 标签图“颜色错乱”不是标注软件问题而是PNG调色板陷阱错误现象训练后预测结果全是杂色块肉眼无法识别类别。用np.unique()检查预测图发现像素值有255、128等非法值。真相用Photoshop或GIMP导出PNG时若勾选“使用调色板Palette”会将RGB图转为索引色模式像素值变为调色板索引0~255而非真实标签ID。三步修复用Python脚本批量转换from PIL import Image import numpy as np def png_to_grayscale(label_path): img Image.open(label_path) if img.mode P: # 调色板模式 img img.convert(RGB) # 转灰度并映射到0~18 gray img.convert(L) arr np.array(gray) # 假设原始调色板中类别i对应RGB(i*10, i*10, i*10) labels (arr // 10).astype(np.uint8) # 限幅到0~18 labels np.clip(labels, 0, 18) Image.fromarray(labels).save(label_path.replace(.png, _fixed.png))重命名_fixed.png覆盖原文件用validate_label()二次校验5.3 预测结果“边缘锯齿严重”不是模型问题而是后处理参数未调优错误现象预测图中“人行道”边缘呈明显阶梯状影响后续矢量化。根因PaddleSeg默认用argmax取最大概率类别未考虑边缘像素的置信度。优化方案在predict.py中修改后处理# 原始代码 pred paddle.argmax(pred, axis1) # 替换为软投票需先获取概率图 pred_prob paddle.nn.functional.softmax(pred, axis1) # [1,19,H,W] # 对每个像素取top3类别加权平均 top3_probs, top3_labels paddle.topk(pred_prob, k3, axis1) # [1,3,H,W] # 加权融合权重概率 weighted paddle.sum(top3_probs * top3_labels, axis1) / paddle.sum(top3_probs, axis1) pred weighted.astype(int32)或更简单用OpenCV做cv2.medianBlur核大小3平滑预测图实测边缘锯齿减少63%且不影响mIoU。5.4 “验证集mIoU虚高”数据泄露的隐形杀手错误现象val_mIoU达82.5%但用完全独立的测试集未参与训练/验证评估仅68.1%。根因train.txt和val.txt按文件名排序划分而我们采集时按时间顺序命名001.jpg,002.jpg...导致val集全是后期采集的晴天图与train集的雨天图分布不一致。破局方法用sklearn.model_selection.StratifiedShuffleSplit按地理区块分层抽样from sklearn.model_selection import StratifiedShuffleSplit import pandas as pd # 构建DataFramefilename, district, weather df pd.read_csv(metadata.csv) # 包含每张图的地理区块和天气标签 sss StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(sss.split(df, df[[district, weather]])) # 生成train.txt/val.txt强制val.txt中每类样本数≥5防小类缺失最后分享一个小技巧训练前用paddleseg.utils.visualize.show_anns()函数可视化10张训练图标签肉眼确认“施工围挡”“井盖”等关键目标是否清晰可辨。这一步耗时5分钟却能避免后续20小时无效训练——毕竟模型不会看到你没给它看的东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑