news 2026/9/10 2:55:55

大脑肿瘤MRI分割数据集详解:从掩码处理到U-Net训练

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大脑肿瘤MRI分割数据集详解:从掩码处理到U-Net训练

简介:面向医学图像分割与深度学习入门者,提供一套大脑肿瘤MRI二维分割数据集,类别设计简洁,聚焦Tumor前景与背景的二分类任务,适合图像分割模型的训练与效果验证。图像统一缩放至416×416分辨率,训练集包含1632张原始图片及1632张对应掩膜,测试集包含240张原始图片与240张掩膜,标签以1和0区分前景与背景,背景简单、前景区域丰富且标注良好,可直接用于U-Net、DeepLab等常见分割网络的输入与精度评估。压缩包共2000个文件,以png/jpg图像文件和一个Python可视化脚本为主体,整体大小48.17MB,目录划分清晰,便于按训练集、测试集快速检索。可视化脚本会随机提取一张样本,同时输出原始图片、GT掩膜以及GT叠加在原图上的蒙版效果,方便直观检验标注质量和模型预测结果。目前已有1879人学习使用,适合医学影像分析课程作业、算法对比试验或科研预研等场景。

1. 大脑肿瘤分割数据集:416×416 MRI切片与二分类掩码的对应关系

拆开压缩包,你会看到一批以 y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.jpg 命名的文件。这种命名不是随意起的,rf 是 Roboflow 的导出标记,后面的长十六进制串是资源内部的对象 ID,前面的 y431 才是原始图像名。这是一份人脑 MRI 切片的大脑肿瘤分割数据集,分辨率统一为 416×416,前景类别只有 Tumor 一个,背景不算类别,所以说的“2 分割”是指标签设定里 0 是背景、1 是肿瘤。训练集 1632 对图片和掩码,测试集 240 对,同时附带了可视化脚本,能把原图、GT 掩码、GT 蒙板一次性画出来。这份数据适合谁用:刚开始做医学图像分割、需要一份干净基准数据来验证 U-Net 系列网络的人;也适合想测试数据增强和损失函数在“背景简单、前景边界明显”场景下表现的人。

2. 解剖大脑肿瘤数据集的目录结构与掩码存储规则

2.1 Roboflow 导出命名:为什么不能用顺序索引对齐图片和掩码

train 和 test 目录内部结构一致,分别挂 images 和 masks 两个子目录。images 下面是 jpg,掩码文件可能以 png 或 jpg 形式出现,取决于导出时选择的编码。这个数据集的关键约束是:每一张 mask 与对应图片共享同一个文件主名(stem),只是扩展名不同。你的数据加载逻辑必须以主名作为关联键,而不是用os.listdir的顺序盲配对,因为文件系统的返回顺序不保证两张表对齐。

brain-tumor-segmentation/ ├── train/ │ ├── images/ │ │ ├── y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.jpg │ │ ├── y492_jpg.rf.d1dc8cb01e14d08c955b0f2ebf441ba4.jpg │ │ └── ... (1632 files) │ └── masks/ │ ├── y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.png │ ├── y492_jpg.rf.d1dc8cb01e14d08c955b0f2ebf441ba4.png │ └── ... (1632 files) ├── test/ │ ├── images/ │ └── masks/ └── visualize.py

上面目录树里掩码我默认写成了 .png,实际如果导出为 jpg 也不影响逻辑。文件名里的 c7ebeffb6e184a65ac9d74065e76d6d8 是 Roboflow 生成的唯一 ID,同一张图在 images 和 masks 里 ID 完全一致。所以最稳妥的匹配方式是:读取图片的 stem,到 mask 目录里去找同 stem 的文件,而不是直接遍历两个目录再 zip。

Roboflow 还可能在文件名里附加_jpg后缀,这是告诉你原图是 jpg 格式。后面做正则匹配或写数据加载器时,要注意这些下划线分隔的段,避免把 stem 截断成只有一个 y431。

