雪崩识别语义分割数据集:labelme格式3351张图的制作与训练指南
做灾害遥感这几年我陆陆续续经手过不少数据集但雪崩识别分割数据集这套labelme格式的单类别3351张图属于看着工程量不大、真正做起来才意识到水深的类型。1个类别意味着标注规范和模型设计都更聚焦但雪这种目标物在影像上极其特别——它和背景同色系、纹理弱、边缘模糊普通分类和检测框根本表达不清灾情的真实范围。所以这篇文章我不打算只简单介绍“哪里能下到这套数据”而是把背后的核心逻辑讲透为什么雪崩任务绕不开语义分割labelme格式的文件究竟长什么样从原始影像到3351张成品需要过哪些坎以及拿到这套数据后该怎么处理、怎么训、怎么评估给准备碰雪崩识别或类似低对比度分割任务的同行一条可以照着走的路。1. 雪崩识别为什么非要做像素级分割1.1 目标检测的框表达不了雪崩的“形状”雪崩在影像上的形态和普通目标差别很大。车辆、行人、建筑物都有相对清晰的边界和固定的几何特征框住它们之后框内内容大部分就是目标本身。雪崩不是这样。崩落区呈现絮状、扇形堆积、条带状沟槽等多种形态边缘和周围积雪坡面往往是渐变的没有一个明确的“框内全是目标、框外全不是”的切分点。如果你只用矩形检测框去标雪崩会出现一种很尴尬的情况一个框打下去框内可能只有40%的区域是真正的崩落物剩下60%是未崩落的积雪和裸岩。模型训练时看到的正样本里混入了大量背景它就会去学“白色区域雪崩”的偷懒特征。等模型上了真实场景遇到冰川、云层、反光强烈的雪坡误检率会明显失控。分割任务就完全不一样了。它要求把每一个像素归类崩落区和非崩落区的边界被精细刻画出来模型被迫去学雪崩的纹理、形态、地形关系而不是靠“大块白色”糊弄过去。这也是为什么雪崩识别领域的主流方案几乎都往语义分割这个方向上走。1.2 分割结果的后续价值远不止画个区域拿到像素级分割掩码之后能做的事比一张检测框多得多。最直接的是面积估算每个像素在实际地面都有对应的空间分辨率统计掩码内的像素数量乘以单像素面积就能算出这次雪崩的覆盖范围。灾害应急里这个数字直接影响救援力量投放和道路封闭决策。再进一步不同时相的雪崩分割结果可以叠在一起看变化趋势某个沟槽是不是反复发生雪崩、堆积扇有没有扩大、冰碛物往下游推进了多少。这些分析在目标检测的矩形框层面是没法做的只有分割掩码能给出边界级的变化信息。配合GIS数据做坡度、坡向、海拔的叠加分析还能反过来总结雪崩发生的规律给山区公路和滑雪场做风险评估实用性非常强。1.3 单类别3351张的定位小而专的基线条目这套数据集只有1个类别这意味着标注时不用考虑多个标签之间的混淆规范制定和模型设计都更聚焦。对做研究或者做项目验证的人来说这种小而专的数据集反而更好用训练时间短、评估指标清晰、baseline容易建立。不过3351张放在深度学习里属于中等偏小的规模尤其分割任务需要学习精细的边界特征单纯从零训练一个深层网络注定会过拟合。合理的打开方式是配合ImageNet预训练的backbone做迁移学习再通过数据增强扩充样本多样性。单类别还意味着背景像素会远多于前景像素训练时类别不平衡的问题基本躲不掉这个在后文模型部分会详细说怎么应对。2. 3351张雪崩图的labelme格式底层拆解2.1 一个标准labelme JSON文件的结构labelme的核心产出是JSON文件每张图片对应一个同名JSON。这个格式最大的优点就是结构极简、可读性强一个文本编辑器就能打开检查。下面是一个典型的雪崩标注文件{ version: 5.2.1, flags: {}, shapes: [ { label: avalanche, points: [ [224, 310], [238, 322], [251, 319], [269, 338] ], group_id: null, shape_type: polygon, flags: {} } ], imagePath: IMG_0042.jpg, imageData: null, imageHeight: 1080, imageWidth: 1920 }各字段的含义可以用下面这张表说清楚字段作用注意事项versionlabelme工具版本号不同版本对flags等字段的处理略有差异flags图像级标签通常为空做多标签分类时可以用到shapes核心数组包含每个标注对象多边形、矩形、线等都由它描述shapes[].label类别名称必须与训练时的类别映射表严格一致shapes[].points坐标点数组绝对像素坐标不是归一化坐标shapes[].shape_type标注类型常用polygon也有rectangle、circle等imagePath原图文件名或相对路径改过文件名后要同步更新imageData图片的base64编码可能为空加载时从imagePath读取imageHeight / imageWidth原图尺寸用于坐标校验和掩码生成2.2 带引号的陷阱imageData和坐标系统我见过不少人在处理labelme格式时栽在imageData这个字段上。标注时保存的JSON里默认会带一份base64编码的原图这会让单个JSON文件体积膨胀到好几兆。3351张图算下来光JSON就占掉不少磁盘空间而且后续批量处理时要反复解码再编码非常浪费时间。所以正规的做法是在标注启动时加上--nodata参数这样保存的JSON里imageData就是null文件体积能缩小十几倍。读取端只要基于imagePath去拼接原图路径就行。另一个坑是图像一旦被resize或者裁剪JSON里的坐标就和原图对不上了。labelme存的是绝对像素坐标坐标系原点在图像左上角所以模型训练前处理时必须记录好resize尺度或者直接用原始分辨率训练避免坐标错位。2.3 从JSON多边形到像素级掩码的转换labelme格式本身不能直接喂给深度学习模型它只是中间标注格式。真正训练时我们需要的是单通道的掩码图每个像素的灰度值代表类别编号。转换逻辑并不复杂核心是借助labelme自带的工具函数。import json import numpy as np import cv2 from labelme import utils json_file IMG_0042.json with open(json_file, r, encodingutf-8) as f: data json.load(f) img cv2.imread(IMG_0042.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) label_name_to_value {_background_: 0, avalanche: 1} # 将多边形转为像素级标签图 label_map, _ utils.shapes_to_label( img.shape, data[shapes], label_name_to_value ) # 生成可视化叠加图便于检查 visual utils.draw_label(label_map, img, label_name_to_value) # 保存掩码 cv2.imwrite(IMG_0042_mask.png, label_map.astype(np.uint8)) cv2.imwrite(IMG_0042_visual.jpg, visual)这段代码跑完会得到一张单通道的掩码图背景是0、雪崩区域是1。如果你想转成COCO格式或者YOLO分割格式思路是同理的先从shapes里把多边形坐标取出来再按目标格式组织成对应的JSON或txt文件。不过要注意YOLO分割格式要求的是归一化坐标计算时必须除以原图宽高这一点和labelme的绝对像素坐标有本质区别。3. 从原始影像到可交付数据集我制定的制作链路3.1 数据来源与初筛先解决“拍不清”和“拍不对”这一套3351张雪崩图采集渠道一般绕不开三种无人机航拍、卫星光学影像、地面固定机位监控。三种来源各有优劣无人机影像分辨率高、角度灵活适合标注细碎的堆积形态卫星影像覆盖范围大但分辨率低只能标注大面积雪崩对细碎崩落基本无能为力地面监控胜在连续时相能捕捉雪崩发生的全过程很多研究型数据集的动态分析都靠它。初筛阶段不要手软我一般按下面几个条件做硬性过滤图像模糊运动模糊或对焦不准的直接剔除边缘都看不清标出来的边界毫无意义曝光异常雪地过曝会导致纹理直接消失掩码边界只能靠猜这种图宁可不要雪崩区域占比过低如果一张航拍图里崩落区只占十几个像素标注价值极低训练时也容易变成噪声镜头畸变严重的广角图边缘区域的形变会影响边界精度除非后续做矫正否则先排除另外多样性比数量更重要。如果3351张图里绝大部分是同一个山坡、同一种光照条件模型泛化能力会很差。我建议在采集阶段就刻意记录拍摄地点、日期、机位高度这些元信息事后划分数据集时才能做到场景隔离。3.2 标注规范边界判定和一致性的三条铁律做分割数据集最大的成本不是画多边形的时间而是“不同人画出来的边界不一致”带来的返工。我们内部定了三条铁律简单但有效。第一贴边原则。多边形的点必须紧贴雪崩堆积物的可见边缘宁可多打几个点把弧度表达清楚也不能为了省事用大直线概括。刚开始标的时候可以12到20个点别用三五个点糊弄一条弯曲的堆积线。第二断连与碎片处理。崩落区可能出现多个不连通的碎片比如主堆积体旁边散落的小雪块规格上统一要求面积大于整张图0.5%的碎片单独标注小于这个阈值的不标。没有这条规则不同标注者对碎片的取舍就会千差万别最后的掩码噪声会让模型学得很混乱。第三不可判定区域直接跳过。当边界被云遮蔽、被阴影完全吞掉时硬标只会引入错误监督信号。统一策略是遇到这种图先放到“待定”目录后期找原始影像或者更多角度确认后再补标绝不允许凭感觉画。3.3 标注质量的交叉检查与交付结构3351张图如果由一个人标完主观偏差会贯穿始终。我们采用两轮交叉验证第一轮由A标注第二轮由B独立标注同一批图的10%然后计算两组掩码之间的IoU。IoU大于0.85就算合格低于这个值说明规范理解有偏差需要重新对齐。交付时目录结构也建议固定下来别东一个文件西一个文件夹avalanche_dataset/ ├── images/ │ ├── IMG_0001.jpg │ ├── IMG_0002.jpg │ └── ... ├── labels/ │ ├── IMG_0001.json │ ├── IMG_0002.json │ └── ... ├── masks/ │ ├── IMG_0001_mask.png │ ├── IMG_0002_mask.png │ └── ... ├── label_names.txt ├── train.txt ├── val.txt └── test.txttrain.txt、val.txt、test.txt里存的是图片路径清单训练时直接按行读比每次临时搞划分脚本要省事得多也方便复现。4. labelme实操配置从安装到标注一张雪崩图4.1 安装与启动二十分钟跑起来labelme的安装不算复杂但Python环境版本会把不少人卡住。我推荐直接用conda先把独立环境建好避免和系统全局Python里的包打架。conda create -n labelme python3.9 conda activate labelme pip install labelme装完之后在终端输入labelme就能打开图形界面。需要注意新版labelme对Python 3.7以下版本支持不好老项目如果还在用Python 3.6建议统一升级。Linux和Windows的安装步骤基本一致只是显示路径的写法不同没有别的差异。启动的时候带上参数会更顺手labelme --labels label_names.txt --nodata --autosave--labels指定类名列表文件内容就是一行一个类别avalanche--nodata去掉JSON里的base64图像数据--autosave则保证每切到下一张图时自动保存当前标注素材多了以后最怕的就是忘记保存这个参数能救大命。4.2 一张雪崩图的完整标注操作用labelme打开图片目录后整个标注流程一般是这样的用Open Dir打开存放雪崩影像的文件夹点击Create Polygons进入画点模式在图中点击多边形顶点沿雪崩堆积区边缘逆时针走一遍回到起点闭合多边形弹窗里输入类别名但这属于新建节点的最后一次确认按CtrlS保存或者直接靠--autosave自动存画点过程中按CtrlZ可以撤销上一个点这对画复杂边界特别有用。如果某个多边形画完还想微调切换到Edit Polygons模式拖拽顶点就可以。常用快捷键我整理成了表建议团队里贴一份操作快捷键说明创建多边形CtrlN进入画点模式绘制后完成多边形CtrlEnter闭合并确认标签编辑多边形CtrlE切换顶点编辑模式撤销CtrlZ画点时撤销上一个点保存CtrlS手动保存JSON删除多边形CtrlJ删除当前选中的多边形4.3 标注实践中的两个小忠告第一个忠告放大到200%再画关键边界。雪崩和雪的边缘在低缩放级别下很容易被忽略放大后你能看到细节纹理的分界边界质量会明显上一个台阶。第二个忠告别一味追求超级密集的顶点。雪崩边缘本身是渐变的顶点太密反而会让模型学到过于锯齿化的边界。每一段边缘用五六个点表达弧度即可这样既准确又不会给转换掩码带来过大的多边形填充运算负担。5. 用这套数据训练分割模型的完整方案5.1 数据划分别让同一片山坡同时出现在训练集和验证集里很多人在划分数据集时直接随机split这在雪崩场景下会埋大雷。同一片山坡、同一台设备拍出来的连续帧训练集和验证集之间特征高度相似模型验证分数会虚高一旦换到新的山坡或新的拍摄条件就立刻露馅。我建议按场景或者按采集时段划分。比如3351张图来自30个不同的机位/航次就按机位或航次来分组随机选80%的组做训练、10%做验证、10%做测试。这样验证集里的山体、光照、角度对模型来说都是相对陌生的评估结果才真正有参考价值。如果做了场景记录直接在train.txt里按组写入即可。5.2 增强策略要贴合雪的物理特性雪崩分割的数据增强不能照搬通用目标检测那套。通用的随机色彩抖动在别的任务里问题不大但在雪场景里过度改变色调会把模型对雪地特有反射特性的判断彻底打乱。我实际跑下来的有效增强组合如下水平翻转、垂直翻转雪崩没有左右方向语义翻转不破坏标注随机缩放与随机裁剪模拟不同航拍高度和不同目标尺度亮度与对比度扰动幅度控制在±15%以内模拟不同日照和阴影环境轻微高斯噪声模拟传感器噪声增强鲁棒性不建议做的增强是大幅度改变色相。雪崩影像本质上是近灰白色调的高光场景训练时把饱和度、色相调得太夸张模型学到的颜色分布会偏离真实场景。这个度要控制好。5.3 模型选型、损失函数与评估指标对于单类别分割任务U-Net是一个很稳的baseline。Encoder换成ResNet50或EfficientNet预训练权重用segmentation_models_pytorch这类库很快就能搭起来。想要更高精度就试DeepLabV3或SegFormer这两个模型对多尺度目标更友好特别是卫星影像里大雪崩和小碎块并存的情况。损失函数建议用Dice Loss与Cross Entropy的组合。公式上就是两个损失相加import torch.nn.functional as F import torch def dice_loss(pred, target, smooth1.0): pred torch.softmax(pred, dim1)[:, 1] target target.float() intersection (pred * target).sum() return 1 - (2.0 * intersection smooth) / (pred.sum() target.sum() smooth) # 训练时 ce F.cross_entropy(logits, label) dice dice_loss(logits, label) loss 0.5 * ce 0.5 * diceDice Loss能直接优化IoU指标特别适合前景占比极小的分割任务交叉熵提供稳定的梯度信号两者搭配不容易出现训练初期不收敛的问题。评估时重点看三个指标mIoU、Dice系数和Pixel Accuracy。但因为背景占绝对主导Pixel Accuracy参考价值不大真正要盯的是前景类的IoU和Dice。一个可用的baseline大概在IoU 0.75到0.85、Dice 0.85到0.92之间具体数值取决于影像分辨率和雪崩尺度分布。跑测试时记得输出每一类的IoU别只给一个被背景拉高了的平均数。6. 雪崩标注的真实坑位与应对经验6.1 雪和云最容易翻车的边界在所有干扰因素里云层是最难处理的。云和积雪在可见光影像上都是亮白色高反射区域纹理很相似哪怕人眼有时候都要仔细分辨机器就更容易把它们混在一起。标注时如果图里出现云影重叠我个人的经验是先看地形雪崩发生在山体表面边界会顺着沟槽、坡脚走云的边缘是自由形状的不会受地形约束。另外云通常在影像中位置较高、边缘带有轻微的模糊层次雪崩堆积则是连续接地、有堆积扇的结构。如果模型在推理阶段频繁把云识别成雪崩优先检查训练集里是不是本身就包含了大量“带云的背景图”。以及后续考虑加入红外波段或多时相数据这类问题靠单波段可见光很难根治。6.2 阴影与纹理缺失边界线到底画在哪里雪山场景里阴影是逃不掉的坑。山体遮挡造成的阴影会把一部分崩落区完全压黑表面纹理全部丢失。许多标注者这时候会犹豫边界画在阴影边缘还是阴影内部。我们统一的经验是只要还能判断阴影下面确实是雪崩延续体的就按地形走向把边界画进阴影区如果连地形都看不出来就跳过这一段标注其他确定的部分。这样处理让模型学到的不是“没有纹理就等于没有目标”而是学会了结合地形上下文去推断推理阶段碰到阴影遮挡也更有韧性。6.3 给小规模单类别数据集使用者的三条建议第一条建议非常实用标注和训练的分辨率一定要统一。有的图是3840x2160有的是1920x1080训练时如果直接resize到统一尺寸大图里的小雪崩块会很精细小图里同样的目标会被压缩到无法分辨。建议标注时就限定在统一分辨率或者训练时把大图切成patch再送进网络避免无谓的信息损失。第二条建议是训练时一定要用验证集做早停。3351张单类别数据训练过程特别容易看到loss下降但验证集IoU会先升后降这是过拟合的特征。提前在验证集上监控IoU并保存最优权重比盲目加训练轮数要可靠得多。第三条建议是可视化结果的价值被严重低估。模型预测完别只看指标我习惯把原图、真值掩码、预测掩码三张图拼在一起看错误情况。有些错误在IoU数字上不明显但看图一眼就能发现比如模型把云边当雪崩边、把阴影区全部认成背景等。数据集的版本迭代和增强策略调整靠的就是这些可视化反馈。最后聊一点个人体会。雪崩识别这个方向数据永远是比模型更稀缺的资源。真实的雪崩事件发生少、采集难、地面真值获取风险高所以每一张标注图都格外珍贵。如果你手里正好有一套3351张的labelme格式数据别急着换各种花哨网络结构先把标注一致性、数据划分、增强策略这些地基打牢U-Net级别的模型就能给出远超预期的效果。这些前处理上的细致功夫最终都会在模型指标上实实在在体现出来。