☰
水域实例分割数据集:COCO转YOLO训练避坑指南
2026/9/28 2:07:58 网站建设 项目流程

简介:这是一份面向计算机视觉与遥感应用场景的水域实例分割数据集,覆盖河流、湖泊、海洋、湿地、池塘及其他水域六大类别,可用于训练YOLO系列模型,服务于水域监测、水体提取、洪水预警等任务。压缩包共2000个文件,主要包含1673个txt格式的分割标注文件(提供多边形掩码与类别标签)、325张高分辨率水域影像、1个yaml配置说明以及1份数据集介绍docx,总体积约103.31MB,结构简洁便于直接接入训练流程。图片采集自卫星、航拍与地面拍摄等多种场景,能帮助模型适应不同视角与光照条件。数据划分明确:训练集1340张、验证集333张,适合作为实例分割任务的训练与评估基准。目前已有179人学习下载,适合需要高质量水域标注数据进行算法验证或项目开发的开发者与研究人员。

1. 水域实例分割数据集:为什么要把“一片水”数成“一处处水”

如果你手里正握着这个“水域实例分割数据集.zip”,大概率不是来泛泛了解什么叫深度学习,而是有明确任务:从无人机正射影像里把河道、鱼塘、蓄洪区一块块提取出来,或者在一张含半个湖泊的地图上统计互不连通的水面数量。水域实例分割数据集要解决的就是这类问题——它不像语义分割那样把整张图里所有水体像素一次性涂成一个颜色,而是把每一处独立水域当成一个对象,输出各自的轮廓和类别,同一张图里的“水”可以被编号成 instance_1、instance_2。

这份 zip 对于做防汛应急、水利巡检、养殖水面确权或者城市内涝评估的人尤其有用。接下来的内容,我会按我拿到这类数据集后的真实顺序来写:先判断 zip 里到底是什么标注格式,再解开可能存在的伪加密和压缩陷阱,然后把标注转成 YOLO 实例分割能直接训练的格式,最后给出训练参数、踩坑记录和验证方法。跟着走一遍,你大概率能在一个晚上把这份数据集跑进自己训练管线里。

2. 先看格式再动手:水域实例分割数据集的三种标注形态

2.1 为什么这份数据集偏要用实例分割而不是语义分割

经常有人把实例分割和语义分割在“水”这上面搞混。语义分割的任务是逐像素分类,图里所有水都标成同一个 water 类别;而实例分割要求区分“哪一片水是哪一片水”。YOLO26 出来后,很多人误以为分割只是在原有检测模型上换了个 head,其实实例分割和语义分割的区别在于输出粒度:实例分割输出的是“对象掩码列表”,每个掩码带独立编号;语义分割输出的是“类别概率图”,同名类别只对比不分组。

落到水域上,区别很大。一片断桥两侧的水面,语义分割会把它们连成一个连通区,而实例分割要把它们记为两个实例,因为实际管理时它们分属不同鱼塘、不同闸口或者不同蓄滞洪单元。再比如洪水影像里水体被道路、堤坝切断,实例分割能告诉你“这张图里有 7 处渍水点”,语义分割只能告诉你有 7% 的像素是水。这个区别直接决定下游统计口径。所以我在用 YOLO 系模型做水域任务时,只要目标是数个数、算单块面积、按块追踪变化,一律选 segment 分支,不用仅做语义分割。

2.2 先解压再说:密码、伪加密和 CRC 校验

水域实例分割数据集.zip 最常见的第一个坑是解压报错。很多数据包用百度网盘或者自建站分发,打包前套了一层密码,更麻烦的是有些包被第三方转存时做了“伪加密”——它在压缩文件头里把加密标志位置为 1,实际数据段根本没有加密,你输什么密码都报错,或者某些解压工具直接让你填密码。先别急着骂数据集,用这个命令看一眼:

zipinfo -v water_seg.zip | head -n 80

输出里会列出每个文件的压缩方式、通用位标志和加密方式。真正的加密会显示Encryption: AES-256或Encryption: ZipCrypto;如果你看到Encryption: none,但解压时仍然要求输密码,那就是伪加密标志在作怪。也可以用 Python 直接看通用位字段:

