☰
无人机河道浮木识别:语义分割数据集与训练全流程拆解
2026/10/21 21:29:42 网站建设 项目流程

1. 为什么无人机拍河道浮木,非要自己造一个数据集

做水利巡检、河道治理或者防汛应急的朋友,大概率都有过这样的经历:拿着无人机沿着河道飞一圈,回来导出的素材几十个G,然后陷入漫长的逐帧看片环节。人工排查浮木、漂浮物、碍洪物,眼睛看花不说,漏判率还高得吓人。我最早接到这个需求,是给某地的河道管理部门做一套无人机影像辅助分析系统。对方的要求很直接——能不能自动识别出水面上的浮木、树枝这类堵塞物,最好顺便把面积和分布位置标出来。

前期调研时我发现一个大问题:市面上公开的遥感分割数据集,大多针对陆地场景,水体目标检测数据集也以船舶、垃圾漂浮物为主。专门针对无人机视角、河道场景、近岸浮木这种小目标的公开数据集基本处于空白状态。这意味着什么?意味着如果我直接拿ImageNet预训练模型去跑河道影像,几乎必死。浮木没有固定的颜色、纹理、形态,在水面上又小又碎,还会随着水流移动、翻转,跟水里的倒影、岸边杂物、暗色水纹混在一起,很难区分。通用模型在COCO上那套先验知识,拿到这个场景里基本帮不上忙。

所以自己做数据集不是“选择”,而是“必须”。这次分享的这个数据集——无人机视角河道河岸浮木识别分割数据集,一共1345张,labelme格式,单类别,就是冲着这个问题来的。它解决的核心痛点就是:给做河道视觉识别的团队,提供一个可以直接用于训练分割模型的、场景聚焦的、标注格式成熟的数据基础。

那这个数据集适合谁?一类是做水利信息化、智慧河湖平台的技术人员,需要把浮木识别能力集成到巡检系统里;另一类是研究语义分割、弱监督学习的学生或工程师,需要一个行业特色鲜明、规模又不算太大的数据集来跑实验、验证模型改进;还有一类是刚入门分割任务,想练手完整流程——数据标注、格式转换、模型训练、评估部署的开发者。后面我会把整条链路拆开讲清楚,每个环节该怎么做、哪些地方容易踩坑,都放到实操层面去说。

老规矩,先说一个反直觉的结论:1345张图,听起来不多,但在无人机的垂视视角下,完全够打一个基线模型。关键在于你怎么用它、怎么组织训练验证集、怎么处理单类别的类别不平衡问题。这个我后面详细展开。

2. Labelme格式拆解:别只看扩展名,要理解JSON里的结构逻辑

2.1 Labelme的JSON到底长什么样

拿到数据集,第一件事不是急着训练,而是先搞清楚标注文件的内部结构。很多初学者喜欢把labelme格式当成一个“黑盒”——标注完,导出,转成coco或者voc就直接开训。但只要中间某个字段理解错了,后期排查问题时会特别痛苦。

Labelme的核心产物是JSON文件,一个图像对应一个JSON。打开之后你会看到这些关键字段:

  • version:标注工具版本号,一般不用管。
  • flags:记录图像级别的布尔标签,通常为空。
  • shapes:这是重头戏,一个数组,每个元素对应一个标注对象。每个对象里包含label(类别名)、points(多边形顶点坐标列表)、shape_type(标注形状类型,常见的是polygon,也可以是rectangle、circle)、group_id(分组编号,可为空)。
  • imagePath:图像文件名。
  • imageData:通常是空字符串,也可以Base64编码嵌入整张图。数据量大的时候建议清空,否则文件会变得很臃肿。
  • imageHeight、imageWidth:图像的高和宽,单位像素。

对于浮木这个场景,shape_type基本都是polygon,也就是用手动多边形把目标轮廓框出来。这里有一个重要习惯:浮木往往形状细长、边缘不规则,标注的质量直接决定模型上限。

2.2 单类别数据集的“类别隐忧”

标题里写了“1类别”,很多人觉得单类别简单啊,模型只用做背景分割和目标分割二分类。其实恰恰相反,单类别的难点在“边界”而不是“种类”。

浮木这一类,内部变化极大。一段枯木、一截树枝、一堆缠绕的水草杂物,都可能被归为“浮木”。但如果标注风格不统一——有人说只标明显的树干,有人把水草都标进去,模型就会学得很困惑。我打开这个数据集检查的时候,发现它的标注一致性做得还算不错,主要集中在较完整的树干、枝杈、明显露出水面的木质漂浮物上。这一点在训练前一定要调研清楚,否则你花大力气清洗数据的时间比标注还长。

