首页 / 资讯中心 / 文章详情

制作CityScapes风格语义分割数据集全流程指南

制作CityScapes风格语义分割数据集全流程指南 ★ FEATURED ARTICLE
1. 为什么CityScapes风格的数据集在语义分割中成了“试金石”在语义分割这个领域里CityScapes 数据集的地位有点像跑车界的纽博格林北环——不是所有模型都必须在这条路上跑一圈但只要它能稳稳跑完、不冲出赛道、不爆胎、不熄火大家心里就有底了。这不是因为它数据量最大它只有5000张精细标注图也不是因为它覆盖场景最全它只聚焦于城市道路场景而是因为它的标注规范极其严苛、类别定义高度统一、评估指标公开透明且难以取巧。我最早接触它是在一个跨平台图像理解项目里当时团队想验证一个轻量化模型能否在车载端实时运行第一反应就是“先拉CityScapes的val集跑个mIoU看看。”结果发现哪怕只是把标注里的“road”和“sidewalk”两个类别合并成一个“ground”mIoU就跳升2.3个百分点——这说明它的价值恰恰藏在那些“看起来很细、实则致命”的边界定义里。而当你决定“用自己的数据制作CityScapes风格数据集”本质上不是在做一个简单的图片标签打包操作而是在复刻一套工业级的标注语言体系。CityScapes的labelIds不是随便编号的0是unlabeled1是ego-vehicle自车2是rectification_border一直到34是out of roi……其中24个是训练用的有效类别每个都有明确的像素级掩码规则。比如“person”不包括被完全遮挡的人体残影“car”不包含后视镜反光中的虚像“traffic light”只标灯体本身不标灯杆或投射阴影。这些细节你在做自己的数据集时如果照着CityScapes的JSON Schema去抠会发现光是写一个校验脚本就要花两天——因为你要检查每张图的instanceId是否连续、labelId是否越界、polygons是否自相交、mask是否与原图分辨率严格对齐。很多人卡在第一步不是不会写代码而是没意识到CityScapes风格的本质是一套可执行、可验证、可复现的视觉语义契约。你做的不是数据是契约的本地化签署。所以当标题里出现“【PaddleSeg】使用自己制作的CityScapes数据集训练模型”它真正要解决的问题其实是三个层面的叠加第一层是数据工程——如何让自己的采集设备可能是某款国产车载摄像头、某高校实验室的无人机航拍、或是某公司内部的街景采集车产出的原始影像通过标注、清洗、格式转换达到CityScapes的结构一致性第二层是框架适配——PaddleSeg虽然支持CityScapes格式但它默认读取的是官方提供的train/val/test划分而你的数据没有test set甚至可能连val都没有只有按时间戳切分的7:2:1第三层才是模型训练本身——你得确认PaddleSeg的config文件里num_classes是不是真的和你数据集里的有效类别数对得上否则loss会nan得毫无征兆。这三层里前两层占了整个项目80%的时间但90%的教程却只讲第三层。今天这篇我们就从第一层开始一砖一瓦地把这座桥搭起来。2. 数据制作从原始影像到CityScapes标准目录结构的七道工序很多人以为“制作CityScapes数据集”就是把图片放进leftImg8bit文件夹、把标签图放进gtFine文件夹然后run。实测下来这种做法在PaddleSeg里大概率会在DataLoader阶段直接报错错误信息往往是“KeyError: xxx.png not found in gtFine”或者“mask shape mismatch”。根本原因在于CityScapes的目录结构不是扁平的它是一个嵌套极深的契约式结构每一层都在传递语义信息。我们来拆解真实项目中必须走完的七道工序每一道都对应一个容易被忽略的坑。2.1 原始影像预处理分辨率对齐与畸变校正不可省略CityScapes官方图像是1024×2048宽×高但你的采集设备几乎不可能输出这个尺寸。常见情况有三种车载摄像头输出1920×1080横屏、无人机航拍输出4000×3000竖屏、手机拍摄输出3840×2160超高清。直接resize到1024×2048不行。因为语义分割对几何形变极度敏感——车道线一旦被非等比缩放曲率就失真模型学到的就是错误的“弯曲逻辑”。正确做法是先做光学畸变校正如果你有相机内参用OpenCV的cv2.undistort如果没有至少用Lensfun库加载常见镜头配置再做等比缩放中心裁剪。具体参数我实测过以1920×1080为例先等比缩放到1024×576保持宽高比再上下补黑边至1024×2048最后用PIL的ImageOps.pad实现。为什么是补黑边而不是拉伸因为PaddleSeg的transforms里默认有Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])黑边像素值0经过归一化后是[-2.11, -2.04, -1.77]远低于正常图像范围[-2.11, 2.64]模型会立刻识别出这是padding区域从而在loss计算时自动mask掉——这比强行拉伸导致车道线扭曲要可靠得多。提示补黑边时务必用PIL而非OpenCV因为OpenCV默认BGR顺序而PaddleSeg的DataLoader读取的是RGB。我曾因这个顺序错位导致所有“road”类别在训练初期loss下降极慢debug三天才发现是通道颠倒。2.2 标注工具选型LabelMe与CVAT的隐性成本差异市面上主流标注工具都能导出CityScapes格式但LabelMe和CVAT的输出结构差异极大。LabelMe导出的是单个JSON文件包含所有polygon坐标CVAT导出的是按帧拆分的XMLPNG掩码。问题来了CityScapes要求gtFine文件夹下必须是gtFine/train/{city}/{city}_{seq:06}_{frame:06}_gtFine_polygons.json这样的路径而LabelMe默认不生成city/seq/frame三级目录。解决方案不是手动建文件夹而是写一个路径映射表CSV格式第一列是原始图片名如cam1_20230501_142301.jpg第二列是目标city名如shanghai第三列是seq如000001第四列是frame如000001。然后用Python脚本批量重命名移动。这个表必须人工维护因为“shanghai”这个city名不是地理概念而是逻辑分组——你可以把所有阴天数据标为shanghai晴天标为beijing这样后续做domain adaptation时就能直接按city切分。CVAT虽然自带路径生成但它导出的polygons.json里instanceId是全局递增的而CityScapes要求同一张图内instanceId从0开始、连续编号。这就需要额外写一个reindex脚本遍历所有polygon按出现顺序重赋instanceId。我统计过一个1000张图的项目LabelMe方案前期省2小时后期补路径重编号多花6小时CVAT方案前期配置多花3小时后期零修补。结论小项目用LabelMe大项目闭眼选CVAT。2.3 JSON Schema校验绕不开的24个字段硬约束CityScapes的polygons.json不是普通JSON它有24个强制字段缺一不可。最常被漏掉的是inLane是否在车道内、isCrowd是否人群密集区、level标注置信度等级。PaddleSeg的CityscapesDataset类在__getitem__里会直接读取这些字段如果缺失就会触发KeyError。更隐蔽的坑是objects数组里的polygon字段它必须是[[x1,y1],[x2,y2],...]格式的整数坐标不能是浮点数不能有重复点不能首尾不闭合。我遇到过一次nan loss追踪到最后发现是标注员用AI辅助工具生成polygon时坐标保留了3位小数而PaddleSeg的mask生成函数polygons_to_mask内部做了int()强转导致坐标偏移1像素边缘出现1px宽的未定义区域。解决方案是写一个校验脚本用jsonschema库加载官方提供的cityscapes_schema.json可在CityScapes官网下载然后对每个JSON文件做validate。脚本里要加一条if not all(isinstance(p[0], int) and isinstance(p[1], int) for p in polygon): raise ValueError(Polygon coordinates must be integers)。这条检查救了我两次——一次是坐标类型一次是polygon里混进了[None, None]这种脏数据。2.4 标签图生成从polygons.json到gtFine_labelIds.png的三步转换很多教程说“用官方脚本convert.py就行”但那个脚本默认只生成labelIds.png而PaddleSeg实际需要的是labelTrainIds.png训练用ID和labelIds.png原始ID两个版本。关键区别在于CityScapes有34个原始labelId但训练只用20个比如-1表示忽略255表示无效而PaddleSeg的config里num_classes必须等于训练ID的数量。所以转换必须分三步第一步用官方脚本生成labelIds.png第二步写一个映射字典train_id_map {0:255, 1:255, ..., 24:19}24个有效类别映射到0~19注意0号unlabeled必须映射到255PaddleSeg的ignore_index默认是255第三步用numpy做向量化映射label_train np.vectorize(train_id_map.get)(label_ids)。这里有个性能陷阱如果用for循环遍历每个像素1024×2048的图要跑3秒用np.vectorize只要0.03秒。另外生成的labelTrainIds.png必须用PIL的modeL保存单通道灰度不能用modeRGB否则PaddleSeg读取时shape会是(1024,2048,3)直接报维度错。2.5 目录结构构建city-level与img-level的双重绑定CityScapes的目录结构是双层绑定的外层是leftImg8bit/{split}/{city}/内层是{city}_{seq:06}_{frame:06}_leftImg8bit.png。这里的{city}不是字符串而是逻辑键——它决定了gtFine/{split}/{city}/下的对应JSON和PNG是否能被正确关联。PaddleSeg的CityscapesDataset类在初始化时会先扫描leftImg8bit/train/下的所有city文件夹再对每个city去gtFine/train/找同名文件夹。如果你的leftImg8bit/train/shanghai/下有100张图但gtFine/train/shanghai/下只有99个JSON那么第100张图就会被静默跳过训练时batch_size会突然变小loss曲线出现诡异抖动。更糟的是这种缺失不会报错只会warning。因此必须写一个完整性检查脚本遍历leftImg8bit/train/下每个city统计该city内png数量再遍历gtFine/train/下同名city统计json数量两者必须完全相等。我加了一行日志if png_count ! json_count: logger.error(fCity {city}: {png_count} imgs but {json_count} labels)。这个检查在我们一个2万张图的项目里揪出了3个city的标注遗漏避免了后续两周的无效训练。2.6 数据划分策略别迷信7:2:1按场景分布切分才靠谱官方推荐train:val:test2975:500:1525但你的数据很可能没有test set甚至val set都是临时划出来的。这时候如果机械地按7:2:1切分会出大问题。举个真实案例某高校实验室用无人机在同一个城区飞了3天第一天阴天第二天小雨第三天晴天。如果按时间顺序7:2:1切分val集全是阴天数据模型在晴天test时mIoU暴跌15个百分点。正确做法是按场景因子分层抽样。我们定义了4个场景因子天气晴/阴/雨、时段早/中/晚、道路类型高速/主干/支路、交通密度空闲/中等/拥堵。用pandas把所有图片按这4个维度打标签然后用sklearn.model_selection.StratifiedShuffleSplit做分层切分确保train/val中每个因子的分布比例一致。脚本核心代码from sklearn.model_selection import StratifiedShuffleSplit df[stratify_key] df[weather] _ df[time] _ df[road_type] sss StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(sss.split(df, df[stratify_key]))这样切出来的val集才能真实反映模型在各种条件下的泛化能力。2.7 元数据文件生成images_list.txt与labels_list.txt的生成逻辑PaddleSeg的CityscapesDataset默认从images_list.txt读取图片路径列表从labels_list.txt读取标签路径列表。这两个文件不是简单地把所有路径写进去就行它们的行序必须严格一一对应。也就是说第n行的图片必须和第n行的标签属于同一张图。很多同学用ls *.png images_list.txt再用ls *.png labels_list.txt结果因为文件系统排序规则不同Linux按ASCIImacOS按Unicode两文件行序错位训练时图片和标签完全不匹配模型学的全是噪声。正确做法是用Python脚本生成import glob import os img_paths sorted(glob.glob(leftImg8bit/train/*/*.png)) label_paths [] for img_path in img_paths: city os.path.basename(os.path.dirname(img_path)) basename os.path.basename(img_path).replace(_leftImg8bit.png, ) label_path fgtFine/train/{city}/{basename}_gtFine_labelTrainIds.png label_paths.append(label_path) with open(images_list.txt, w) as f: f.write(\n.join(img_paths)) with open(labels_list.txt, w) as f: f.write(\n.join(label_paths))注意sorted()保证了跨平台一致性os.path.basename提取city名保证了路径拼接准确。这个脚本我们已封装成gen_cityscapes_lists.py在所有项目里复用。3. PaddleSeg框架适配config文件修改与DataLoader调试的五个关键点当你终于把数据目录结构搭好满怀希望地运行python train.py --config configs/segformer/segformer_b0_cityscapes_1024x1024_160k.yml时大概率会遇到五个经典报错。这些报错不来自模型本身而来自PaddleSeg对CityScapes数据的“刻板印象”。下面逐个拆解告诉你怎么改config、怎么调DataLoader、怎么绕过框架的默认假设。3.1 num_classes陷阱config里写的20实际数据可能是19或21PaddleSeg的CityScapes config默认设num_classes: 20对应CityScapes的19个训练类1个背景。但你的数据集很可能删减或增加了类别。比如你只关心road、sidewalk、car、person四个类那num_classes就得改成4同时ignore_index必须设为255保持不变并且在train_id_map里只映射这4个类。但问题来了config里num_classes: 4之后模型head的权重矩阵会自动变成[4, C_in, 1, 1]而原始SegFormer的head是[20, C_in, 1, 1]直接加载预训练权重会shape mismatch。解决方案有两个一是用--resume_model加载完整权重后在model.py里手动裁剪head层二是更稳妥的——在config里加一行pretrained: null从头训。我实测过对于4类小任务从头训比裁剪后finetune收敛更快因为裁剪会破坏原有权重的分布特性。另外num_classes还影响loss计算PaddleSeg的CrossEntropyLoss默认axis1如果num_classes设错logits的channel数就不对loss会nan。所以每次改num_classes必须同步检查model.backbone和model.head的输出channel是否匹配。3.2 数据路径硬编码config里写的dataset/cityscapes必须替换成你的绝对路径打开configs/segformer/segformer_b0_cityscapes_1024x1024_160k.yml你会看到TRAIN: DATASET: TYPE: CityscapesDataset DATASET_ROOT: dataset/cityscapes TRAIN_PATH: leftImg8bit/train TRAIN_LABEL_PATH: gtFine/train这里的DATASET_ROOT是相对路径但PaddleSeg实际运行时会把它拼接到当前工作目录下。如果你在/home/user/paddleseg/下运行它会去找/home/user/paddleseg/dataset/cityscapes而你的数据可能在/data/my_cityscapes。最安全的做法是把DATASET_ROOT改成绝对路径/data/my_cityscapes并确保TRAIN_PATH和TRAIN_LABEL_PATH是相对于这个根目录的子路径。但还有个隐藏坑TRAIN_PATH的值leftImg8bit/train末尾不能有斜杠否则PaddleSeg会拼出/data/my_cityscapes/leftImg8bit/train//xxx.png双斜杠导致路径无效。我加了一个pre-check脚本在train.py开头加assert not cfg.TRAIN.DATASET.TRAIN_PATH.endswith(/), TRAIN_PATH must not end with / assert not cfg.TRAIN.DATASET.TRAIN_LABEL_PATH.endswith(/), TRAIN_LABEL_PATH must not end with /这个断言帮我在一个项目里提前发现了路径配置错误。3.3 transforms配置ResizeStepScaling的scale_range必须与你的数据分辨率匹配CityScapes官方config里transforms有一段- type: ResizeStepScaling min_scale_factor: 0.5 max_scale_factor: 2.0 scale_step_size: 0.25这个变换的作用是在训练时随机把输入图resize到原尺寸的0.5~2.0倍步长0.25。但你的数据如果是1024×2048resize到2.0倍就是2048×4096显存直接爆掉。而PaddleSeg的ResizeStepScaling没有max_size限制它只认scale_factor。解决方案是把ResizeStepScaling换成Resize并指定固定尺寸- type: Resize target_size: [1024, 2048] interp: bilinear但这里有个精度损失Resize是双线性插值会模糊边缘ResizeStepScaling是随机尺度能增强模型对尺度变化的鲁棒性。权衡之下我采用折中方案min_scale_factor: 0.75,max_scale_factor: 1.25,scale_step_size: 0.125这样最大尺寸是1280×2560RTX 3090刚好能塞下。另外interp参数必须是bilinear不能是nearest因为nearest插值会导致polygon边缘出现锯齿影响mask精度。3.4 DataLoader的num_workers与shared_memory冲突Linux与Windows的差异化处理PaddleSeg默认num_workers: 2但在Linux服务器上如果num_workers 0DataLoader会启用多进程而多进程在共享内存shared memory不足时会卡死。症状是训练启动后GPU显存占满但GPU利用率一直是0%nvidia-smi显示No running processes found。根本原因是PaddlePaddle的DataLoader在Linux下依赖/dev/shm而很多Docker容器默认/dev/shm只有64MB。解决方案有两个一是启动容器时加参数--shm-size2g二是在config里把num_workers: 0即单进程。我测试过num_workers: 0时CPU预处理速度慢30%但训练稳定num_workers: 2时预处理快2倍但必须配--shm-size。Windows用户不用操心这个因为Windows的DataLoader不走shared memory但要注意num_workers设为0否则会报Cannot pickle错误Windows的multiprocessing机制不同。3.5 验证集路径错位val_dataset_root必须与train_dataset_root独立配置PaddleSeg的config里VAL部分的DATASET_ROOT默认和TRAIN一样但你的val数据很可能放在另一个磁盘。比如train在/data/trainval在/ssd/val。如果只改TRAIN.DATASET.DATASET_ROOTVAL.DATASET.DATASET_ROOT还是旧路径验证时就会报错。必须显式配置VAL: DATASET: TYPE: CityscapesDataset DATASET_ROOT: /ssd/my_cityscapes # 独立路径 VAL_PATH: leftImg8bit/val VAL_LABEL_PATH: gtFine/val更关键的是VAL_PATH和VAL_LABEL_PATH的目录结构必须和train完全一致——即leftImg8bit/val/{city}/下要有图片gtFine/val/{city}/下要有对应JSON。我见过最离谱的错误是有人把val图片放在leftImg8bit/val/无city子目录而JSON放在gtFine/val/shanghai/结果PaddleSeg在gtFine/val/下找不到val/这个city直接报FileNotFoundError。所以val集的目录结构检查必须和train集用同一套脚本跑一遍。4. 训练过程监控与mIoU可信度验证不只是看数字要看像素级对齐当模型终于跑起来了终端开始刷[TRAIN] epoch: 1, iter: 100/160000, loss: 1.2345, lr: 0.001000很多人就松了一口气。但真正的挑战才刚开始——因为mIoU这个数字很容易“骗人”。我经历过三次mIoU虚高第一次是标签图生成时labelTrainIds.png里把所有无效区域都填了0应该是255导致模型把0类学成了“万能背景”mIoU冲到78但实际推理全是错的第二次是DataLoader的ignore_index没设对loss计算时把255当有效类参与了梯度更新第三次最隐蔽验证时用的图片和训练时augmentation不一致比如训练用了RandomHorizontalFlip但验证没关导致左右翻转后的图片被送进模型mIoU计算时把翻转结果和原标签比对分数自然失真。所以训练监控必须深入到像素级。4.1 实时可视化用VisualDL看中间特征图比看loss曲线更有价值PaddleSeg默认集成VisualDL但很多人只用它看loss和lr。其实最有价值的是feature map面板。在config里加VISUAL: ENABLE: True FEATURE_MAP: True FEATURE_MAP_FREQ: 1000训练时打开visualdl --logdir output/visual在浏览器里看feature map标签页。重点关注backbone.stages.3.blocks.1.conv2这一层的输出——这是SegFormer最后一级特征图。正常情况下它应该清晰地勾勒出道路、车辆、行人的大致轮廓如果全是噪点或一片模糊说明backbone没学好特征如果轮廓很清晰但边缘发散比如车顶的mask溢出到天空说明head的上采样不够精准。我靠这个发现了两次问题一次是conv2层输出全黑查出来是BN层的running_mean没更新一次是边缘发散把decoder_head.up_ratio从2改成4后解决。这些细节loss曲线永远给不了。4.2 mIoU计算原理深挖PaddleSeg的eval.py到底在算什么PaddleSeg的evaluate.py里mIoU计算核心代码是pred pred.argmax(axis1) # 取最大概率类别 pred pred * (label ! ignore_index) # mask掉ignore区域 inter np.logical_and(pred label, label ! ignore_index) union np.logical_or(pred label, label ! ignore_index)注意union的计算是logical_or(pred label, label ! ignore_index)意思是只要label不是ignore_index就算进union。这导致一个关键事实——mIoU的分母是所有非ignore像素的总数而不是所有像素的总数。所以如果你的ignore_index设错了比如设成0那所有label0的像素都会被排除在分母外mIoU就会虚高。验证方法很简单在eval.py里加一行print(Valid pixels:, np.sum(label ! ignore_index))对比你的数据集总像素数。比如1024×20482097152像素如果valid pixels只有180万说明有29万像素被当成了ignore必须查labelTrainIds.png里是不是有不该是255的区域。4.3 标签-预测像素级对齐验证用diff图定位系统性偏差mIoU只是一个宏观指标它掩盖了大量细节。比如模型可能把所有“traffic light”都标成了“pole”但因为“pole”类样本少对整体mIoU影响不大。要发现这种系统性偏差必须做像素级diff。我写了一个diff_visualizer.py脚本import numpy as np from PIL import Image # 读取预测图和真值图 pred np.array(Image.open(pred.png)) # shape (H,W) label np.array(Image.open(label.png)) # shape (H,W) # 生成diff图0正确1预测为A真值为B2预测为B真值为A diff np.zeros_like(pred) diff[(pred ! label) (label ! 255)] 1 # 错误区域标1 # 保存为彩色diff图 diff_img Image.fromarray(diff.astype(np.uint8) * 128) # 灰度图 diff_img.save(diff.png)然后在diff.png上用画图软件放大查看如果错误区域集中在图像底部道路区域说明模型对低频特征学习不足如果集中在顶部天空说明backbone的高层特征没学好。我们曾靠这个发现模型在阴天数据上对“road”类的误判集中在积水反光区域于是加了RandomGrayscale增强mIoU提升了1.2。4.4 类别级IoU分析不是所有类都平等要盯住你的KPI类PaddleSeg的eval输出里最后一行是Class IoU: [0.8234 0.7651 ...]但这个数组顺序是按labelId排列的不是按你关心的类别。比如你只关心road(labelId0)、car(labelId26)、person(labelId24)那就要从数组里取出索引0、26、24的值。但等等——labelTrainIds.png里road可能被映射到了0car映射到了1person映射到了2所以实际要看索引0、1、2。这就要求你必须维护一个train_id_to_name字典并在eval后做映射train_id_to_name {0: road, 1: car, 2: person} iou_array np.array([0.8234, 0.9123, 0.7651]) # eval输出的Class IoU for i, name in train_id_to_name.items(): print(f{name}: {iou_array[i]:.4f})这样输出的road: 0.8234,car: 0.9123,person: 0.7651才是你真正要优化的KPI。如果person的IoU一直上不去就要专门分析person类的diff图看是漏检pred0, label2还是错检pred1, label2。4.5 过拟合预警train_loss与val_mIoU的剪刀差必须监控一个健康的训练过程train_loss应该单调下降val_mIoU应该单调上升两者曲线应该像一把缓慢张开的剪刀。如果出现以下任一情况就是过拟合预警val_mIoU连续5个epoch不升反降而train_loss还在降train_loss降到0.1以下val_mIoU停滞在75说明模型记住了训练集噪声val_mIoU上升但train_loss下降变缓说明模型在val集上泛化但训练效率降低。这时必须干预。我的标准动作是立即保存当前best model在config里加DropBlocktype: DropBlock,block_size: 7,keep_prob: 0.9把learning_rate乘以0.5重启训练。这套组合拳在三个项目里都把val_mIoU又推高了0.8~1.3。DropBlock比DropOut更适合分割任务因为它按块丢弃特征能强迫模型学习更鲁棒的局部模式。5. 模型部署前的终极校验从PaddleSeg到ONNX再到推理引擎的三重验证训练完成的模型.pdparams文件只是起点。真正落地时它要经历Paddle Inference、ONNX、TensorRT三重转换每一步都可能引入偏差。很多团队卡在最后一步训练mIoU 78部署后推理mIoU只剩62。问题往往不出在模型本身而出在预处理与后处理的微小不一致。下面是我总结的三重验证法确保从训练到部署像素级对齐。5.1 Paddle Inference原生验证绕过Python用C API测纯框架性能PaddleSeg训练完会生成output/best_model/model.pdparams和output/best_model/model.pdiparams。但直接用Python的paddle.inference加载会受Python GIL和内存管理影响测不出真实性能。必须用PaddlePaddle的C inference API做验证。步骤如下编译PaddlePaddle C inference库需CUDA 11.2cudnn 8.2写一个minimal C程序加载model.pdiparams输入一张1024×2048的numpy array用cv2.imread读取BGR→RGB→normalize输出logits用np.argmax(logits, axis1)得到预测图与Python版预测图做np.array_equal()比对。如果两者不等说明Python和C的normalize参数不一致。PaddleSeg Python版默认mean[0.485, 0.456, 0.406],std[0.229, 0.224, 0.225]但C inference的Config::SetMeanStd接口要求mean/std是float数组且顺序是RGB不能错。我曾因C里写了{0.406, 0.456, 0.485}BGR顺序导致所有预测全错。所以C验证的第一步就是确保SetMeanStd({0.485f, 0.456f, 0.406f}, {0.229f, 0.224f, 0.225f})。5.2 ONNX导出陷阱dynamic_axes必须精确到batch和height维度PaddleSeg提供export_model.py导出ONNX但默认参数--input_shape 1,3,1024,2048是静态shape无法用于动态batch。要支持batch1~4必须加--dynamic_axespython export_model.py \ --config configs/segformer/segformer_b0_cityscapes.yml \ --model_path output/best_model/model.pdparams \ --save_dir output/onnx \ --input_shape 1,3,1024,2048 \ --dynamic_axes
阅读完成 · 觉得有帮助?
咨询建站