2.2 掩码像素值语义:0 是背景,但前景可能是 1 也可能是 255

摘要里说“标签的 mask 图像为 1 的阈值,0 为背景”,这句话在落地时要非常小心。Roboflow 导出二分割标签时有两种常见编码:一种是单通道灰度图,背景像素值 0、前景像素值 1;另一种也是单通道灰度图,但背景 0、前景 255。如果你直接对像素值做mask / 255.0,第一种编码下前景 1 会被压成 0.0039,损失函数直接崩溃;如果直接mask == 1,第二种编码又什么都匹配不到。正确做法是用阈值比较mask > 0,把任何非零值统一映射成前景,再转成 float 张量。

import cv2 import numpy as np from pathlib import Path mask_path = Path("train/masks/y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.png") mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) print("unique pixels:", np.unique(mask)) # 输出可能是 [0 1],也可能是 [0 255] mask_bin = (mask > 0).astype(np.uint8) print("foreground ratio:", round(float(mask_bin.mean()), 4))

这段代码在拿到数据后应该第一个跑。np.unique(mask)直接告诉你掩码到底是 0/1 还是 0/255 编码,mask > 0则把两种编码统一成布尔标签。我一般在数据加载的__getitem__里直接做这一步,而不是提前把所有 mask 暴力重写一遍,因为 1872 个文件重写后还要再校验一次,没必要。foreground ratio能帮你快速判断肿瘤区域占整张图的比例,这个比例直接决定后面损失函数要不要加权重。

2.3 用脚本核对图片与掩码的完备性

下载之后先别急着写模型。Roboflow 导出的归档偶尔会出现某张图片缺掩码,或者标签文件里混进多余 mask 的情况。我一般先跑一遍配对检查,把缺失和多余的文件名列出来,确认没有问题再进训练循环。

from pathlib import Path SUPPORTED_MASK_EXT = {".png", ".jpg", ".jpeg", ".bmp"} def verify_pair(img_dir: str, mask_dir: str): img_dir = Path(img_dir) mask_dir = Path(mask_dir) images = {p.stem: p for p in img_dir.glob("*.jpg")} masks = {p.stem: p for p in mask_dir.glob("*.*") if p.suffix.lower() in SUPPORTED_MASK_EXT} missing = sorted(set(images) - set(masks)) extra = sorted(set(masks) - set(images)) print(f"images: {len(images)}, masks: {len(masks)}") print(f"missing masks: {len(missing)}") for name in missing[:10]: print(" MISSING", name) print(f"extra masks: {len(extra)}") for name in extra[:10]: print(" EXTRA", name) return missing, extra verify_pair("train/images", "train/masks") verify_pair("test/images", "test/masks")

脚本做了两件事:一是用集合差集找出缺失和多余的掩码,二是打印统计量。注意missing只列了前 10 个,因为如果数据真的出错,往往是一整段切片序列连续缺,列太多反而刷屏。extra存在的可能原因包括标注工具导出了额外的空掩码或备份文件,这类文件训练时如果被加载器扫到,会把一个纯背景样本混进训练集,肿瘤区域占比被拉低,训练倒不至于崩,但验证集指标会带着噪声。这里我用集合的键是stem,天然屏蔽了扩展名差异,png 和 jpg 混存的情况也能对齐。

3. 把可视化代码改成批量蒙版校验工具

3.1 三张子图的绘制逻辑:原图、GT、GT 蒙板的关系

数据自带的 visualize.py 干的事是:随机抽一张图,把原图、GT 灰度掩码、GT 在原图上的蒙板三个图横向排开。这个思路对分割数据是够用的,因为单张 GT 灰度图肉眼直接看并不直观,尤其是肿瘤边界和脑组织灰度接近的时候。蒙板层的作用是把掩码覆盖到原图上,让你一眼看出标注边界是否贴合解剖结构。