另外,单类别意味着没有“负样本”的显式标注。岸边的石头、混凝土块、水面强烈反光区域,模型都会天然当成候选区域去学习。所以训练时负样本的挖掘策略很关键,后面我会专门讲到。

2.3 从Labelme到分割训练:三步走的转换流程

绝大部分训练框架并不直接吃labelme的JSON,你需要把它转换成模型可用的格式。这里我强烈建议转换成分段后的PNG掩码图(mask),或者标准COCO格式的JSON。具体流程三步:

第一步:解析JSON中的shapes数组,把每个points多边形通过OpenCV的fillPoly函数画到一张全零底图上,目标区域填255。

第二步:把原始图像和掩码图重命名对齐,例如IMG_0001.jpg对应IMG_0001_mask.png。注意掩码图只用灰度格式,不要存成三通道彩色图,否则后处理时还要去通道。

第三步:按照6:2:2或者7:2:1的比例,把图像和掩码同步划分为训练集、验证集、测试集。划分时要特别注意:同一段河道的连续帧图片不要拆开到不同集合里。如果你的图是飞行航线连续采集的,相邻帧几乎同一个场景,直接随机划分会导致验证集和训练集高度相似,指标虚高。正确做法是先把航线分组,按组划分。

下面是一段我常用的转换脚本,跑通之后能直接用:

import json import cv2 import numpy as np import os from tqdm import tqdm def labelme_to_mask(json_path, img_shape, mask_path): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) mask = np.zeros((img_shape[0], img_shape[1]), dtype=np.uint8) for shape in data["shapes"]: points = np.array(shape["points"], dtype=np.int32) cv2.fillPoly(mask, [points], 255) cv2.imwrite(mask_path, mask) img_dir = "images" json_dir = "jsons" mask_dir = "masks" os.makedirs(mask_dir, exist_ok=True) for json_name in tqdm(os.listdir(json_dir)): if not json_name.endswith(".json"): continue json_path = os.path.join(json_dir, json_name) img_name = json_name.replace(".json", ".jpg") # 根据实际情况调整后缀 img = cv2.imread(os.path.join(img_dir, img_name)) if img is None: print(f"图片读取失败: {img_name}") continue mask_path = os.path.join(mask_dir, json_name.replace(".json", "_mask.png")) labelme_to_mask(json_path, img.shape, mask_path) print(f"已处理: {img_name}")

转换时还有一个小坑:labelme的坐标原点在图像左上角,单位是像素,且是浮点数。直接用numpy数组做索引时,要先转int32,否则会报错。另一个清空imageData的操作,能显著减小JSON体积,1345个文件能差出几百MB。

3. 1345张图怎么分配:训练集设计的策略,远比模型选型更重要

3.1 数量不大,但是场景覆盖广

1345张图单独拿出来说,确实不算大规模数据集。但无人机视角的河道影像有一个特点:单张图内的信息密度高,目标数量多,场景结构相对类似。你不需要像ImageNet那样准备百万张,只需要保证场景多样性和目标形态多样性到位就行。

我在实际使用中核对过这个数据集的场景构成。它覆盖了几种典型的河道区段:宽阔主河道的中央航拍、靠近河岸的侧视角、有植被覆盖的河漫滩区域、桥梁附近的水域(这是浮木高发区,因为水面收窄后漂浮物容易堆积)、以及部分水面有强烈反光的时段。这些场景基本覆盖了实际巡检中会遇到的绝大多数情况。密集树影下的暗色浮木、与水面反光重叠的半浸没浮木,这些难样本在数据集中有专门分布,训练时务必保证这些样本进入训练集而不是全扔进验证集。

大概算一笔比例账。假设1345张图中,80%用于训练,也就是1076张。每张图假设平均标注2到3个浮木实例,训练集中就有大约2500个有效目标实例。这个量级对单类别的二分类分割来说,已经足够让U-Net或DeepLabV3+收敛到一个可用的水平。

3.2 划分策略:按航线分组,而不是随机乱切

刚才提过,无人机采集的影像序列有很强的帧间相关性。如果同一段航线飞出200帧连续画面,你把它随机拆到训练和验证集中,模型很可能“作弊”——验证集里出现的浮木,在训练集里有几乎一样的角度、光照和背景。最后的mIoU做出来90%以上,你以为模型学好了,一到实际部署就露馅。

正确策略是:把每一条飞行航拍记录视为一个完整单元,在单元级别做划分。比如某次飞行覆盖了3公里河道,产出了300张有效帧,这300张要么全进训练集,要么全进验证集。划分比例按航线段来算,而不是按单帧来算。

