简介:一份面向水下目标识别与语义分割任务的数据集,适合计算机视觉研究者与算法学习者用于模型训练与评估。数据源自水下场景,图像统一为 640×480 分辨率,分割前景包含人类、海草、珊瑚、岩石、鱼类等 8 类目标,背景以 0 标记,掩膜采用调色后的彩色图像,便于直观检查。数据集已划分训练集与测试集:训练集共 1525 张原始图像及对应掩膜,测试集共 110 张原始图像及对应掩膜,并预先做了随机旋转等预处理以增强数据多样性。资源包共 2000 个文件,以 bmp 掩膜和 jpg 原图为主,另含 1 个 Python 可视化脚本,整体约 159.07MB(7z 压缩包)。附带的可视化脚本可随机抽取一张样本,将原始图像、真值掩膜和叠加蒙版效果并列展示并保存,便于快速确认标注质量与分割效果。目前已有 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, f"image/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, dtype=np.uint8) for class_id, rgb in rgb_to_id.items(): ids[np.all(flat == rgb, axis=1)] = 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_counts=True) for v, c in zip(values, counts): print(f"class {v}: {c / arr.size * 100:.2f}%")这段脚本解决了两个常见问题:一是自动判断掩码是 RGB 还是 P 模式,二是把 RGB 颜色逐像素映射成类别 ID。参数rgb_to_id的 key 是类别 ID,value 是标注工具里对应的颜色,映射时用向量化的np.all(flat == rgb, axis=1)做批量匹配,比逐像素 for 循环快一个数量级。如果你发现统计结果里出现不在映射表里的颜色,说明标注工具导出时用了抗锯齿或者调色板错位,这类样本要直接挑出来清理。
2.3 P 模式与 RGB 模式:索引化的掩码才适合做语义分割训练
这里单独把小节留出来,是因为模式问题几乎每个新手都会踩一次。很多图像处理库读 PNG 时默认展开成三通道 RGB,于是掩码变成了一张「看起来和原图一样的彩色图」。但语义分割训练要求的标签是二维索引图:每个像素位置的值只能是 0 到 7 的整数,代表类别 ID,而不是 RGB 三元组。
P 模式(调色板模式)的 PNG 本身就是索引图,PIL 里一个convert("P")就能拿到单通道数组。RGB 模式则必须先查表映射。混淆这两者最典型的翻车现场是:训练时把三通道 RGB 掩码直接当标签,损失函数里num_classes=8,于是网络输出的 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, interpolation=T.InterpolationMode.BILINEAR) # 掩码用最近邻,类别索引不会被插值污染 mask = TF.resize(mask, size, interpolation=T.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), keepdims=True) # 灰度世界假设:三通道均值应相等,取整体均值做基准 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, eps=1e-6): # pred_logits: (N, C, H, W),mask: (N, H, W),C = 8 pred = F.softmax(pred_logits, dim=1) 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_classes=num_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()这里的eps=1e-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.2,mIoU 看起来依然好看。这也是遥感影像语义分割里反复出现的教训:必须逐类输出 IoU,而不是只看平均数字。
import numpy as np def per_class_iou(pred_idx, mask_idx, num_classes=8): 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_classes=8) for cid, iou in iou_dict.items(): print(f"class {cid}: IoU = {iou:.4f}")num_classes=8是最大类别数加一,如果你的数据集约定 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,不需要碰 matplotlib:
from 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, alpha=0.4): img = cv2.imread(image_path) # BGR mask = np.array(Image.open(mask_path).convert("P"), dtype=np.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,否则整体亮度会偏移;alpha=0.4是经验值,目标越小越可以把 alpha 降到 0.3,让掩码边界更贴近实物边缘。color_mask是纯色填充版本,类别 0 保持黑色不会影响原图。批量跑完全部样本后,按「掩码与实物边缘偏差超过 5 像素」的标准人工过一遍,能筛掉大部分坏样本。
6.3 一个检验习惯:同时导出索引图和叠加图再对比
我头一回处理水下分割数据时,图省事只导出了叠加图,看着掩码和鱼的位置对得上就以为没问题。后来训练时发现 fcn 语义分割模型在鱼尾巴的位置总是输出一片噪声,回头翻原始掩码才发现那一帧的标注把尾巴截断成了两段,叠加图里颜色相近、肉眼根本看不出来。从那之后我每次做数据集质检都强制同时导出索引图、原图和叠加图三份,索引图看类别完整性,叠加图看边缘对齐度。这个习惯看着笨,但对语义分割这种像素级任务,它比任何自动化指标都可靠。
可视化代码不只是交付物,更是你在这个数据集上前期投入回报率最高的调试工具。希望这套流程能帮你把水下目标图像语义分割的每一步走得稳一点,少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取