原版脚本逻辑通常长这样,我用 PyTorch 之外依赖更少的方式重写了一份等效版本。

import cv2 import numpy as np import matplotlib.pyplot as plt from pathlib import Path def find_mask(img_path: Path, mask_dir: Path) -> Path: candidates = list(mask_dir.glob(img_path.stem + ".*")) assert len(candidates) == 1, f"mask 匹配异常: {img_path.stem}: {candidates}" return candidates[0] img_dir = Path("train/images") mask_dir = Path("train/masks") img_paths = list(img_dir.glob("*.jpg")) picked = np.random.choice(len(img_paths), size=4, replace=False) for idx in picked: img_path = img_paths[idx] img = cv2.imread(str(img_path)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(str(find_mask(img_path, mask_dir)), cv2.IMREAD_GRAYSCALE) mask_bin = (mask > 0).astype(np.uint8) overlay = img.copy() overlay[mask_bin > 0] = (255, 0, 0) # 红色蒙版 blended = cv2.addWeighted(overlay, 0.35, img, 0.65, 0) fig, axes = plt.subplots(1, 3, figsize=(14, 5)) axes[0].imshow(img) axes[0].set_title("MRI Image") axes[0].axis("off") axes[1].imshow(mask_bin, cmap="gray") axes[1].set_title("GT Mask") axes[1].axis("off") axes[2].imshow(blended) axes[2].set_title("GT Overlay") axes[2].axis("off") plt.tight_layout() plt.savefig(f"vis_{img_path.stem}.png", dpi=150, bbox_inches="tight") plt.close(fig)

这段代码把随机抽 1 张改成抽 4 张,每张单独保存成vis_<原文件名>.pngoverlay[mask_bin > 0] = (255, 0, 0)这行是在原始图像的副本上,把所有掩码覆盖的像素点直接染成红色,然后用addWeighted按 0.35 的权重融合。为什么要先染红再融合,而不是直接alpha混合?因为直接混合需要把掩码区域外的 alpha 也做一次 mask 操作,代码会多两行,而先染再融最简单,还能保留脑沟回处的灰度对比。

3.2 服务器无显示环境时 matplotlib 的兜底配置

你很可能是在云主机或远程容器里跑这份数据,根本没有显示器。matplotlib 默认的plt.show()在无 GUI 环境会直接报错,所以上面代码里我全部用plt.savefig并且plt.close(fig)关掉画布。如果你还是想先看一次交互窗口,可以在脚本开头强制指定 Agg 后端:

import matplotlib matplotlib.use("Agg")

Agg是不依赖窗口系统的非交互式后端,渲染结果直接写文件,几乎在所有 Linux 服务器上都可用。如果你本地有显示器并且想弹窗看,把这行注释掉就行。

3.3 批量可视化时怎么筛出异常掩码

单张可视化看着没问题还不够,我习惯把测试集 240 张全部跑一遍叠加图,然后按掩码区域面积排序,重点看面积最大和最小的几张。面积最大的可能是标注把脑室或水肿一起圈了进来,面积最小的可能是误标注的孤立噪点,这两种情况都会影响训练时对边界的约束。排序脚本只需要在上面循环里加一行:mask_bin.sum()作为 key。

另一个常见坑是:如果find_mask匹配不到任何文件,脚本会抛AssertionError,这其实是在提醒你 mask 目录路径给错了。Roboflow 导出时掩码有时会保存到train/labels而不是train/masks,下载后解压先看一眼目录名,别想当然。

4. 基于 2D U-Net 的大脑肿瘤分割训练配置与损失函数选择

4.1 为什么从 2D U-Net 起步,而不是 3D 或 Transformer

这份数据集给的是 2D 切片,每张 416×416 的 jpg 是一张独立的 MRI 横断面,文件之间没有提供层厚、扫描序列、患者 ID 这类三维重建信息。也就是说,你无法直接把图片堆成 3D volume 去训练 3D U-Net。BraTS 这类基准数据集用 3D U-Net 是因为它提供完整的 nii.gz 体数据,而这里强行按文件名顺序堆 z 轴,相邻切片可能来自完全不同的扫描,模型会学到一堆假纹理。

2D U-Net 在这个数据规模下是最合理的起点,另一个原因是你只有 1632 张训练图,而 U-Net 是出了名的在小数据集上也能收敛的架构。它的跳跃连接把编码器每层特征图直接拼到解码器,保证 416 分辨率下的边界细节不丢失。相比之下 DeepLab V3+ 的 ASPP 模块在 16 倍下采样下工作,对小肿瘤边界的敏感度差一些;SegFormer 这类 Transformer 结构没有 ImageNet 预训练权重保护,1632 张图很难训稳。

4.2 数据加载与数据增强实现

数据加载要解决的问题有两个:文件路径匹配和掩码二值化。下面的 Dataset 类把 2.2 节的阈值逻辑直接合入,此外把掩码转成(1, 416, 416)的 float 张量,配合 PyTorch 的 BCELoss 或 DiceLoss 使用。

import torch import cv2 import numpy as np import albumentations as A from torch.utils.data import Dataset from pathlib import Path class BrainTumorSegDataset(Dataset): def __init__(self, img_dir, mask_dir, transform=None): self.img_paths = sorted(Path(img_dir).glob("*.jpg")) self.mask_dir = Path(mask_dir) self.transform = transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path = self.img_paths[idx] img = cv2.imread(str(img_path)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask_matches = list(self.mask_dir.glob(img_path.stem + ".*")) assert len(mask_matches) == 1, f"mask mismatch: {img_path.name}" mask = cv2.imread(str(mask_matches[0]), cv2.IMREAD_GRAYSCALE) mask = (mask > 0).astype(np.float32) # 统一为 0/1 标签 if self.transform is not None: aug = self.transform(image=img, mask=mask) img, mask = aug["image"], aug["mask"] img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask = torch.from_numpy(mask).unsqueeze(0) # (1, H, W) return img, mask train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.Rotate(limit=10, border_mode=cv2.BORDER_CONSTANT, p=0.3), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3), ])

assert len(mask_matches) == 1这行是安全阀,如果掩码缺失或重复命名会立刻暴露。permute(2, 0, 1)把 HWC 的 numpy 数组转成 CHW 的 PyTorch 张量。增强里我没有加弹性形变,因为脑部解剖结构对空间形变的容忍度低,过分形变会扭曲脑室相对位置;水平翻转在医学上对应人体左右对称,属于相对安全的增强,旋转控制在 10 度以内,避免把脑干这类位置敏感结构转得过分夸张。

4.3 DiceLoss 与 BCE 的组合公式

肿瘤区域在这类 MRI 切片里可能只占整图的 5% 到 15%,纯二值交叉熵会被大面积背景主导,模型很快收敛到“全预测背景”的局部最优。DiceLoss 天然不敏感类别不平衡,它只关注预测掩码和 GT 掩码的交叠比例,所以这里把两个损失加起来。

import torch.nn.functional as F def dice_loss(pred_logits, target, smooth=1.0): pred = torch.sigmoid(pred_logits) num = 2 * (pred * target).sum(dim=(2, 3)) + smooth den = pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) + smooth return 1 - (num / den).mean() def combined_loss(pred_logits, target): bce = F.binary_cross_entropy_with_logits(pred_logits, target) dice = dice_loss(pred_logits, target) return bce + dice

pred_logits是模型输出的未经过 sigmoid 的原始 logits,所以 BCE 用binary_cross_entropy_with_logits,Dice 分支内部自己再做sigmoid。两个损失的梯度量级不同,BCE 大致在 0.1 到 1 之间,DiceLoss 在 0 到 1 之间,直接相加问题不大。如果你发现训练初期损失下降很快、后期震荡,可以改成0.5 * bce + dice让 Dice 主导边界优化。

4.4 训练超参与显存参考

下面这套配置在 416×416 输入、2D U-Net 基础通道数 32 的前提下,12GB 显存的卡可以跑 batch size 8,24GB 显存可以跑到 16。

配置项参考值说明
输入尺寸416×416保持数据集原始分辨率,不额外 resize
batch size8 / 1612GB 显存用 8,24GB 用 16,优先降 batch 别降分辨率
基础通道数32U-Net 第一层卷积通道,逐层翻倍到 512
优化器AdamW, lr=1e-3, wd=1e-4小数据集上比 SGD 收敛稳,权重衰减防过拟合
训练轮数80配合早停,验证 Dice 连续 10 轮不涨就中断
输入归一化除以 255MRI 不是自然图像,不做 ImageNet 均值方差归一化

关于输入归一化多说一句:自然图像的 ImageNet 均值方差统计对 MRI 灰度图没意义,直接img / 255.0让像素落在 0 到 1 区间即可。用 ImageNet 预训练权重时另说,但这里 U-Net 是随机初始化,不需要那套均值方差。

5. 掩码阈值、类别不平衡与数据划分的三个边界问题

5.1 阈值归一化放哪个位置,决定了你后面改模型要动多少代码

我在 2.2 节埋了个问题:mask 可能是 0/1 也可能是 0/255。如果你在数据预处理的早期阶段就把所有掩码统一重写为 0/255 的 PNG,丢给可视化脚本没问题,但喂给神经网络时还要再除一次 255。更麻烦的是,有些标注工具导出时会对多边形边缘做抗锯齿处理,边缘像素值不是单纯的 0 或 255,而是 128 一类的中间灰度。此时mask > 0会把抗锯齿像素全归为前景,边界比真实标注粗一圈;mask > 127会把抗锯齿像素归为背景,边界往里缩一圈。

我一般会先跑一个像素直方图看掩码有没有中间灰度带。如果只有两个峰,阈值写mask > 0最安全;如果出现第三个峰,说明边缘有插值,就要想清楚你自己要“粗半像素”还是“细半像素”的边界。对脑肿瘤这种病理边界本身模糊的数据,这个误差可以忽略,但对血管分割这类极细结构,抗锯齿像素可能比血管主干还宽,处理方式直接决定你最终 IoU 上限。

5.2 肿瘤区域占比先量化再定损失权重

类别不平衡不能靠感觉,先跑统计代码,拿到训练集所有掩码的前景占比分布,再决定要不要给交叉熵加权。下面的脚本扫一遍训练集掩码,输出平均前景比例和分位数。

import cv2 import numpy as np from pathlib import Path def foreground_stats(mask_dir: str): ratios = [] for m in Path(mask_dir).glob("*.*"): mask = cv2.imread(str(m), cv2.IMREAD_GRAYSCALE) if mask is None: continue ratios.append(float((mask > 0).mean())) ratios = np.array(ratios) print(f"avg: {ratios.mean():.4f}, " f"p10: {np.percentile(ratios, 10):.4f}, " f"median: {np.median(ratios):.4f}, " f"p90: {np.percentile(ratios, 90):.4f}") foreground_stats("train/masks")

输出结果如果中位数低于 0.10,说明大部分切片里肿瘤只占十分之一不到,这就是典型的极端不平衡。这种情况下单独的 BCE 会让模型倾向于输出全背景,DiceLoss 的梯度能强制模型关注前景区域。如果中位数在 0.2 以上,前景相对充裕,可以调低 Dice 的权重,比如用bce + 0.5 * dice,让 BCE 帮助稳定像素级分类。

5.3 文件名前缀可能暗示同源切片,划分时要注意泄漏

这个数据集的划分是随机的,train 1632 张、test 240 张已经是定死的。但从文件名结构看,y431、y492、y374 这些前缀很可能来自原始扫描序列的不同帧,同一个前缀的不同图片在病灶纹理、位置、亮度上高度相关。如果你要做严格的泛化评估,随机划分会把同源切片同时放进训练集和测试集,导致 Dice 虚高。检查办法是把测试集文件名前缀和训练集做交集。

import re from pathlib import Path def extract_prefix(filename: str) -> str: m = re.match(r"^(.*?)_(?:jpg|png|jpeg)", filename) return m.group(1) if m else filename train_prefix = {extract_prefix(p.name) for p in Path("train/images").glob("*.jpg")} test_prefix = {extract_prefix(p.name) for p in Path("test/images").glob("*.jpg")} overlap = train_prefix & test_prefix print("overlap prefixes:", len(overlap)) for item in sorted(overlap)[:10]: print(item)

如果 overlap 数量很大,说明你有相当一部分同源切片跨集分布。要缓解,你可以自己按前缀重新划分,把同一前缀的所有切片全部放进训练集或测试集。重新划分后 train 和 test 的数量不再是 1632/240,但泛化指标会更诚实。这里要注意的是,Roboflow 导出时文件名里的 y431 并不保证一定是患者编号,也可能是原始图片名,所以这个检查的意义在于“怀疑并验证”,而不是直接断定泄漏。

5.4 想套 YOLOv8 或检测框架时的格式转换陷阱

热词榜里“yolov8 训练自己的数据集”搜索量不低,不少人是拿这份分割数据往 yolov8-seg 里灌的。YOLOv8 的分割标签要求是归一化的多边形顶点坐标文件,每行一个目标类别加一组 xy 坐标点,不是逐像素掩码。把这份数据的掩码转成 polygon 会遇到一个实际问题:脑肿瘤边界经常带毛刺和细小凸起,转多边形时顶点数量一旦设得太大,训练时损失计算开销猛涨;设得太小,小肿瘤会被拟合成三角形或四边形,形状信息直接丢失。

我的建议是:常用语义分割网络就用逐像素的掩码监督,不要转检测框架。如果你确实要在同一套工作流里做目标检测和分割,可以用 Label Studio 这类标注软件的导出插件重新导出 COCO 格式,或者把掩码做轮廓提取后再根据轮廓面积筛选顶点数。这个过程里最容易出错的是多边形顶点顺序必须为顺时针,否则后处理画 mask 时会得到空洞。

6. 评估大脑肿瘤分割预测:Dice 系数计算与预测蒙版输出

6.1 在测试集上计算 Dice 与 IoU

训练完的模型最后都要在 test 的 240 张图上跑一遍,得到打分。分类任务看 accuracy 在这里没有意义,因为全预测背景也能有 85% 以上的 accuracy。分割任务看 Dice 和 IoU,下面的函数同时输出这两项。

import torch import numpy as np @torch.no_grad() def evaluate_segmentation(model, test_loader, device, thr=0.5): model.eval() dice_list, iou_list = [], [] for img, mask in test_loader: img = img.to(device) mask = mask.to(device) logits = model(img) pred = (torch.sigmoid(logits) > thr).float() inter = (pred * mask).sum(dim=(2, 3)) union = (pred + mask).clamp(max=1).sum(dim=(2, 3)) dice = (2 * inter / union.clamp(min=1e-6)).cpu().numpy() iou = (inter / union.clamp(min=1e-6)).cpu().numpy() dice_list.append(dice) iou_list.append(iou) dice = float(np.concatenate(dice_list).mean()) iou = float(np.concatenate(iou_list).mean()) print(f"Dice: {dice:.4f}, IoU: {iou:.4f}") return dice, iou

union我用(pred + mask).clamp(max=1)计算,这比pred | mask更适合张量运算,clamp 把重叠区域的 2 压回 1,等价于并集。预测阈值thr默认 0.5,但要注意 0.5 不一定是最优,肿瘤边界模糊的样本把阈值提到 0.6 能压掉部分假阳性,降到 0.4 能提高召回,具体取多少要先在自己的验证集上扫一遍。

6.2 输出预测蒙板与 GT 的并排对比图

评估指标只看数字不够,你得亲眼看到预测边界和 GT 差在哪。我把 5.2 节的可视化逻辑复用一下,改成调用模型输出预测掩码,并把预测蒙板用青色画出,和 GT 的红色蒙板在同一张原图上对比。

def save_prediction_overlay(model, test_loader, device, save_dir="pred_vis"): import cv2, matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from pathlib import Path Path(save_dir).mkdir(exist_ok=True) model.eval() count = 0 for img, mask in test_loader: with torch.no_grad(): logits = model(img.to(device)) pred = (torch.sigmoid(logits) > 0.5).float() batch = img.cpu().numpy().transpose(0, 2, 3, 1) for i in range(batch.shape[0]): if count >= 16: return orig = (batch[i] * 255).astype(np.uint8) gt = mask[i, 0].cpu().numpy() pr = pred[i, 0].cpu().numpy() overlay_gt = orig.copy() overlay_gt[gt > 0] = (255, 0, 0) # GT 红色 overlay_pr = orig.copy() overlay_pr[pr > 0] = (0, 255, 0) # 预测 绿色 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(overlay_gt); axes[0].set_title("GT") axes[1].imshow(overlay_pr); axes[1].set_title("Prediction") axes[2].imshow(orig); axes[2].set_title("Origin") for ax in axes: ax.axis("off") plt.tight_layout() plt.savefig(f"{save_dir}/sample_{count:03d}.png", dpi=120, bbox_inches="tight") plt.close(fig) count += 1

保存对比图时,固定随机种子比你想的更关键。如果不固定,每次评估抽到的 16 张样本不同,某一轮抽到一个边界模糊的样本,Dice 下降 0.02,你会误判模型变差了。在脚本开头加上random.seed(42)np.random.seed(42)torch.manual_seed(42),确保多次评估看到的是同一批样本,这样模型对比才有意义。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:55:05

ZYNQ PL驱动AD7606多通道同步采样与FFT频谱分析实战

简介&#xff1a;面向ZYNQ开发者的AD7606数据采集与FFT分析工程包&#xff0c;适合学习可编程逻辑&#xff08;PL&#xff09;与数字信号处理联动的嵌入式开发者。工程完整覆盖从AD7606接口配置、采样时序控制到数据缓冲与快速傅里叶变换的典型流程&#xff0c;可帮助读者掌握基…

作者头像 李华
网站建设 2026/9/10 2:52:13

中式古建场景建模全流程:从阿房宫外景到PBR贴图实战

1. 项目解析&#xff1a;为什么阿房宫是中式场景建模的“试金石”做中式古建外景&#xff0c;绕不开一个核心问题&#xff1a;如何用现代三维技术还原传统木构建筑的灵魂。不少新手接到“中式古代宫殿”需求&#xff0c;第一反应就是去资源站下载现成模型&#xff0c;结果要么面…

作者头像 李华
网站建设 2026/9/10 2:50:22

微信生产级AI模型开源:工业级部署与业务耦合架构解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:47:50

SSM+JSP+Layui电影系统实战:稳定交付与工程落地指南

简介&#xff1a;这是一套基于SSM框架开发的电影在线观看系统完整源码&#xff0c;面向Java Web初学者与中级开发者&#xff0c;适用于课程设计、毕业设计或Web全栈技能实战训练。系统采用JSP前端页面配合Layui UI组件&#xff0c;后端整合Spring、SpringMVC与MyBatis&#xff…

作者头像 李华
网站建设 2026/9/10 2:47:43

CPU PCIe通道不够用?PCIe Switch解决扩展难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华