如果一个数据集已经被人为打乱了顺序,没有航线信息,那我建议你再看看文件名的EXIF拍摄时间或者GPS定位信息,尽最大努力还原采集轨迹。实在还原不了,至少做一次基于场景相似度的聚类再划分,能做到多少算多少。这个细节直接决定你的验证集是“真验证”还是“自欺欺人”。

3.3 验证集和测试集的角色分工

我习惯把数据拆成三个部分组织。训练集用来学习,验证集用来做模型选择——比如判断训练多少轮、是否早停、是否调学习率——而测试集只在最终模型定下来之后跑一次,用于报告接近真实部署环境的指标。

  • 训练集:约940张
  • 验证集:约270张
  • 测试集:约135张

比例大致是70%、20%、10%。测试集一旦确定,中途绝不动它。每改一次模型结构、每换一次损失函数,都先在验证集上对比,等全部调完再用测试集做最终评估。这是朴素但极其有效的防过拟合策略,尤其是数据量只有1345张的时候,测试集被“污染”的影响会被急剧放大。

4. 河道浮木轻量分割的实操流程:数据增强、模型选型到损失函数

4.1 关键预处理:畸变、裁剪与归一化

无人机影像通常带有一定的镜头畸变和角度倾斜,除非出厂前做过内参标定,否则直接喂给模型会遇到一个麻烦:模型会记住镜头畸变的分布规律,而不是真正学会识别浮木。如果数据集中的图片尺寸很大,比如4000×3000像素,直接resize到512×512会丢失大量细节,浮木这种目标本身可能只有几十个像素宽。

我的建议是分两步走。

第一步,把原始大图做滑动窗口裁剪,切成512×512或者640×640的切片。切的时候窗口要有重叠,重叠率15%到20%比较合适,防止浮木刚好落在窗口拼接线附近被截断。切出来的子图如果完全没有目标(纯水面、纯岸地),建议保留一部分作为负样本,比例控制在总样本量的20%左右。

第二步,对于每个切片,做归一化。具体是除以255后,减去均值再除以标准差。均值标准差可以从整体数据上估算,不需要特别精确,一套通用统计值就够用。

另外要注意,labelme的JSON坐标是相对于原始大图的,裁剪切片后必须同步更新标注坐标。这几乎是所有从高分辨率影像做分割的项目里最容易翻车的一步。我之前就见过某团队训练完了才发现掩码和图像错位了整整一个窗口大小,模型指标接近随机水平。用transform的RandomCrop或者CenterCrop时,要同时给图像和掩码传入同样的随机种子,保证空间变换一致。

4.2 模型选型:别一上来就上Transformer

现在分割模型五花八门,很多同学一上手就祭出Swin Transformer、SegFormer、Mask2Former。不是说不行,而是就1345张数据量、单类别分割任务来说,这些高复杂度模型反而容易过拟合,训练周期长,调参成本高。我的习惯是先跑一个U-Net或者U-Net++的baseline,确认数据链路和损失函数没问题,再逐步升级。

具体到浮木场景,我用下来最顺手的组合是ResNet34编码器加U-Net解码器。参数量适中,显存占用友好,在1080Ti级别以上的GPU上跑512×512输入,batch size能开到16以上,训练速度很快。DeepLabV3+也是一个好选择,尤其在浮木边缘细节上表现更锐利,但推理速度略慢。如果你要部署到无人机地面站的便携设备上,MobileNetV3+U-Net的轻量组合可以放进备选清单,精度掉一两个点,但推理帧率能翻几倍。

我在这个数据集上对比过几组常见配置,简单列个参考(不同硬件、不同划分下会有出入):

模型组合输入尺寸参数量训练耗时(约)验证mIoU(约)
ResNet34 + U-Net512×51244M2~3小时78%
ResNet50 + DeepLabV3+512×51264M4~5小时81%
MobileNetV3 + U-Net512×51215M1.5小时72%
SegFormer-B0512×5123.7M3小时74%

注意这只是我本地的参考值,你的划分方式、增强策略不同,结果会有波动。重点是别只看精度,要看精度和成本的平衡。

4.3 数据增强组合:围绕“水”这个场景定制

通用增强——随机翻转、随机旋转、随机亮度对比度——都能用,但浮木场景有它的特殊性。水面的光照变化极其剧烈,同一个浮木,在顺光、逆光、多云遮挡下拍出来的视觉特征差异巨大。所以我额外加了三类针对性增强:

