ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

水下目标语义分割8分割数据集:标签处理、可视化与训练实战

水下目标语义分割8分割数据集:标签处理、可视化与训练实战 简介一份面向水下目标图像语义分割任务的数据集与配套可视化代码包聚焦水下场景中人类、海草、珊瑚、岩石、鱼等八类前景目标的像素级分割适合计算机视觉方向的学生、研究者和算法工程师用于模型训练、算法验证与毕设课题。数据集已划分为训练集与测试集其中训练集包含1525张原始图像及对应mask测试集包含110张图像及对应mask图像分辨率为640×480mask采用调色后的彩色图背景简单而前景区域丰富且标注精细。压缩包共2000个文件主要包括1525个bmp标注文件、474个jpg原图文件和1个Python可视化脚本整体约159MB此外数据在预处理阶段加入了随机旋转等增强操作能够提升模型泛化能力。配套脚本可随机抽取样本将原始图像、GT掩码以及GT叠加到原图的效果一并输出并保存极大方便快速预览与结果检查。资源目前已有1080人浏览学习对于需要高质量水下分割数据与可视化工具的开发者来说具有直接参考价值。1. 图像分割数据集为何绕不开“8分割”这个设定拿到「水下目标图像语义分割8分割」这个标题时真正要回答的问题不是“怎么把模型跑起来”而是“这 8 个类别到底怎么对齐到我的业务里”。很多人在这一步翻车解压后直接拿语义分割网络去训练mIoU 却低得反常最后发现原因根本不是模型结构而是标签掩膜里像素值从 1 开始编号而损失函数里ignore_index没做对应处理模型把背景和第一类目标学混了。水下目标检测与分割这类任务里数据集的“8分割”意思是模型输入一张水下光学图像输出一张尺寸相同的类别索引图每个像素只属于 8 个类中的一个。这套设定常见于水下养殖巡检、捕捞机器人、海洋生态监测也常被用来做遥感影像语义分割复现前的预训练数据。适合读这篇文章的人是打算拿现成数据集练手、或者要自建水下数据集并写配套可视化代码的工程师。这里最容易混淆的一点是8 不是颜色通道数而是语义类别总数类别标签文件也不只是给人看的说明而是决定训练时损失函数和评估指标怎么写的契约。2. 水下目标语义分割的标签体系类别表、掩膜与索引边界2.1 8 个类别怎么划分才不容易在训练时打架水下目标语义分割的类别划分决定了模型能区分什么、会混淆什么。常见的 8 分类设计是这样拆的背景是一类水体本身单独一类然后按生物形态把目标拆成具体物种。如果原始数据里“海参”和“海胆”在外观上高度相似但名称上已经是两个独立 class就要确认掩膜里有没有把两者做成连续且不同的像素值。索引类别名像素含义与典型覆盖范围0背景非目标区域通常指岩石、人工设施、无生物的水底1水体与背景区分开的远端水体或悬浮物区域2鱼类单个鱼体含鱼鳍与眼睛区域3海参深色长条形生物体4海胆圆形带刺生物体5螃蟹蟹壳及蟹腿包络范围6贝壳/扇贝壳面高光与纹理区域7礁石/水底地形大范围非生物结构属于可学习语义而非杂物这个表不是标准答案而是最稳妥的默认划分逻辑。8 分割的意思就是输出层最后变成 8 个通道的 logits然后取argmax得到索引图。索引边界最怕出现“0 既是背景又是 ignore_index”的设计冲突。常见做法是把 0 保留给背景ignore_index单独用一个像素值如 255 标记不确定区域不要和任何有效类别共用值。如果你拿到的那份数据集的类别标注从 1 开始而 0 是训练时被忽略的边界区域那就要在 Dataset 类里做一个label - 1的映射而不是直接喂给网络。2.2 读掩膜时最容易踩的坑用 convert(“L”) 读成单通道并核对像素值水下语义分割数据集的标签通常是 PNG 灰度图但很多采集端保存时会带上透明度通道直接np.array(Image.open(...))读出来是 H×W×4后面所有代码都会错位。处理这类标签体系的第一步永远是先写一段检查脚本把掩膜读成单通道然后统计唯一值。from PIL import Image import numpy as np mask np.array(Image.open(data/masks/0001.png).convert(L)) print(mask shape:, mask.shape) print(unique values:, np.unique(mask)) class_names { 0: 背景, 1: 水体, 2: 鱼类, 3: 海参, 4: 海胆, 5: 螃蟹, 6: 贝壳/扇贝, 7: 礁石/水底地形, } for val in np.unique(mask): count (mask val).sum() name class_names.get(int(val), 未知/待确认) print(fvalue{val}: {count:10d} pixels, class{name})这里convert(L)的作用是把掩膜固定到单通道 8bit 灰度避免 RGBA 和调色板格式带来的脏读取。np.unique输出的值域直接决定了你的类别标签表是否有效。如果打印结果里出现了 255、254 这类值说明原图有未标注边界或透明度残留正确做法是先决定把这些像素全部归到背景还是设为消融区。像素数统计则是为后面的类别不均衡分析做铺垫水下目标分割最典型的问题就是“水体”占了 70% 像素而目标物种合计不到 5%不提前摸底训练出来的模型会表现出对水体的严重偏置。2.3 类别标签文件建议做成 CSV而不是只写一段 readme语义分割代码里类别标签文件一般用来做两件事一是和原始标签唯一值相互校验二是在可视化时把索引映射成可读名称和颜色。很多人低估了这个文件的作用随手在 readme 里写一段文字结果训练脚本和可视化脚本各维护一套类别名类目顺序一旦微调就出问题。name,index,color_r,color_g,color_b,description background,0,0,0,0,背景与无目标区域 water,1,255,255,255,水体与远距离悬浮物 fish,2,255,0,0,鱼类个体 sea_cucumber,3,0,255,0,海参 sea_urchin,4,0,0,255,海胆 crab,5,255,255,0,螃蟹 shellfish,6,255,0,255,贝壳与扇贝 reef,7,0,255,255,礁石与地形这份 CSV 就是你的调色板来源。可视化代码里颜色的顺序必须与 CSV 中的行顺序一致索引 0 永远是背景不要用随机颜色去绘制语义分割结果否则每次跑图颜色都在变也无法用于后续论文配图或检测结果复核。description 字段的用途是给下游同事或模型对齐时用的避免光看索引对不上业务含义。类别标签文件建议放进数据集的class_dict.csv和train.txt、val.txt同目录后续做遥感影像语义分割复现或点云语义分割转二维投影时也能复用同一套索引体系。3. 可视化代码从灰度掩膜到三联图的落地方案3.1 写一个固定调色板函数颜色与类别索引严格绑定可视化代码是数据集交付物里最容易被忽略的部分但实际使用频率最高。语义分割可视化不是简单的plt.imshow(mask)灰度掩膜直接渲染出来的图对人眼没有区分度因为 8 个类别的灰度值差异太小。固定调色板的思路是每个类别索引对应一个确定 RGB 色值多年后回看可视化结果仍然能知道每个颜色代表什么。import numpy as np import matplotlib.pyplot as plt from PIL import Image def load_palette(csv_pathdata/class_dict.csv): import csv palette {} with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: idx int(row[index]) palette[idx] ( int(row[color_r]), int(row[color_g]), int(row[color_b]), ) return palette def mask_to_rgb(mask, palette): h, w mask.shape rgb np.zeros((h, w, 3), dtypenp.uint8) for idx, color in palette.items(): rgb[mask idx] color return rgb这个函数的核心逻辑是逐类做布尔掩膜索引赋值。mask idx得到布尔矩阵把它作为索引给rgb对应位置的三个通道同时赋值效率远高于逐像素循环。这里的颜色值就是 2.3 节 CSV 里的 color_r、color_g、color_b保持一致后训练代码、评估代码、可视化代码共用同一套语义不会出现训练时按 8 类可视化时却用了 matplotlib 默认 colormap 导致类目颜色错位的低级问题。3.2 三联图布局原图、彩色掩膜、半透明叠加语义分割数据集的检查通常靠三联图完成左边原始水下图像中间彩色掩膜右边掩膜叠加在原图上。半透明叠加是判断标注边界是否贴合的快捷方式如果标注边界偏离目标轮廓超过几个像素叠加图上一眼就能看出光晕或空边。def visualize_sample(image_path, mask_path, palette, save_pathNone): image np.array(Image.open(image_path)) mask np.array(Image.open(mask_path).convert(L)) mask_rgb mask_to_rgb(mask, palette) fig, axes plt.subplots(1, 3, figsize(18, 6)) axes[0].imshow(image) axes[0].set_title(Original Underwater Image) axes[0].axis(off) axes[1].imshow(mask_rgb) axes[1].set_title(Semantic Mask (8 classes)) axes[1].axis(off) overlay (image * 0.6 mask_rgb * 0.4).astype(np.uint8) axes[2].imshow(overlay) axes[2].set_title(Overlay) axes[2].axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi180, bbox_inchestight) print(saved to:, save_path) plt.close(fig)overlay的计算方式是原始图像和掩膜 RGB 的加权混合0.6 和 0.4 的比例更偏原图适合看标注边界是否贴合目标轮廓。如果你更关心类别分布情况把比例反过来即可。plt.close(fig)这行不要省批量可视化几百张图时不关 figure 会造成内存持续增长。图窗尺寸 18×6 英寸的三联图能保证每个图里的目标区域不至于小到看不清。3.3 掩膜里混入 255 时的可视化兜底水下图像采集过程中不可能完全避免标注遗漏有些标注工具会把未标注区域写成 255。可视化阶段碰见这个值不要直接把 RGB 算成纯白色否则会和本就存在的水体/背景混在一起。palette load_palette() unknown_color (128, 128, 128) palette[255] unknown_color把 255 映射成灰色在解读掩膜时能立刻识别出未标注区域和背景的区别。如果整个数据集里 255 占的比例很高比如超过 5%那就要考虑是不是原始标注任务没有完成而不是靠模型去学习不确定区域。这里不需要修改任何训练逻辑纯粹是让可视化结果能暴露数据问题。3.4 类像素占比统计判断数据是否值得直接训练可视化代码除了画图常会附带一个批量统计脚本计算所有掩膜中每个类别的像素占比。这是判断水下目标语义分割数据可用性的第一道筛子。total_counts np.zeros(8, dtypenp.int64) mask_files [data/masks/0001.png, data/masks/0002.png] for path in mask_files: m np.array(Image.open(path).convert(L)) for c in range(8): total_counts[c] (m c).sum() ratios total_counts / total_counts.sum() for c, r in enumerate(ratios): print(fclass {c}: {r:.4f} ({total_counts[c]} px))运行结果通常会呈现极端的类别不平衡水体占 60% 以上螃蟹可能只有 0.5%。这就是为什么训练语义分割模型时要考虑加权损失或采用 minival 策略。不要急着直接跑训练先把上面这段统计数据记录下来后面模型训练完再回看能确认模型到底是在学习目标区域还是在水体上过拟合。将统计结果与 2.2 的 unique 检查放在一起就完成了数据集的初筛。4. 训练语义分割模型前的 3 项数据体检标签对齐、透明通道、数据划分4.1 检查标签唯一值是否构成连续区间语义分割模型输出的是连续类别的概率分布如果标签掩膜里的唯一值是[0, 1, 2, 6, 7]缺少 3、4、5损失函数计算时不会报错但模型反向传播时会持续给缺失类别的权重施加无用梯度并显著拉低真实类别的收敛效率。import numpy as np from PIL import Image import glob mask_files sorted(glob.glob(data/masks/*.png)) seen set() for f in mask_files: m np.unique(np.array(Image.open(f).convert(L))) seen.update(m.tolist()) seen sorted(seen) print(all unique values:, seen) missing [i for i in range(8) if i not in seen] print(missing classes:, missing)这里的逻辑很简单却能在训练前拦截大多数标签错误。结果中missing_classes里任何一个类被输出都要回到原始数据集里重新检查那一类是不是压根没标注。别指望模型能无中生有地学会一个从未出现在标签里的类。对于类别缺失比较严重的数据子集宁可先剔除也不要带着空洞训练。4.2 JPEG 存原图、PNG 存标签alpha 通道一律丢弃水下目标检测与分割数据集最常见的交付形态是原图为 JPEG、掩膜为 PNG。JPEG 有损压缩对 RGB 原图影响可以接受但对标签图却是灾难压缩产生的锯齿会让边界像素出现不属于任何类别的中间值。所以标签文件必须用无损 PNG且不可带 alpha 通道。验证手段就一行命令python -c from PIL import Image; import numpy as np; anp.array(Image.open(data/masks/0001.png)); print(a.shape)如果输出是(H, W, 4)说明该掩膜带透明度。水下图像常以 PNG 格式采集保存掩膜时不少脚本会默认把三通道模式写成 RGBA这会让后面的损失函数把透明通道也算进通道维去过。处理方式是统一转换mogrify -format png -define png:color-type0 data/masks/*.png或者用 Python 批量重新保存为带L模式的灰度 PNGfrom PIL import Image import glob for f in glob.glob(data/masks/*.png): img Image.open(f).convert(L) img.save(f, formatPNG, compress_level1)compress_level1是为了保留细节纹理同时控制体积越高的压缩级别对边缘像素的影响越明显语义分割标签对边缘锐度非常敏感不需要最高压缩比。4.3 用 shuf 做稳定的 train/val/test 划分语义分割数据集的划分讲究“按图划分”而不是“按像素划分”目的是防止同一张图像的不同像素既出现在 train 又出现在 val。用shuf加文件列表就能得到稳定的划分脚本这里我一般按照 80/10/10 去做。mkdir -p data/images/train data/images/val data/images/test mkdir -p data/masks/train data/masks/val data/masks/test find data/images -name *.jpg | shuf --random-source(yes 2024) all_images.txt total$(wc -l all_images.txt) train_num$((total * 8 / 10)) head -n $train_num all_images.txt train_images.txt sed -n $((train_num 1)), $((total * 9 / 10))p all_images.txt val_images.txt tail -n $((total - total * 9 / 10)) all_images.txt test_images.txtshuf --random-source(yes 2024)是把随机种子固定下来同一份数据无论跑多少遍划分结果一致。train/val/test 三份列表各自存一份不要直接把文件移动到新目录里这样后续要调整比例或做交叉验证时成本更低。如果你的水下目标场景里某些类别集中出现在特定视频片段纯随机划分会导致 val 里完全没有螃蟹样本。5. 用这个数据集跑通 Deeplabv3 的最小实验5.1 Dataset 类双读取与图像尺寸统一用语义分割数据集跑 Deeplabv3 做复现实验不需要额外写复杂的预处理前置步骤但 Dataset 类的设计决定了训练能否稳定。水下图像尺寸差异很大直接 collate 会报维度错误常见的做法是统一 resize 或 pad 到 512×512。若原图分辨率较高可选 416 或 320但 8 分类任务里小目标占比高尺寸太小会导致海参这类细长目标直接丢像素。import torch from PIL import Image from torch.utils.data import Dataset import torchvision.transforms as T class UnderwaterSegDataset(Dataset): def __init__(self, image_paths, mask_paths, img_size512): self.image_paths image_paths self.mask_paths mask_paths self.img_size img_size self.image_transform T.Compose([ T.Resize((img_size, img_size), interpolationT.InterpolationMode.BILINEAR), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) self.mask_transform T.Compose([ T.Resize((img_size, img_size), interpolationT.InterpolationMode.NEAREST), T.ToTensor(), ]) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image Image.open(self.image_paths[idx]).convert(RGB) mask Image.open(self.mask_paths[idx]).convert(L) image self.image_transform(image) mask self.mask_transform(mask).squeeze(0).long() return image, mask关键点在两个 transform 的插值方式不同图像用 BILINEAR 保留细节掩膜必须用 NEAREST否则双线性插值会产生 0.5 这类非整数像素值。squeeze(0)把掩膜从 [1, H, W] 压回 [H, W]后面的.long()则将标签转成 PyTorch 期望的整数索引类型。水下目标语义分割的边界质量对这类细节很敏感采样时宁可多花一点 CPU 时间在 resize 上也不要让标签产生模糊。5.2 损失函数里 ignore_index 的写法直接决定背景是否溢出Deeplabv3 最小实验的损失函数通常直接采用交叉熵但这个交叉熵的参数里藏着一个水下分割特有的坑ignore_index到底设成多少。前面检查过掩膜唯一值只有 0 到 7损失函数就可以写成CrossEntropyLoss(ignore_index255)其实不会对训练产生实际影响但保留 255 作为未知区域是一个好习惯后续若补充了带边界消融的数据可以无缝兼容。criterion torch.nn.CrossEntropyLoss(ignore_index255)不要设ignore_index0。很多人习惯把背景设成 ignore想着让模型只看目标在水下目标检测的公开数据集里偶尔有人这么干但对语义分割来说这是灾难。背景区域占整张图比例很高一旦把 0 排除在损失之外模型对背景的输出完全失控预测图里会出现大范围的杂色点和边界外溢。正确的做法是背景也参与训练类别不平衡问题用权重去解决而不是用 ignore 去绕过。class_weights torch.tensor([0.5, 0.2, 1.0, 1.2, 1.2, 1.5, 1.5, 1.0]) criterion torch.nn.CrossEntropyLoss(weightclass_weights, ignore_index255)这里权重的设置逻辑是水体这类大面积类别权重低于 1稀有生物类别权重高于 1.2。权重比例应来自 3.4 节统计出的像素占比倒数而不是拍脑袋。如果你的统计结果显示螃蟹占比只有 0.3%而水体占 65%给螃蟹设 1.5 大概率不够先按weight (1 - p) / (1 - p_sum / N)这类常见公式粗算再做一轮小步长迭代。5.3 按类输出 IoU不要只看整体 mIoU训练完 Deeplabv3 后损失函数的下降不能作为成功的判断标准要看按类 IoU。水下目标语义分割的效果验证尤其不能只看 mIoU 高不高因为水体这一类 IoU 往往能到 0.95 以上直接把平均分拉高掩盖了螃蟹、海参这类小目标的低性能。def compute_iou_per_class(pred_mask, gt_mask, num_classes8): ious [] for c in range(num_classes): pred_c pred_mask c gt_c gt_mask c inter (pred_c gt_c).sum().item() union (pred_c | gt_c).sum().item() ious.append(inter / union if union 0 else float(nan)) return ious这组计算用逐类交并比来诊断模型短板。输出的ious列表里如果某个类是nan说明验证集里根本没有该类别样本这一轮的评估结果不能作为该类性能的依据。真实业务里遇到这种结果需要回到 4.1 节的 missing classes 检查把那些不包含稀缺类的样本补充进来再做验证。将每轮训练后的 per-class IoU 落盘成 JSON能直观定位哪几类长期不涨、哪几类在某一轮后崩掉远比只记录一个 mIoU 更能指导下一步调参。6. 把 8 分割结果转成发布级可视化的 3 个细节6.1 调色板固定后连发布的格式也要固定模型推理出来的结果如果只是用来画个示意图随手用 Jet 或 viridis 看不出问题但要发布成数据集配套成图就必须用第 3 章的固定调色板。水下目标语义分割图里背景是黑色水体是白色鱼类是红色这套颜色约定要贯穿训练样例、论文插图、模型输出的所有图。不要用随机 colormap 覆盖它否则一张图里水域和背景的关系可能会因为颜色映射的不同产生相反含义。6.2 保存推理图时检查数据类型不要存成 float推理结果从argmax出来后形状是 H×W 的整数索引图。直接plt.imsave有时会把 float 型数据当作 0~1 范围写入导致输出全是接近黑色的一片。正确做法是先做一步类型转换。import numpy as np from PIL import Image pred torch.argmax(logits, dim1)[0].cpu().numpy().astype(np.uint8) pred_rgb mask_to_rgb(pred, palette) Image.fromarray(pred_rgb, modeRGB).save(output/result.png)astype(np.uint8)这一步不要省。掩膜索引理论上有 8 个值但 uint8 之外的高位数值在存入 PNG 后会被截断或产生颜色偏移。用 PIL 保存比plt.imsave更可控modeRGB确保是三通道而不是调色板索引图。6.3 最后一遍核对在叠加图上随机抽 3 个坐标点比对像素值发布前的最后一轮验证是人工核对几个点的预测值与真值。抽取方式用固定随机种子避免每次核对抽到的点都不一样。rng np.random.default_rng(42) h, w pred.shape points rng.integers([0, 0], [h, w], size(3, 2)) for y, x in points: print(fpoint ({y},{x}): pred{pred[y, x]} gt{gt_mask[y, x]})打印结果里只要出现pred... gt...不一致就需要先回到可视化叠加图上确认那个位置是否属于类边界如果是边界像素1 像素漂移是可接受的如果差值达到 2 以上说明后处理里存在类别索引错位立刻检查调色板文件和类别表的顺序是否一致。这套核对流程做完数据集和配套可视化代码就可以正式交付了。本文还有配套的精品资源点击获取
返回列表