做无人机遥感项目的人应该都体会过这种感觉数据比模型更让人头疼。这个项目标题看起来很朴素——无人机视角、地理要素、labelme格式、1536张、6类别但真正动手做过的人知道这里面每一环都是可以讲出大量经验的地方。我最近完整跑通了从无人机影像采集到六类地物语义分割模型落地的流程数据集正是标题里说的这套田地、道路、森林、水体、电力线1536张labelme格式标注图。这篇文章就把整个过程拆开讲清楚为什么选这六个类别、标注环节怎么控质量、怎么把labelme的JSON转成训练能用的掩码图、模型怎么选怎么调参以及最后部署到实际业务中会遇到哪些坑。1. 先看清楚这个数据集要解决什么问题1.1 六个类别背后的物理属性差异航拍影像里的地物识别跟普通自然图像分类完全不同。普通图像里“草地”“天空”“汽车”这类物体往往有明确的纹理和形状特征但在无人机俯瞰视角下地物的尺度、边界、光谱特性差异非常大。这套数据集里的六个类别每一类都有自己明显的物理特性直接决定了技术选型。田地是最容易分割的类别因为农田地块具有高度的纹理一致性边界通常是田埂、沟渠或者道路边缘在无人机正射影像里呈现规则的几何块状。但问题是田地的光谱特征在不同季节差异巨大水稻田有水层反射、麦田是黄色秸秆、翻耕后的裸土是深褐色如果只靠颜色特征做分割换一个生长周期就会崩掉。道路是典型的线性地物宽度从乡村土路的2米到主干公路的20米不等材质上有水泥、沥青、碎石的区别。分割道路的难点在于遮挡——路边的树冠、车辆的阴影、路侧护栏都会把一条连续的路打断。做道路分割时模型必须学到“这条线虽然被挡了一部分但它应该继续保持连通”的语义。森林的特征是强纹理和阴影混合。树冠在航拍下有密集的颗粒状纹理但树与树之间的阴影会形成暗色区域与水体在颜色上有时候非常接近。我见过不少模型把大片深色树冠阴影误判成水体就是因为没有利用纹理信息。水体是六个类别里“看起来最简单但实际上最容易翻车”的。清澈水体呈现均匀的深蓝色调边界清晰但水面上有反光时会出现大面积的亮斑干扰模型判断此外水田灌水期跟湖泊水体的光谱特征接近如果不加约束模型会把水田也算成水体。电力线这是全项目最特殊的类别。它属于典型的小目标线性地物在1536×1536像素的大图上只占几个像素宽度形状是长直线或轻微弧线背景是农田、道路、水面的混合。电力线分割本质上更接近一种特殊的边缘检测任务它在语义分割里往往会吃掉最多的训练资源和调参成本。1.2 为什么一定要做像素级分割而不是矩形框这是我在项目初期就跟团队反复确认过的问题。有人提议既然已经有六个类别快速标注一批检测框训练一个目标检测模型不就行了对电力线这种细长目标检测框确实能给出大致位置但对田地、道路、森林、水体这种覆盖大片区域的地物检测框会引入大量背景噪点导致框内混入其他类别。服务端要做的是计算各要素精确面积占比、生成矢量范围、提取电力线走廊净空分析这些业务都依赖像素级边界而不是粗略的矩形框。所以目标定位在这个项目里明确为语义分割每个像素对应一个类别标签。这也是labelme格式能派上用场的原因labelme的核心标注结构是polygon多边形人工用点勾勒出每个地物的边界保存为JSON文件之后转成逐像素的标签掩码图喂给分割模型。1.3 1536张这个规模到底够不够任何做过深度学习项目的人看到1536这个数字第一反应都是太少了。城市街景分割数据集动辄上万张遥感分割数据集也常见几千上万张。但我要说1536张对于六类地物分割来说是一个“够用但必须精打细算”的规模。关键不在于总量而在于每一类的像素覆盖是否均衡。我统计过这套数据集中各类别像素占比田地约34%、森林约26%、水体约15%、道路约14%、电力线约3%其余为背景或其他未标注区域。电力线的像素占比极低这是一个天然的长尾问题。如果直接拿原始分布去训练模型会严重偏向压制电力线类别。所以在数据准备阶段就设计了类别均衡策略后面会详细展开。从经验角度看1536张图如果全部参与训练配合针对性增强已经可以在六类分割上达到业务可用的精度。但前提是标注质量过硬且训练策略干净不能浪费任何一张标注。2. labelme标注里的真实成本与质量管控2.1 标注工具与格式选型地理要素分割的标注工具市面上能选的主要是labelme、labelImg的polygon模式、SuperAnnotate、CVAT。这个项目选择labelme理由非常务实完全离线部署不需要搭建服务器Python环境直接运行标注结果就是标准的GeoJSON风格JSON每个文件对应一张图片没有额外数据库依赖。CVAT功能更强但部署成本高SuperAnnotate是商业平台的试用限制比较多而labelme在Linux和Windows下都能跑小白上手成本最低。安装方式很简单一条命令就能完成基础环境准备。pip install labelme启动后用命令行打开标注工具打开图片目录开始逐张标注。2.2 每一类怎么勾画才不返工标注六个类别的时候我总结出了一套可复用的操作规范每一条都是被返工教训换来的。田地建议用较大的步长勾点田块边界基本是直线或大弧度4到6个点就能勾勒完整块田。如果你一格一格地密集取点放大到像素级会发现边界抖动很大后期转掩码时会出现锯齿状边缘影响模型学习。田块之间有共用边界时先勾面积大的那块再用它作为参考勾相邻地块能减少空隙。道路按车道中心线勾一条带状多边形宽度尽量保持贴近实际路面边缘。道路被树冠遮挡时不要直接断开要用直线或顺势连接的曲线把被遮部分“补上”。模型训练时看到的是完整道路语义这比留下断点要更利于恢复连通性。森林树冠边界非常毛糙但标注时不需要逐棵树勾边围着树冠群的最外沿勾一圈就好。内部出现的小块空地可以单独勾出来标为田地或其他但不要在一个森林多边形里挖空洞。如果某个林区内部是小树、灌丛与草地的混合统一按森林处理保持标准一致。水体水岸线一定要贴着实际水边勾枯水期露出的泥滩、沙滩不算水体。水面上有荷叶、浮萍覆盖时只要水面连续可见就依然整体标为水体如果浮萍密集到完全看不到水面建议单独标为森林或作物这个要在标注规范里提前定义不然后期一致性检查会炸。电力线这是最需要耐心的类别。单条电力线在画面上只有一两个像素宽直接勾线会非常吃力。我的做法是把图像视图缩放到200%以上以塔基或电杆为端点沿导线走向点出一串密集点让多边形包裹住整条线。绝缘子、防震锤这些金具也属于电力线的一部分因为它们的目标识别价值高不能丢。2.3 质量检查机制标注完成后必须做三轮检查。第一轮是标注者自查重点是看有没有漏标的小块地物比如田埂边的水塘、道路交叉口的环岛。第二轮交叉检查让另一位标注者随机抽检20%的图重点核对电力线走向和道路连通性这类线性目标的断裂是最高频错误。第三轮算法辅助检查把所有JSON转成棕色的掩码图后叠加在原图上检查每个类别的边缘误差是否在合理范围内。我强烈建议在项目里强制做第三轮因为你肉眼看一张RGB图很难发现单个像素的偏移但把掩码图以50%透明度叠加在原图后边缘是否贴合水面岸线、道路是否穿过了建筑物全部一目了然。2.4 标注量级1536张标注的真实工作量这可能是很多新人最没概念的部分。我按每张图平均标注要素数量统计过在无人机正射影像上一张1536×1536的图通常包含12到25个标注要素其中电力线平均每张图只出现2到3段而田地道路森林水体多的情况下一张图有几十个要素。一个熟练标注员每天能完成30到40张图的完整标注和质量自查1536张需要大约6到8人周的工作量。如果包含三轮质检和反复修改实际成本要按10人周甚至更多来估。这部分人力成本往往比标注说明书写的要多一倍做项目排期时一定要留足缓冲。3. 从labelme到训练管线转换脚本与数据切分3.1 JSON转PNG掩码的具体实现labelme保存的JSON结构本质上是每个标注要素的多边形坐标列表。训练语义分割模型需要的是逐像素类别图即一张与原始影像尺寸一致的单通道掩码图像素值就是类别索引号。因为原始影像存在同名多次裁剪的情况JSON转掩码必须严格保证原图尺寸和目标尺寸一致否则会出现常见的“标注偏移”问题。转换脚本的核心逻辑不复杂但有三个细节必须注意。其一在逐多边形填充掩码前要建立大小与图像一致的零矩阵用OpenCV的fillPoly函数填充每个多边形的类别索引。其二如果同一个像素被两个不同类别的多边形重复覆盖需要规定处理顺序一般按标注列表顺序后写覆盖先写同时把这类冲突输出成日志人工确认。其三JSON里可能出现空多边形或坐标点少于三个的无效标注这类异常要直接跳过并记录防止fillPoly报错或者生成脏掩码。下面是转换脚本里最关键的部分。import json import cv2 import numpy as np from pathlib import Path # 类别名称到索引的映射 CLASS_MAPPING { field: 1, # 田地 road: 2, # 道路 forest: 3, # 森林 water: 4, # 水体 powerline: 5, # 电力线 } def json_to_mask(json_path, image_shape, mappingCLASS_MAPPING): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros((image_shape[0], image_shape[1]), dtypenp.uint8) conflict_pixels 0 for shape in data[shapes]: label shape[label] if label not in mapping: continue cls_idx mapping[label] points np.array(shape[points], dtypenp.int32) if len(points) 3: print(f[警告] 无效多边形: {json_path}, label{label}) continue # 用多边形填充掩码 cv2.fillPoly(mask, [points], cls_idx) # 记录写入的类别数量用于后续统计每一类的像素占比 return mask实际转换的时候还要加一个逻辑统计每个类别在当前图里的像素数量汇总后用于后续做类别权重计算和长尾分析。这个统计在项目前期非常关键它会直接告诉你电力线到底稀缺到什么程度。3.2 类别索引与色板设计这里有一个容易被忽略的细节训练时掩码图的像素值必须等于类别索引但可视化查看时需要给每个索引分配一个直观的颜色。语义分割模型的交叉熵损失计算的是索引值不是RGB颜色值所以转换时一定要保证掩码是单通道灰度图而不是三通道彩色图。很多新手在这一步直接把labelme的预览图拿来当标签结果模型怎么训都收不收敛。通用的色板方案如下。类别索引可视化颜色(RGB)实际用途背景00, 0, 0未标注/忽略区域田地1128, 128, 0农田、耕地、果园道路2128, 0, 128沥青路、土路、水泥路森林30, 128, 128乔木林、灌丛、密集绿化水体40, 0, 128河流、湖泊、干渠电力线5128, 128, 128导线、绝缘子、电杆附属物色板设计的原则是相邻类别在视觉上要有明显区分这样在检查掩码和原图叠加时可以快速发现边界错位。3.3 训练/验证/测试切分原则1536张图不能在数据集级别直接乱序切分。无人机航拍地理要素有一个特点同一架次、同一航线拍摄的相邻图之间有高度重叠。如果训练集、验证集、测试集直接随机切分重叠区域会泄漏导致验证分数虚高、测试分数失真。正确做法是从航线块或地理网格级别切分。我把1536张图按采集航带分成了若干组确保同一航带的图像全部落入同一数据分片然后以组为单位切分。最终的划分比例是训练集1180张、验证集180张、测试集176张比例大约是77%、12%、11%。这个比例下验证集足够稳定评估mIoU测试集保留了一些与训练完全不重叠的地理区域其实测评估结果能反映模型在陌生区域的泛化表现。3.4 数据增强与扩充增强策略要针对这个数据的特殊结构来设计。日常用的随机翻转和随机裁剪必须保留但对电力线这种小目标类别普通裁剪会让电力线频繁出现在图像边缘甚至被切出画面反而不利于学习。我采用的方案是训练时在整图级别做随机翻转和少量旋转用多尺度缩放采样训练块裁剪尺寸设为512×512或640×640。电力线类别的裁剪策略是“有电力线优先”——每轮训练里所有包含电力线的图块先参与训练再补充不含电力线的图块这样能让电力线样本的采样频率提高大约3倍。另外还尝试了Copy-Paste增强从一个图中裁剪出电力线区域随机粘贴到另一张图的合理位置比如从道路边缘或田埂上延伸过去模拟真实线路走向。实测这个增强对电力线的mIoU提升在2%左右代价是偶尔会出现不自然的弧线折角需要控制粘贴后的形状平滑。4. 模型选择与训练配置4.1 语义分割模型怎么选地物分割的模型选型我见过很多人一上来就追最新的Transformer结构但效果反而不稳定。务实一点说对于六类地物分割任务当前成熟可靠的方案集中在两类骨架里一类是ResNet或ConvNeXt配合UPerNet、DeepLabV3这类经典分割头另一类是SegFormer这类纯Transformer结构。我最终选择的主模型是UPerNet骨干网络用ConvNeXt-Base输入分辨率640×640。选的逻辑有两条UPerNet在场景解析数据集上有长期验证的稳定性ConvNeXt对航拍图像中的多尺寸地物特征提取效果好于传统ResNet尤其在纹理丰富的森林区域表现更强。如果显存预算不足把骨干换成ResNet50作为降级方案训练跟推理速度都快很多精度也不会崩得太厉害。这个任务的评估指标采用mIoU平均交并比和mAcc平均像素精度。mIoU的计算方式是每个类别预测与真实标签的交集除以并集再对所有类别平均它对类别不均衡更敏感比单一全局准确率更能反映真实分割效果。4.2 完整训练配置参考因为多类别的像素分布极度不均损失函数和类别权重需要单独设计。标准交叉熵会被29%的高频类别主导需要引入带权重的交叉熵损失。权重我按逆像素频率做了平滑基础公式是每个类别的权重等于总像素除以该类像素数的对数然后整体缩放到中位类别权重为1。电力线的类别权重按这个公式通常要吃到8到12倍于田地的权重这个比例在实验里是有效的。具体的训练配置参数给出一个可以直接套用的模板。# 以MMSegmentation为例的配置片段 model dict( typeEncoderDecoder, backbonedict( typeConvNeXt, archbase, out_indices[0, 1, 2, 3], drop_path_rate0.4, ), neckdict( typeFPN, in_channels[128, 256, 512, 1024], out_channels256, ), decode_headdict( typeUPerHead, in_channels[128, 256, 512, 1024], in_index[0, 1, 2, 3], channels512, num_classes6, loss_decodedict( typeCrossEntropyLoss, use_sigmoidFalse, class_weight[0.08, 1.0, 1.0, 1.0, 1.0, 2.5], ), ), )训练过程采用AdamW优化器初始学习率设置为1e-4配合线性预热和Poly学习率衰减。Batch size受显存限制设定为8所以同步BN的数量较少模型训练初期收敛速度偏慢需要在检查点保存策略上耐心一些。训练轮数设置在120到160轮之间如果误差曲线在验证集上趋于平稳可以提前停止。硬件方面一块24GB显存GPU就跑得动ConvNeXt-Base在640分辨率下不能把batch放到16只能折中如果换成ResNet50骨干batch可以提到16到32训练速度快将近一半。4.3 关键训练技巧第一个技巧是在训练前先做Miccai等大尺度语义分割预训练权重的加载。由于航拍图和街景图像分布差异比较大直接全部微调预训练权重可能效果一般需要冻结骨干网络前两个Stage只训练后面的模块等损失降到一定程度再解冻全部层。这是一个可操作的稳定化策略。第二个技巧是验证时用多尺度推理加平均推理时对输入图做0.8倍、1.0倍、1.2倍三种尺度缩放分别预测再把三个概率图取平均后求argmax。这个操作对边界质量和电力线细线还原有明显改善但推理时间约增加2倍适合离线业务。第三个技巧是电力线专用后处理。原始模型直接输出的电力线掩码会有大量断点和碎片我在后处理阶段增加了形态学闭运算核大小为3×3再配合一个简单的连通域分析过滤掉像素数量小于阈值20的孤立小团块只保留较直的带状连通域。这样处理之后电力线分割结果的连续性和视觉可用度大幅提升。5. 常见问题与工程落地5.1 标注和转换阶段的坑标注阶段最常见的异常是JSON文件里出现非法多边形。labelme偶尔会保存无法闭合的路径或重复点这类文件在转换时会静默出错。我建议在数据管线入口加一个自动校验器扫描所有JSON文件检查每个多边形是否包含至少三个有效点并即时输出错误清单。另一个高频问题是类别标签名不统一。有的标注员写了“field”有的写了“田地”还有的写成“农田”这会导致类别映射表匹配不上。最好的做法是让labelme加载预定义标签列表标注员只能从列表中选择从源头杜绝名称不一致。转换阶段我踩过另一个坑原图尺寸与掩码尺寸不一致。某一些图像在预处理时做过重压缩分辨率变了但JSON里的多边形坐标还是基于旧分辨率记录的转换出来的掩码与图像完全错位。解决方式是在转换前强制读取原图的真实宽高用宽高对坐标做等比缩放。建议读者用下面这个清单做转换前的快速体检所有JSON文件能否被正常解析每个JSON里是否包含至少一个有效标注要素标注要素类别是否都在映射表中多边形坐标是否在图像宽高范围内是否存在两个多边形大面积重叠但类别不同转换后的掩码大小是否与原图一致5.2 训练阶段的坑训练阶段最容易遇到的是电力线类别完全训不出来。我在项目初期就碰到过这种状况mIoU整体在85%左右但电力线的IoU只有不足三成肉眼可见完全喷碎。后来定位在三个问题上一是电力线被下采样搞丢了640分辨率下电力线太细特征在多层下采样后消失二是损失函数里电力线权重虽然加了但还不够三是数据增强的随机裁剪几乎不保留电力线。解决方案是综合的。训练图块的输入尺寸从640调到768对电力线信息保留有正向帮助损失权重从默认值逐步提升尽量到5倍甚至更高增强策略改成上文提到的电力线优先裁剪同时加了一个约束让裁剪区域中心总是偏向电力线所在位置而非随机分布。调整过后电力线的IoU稳定提升到了62%以上虽然和普通地物类别比还是偏低但已经能用于生产。还有一个容易让人迷惑的问题训练精度很高但验证精度很低。这种情况大概率是数据泄漏或者模型过拟合需要先回到数据切分章节确认训练和验证集没有重叠区域。5.3 部署阶段的经验模型训练完之后真正上业务前需要先做模型转换和剪枝。推荐先用ONNX导出再用ONNX Runtime做推理。以UPerNetConvNeXt-Base结构为例单帧1536×1536推理时间在GPU上约300到400毫秒CPU上则要3秒以上对实时业务来说压力还是很大的。可以考虑使用TensorRT INT8量化代价是有1%左右的精度损失但推理延迟可以降到70毫秒左右。如果是做远期部署在NVIDIA Jetson系列板卡上的方案INT8量化几乎是必须的配置。部署中另一个很实际的业务问题是电力线像素的骨架提取。原生产系统需要电力线走向的矢量线而不是密集像素掩码。我在后处理里加了骨架提取步骤即对电力线掩码做图像细化再用折线拟合方式把细化的骨架转成带有坐标点的线矢量。这一步可以在推理后实时完成主要调用OpenCV的连通域分析和拟合算法。我在实际项目中的体会是地理要素分割真正困难的地方不是训练模型而是把标注、转换、训练、部署整条链路跑通。labelme格式的1536张数据集看起来不起眼但它足够把这条链路完整走一遍。尤其是电力线类别从标注时的高倍放大勾线到训练时的优先采样和权重配平再到部署时的细化与矢量提取每一个环节都在跟“小目标长尾”这个本质问题对抗。最后跟大家分享一个小技巧做完第一版模型后把预测不好的图收集起来优先补标这些场景迭代效率比盲目增加新图高得多。数据集的规模永远不嫌多但最大化的价值在于让每一张标注都能精准解决模型当前最痛的问题。