第一是随机色彩扰动,重点增强色相和饱和度的变化幅度。枯木的颜色可以从深褐到浅黄,水面颜色可以从浑浊黄绿到深蓝灰,保守的增强幅度不够,大胆一点反而更贴合实际。

第二是随机添加高斯噪声和轻微的模糊。很多浮木处在画面边缘或景深区域之外,本身就有轻微失焦。给训练集注入噪声,能提升模型的鲁棒性,防止它对纹理细节过度敏感。

第三是混合增强,把两张训练图各自裁剪一半拼在一起,标签也对应拼接。这能显著增加目标与背景的多样化组合,让模型不至于只学会水面+浮木这一种固定构图。实测下来混合增强能让验证集mIoU提升2个百分点左右,在数据量有限的前提下性价比很高。

然后是至关重要的一步:在训练过程中对mask同步做增强。PyTorch里可以用torchvision.transforms配合Compose,但一定要给图像和掩码用同一个随机变换实例。最常见的错误写法是分别创建两个Compose,图像翻转了而掩码没翻转,训练出来模型完全学不到东西,损失函数还不下降。

4.4 损失函数:Focal Loss边界情况下的优势

单类别分割的默认配置是交叉熵损失。但水上浮木这个场景,背景(水、岸、植被)占比远高于目标区域。极端情况下,一张图上浮木可能只占0.5%的像素,交叉熵会把大量梯度集中在背景上,模型倾向于全预测为背景,准确率很高但mIoU惨不忍睹。

针对类别不平衡,我推荐用Focal Loss或者Dice Loss。Focal Loss通过调制系数降低易分类样本的权重,让模型更关注那些难分的边界像素和细长浮木;Dice Loss直接优化预测和真实掩码之间的重叠区域,对小目标更为友好。实践中常见的做法是两者加权组合,我记得有一组相对鲁棒的配置是0.5*Focal + 0.5*Dice,你可以按实际效果微调。

浮木分割还有一个独特难点:类别内部的“像素级不平衡”。粗壮的树干,中间大片区域非常好分类,难的是边界的细枝末节。这类像素恰好在Focal Loss中会被持续放大权重,所以它对提升边界精度、召回率都有显著帮助。

5. 踩坑记录:无人机视角下的三大“视觉陷阱”与应对方案

5.1 水面反光:最容易被误判成木质结构的干扰项

水面对阳光的镜面反射,在航拍影像里经常形成亮白色或者暖黄色的条纹。这些条纹的走向有时恰好和浮木的细长形态相似,模型很容易把反光区域判定成目标,产生大量假阳性。

我最初训练时精度不低,但一看可视化结果,一大片假阳性全标在水面的亮斑上。这个问题的根源在于训练数据里反光区域的特征没有充分“暴露”给模型。单靠模型自己学到“水面反光不是浮木”,效率太低。

我的解决方案有三层。第一层是在训练数据里多保留反光强烈的负样本切片,让模型有机会学到反光区域的负例特征。第二层是后处理时用HSV空间的亮度通道加一个阈值:浮木的边缘虽然有高光,但整体平均亮度不会像水面反光那样接近纯白,凡是掩码区域亮度均值超过某个阈值的,直接清零。第三层是如果拍摄时间可选,尽量选在上午9点到下午3点之间,光线入射角相对垂直,水面反光面积最小。

5.2 岸际植被与枯草:特征混淆的重灾区

树影、岸边倒伏的枯草、被风吹动的落叶堆,形态和颜色上跟浮木高度重叠。岸际植被属于陆地背景,如果标注时不小心把它们圈进去,模型就学歪了。我在这个数据集里特别检查过这部分——标注质量整体在线,绝大多数目标确实是水中的木质漂浮物,岸上的枯枝基本没标。这一点很重要,不然训练阶段就会积累系统性偏差。

真正麻烦的部署场景是浮木紧贴岸边、半截在岸上、半截在水里。这时候模型输出的掩码容易延伸到岸上植被区,导致面积高估。我的经验是给最终掩码增加一个后处理规则:结合岸线掩码,如果模型预测区域延伸到了陆地,将这些延伸部分裁掉。也就是说可以叠加一个水陆分割模型作为辅助约束。但这是进阶玩法,基线阶段先不用加。

5.3 目标尺度差异:树枝是树枝,树干也是树干

无人机飞行高度变化,导致同一类目标在图像上所占像素跨度极大。飞行高度50米,一棵大树的浮木可能在图上占几百像素宽;飞行高度150米,同样一个目标只剩下几十像素的微小区域。一个在512×512输入下能明显识别的目标,下采样到特征图后可能只剩一个点。