import zipfile z = zipfile.ZipFile("water_seg.zip") for info in z.infolist()[:5]: flags = info.flag_bits is_encrypted = bool(flags & 0x1) print(f"{info.filename}: flag_bits={hex(flags)} 加密标志={is_encrypted}")

flag_bits 的 bit0 表示加密标志位,bit3 表示是否有数据描述符,bit11 表示文件名是否用了 UTF-8。如果这里 bit0 为 1,但用 010 Editor 或 HxD 打开 zip 后,对应本地文件头里找不到合法的密码头部,基本可以断定是伪加密。伪加密在 zip 格式里并不罕见,常见做法是数据商为了避免爬虫批量下载,手工改了标志位而不是真正加密。

我一般遇到这种包不会去研究数据段有没有真密码,而是直接把两个位置的加密位清掉,再重新打包:

def clear_encrypt_flag(src_path, dst_path): data = bytearray(open(src_path, "rb").read()) count = 0 i = 0 # PK\x03\x04 是本地文件头,PK\x01\x02 是中央目录头 while i < len(data) - 4: if data[i:i+4] == b"PK\x03\x04" or data[i:i+4] == b"PK\x01\x02": # 通用位字段在头部偏移 6 处,bit0 置 0 data[i+6] &= 0xFE count += 1 i += 4 else: i += 1 open(dst_path, "wb").write(data) print(f"清除了 {count} 个头的加密标志位") clear_encrypt_flag("water_seg.zip", "water_seg_fixed.zip")

这个脚本可靠的前提是原始 zip 确实没有真密码且数据段没有加密;如果真加密,这样改完会发现解压出来的文件 CRC 校验失败,因为压缩数据段本身就带了密码头的处理。另一点要注意的是,伪加密清完还需要看 CRC 是否通过,流式解压工具会忽略 CRC 直接产出损坏文件,所以解压后一定要校验文件大小和内容。我做这一步的一部分原因是为了让人能读自己合法获得的数据集,而不是用来绕过别人的付费机制,边界自己把握。

2.3 解压后的目录结构:images、masks、annotations 三类文件要点

清除这些障碍后,数据集解压出来通常是三类内容:原始图像目录、掩码目录、标注文件。我拆过不少这样的包,最常见的组织方式:一个images目录放着 RGB 影像,按train、val划分;一个annotations目录放着 COCO 格式的 JSON;掩码则要么是独立的 PNG 单通道文件,要么直接内嵌在 JSON 的 segmentation 字段里。如果这份数据集源头是遥感,还可能带一个geoinfo目录,记录每张影像对应的地理坐标和投影参数,训练用不上,但做成果导出时能找回真实位置。

先搞清楚标注藏在哪儿,再决定怎么处理。我拿到任何实例分割数据集的第一件事,是打开一个 JSON 看它的结构。这里引用最通用的 COCO2017 数据集结构作为参照:JSON 顶层必须有images、annotations、categories三组;images里记录id、file_name、width、height;annotations里每个实例带image_id、category_id、segmentation、area、bbox、iscrowd。水域数据最大的特点是category_id多半只有一个 water 类别,但也有混入“堤岸”“桥梁”“漂浮物”的多类变体;多类变体比单类难处理得多,因为掩码边缘一个像素的错位就会导致类别标签抖动。

掩码的表现形式也决定后续工作量。COCO 格式里,segmentation字段可以是一个多边形点阵的列表,也可以是一个 RLE 编码的字典。多边形适合人工标注的水域边界,干净且文件小;RLE 适合遥感软件自动生成的掩码,边界细碎但完整。下载下来的数据如果只有一张张 PNG 掩码而不是 JSON,那我建议先把它当语义分割数据验一下,再看每个掩码里连通域数量,因为同一个水体在影像里被道路切断会形成多个连通区,这是否需要拆成多个实例,取决于原数据作者的标准。这个判断直接影响后面转换出来的 YOLO 标签质量,后面避坑章节会专门讲。

2.4 用 pycocotools 先做个体检:实例数量、面积分布和坏标注

在动手转换格式前,先用 pycocotools 把这份水域实例分割数据集的统计信息拉出来,这一步能避免后面反复返工。很简单的四五行代码,就能得到每个类别的实例数、面积中位数和最大最小范围:

