恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
水下目标语义分割数据集工程实践:掩码格式、预处理与避坑指南
首页
资讯中心
/
水下目标语义分割数据集工程实践:掩码格式、预处理与避坑指南
水下目标语义分割数据集工程实践:掩码格式、预处理与避坑指南
发布时间:2026/10/11 2:01:48
简介一份面向水下目标识别与语义分割任务的数据集适合计算机视觉研究者与算法学习者用于模型训练与评估。数据源自水下场景图像统一为 640×480 分辨率分割前景包含人类、海草、珊瑚、岩石、鱼类等 8 类目标背景以 0 标记掩膜采用调色后的彩色图像便于直观检查。数据集已划分训练集与测试集训练集共 1525 张原始图像及对应掩膜测试集共 110 张原始图像及对应掩膜并预先做了随机旋转等预处理以增强数据多样性。资源包共 2000 个文件以 bmp 掩膜和 jpg 原图为主另含 1 个 Python 可视化脚本整体约 159.07MB7z 压缩包。附带的可视化脚本可随机抽取一张样本将原始图像、真值掩膜和叠加蒙版效果并列展示并保存便于快速确认标注质量与分割效果。目前已有 1081 人学习下载适合需要现成水下分割数据集开展实验、对比算法或完成课程设计的开发者直接使用。1. 图像分割数据集水下目标 8 分割为什么比想象中难做做水下机器人或渔业资源调查的人刚看到「图像分割数据集水下目标图像语义分割8分割」这个标题第一反应多半是不就是把画面里的鱼、珊瑚、海草按像素分个类嘛。真正把数据下下来才发现坑全在看不见的地方类别标签是 RGB 还是索引模式、8 个类别的像素占比悬殊到什么程度、可视化代码能不能把掩码跟原图叠在一起当质检工具。这个数据集要解决的其实是「拿到一份带像素级标注的水下影像后怎么安全地把它变成语义分割模型的训练数据」。适合刚起步的语义分割团队、想做海洋环境监测或养殖自动化的工程人员也适合从 VOC 转过来想试试水下场景的老手。2. 语义分割数据集的标签格式8 类掩码读进来是三维彩色图还是二维索引图2.1 images / masks 双目录先数一遍文件再谈训练这类数据集最常见的组织方式是双目录images/放原始水下影像masks/或labels/放同名掩码 PNG。文件命名通常带拍摄场景前缀比如reef_001.jpg和reef_001.png目的是让你能通过文件名把图像和标签一一对应。拿到手第一件事不是急着写加载器而是先确认两边的文件数量完全一致、文件名能对上这一步能省掉后面大半的莫名其妙报错。我一般会先把目录里的文件名抽出来做一次校验顺便统计每个场景序列有多少张图。这个动作对后续按序列划分训练集、验证集非常关键import os, glob image_files sorted(glob.glob(images/*.jpg)) mask_files sorted(glob.glob(masks/*.png)) # 文件名取主名去掉扩展名 img_names [os.path.splitext(os.path.basename(p))[0] for p in image_files] mask_names [os.path.splitext(os.path.basename(p))[0] for p in mask_files] # 两边不一致时立刻报警而不是等到训练时报错 assert img_names mask_names, fimage/mask 数量不一致: {len(img_names)} vs {len(mask_names)}这段脚本的核心是断言很多数据集在打包时会把个别掩码漏掉或重复命名glob排序后的列表比对能直接暴露问题。命名规则上我习惯用os.path.splitext去掉扩展名再比较因为 JPG 和 PNG 的扩展名本来就不同直接比文件名字符串一定会误报。2.2 类别标签表常见 8 类水下目标的像素映射与统计脚本8 分割指的是像素级标注共 8 个语义类别。这套数据集的常见做法是让 ID 0 表示水体背景ID 1 到 7 表示七个目标类别具体类别名称不同项目有差异最常见的是岩石、珊瑚、海草、鱼、海星、螃蟹和「其他生物」。需要注意作者导出标注时用的可能是 RGB 三通道彩色掩码也可能是带调色板的索引模式 PNG这两种格式在读取方式上完全不同。class_id常见 RGB 取值类别含义0(0, 0, 0)水体背景1(128, 128, 128)岩石 / 礁石2(255, 0, 0)珊瑚3(0, 255, 0)海草 / 藻类4(0, 0, 255)鱼5(255, 255, 0)海星6(255, 0, 255)螃蟹 / 甲壳类7(0, 255, 255)其他水下目标这张表不是凭空猜的而是这类数据集的通用惯例背景用全黑目标类别用饱和色方便标注工具里的肉眼检查。但每个数据集的实际 RGB 取值都可能调换所以拿到手后不要照搬而是应该先写脚本把掩码里的所有颜色值抄出来跟类别清单核对一遍再训练。统计每类像素占比是训练前必须做的一步它能直接告诉你类别不均衡有多严重import numpy as np from PIL import Image def load_mask_as_id(mask_path, rgb_to_id): mask Image.open(mask_path) arr np.array(mask) # RGB 三通道彩色掩码需要查表映射成 ID if mask.mode RGB: h, w, _ arr.shape flat arr.reshape(-1, 3) ids np.zeros(h * w, dtypenp.uint8) for class_id, rgb in rgb_to_id.items(): ids[np.all(flat rgb, axis1)] class_id arr ids.reshape(h, w) # P 模式掩码本身就是索引直接可用 return arr rgb_to_id { 0: (0, 0, 0), 1: (128, 128, 128), 2: (255, 0, 0), 3: (0, 255, 0), 4: (0, 0, 255), 5: (255, 255, 0), 6: (255, 0, 255), 7: (0, 255, 255), } arr load_mask_as_id(masks/reef_001.png, rgb_to_id) values, counts np.unique(arr, return_countsTrue) for v, c in zip(values, counts): print(fclass {v}: {c / arr.size * 100:.2f}%)这段脚本解决了两个常见问题一是自动判断掩码是 RGB 还是 P 模式二是把 RGB 颜色逐像素映射成类别 ID。参数rgb_to_id的 key 是类别 IDvalue 是标注工具里对应的颜色映射时用向量化的np.all(flat rgb, axis1)做批量匹配比逐像素 for 循环快一个数量级。如果你发现统计结果里出现不在映射表里的颜色说明标注工具导出时用了抗锯齿或者调色板错位这类样本要直接挑出来清理。2.3 P 模式与 RGB 模式索引化的掩码才适合做语义分割训练这里单独把小节留出来是因为模式问题几乎每个新手都会踩一次。很多图像处理库读 PNG 时默认展开成三通道 RGB于是掩码变成了一张「看起来和原图一样的彩色图」。但语义分割训练要求的标签是二维索引图每个像素位置的值只能是 0 到 7 的整数代表类别 ID而不是 RGB 三元组。P 模式调色板模式的 PNG 本身就是索引图PIL 里一个convert(P)就能拿到单通道数组。RGB 模式则必须先查表映射。混淆这两者最典型的翻车现场是训练时把三通道 RGB 掩码直接当标签损失函数里num_classes8于是网络输出的 8 通道和标签的 3 通道对不上维度报错还算好的更隐蔽的是某些框架自动忽略多出的通道导致模型学了个寂寞。判断掩码格式不用猜一行代码就能看from PIL import Image mask Image.open(masks/reef_001.png) print(mask.mode) # 常见输出: P / RGB如果是P直接np.array(mask)得到的就是 H×W 的整数索引数组如果是RGB就必须用 2.2 节的映射脚本转一遍。另外一个细节是调色板模式下np.array(mask)取出的是调色板索引不是调色板里的 RGB 值这一点和Image.open后convert(RGB)的行为完全不同。我自己的习惯是给训练流程单独写一个mask_loader统一把任何模式的掩码都转成 int64 的索引数组后面接进模型就不再关心原始存储格式。3. 预处理水下图像尺寸统一、颜色校正与数据划分的三个关键参数3.1 图像 resize 用双线性、掩码 resize 必须用最近邻训练语义分割模型前要统一输入尺寸这是常规操作。但同一个resize操作用在图像和掩码上插值算法必须分开图像用双线性BILINEAR掩码用最近邻NEAREST。原因很好理解双线性插值会按周围四个像素加权生成中间值图像上这叫平滑掩码上这就成了「幽灵类别」——比如物体边缘的像素本来是类别 2双线性插值后变成 1.75四舍五入后类别完全错乱。如果是用 PyTorch 的torchvision.transforms记得掩码的插值参数单独传from PIL import Image import torchvision.transforms as T import torchvision.transforms.functional as TF def resize_pair(image, mask, size(512, 512)): # 图像用双线性保持边缘平滑 image TF.resize(image, size, interpolationT.InterpolationMode.BILINEAR) # 掩码用最近邻类别索引不会被插值污染 mask TF.resize(mask, size, interpolationT.InterpolationMode.NEAREST) return image, mask img Image.open(images/reef_001.jpg).convert(RGB) mask Image.open(masks/reef_001.png).convert(P) img, mask resize_pair(img, mask, size(512, 512))InterpolationMode.NEAREST是这段代码的关键参数它不做任何加权平均直接在原图中取最近的像素值所以掩码里的类别 ID 只会保持原值不会冒出新的中间类别。size(512, 512)是常见默认配置如果你的显存有限可以降到 384但要注意数据集里的小目标比如远处的小鱼在低分辨率下可能只剩几个像素损失函数会直接忽略它们。3.2 水下偏色怎么校正白平衡先做还是交给网络学水下图像普遍偏蓝绿色这是因为水对红光的吸收远强于蓝绿光。这个物理特性会在数据里留下非常明显的偏色痕迹10 米深拍摄的图和 30 米深拍摄的图颜色差异巨大甚至同一段视频里不同帧的白平衡都不一样。很多从 ImageNet 预训练模型迁移过来的人会直接用 ImageNet 的均值方差做归一化结果发现模型在前几个 epoch 损失下降很慢就是因为输入分布和预训练分布偏移太大。常见的做法是先做颜色校正再做归一化。最简单有效的校正是灰度世界假设白平衡假设整幅图像的 R、G、B 三个通道的平均值应该相等然后按比例缩放通道。配合torchvision的归一化参数使用import numpy as np from PIL import Image def gray_world_white_balance(img_np): # img_np: float32 数组shape (H, W, 3)取值范围 0~1 mean_rgb img_np.mean(axis(0, 1), keepdimsTrue) # 灰度世界假设三通道均值应相等取整体均值做基准 target mean_rgb.mean() gain target / (mean_rgb 1e-6) return np.clip(img_np * gain, 0.0, 1.0)这段代码的物理含义是给每个通道算一个增益系数蓝绿色通道均值高增益小于 1会被压下去红色通道均值低增益大于 1会被提亮。参数1e-6是防止某个通道均值为 0 时除零。做完白平衡再套 ImageNet 的标准化输入分布和预训练模型就贴近多了。需要注意白平衡会改变颜色分布如果你的验证集里个别图像严重过曝校完反而放大噪声这种情况可以给增益设置上下限比如限制在 0.5 到 2.0 之间。3.3 按拍摄序列划分三折避免场景泄漏数据划分是这类数据集里最容易被忽视的环节。水下影像通常按拍摄序列组织同一段视频里相邻帧的内容高度相似——同一个珊瑚礁从稍微不同的角度拍了几十张。如果随机打乱后划分训练集和验证集同一个场景的相似帧会同时出现在两边验证集指标虚高模型的实际泛化能力被严重高估。这在遥感图像语义分割里同样是经典问题只是水下数据集里大家更容易忽略。我一般会按文件名前缀的场景 ID 分组而不是按单张图随机划分import random from collections import defaultdict # 假设文件名是 scene_001_003.jpg中间段是场景 ID def extract_scene_id(filename): return os.path.basename(filename).split(_)[0] scene_to_files defaultdict(list) for img_path in sorted(glob.glob(images/*.jpg)): scene_to_files[extract_scene_id(img_path)].append(img_path) scenes sorted(scene_to_files.keys()) random.Random(42).shuffle(scenes) # 固定种子保证可复现 train_scenes scenes[:int(len(scenes) * 0.7)] val_scenes scenes[int(len(scenes) * 0.7):int(len(scenes) * 0.85)] test_scenes scenes[int(len(scenes) * 0.85):]这段脚本把划分单位从「张」提升到「场景」random.Random(42)固定随机种子保证每次运行结果一致shuffle只打乱场景列表不动场景内部的帧顺序。比例 70/15/15 是常见配置如果你的场景数量少比如不足 20 个可以改成 60/20/20保证验证集至少覆盖 4 个以上独立场景。4. 接进语义分割模型Dataset、Dice Loss 与每类 IoU 的工程写法4.1 自定义 Dataset掩码用 convert(P) 读取而不是转 RGB把数据集接进语义分割模型核心工作是写一个 PyTorchDataset类。最容易出错的点是掩码读取方式必须用convert(P)拿到索引模式而不是convert(RGB)。原因在 2.3 节讲过这里落实到代码上import glob import numpy as np from PIL import Image from torch.utils.data import Dataset class UnderwaterSegDataset(Dataset): def __init__(self, image_dir, mask_dir, size(512, 512)): self.image_paths sorted(glob.glob(f{image_dir}/*.jpg)) self.mask_paths sorted(glob.glob(f{mask_dir}/*.png)) assert len(self.image_paths) len(self.mask_paths), 图片和掩码数量不一致 self.size size def __getitem__(self, idx): image Image.open(self.image_paths[idx]).convert(RGB) mask Image.open(self.mask_paths[idx]).convert(P) # 关键保留索引 if self.size is not None: image image.resize(self.size, Image.BILINEAR) mask mask.resize(self.size, Image.NEAREST) # 掩码不可双线性 image np.array(image).astype(np.float32) / 255.0 mask np.array(mask).astype(np.int64) # 转成 CHW 便于 PyTorch 使用 image image.transpose(2, 0, 1) return image, mask这段代码有四个关键参数convert(P)保证掩码是二维索引数组resize的插值方式一图一掩码分开指定np.int64是交叉熵损失对标签类型的要求用uint8在某些损失函数实现里会报类型错/ 255.0把图像归一化到 0~1后续标准化处理放在训练脚本里做。如果你的掩码里有未标注区域通常 ID 255在__getitem__里可以直接把它替换成 0 或某个忽略索引否则np.unique会多出一个训练时根本不存在的类别。4.2 长尾类别不均衡交叉熵为主、Dice 兜底的组合损失水下数据集的类别分布几乎一定是长尾的水体背景可能占 60% 以上像素鱼、海星这类目标占比极小。如果你直接用交叉熵模型会倾向于把所有像素都预测成背景因为这样损失已经很低了。这个场景下只调权重是不够的更稳妥的做法是交叉熵配合 Dice Loss 一起用。Dice Loss 对类别不均衡天然不敏感因为它直接度量预测区域和标注区域的重叠度不看绝对像素数量。多分类版本实现如下import torch import torch.nn.functional as F def multiclass_dice_loss(pred_logits, mask, eps1e-6): # pred_logits: (N, C, H, W)mask: (N, H, W)C 8 pred F.softmax(pred_logits, dim1) num_classes pred.shape[1] # 展平成 (N*H*W, C)方便按类计算 pred pred.permute(0, 2, 3, 1).reshape(-1, num_classes) mask mask.reshape(-1) mask_onehot F.one_hot(mask, num_classesnum_classes).float() intersection (pred * mask_onehot).sum(0) cardinality pred.sum(0) mask_onehot.sum(0) dice (2 * intersection eps) / (cardinality eps) return 1 - dice.mean()这里的eps1e-6是平滑项防止某类在样本中完全不存在时除零F.one_hot把索引标签转成 one-hot注意num_classes必须和模型输出通道数一致如果掩码里混入了 255 的未标注像素one-hot 会出现 256 维直接报错。实际训练时我用 0.7 的交叉熵加权 0.3 的 Dice前几个 epoch 交叉熵把类别边界拉大后期 Dice 把小类别区域补齐。如果发现瓶颈类别比如海星的 IoU 一直上不去可以把 Dice 权重提到 0.5。4.3 评估 8 分类效果逐类 IoU 比平均 mIoU 更值得盯训练完看指标很多人只盯 mIoU但这恰恰会骗过你。背景类占了绝大多数像素它的 IoU 通常轻松到 0.95 以上把平均值拉高一大截鱼、海星这种小类别即使 IoU 只有 0.2mIoU 看起来依然好看。这也是遥感影像语义分割里反复出现的教训必须逐类输出 IoU而不是只看平均数字。import numpy as np def per_class_iou(pred_idx, mask_idx, num_classes8): ious {} for cid in range(num_classes): p pred_idx cid t mask_idx cid inter np.logical_and(p, t).sum() union np.logical_or(p, t).sum() ious[cid] inter / (union 1e-6) # 加平滑避免除零 return ious # 假设 pred 是模型 argmax 后的索引图mask 是真实标签 iou_dict per_class_iou(pred, mask, num_classes8) for cid, iou in iou_dict.items(): print(fclass {cid}: IoU {iou:.4f})num_classes8是最大类别数加一如果你的数据集约定 ID 0 是背景那这个参数就是 8正好对应 0-7如果作者用了 ID 1-8 不含背景你还得做一次减一映射。加1e-6是为了防止某个类别在验证集里完全没有出现时除零但要注意如果某类在验证集里都没出现它的 IoU 是 0.0 还是空值要在报告里写清楚否则别人会以为模型完全不认识这个类别。我自己的做法是同时对背景类单独看、对 7 个目标类算平均两个数一起报。5. 避坑手册水下低对比度与边缘泄漏的五个排查记录5.1 随机种子不同步掩码被颜色抖动改花现象训练十几轮后可视化 Batch 输出发现掩码和原图对不上目标的边缘明显偏移甚至整块区域颜色错乱。原因很多人在数据增强时先对图像做ColorJitter、随机旋转然后单独对掩码做另一套随机变换。由于两套变换各自调用了随机数生成器且没有对齐图像旋转了 15 度掩码可能只转了 10 度标签自然漂移。解决把图像和掩码放进同一个变换函数里让它们共享同一个随机状态。无论是写transform(image, mask)还是用torchvision.transforms的 Compose 包两层都必须保证旋转角度、翻转概率、缩放尺度用的是同一个随机数。我习惯在每个样本的处理函数开头用seed torch.randint(0, 2**32, (1,)).item()锁定本样本的随机种子再分别传给图像和掩码的几何变换。5.2 resize 后的幽灵像素类是 8 个标签变成 300 个现象训练前统计类别只有 8 个跑起来之后损失函数报错Target size must be 8但检查原始掩码文件没有任何异常。原因预处理管线里用了统一的transforms.Resize(size)默认插值是双线性。掩码经过双线性插值后边缘像素出现 2.7、3.4 这类中间值四舍五入或直接取整后产生大量超出 0-7 的新整数。解决回到 3.1 的代码掩码的 resize 必须显式指定Image.NEAREST。已经污染的数据不用重标写一段脚本把掩码重新加载、用最近邻插值重放一遍即可。另外注意 PyTorch 的InterpolationMode枚举和 PIL 常量在行为上完全一致选一个用到底不要在同一个流程里混用。5.3 边缘泄漏低对比度目标的标注误差被交叉熵放大现象训练曲线后期 loss 降不下去mIoU 卡在 0.65 左右。可视化叠加图发现鱼和岩石的边界区域有一圈半透明像素标注边缘和实际物体边缘差了三四个像素。原因水下图像对比度低鱼的颜色和背景水色接近标注员在边界处本来就标不准交叉熵损失对每个像素一视同仁边缘一堆噪声标签持续给梯度模型在边界上怎么学都学不对。解决这类问题靠调模型没用两个有效手段。第一是训练时对边缘区域做标签平滑把 one-hot 的硬标签从 1.0 降成 0.9给网络一点容忍度第二是可视化时把掩码叠加在原图上逐个检查边缘误差集中的样本把标注质量差的帧从训练集里剔掉。边缘泄漏不只在潜水场景出现遥感图像分割里建筑物边界同样如此处理思路完全一致。5.4 背景类 IoU 虚高掩码全预测成水也能有 85 分现象训练完打印 mIoU 是 0.83看起来不错。但逐类打印发现背景 IoU 0.99鱼、海星两类只有 0.1模型实际上把所有目标全部预测成了背景。原因水体背景占像素总量的比例过高模型不需要识别任何目标就能把 loss 压到很低。mIoU 做平均时背景类权重和其他类一样一个 0.99 就能掩盖四个 0.1。解决报告指标时把「背景类 IoU」「目标类平均 IoU」分开列不要只写一个 mIoU。训练层面用 4.2 节的 Dice Loss 或给目标类加更高的类别权重推理层面可以检查验证集的预测图如果目标类像素占比远低于训练集的真实占比基本就是模型退化成了全背景预测。这个坑最隐蔽的地方在于——只看 loss 曲线下降得很漂亮完全看不出问题。6. 可视化代码的三种实战用法调色板渲染、半透明叠加与坏样本过滤6.1 用调色板把索引掩码渲染成伪彩色 PNG训练前把掩码渲染成伪彩色图是检查标注质量最快的方式。核心逻辑是给 8 个类别 ID 各自绑定一个 RGB 颜色生成带调色板的索引 PNG。这里直接操作 PIL 的putpalette不需要碰 matplotlibfrom PIL import Image import numpy as np PALETTE [ 0, 0, 0, # 0 背景 128, 128, 128, # 1 岩石 255, 0, 0, # 2 珊瑚 0, 255, 0, # 3 海草 0, 0, 255, # 4 鱼 255, 255, 0, # 5 海星 255, 0, 255, # 6 螃蟹 0, 255, 255, # 7 其他 ] def render_label_as_color(mask_path, out_path): mask Image.open(mask_path).convert(P) mask.putpalette(PALETTE) mask.save(out_path)这段代码把 P 模式的索引图直接套上调色板输出 PNG 在系统看图器里会显示成彩色掩码。putpalette接收的是一维列表长度必须是 3 × 类别数少了会报错多了会被忽略。保存时不需要转 RGB索引模式 PNG 配合调色板体积比 RGB 小很多批量导出几百张也不占空间。6.2 原图与掩码半透明叠加质检坏样本的标准姿势单看掩码图看不出标注和实物的偏差必须把彩色掩码叠到原图上。透明度 alpha 控制在 0.4 左右既能看清楚目标边界又不至于完全盖住原图纹理。import cv2 import numpy as np from PIL import Image def blend_mask_on_image(image_path, mask_path, out_path, alpha0.4): img cv2.imread(image_path) # BGR mask np.array(Image.open(mask_path).convert(P), dtypenp.uint8) color_mask np.zeros_like(img) cmap np.array([ [0, 0, 0], [128, 128, 128], [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [255, 0, 255], [0, 255, 255] ]) for cid in range(8): color_mask[mask cid] cmap[cid] # alpha 控制掩码占比1-alpha 保留原图信息 out cv2.addWeighted(img, 1 - alpha, color_mask, alpha, 0) cv2.imwrite(out_path, out)addWeighted的两个权重参数加起来要等于 1否则整体亮度会偏移alpha0.4是经验值目标越小越可以把 alpha 降到 0.3让掩码边界更贴近实物边缘。color_mask是纯色填充版本类别 0 保持黑色不会影响原图。批量跑完全部样本后按「掩码与实物边缘偏差超过 5 像素」的标准人工过一遍能筛掉大部分坏样本。6.3 一个检验习惯同时导出索引图和叠加图再对比我头一回处理水下分割数据时图省事只导出了叠加图看着掩码和鱼的位置对得上就以为没问题。后来训练时发现 fcn 语义分割模型在鱼尾巴的位置总是输出一片噪声回头翻原始掩码才发现那一帧的标注把尾巴截断成了两段叠加图里颜色相近、肉眼根本看不出来。从那之后我每次做数据集质检都强制同时导出索引图、原图和叠加图三份索引图看类别完整性叠加图看边缘对齐度。这个习惯看着笨但对语义分割这种像素级任务它比任何自动化指标都可靠。可视化代码不只是交付物更是你在这个数据集上前期投入回报率最高的调试工具。希望这套流程能帮你把水下目标图像语义分割的每一步走得稳一点少踩几个我踩过的坑。本文还有配套的精品资源点击获取