为了减轻这个问题,我一般会在标注阶段检查GT掩码中每个连通域的像素面积,统计分布情况。如果发现小目标占比太高,就增加多尺度输入训练,比如同batch内混着输入384×384、512×512、640×640,让模型适应不同尺度。推理阶段也可以做多尺度推理,把多张不同尺度的预测结果做平均融合,对小目标的漏检有明显改善。

6. 从训练集到部署:评估指标、可视化检查与模型压缩的一课

6.1 别只盯mIoU,还要看召回率和连通域数量

分割任务最常见的主指标是mIoU、mPA(平均像素精度),但对浮木场景来说,这些指标还不够。业务方关心的是“河道里究竟有没有大块浮木”“有多少块”“分布在哪”。这意味着你需要更贴近业务的指标:

  • 召回率(Recall):漏检一个浮木,可能意味着下游调度疏漏,比误检更严重。
  • 连通域数量:预测掩码中能够连成一个整体区域的数量,直接对应“有几根浮木”。
  • 单体面积分布的误差:预测出的单块目标面积与实际面积的偏差。

我建议训练完成后,对所有验证集预测结果做连通域分析,统计预测连通域数量和GT连通域数量的接近程度。如果图像级mIoU不错,但连通域数量偏差大,说明模型虽然画得准,但把一块目标切成了几段或者把几段连成一片。

以业务为驱动的评估会让你避免一种常见尴尬——交了一份85%mIoU的报告,对方却问“那河里的那根大木头为什么显示成三截”。

6.2 可视化调参:每20轮就翻一次预测图

现代训练流程容易让人过度依赖终端打出的loss曲线。Loss降了不代表模型学到了浮木的结构特征。我固执地保持一个习惯:每训练20个epoch就保存一批验证集预测图,单独打开看哪里错、哪里漏。浮木分割有非常强的视觉可解释性,你不看图,根本无法发现模型在靠水面纹理“作弊”。

具体看图时重点看三类情况:第一类,背景被标成目标的假阳性,观察它们的共同特征——都是某种颜色或者特定位置;第二类,小目标被完全忽略,说明模型对小目标关注不够,需要加强多尺度或调整损失权重;第三类,低对比度浮木的轮廓被割裂,说明边界细节还有问题,可能需要在解码器端加一点深度监督。通过看图迭代,比单纯调学习率快得多。

6.3 模型压缩:部署到无人机地面站前的一步

模型训练完远不是结束。实际巡检场景,你大概率不是在一台高配服务器上实时推理,而是要在无人机地面站、便携电脑甚至嵌入式设备上跑。我从ResNet34+U-Net的baseline出发,做了两步压缩:

第一步做剪枝,把编码器后几层冗余的通道剪掉一部分,精度下降0.5%,但参数量减少30%。第二步做INT8量化,用验证集做校准数据,量化和蒸馏结合,最终模型体积缩小到原来的四分之一,单帧推理时间从120ms降到40ms左右。如果你的设备内存更紧张,还可以转ONNX再转TensorRT,进一步榨取性能。

这一步的实践顺序建议是:先跑通未压缩的完整模型,确认任务效果达标,再做压缩。不要一上来就压缩到极限,否则踩了坑还不知道是量化导致的精度下降还是模型本身就没训好。

7. 这类项目后续还能怎么扩展

说实话,1345张图的数据集,直接用来训练一个浮木分割模型绰绰有余,但这个场景往深了做还有很多延伸空间。

第一个方向是时序信息融合。河道浮木不是静止的,会漂移、旋转、聚集成群。利用无人机视频的多帧信息做时序分割,效果会比单帧明显更稳定。现在的数据集是单帧静态标注,如果要扩展,可以从同一航线的连续帧中抽取三到五帧,标注同一目标的运动轨迹,做成带跟踪标签的数据集。

第二个方向是和水位、流速传感器融合。模型识别出浮木后,结合当时的水文数据,可以预测浮木下一段时间的漂移轨迹。这对于防汛决策有很高的实用价值。这部分的思路是把分割结果转换成目标中心点坐标序列,再用卡尔曼滤波或者简单的线性外推做轨迹预测。

第三个方向是域自适应。这个数据集主要覆盖晴好天气的河道场景,到了雾天、雨天或者傍晚弱光环境,模型性能可能崩盘。利用域自适应或测试时增强,用少量目标域样本适应新环境,是后续值得探索的方向。

这次就先聊到这里。数据是拿来用的,不是拿来供着的。开源数据集最大的价值不在于下载的一瞬间,而在于你把它跑通、跑出结果、踩过该踩的坑之后,那份能够迁移到相似场景的经验。希望这篇拆解能帮你少走一点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询