from pycocotools.coco import COCO import numpy as np coco = COCO("annotations/instances_train.json") cat_ids = coco.getCatIds() for cid in cat_ids: anns = coco.loadAnns(coco.getAnnIds(catIds=cid)) areas = np.array([a["area"] for a in anns if a["iscrowd"] == 0]) if len(areas) == 0: print(f"类别 {cid}: 无有效实例") continue print(f"类别 {cid}: {len(areas)} 个实例, " f"面积中位 {np.median(areas):.0f} px, " f"最大 {areas.max():.0f} px, 最小 {areas.min():.0f} px")

注意iscrowd字段。COCO 里iscrowd=1表示该标注是一整片密集对象,不参与实例分割指标计算;很多水域数据商把无法逐块标注的连片水面全标成iscrowd=1,如果你没过滤就直接转 YOLO 标签,会发现这些大块水体全部变成训练负样本,最后模型学出来只认小水塘不认湖泊。面积分布也要看中位数而不是平均值,水域实例里极少数超大水面会把平均值抬高,让你误以为所有目标都很大,从而把imgsz调得过大。

如果这一步发现 JSON 中所有掩码面积都是 0 或者 bbox 越界,说明文件在传输或伪加密修改过程中损坏了。这种情况下不用急着删数据重下,先检查images里有没有和 JSON 对不上的文件名,常见原因是某些软件导出 JSON 时把.tif写成了.jpg。把验证集和训练集都跑一遍同样的体检,能过滤掉后面训练时 90% 的“莫名奇妙不收敛”问题。

3. 把 COCO 标注转成 YOLO 分割格式:坐标系、顶点上限和贴合边界

3.1 先说清 YOLO 分割标注的存放约定:一个实例一行文本

YOLO 实例分割的训练标签不是 JSON,而是每张图对应一个同名.txt文件,放在labels/train和labels/val目录下。文本每行表示一个实例,格式为“类别id 顶点x1 顶点y1 顶点x2 顶点y2 ...”,坐标必须归一化到 0~1,也就是实际像素坐标除以图片的宽度或高度。水域数据通常只有一个 water 类,那每行开头都是 0;如果包含建筑、桥梁、船等多类,就按它们在classes.yaml里的索引编号。

目录结构我推荐这样组织,和 ultralytics 的习惯一致:

water_seg/ images/train/*.jpg images/val/*.jpg labels/train/*.txt labels/val/*.txt classes.yaml

标签文件必须与图像文件同名,后缀不同。比如img_00012.jpg对应img_00012.txt。这个约定新手经常记反,把标签文件名写成img_00012.jpg.txt,训练时 YOLO 会静默跳过这些样本,表现为训练集 loss 始终很小但验证集精度上不去,因为有效训练样本少了。

3.2 转换脚本:多边形和 RLE 掩码统一处理

COCO JSON 里segmentation是列表时直接取出多边形坐标;如果是 RLE 字典,需要先解码成二值掩码,再提取轮廓。下面这段转换代码,足以应付大部分水域实例分割数据集:

import json import numpy as np import cv2 from pathlib import Path from pycocotools import mask as mask_utils def mask_to_polygon(segmentation, img_w, img_h, epsilon=2.0): """把 COCO segmentation 转成归一化多边形点列表""" if isinstance(segmentation, list): # 多边形格式,直接拿第一组点 coords = np.array(segmentation[0], dtype=np.float32).reshape(-1, 2) else: # RLE 格式:解码成掩码再提取最大外轮廓 m = mask_utils.decode(segmentation) if m.ndim == 3: m = m[:, :, 0] m = (m > 0).astype(np.uint8) contours, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None coords = max(contours, key=cv2.contourArea).reshape(-1, 2).astype(np.float32) # Douglas-Peucker 简化,减少顶点数量 approx = cv2.approxPolyDP(coords.astype(np.float32), epsilon, True) if len(approx) < 3: return None approx[:, 0] /= img_w approx[:, 1] /= img_h return approx.reshape(-1).tolist() def convert_coco_to_yolo(json_path, labels_out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_map = {img["id"]: img for img in data["images"]} Path(labels_out_dir).mkdir(parents=True, exist_ok=True) for ann in data["annotations"]: if ann["iscrowd"] == 1: continue img = img_map[ann["image_id"]] poly = mask_to_polygon(ann["segmentation"], img["width"], img["height"]) if poly is None: continue txt_name = Path(img["file_name"]).stem + ".txt" line = f"{ann['category_id']} " + " ".join(f"{v:.6f}" for v in poly) with open(Path(labels_out_dir) / txt_name, "a", encoding="utf-8") as f: f.write(line + "\n") convert_coco_to_yolo("annotations/instances_train.json", "labels/train")

逻辑说明:先把 RLE 解码成二值图,再用cv2.findContours提取外轮廓;RETR_EXTERNAL只取最外层轮廓,避免水体中间有岛时把洞的轮廓也当新实例。epsilon=2.0是轮廓简化阈值,单位是像素,阈值越大顶点越少、边界越粗糙。水域岸线细节很重要,我的经验是 epsilon 不要超过 3,否则细长的河汊会被磨平。多边形简化后过滤掉顶点数少于 3 的残渣,防止后面 YOLO 解析时崩溃。

3.3 归一化、越界裁剪和最小面积过滤:三个必调参数

转换时最容易翻车的不是格式本身,而是边界条件。第一,坐标归一化一定要把 x 除以原图宽度、y 除以原图高度,不能图省事统一除以最大边长。水域影像常见长宽比失衡的航测图,比如 8000×6000 的大图裁出来的瓦片,除以统一值会让 x 和 y 的还原比例不一致,掩码在训练时被拉伸变形。第二,有些标注多边形的顶点正好压在图像边缘,四舍五入后可能落进 1.0 之外,YOLO 训练时会警告坐标越界。对这种情况我通常直接对坐标做np.clip(poly, 0.0, 1.0),并把裁剪后重复的相邻顶点删掉。

还有一个必须做的过滤:最小面积。COCO 转换时全图 50% 都是水的超大实例要保留,但只有几个像素的碎块要丢掉。因为 YOLO 的 mask 分支在imgsz=640下会把小目标缩成一个点,那些面积不足 20 像素的实例不仅学不到岸线特征,还严重拖慢训练时的正样本匹配。我在实践里设了两个规则:原始面积小于 36 像素的实例直接跳过;简化后坐标范围最大跨度小于 1.5 个像素的也跳过。这些阈值写在转换脚本里,以后换一份数据集也不用改代码,只调常量。

大图遥感数据还有另一个特有问题,就是分块裁剪。文件夹里放着的可能不是完整大图,而是按网格切的瓦片,每块瓦片存成独立图片。如果一份瓦片恰好把一个大湖裁成四块,湖本体在这四张图上重复出现,而这四张图又被同时划进训练集和验证集,就会造成数据集泄漏。我做训练集划分时,会检查所有图片文件名是否带行列号前缀,如果有,就按网格而不是按文件随机划分。网格划分这个思路,和有人拿 mmrotate 训练 DOTA 数据集时遇到的问题一模一样:遥感切片数据最怕同源瓦片既在训练又在验证,评估结果虚高得离谱。

4. 用 YOLO 实例分割训练水域数据集:最小命令和关键参数

4.1 最小示范:装好 ultralytics,第一条训练命令现在就跑

如果你的环境是干净的,直接安装依赖并启动训练。普通单卡 8GB 显存也能跑通,只是慢一点:

pip install ultralytics yolo segment train \ data=water_seg/classes.yaml \ model=yolov8n-seg.pt \ epochs=200 \ imgsz=640 \ batch=16 \ device=0

集成在 ultralytics 里的 YOLO 实例分割训练命令就是yolo segment train,注意不是yolo train,也不是yolo detect train;检测和分割的模型、标签格式、损失函数、预测输出都不同。data必须指向 YAML,里面写好训练集和验证集绝对路径或项目路径。classes.yaml 我这样写,类别从 0 开始顺序编号:

path: /path/to/water_seg train: images/train val: images/val names: 0: water

如果你的标注里还有“岸线”“桥墩”等类别,name 逐行往下加即可,但必须和转换脚本写入的category_id一致。model=yolov8n-seg.pt表示从预训练分割权重开始微调;换成yolov8s-seg.pt或更大的版本能提升边界精度,但显存占用和训练时长线性增加。我一般在验证数据量只有几百张时先用 nano 跑通管道,确认能收敛后再上 s 或 m 模型节省时间。

4.2 水域任务要重点调的六项训练参数:从方案到调参参考

训练命令跑通只是第一步,水域实例分割的难点在于背景复杂、目标边界弱、同一幅图里水体面积跨度大。下面这组参数是我在多个水域数据集上试出来的调节方向,适合大多数单波段 RGB 影像,不是唯一解:

参数默认值水域任务参考原因
imgsz640640~960河道和岸线细长,过小分辨率会磨平边界
batch16按显存调 8~16mask 分支计算量比检测大,batch 高不代表收敛快
hsv_h0.0150.005水域颜色单调,过强色相增强会让水体改色
hsv_s0.70.3防止把黄色泥沙水增强成红色水体
fliplr0.50.0~0.3左右翻转会颠倒水流方向和倒影特征
mosaic1.00.5大水体被四图拼接切碎,反而破坏整体结构

参数说明:imgsz影响最大。水面是典型的大而空目标,太大显存扛不住,太小岸线纹理丢失。我常用瓦片尺寸是 1024 但训练时缩到 768,既保留细长水体又避免 OOM。mosaic增强是 YOLO 训练精度的关键,但水域图像特殊:四张图随机拼贴后,一块完整大湖会被切断,同一个实例的掩码碎在拼图边缘,模型被迫学“破碎的水面”。把它从默认 1.0 降到 0.5,可以让模型更多看到完整实例。还有一点,YOLO26 这类新版本默认会在最后 10 个 epoch 关闭 mosaic,这个行为是刻意的,不要手动改成全程开启,否则验证阶段会出现大量碎块预测。

4.3 训练中看什么:loss 曲线和类别损失的阅读姿势

训练启动后在终端输入yolo segment train ...同目录下打开runs/segment/train/,用 tensorboard 或者直接看终端输出。分割训练一共输出四类 loss:box_loss是检测框损失,seg_loss是掩码损失,cls_loss是类别损失,dfl_loss是边界框分布损失。对水域这个单一类别任务,最值得盯的是seg_loss,它是直接决定掩码质量的部分。

如果前 50 个 epoch 里seg_loss从 1 掉到 0.1 左右,训练基本正常。但有一个常见假象:box_loss下降很快而seg_loss缓慢,说明模型先找到了“水体可能在哪”,却还没学会贴着岸线细抠边界,这是水体边界与背景像素可分性差导致的,正常,加 epoch 或换更大模型都能解决。另一个极端,seg_loss快速掉到 0.01 以下、验证集 mAP 0.5 却很低,通常是过拟合,需要减少 epoch 或者添加 dropout、提高 lr 衰减。水域任务不比检测,漏掉一个小河汊不影响 loss 但严重影响下游面积统计,所以不必追求 train loss 无限低,优先看验证集掩码贴得是否准。

4.4 半监督标注:用现成权重预标注,快速扩充自己的水域数据

如果你手上的水域实例分割数据集.zip 只覆盖了河流湖泊,而你要识别的是鱼塘或排水渠,直接微调可能不够。这时候常见的做法是先用自己的少量标注训练一个粗糙模型,再让它去预标注未标注影像,人工只修正错漏。我在水域项目里会用训练好的yolov8s-seg.pt批量预测新影像,然后写一个脚本把高置信度的预测结果写回 COCO 格式,再合并进原数据集。

这个链路里最有用的命令是预测加导出:

yolo segment predict \ model=runs/segment/train/weights/best.pt \ source=raw_unlabeled/ \ save_txt=True \ save_conf=True \ conf=0.6

save_txt=True会把预测掩码写成 YOLO 格式的标签,save_conf=True在每个标签行末尾追加置信度,方便你后续按 0.9、0.7 分档筛选。半自动标注只做两步:高置信度样本直接进训练集,低置信度样本排队人工重画。实际经验告诉我,水域掩码的难点集中在“水陆过渡带”——有泥沙、倒影、水草的区域,AI 预标注在这些地方会频繁出错,所以人工修正时不需要全图检查,只看岸线附近的预测掩码是否越界。

5. 水域实例分割数据集的避坑记录:解压、转换、训练三条路上的翻车点

5.1 解压时报“密码错误”,密码移除后仍然报错——伪加密在捣鬼

现象:用解压工具打开水域实例分割数据集.zip,要求输入密码;从说明文档里找到密码填进去,依然提示文件头损坏或密码错误。更奇怪的是某些文件能解开、某些文件解到一半中断。

原因:这是很典型的 zip 伪加密。数据商把文件头的通用位加密标志位改成了 1,但数据本身没有用任何密钥加密。解压工具看到加密标志就进入解密流程,去读一个不存在的密码头部,自然全盘报错。

解决:先用 2.2 节里的 Python 脚本读flag_bits,如果 bit0 为 1,用十六进制工具查看对应本地文件头的加密头部是否存在;确认是伪加密后,执行清除加密位的脚本,再重新解压。要注意脚本执行后必须逐文件对比 CRC 和文件大小。真加密的数据段带密码头部,伪加密没有,如果改完标志位后解压出来一堆文件损坏,那就不是伪加密,而是数据本身损坏或传输中丢包。

5.2 掩码只有黑白两色,连通域处理成一片——语义分割标注重了实例分割的牌

现象:数据集里提供的是 PNG 掩码,打开黑底白图,原本在影像里被桥梁分割开的两个相邻水塘,在掩码里是同一个白色连通区。拿它做实例分割训练,模型永远学不会“桥左桥右是两个实例”。

原因:这份数据集可能是从语义分割项目转卖或派生出来的,作者用连通域分析把语义掩码自动拆成实例,却忽略了中间有 1 像素的连接缝隙或桥下阴影连通。自动连通域拆出的实例和人工判定不一致,桥下阴影会黏连两片水。

解决:如果必须基于这份掩码训练,先做形态学开运算切断细连接,再用cv2.connectedComponents重新编号每个独立实例,最后核对拆出的边界是否合理。如果目标是防洪调度级别的高精度成果,这一步不要全自动,抽 20% 样本人工校验两张图:原始影像和实例编号图叠着看。顺便说一句,这也能解释为什么“实例分割数据集”和“语义分割数据集”不能互相混用,标注口径在源头就不同。

5.3 生成的 YOLO 标签坐标出现大于 1 的数值,训练报警告

现象:转换完标签后,训练日志里出现WARNING: normalize coord out of range,部分标签行里坐标值等于 1.02、1.30 之类。

原因:标注多边形有顶点贴图边缘,归一化时四舍五入或者轮廓提取时cv2.approxPolyDP生成了超出原图边界的点。航测瓦片数据里尤其常见,因为岸边会有少量像素被裁掉一半。

解决:转换脚本里对所有坐标做clip到 0~1,然后把裁剪后相邻重复点删掉。还有一种隐蔽情况:某个实例的掩码面积很小,简化轮廓时因为 ε 太大产生了自交多边形,坐标虽然都在 0~1 但轮廓线来回折叠。对这种对象直接过滤掉,不要试图修正它的顶点顺序。

5.4 训练完效果挺好的,推理时水面却碎成几块——桥墩和阴影拆散了实例

现象:训练集里明明有完整的大湖面,验证集结果却把湖预测成几十个碎块,每一块面积很小、互相之间隔着桥墩、船影或堤坝。

原因:训练标注本身把每个可见切片当成了独立实例,比如同一片湖被桥墩分成三块,标注者标了 3 个实例。模型学到了“凡是桥墩遮挡处,水体就断开”,推理时遇到阴影也会触发同样的断连逻辑。这不是训练 bug,是标注口径与你的业务口径不一致。

解决:先想清楚你的下游任务是“按自然连通域计数”还是“按行政管理单元统计”。如果是前者,训练前把同一管理单元内因障碍物断裂的水体合并成一个实例;如果是后者,保持现状即可。我的经验是在标注说明里明确写一行“桥下水面视为同一实例,船舶阴影处不要断开”,并配 3 张示例标注图,能让不同标注员的认知统一起来。这块的坑在于“同一片水”的判断依赖语义理解,没法靠后处理完全修复。

5.5 训练曲线正常,但推理掩码粗糙如锯齿——边界细节的退化

现象:seg_loss一直下降,验证集 IoU 也不差,但放大推理结果,岸线边缘呈明显锯齿,有的地方还多出一圈 2~3 像素的假阳性水边。

原因:常见于三处叠加:转换脚本里eps=2.0甚至更大的轮廓简化过度;原图标注就存在多边形抽稀;训练时imgsz=640下岸线的细节被下采样丢失。

解决:把转换脚本eps降到 1.0 重新生成标签;训练用imgsz=960或 1024;推理时不直接输出 mask 而是做一次 3×3 形态学闭运算。前两个是治本,最后一个是救急。水域不像车辆行人那样有刚硬边界,水陆交界是连续过渡的,标注权威性很难有一个像素级的 ground truth,所以细节退化的容忍阈值要看下游,如果用于面积统计,锯齿带来的误差通常小于 1%,可接受;如果做红线执法,那必须回到人工精修标注集。

6. 效果真不真:用水域掩码的 IoU、岸线误差和可视化做最后一道验证

训练完成后,不要只看验证集 mAP,水域任务的分数漂亮不代表岸线位置准。我习惯了自家流程里的三道验证:第一道是标准 COCO 评估,第二道是单独算岸线边界误差,第三道是把预测掩码叠到原图上肉眼抽查。第一道可以用 ultralytics 自带的验证命令跑:

yolo segment val \ model=runs/segment/train/weights/best.pt \ data=water_seg/classes.yaml \ iou=0.5

默认输出 mAP50 和 mAP50-95,水域任务里我更看重 mAP50-95,因为它对掩码重叠度更敏感,能暴露“位置对了但边界差几像素”的问题。第二道岸线误差,我单独写一个小脚本,思路是分别提取预测掩码和真实掩码的边界像素,计算边界点之间的平均最近距离:

import cv2 import numpy as np from scipy.ndimage import binary_erosion def boundary_distance(gt_mask, pred_mask): gt_edge = gt_mask & ~binary_erosion(gt_mask) pred_edge = pred_mask & ~binary_erosion(pred_mask) gt_pts = np.argwhere(gt_edge) pred_pts = np.argwhere(pred_edge) if len(gt_pts) == 0 or len(pred_pts) == 0: return None # 对预测边界每个点,找最近的真实边界点距离 from scipy.spatial import cKDTree tree = cKDTree(gt_pts) dist, _ = tree.query(pred_pts) return dist.mean(), dist.max() mean_d, max_d = boundary_distance(gt, pred) print(f"岸线平均偏移 {mean_d:.2f} px,最大偏移 {max_d:.2f} px")

这段代码里binary_erosion让掩码向内收缩一圈,原掩码减掉收缩结果就是边界像素;然后用 KDTree 算预测边界到真实边界的最短距离。平均偏移小于 3 像素可以放心用于大量统计;超过 10 像素说明岸线整体偏移,通常不是训练问题,而是训练标签本身岸线标粗了,我遇到过一次,是把掩码从 RLE 转多边形再转回 RLE 时连续两次重采样造成的系统性漂移,重转标签后才恢复。

第三道永远不能省:把验证集里最好和最差的各挑十张图,预测掩码半透明叠加到原图上,盯着桥下阴影、干湿交界、泥沙地三处看。有一次我发现模型把岸上的灰色防渗膜全部识别成水,mAP 却还很高,因为验证集里防渗膜图太少,统计数据被淹没了。从那次以后,我每次训练完都会先干这一件事,再决定是否要调节训练集或 loss 权重,而不是直接买单 mAP 数字。

希望这份水域实例分割数据集的拆解和训练流程,能帮你在自己的水域任务里少走几步弯路;下次再拿到一份 .zip,先别急着解压,按格式、真伪加密、标注口径、转换、参数、验证这六步走,结果一般都不会太差。

本文还有配套的精品资源,点击获取

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

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

立即咨询