BraTS2025(UNETR)

环境

pip install   -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

其中monai的版本过低,不能用0.7

pip install "monai>=0.8.1,<1.0"

torch不能用文件里面的1.9,太老了

预处理管道修改

# ...existing code...
train_transform = transforms.Compose(
    [
        transforms.LoadImaged(keys=["image", "label"]),
        transforms.AddChanneld(keys=["label"]),  # 只对label加通道
        transforms.Orientationd(keys=["image", "label"], axcodes="RAS"),
        transforms.Spacingd(
            keys=["image", "label"], pixdim=(args.space_x, args.space_y, args.space_z), mode=("bilinear", "nearest")
        ),
        transforms.ScaleIntensityRanged(
            keys=["image"], a_min=args.a_min, a_max=args.a_max, b_min=args.b_min, b_max=args.b_max, clip=True
        ),
        transforms.CropForegroundd(keys=["image", "label"], source_key="image"),
        transforms.RandCropByPosNegLabeld(
            keys=["image", "label"],
            label_key="label",
            spatial_size=(args.roi_x, args.roi_y, args.roi_z),
            pos=1,
            neg=1,
            num_samples=4,
            image_key="image",
            image_threshold=0,
        ),
        transforms.RandFlipd(keys=["image", "label"], prob=args.RandFlipd_prob, spatial_axis=0),
        transforms.RandFlipd(keys=["image", "label"], prob=args.RandFlipd_prob, spatial_axis=1),
        transforms.RandFlipd(keys=["image", "label"], prob=args.RandFlipd_prob, spatial_axis=2),
        transforms.RandRotate90d(keys=["image", "label"], prob=args.RandRotate90d_prob, max_k=3),
        transforms.RandScaleIntensityd(keys="image", factors=0.1, prob=args.RandScaleIntensityd_prob),
        transforms.RandShiftIntensityd(keys="image", offsets=0.1, prob=args.RandShiftIntensityd_prob),
        transforms.ToTensord(keys=["image", "label"]),
    ]
)
val_transform = transforms.Compose(
    [
        transforms.LoadImaged(keys=["image", "label"]),
        transforms.AddChanneld(keys=["label"]),  # 只对label加通道
        transforms.Orientationd(keys=["image", "label"], axcodes="RAS"),
        transforms.Spacingd(
            keys=["image", "label"], pixdim=(args.space_x, args.space_y, args.space_z), mode=("bilinear", "nearest")
        ),
        transforms.ScaleIntensityRanged(
            keys=["image"], a_min=args.a_min, a_max=args.a_max, b_min=args.b_min, b_max=args.b_max, clip=True
        ),
        transforms.CropForegroundd(keys=["image", "label"], source_key="image"),
        transforms.ToTensord(keys=["image", "label"]),
    ]
)
# ...existing code...

image不能再加通道,不然会报错

main文件

# ...existing code...
parser.add_argument("--in_channels", default=4, type=int, help="number of input channels")  # 改为4
parser.add_argument("--out_channels", default=5, type=int, help="number of output channels")  # 改为5
# ...existing code...

def main_worker(gpu, args):
    # ...existing code...
    if (args.model_name is None) or args.model_name == "unetr":
        model = UNETR(
            in_channels=args.in_channels,      # 已自动适配
            out_channels=args.out_channels,    # 已自动适配
            img_size=(args.roi_x, args.roi_y, args.roi_z),
            feature_size=args.feature_size,
            hidden_size=args.hidden_size,
            mlp_dim=args.mlp_dim,
            num_heads=args.num_heads,
            pos_embed=args.pos_embed,
            norm_name=args.norm_name,
            conv_block=True,
            res_block=True,
            dropout_rate=args.dropout_rate,
        )
    # ...existing code...
    post_label = AsDiscrete(to_onehot=True, n_classes=args.out_channels)  # 已自动适配
    post_pred = AsDiscrete(argmax=True, to_onehot=True, n_classes=args.out_channels)  # 已自动适配
    # ...existing code...

只需要修改输入输出通道数量

test文件修改

# ...existing code...
parser.add_argument("--in_channels", default=4, type=int, help="number of input channels")  # 改为4
parser.add_argument("--out_channels", default=5, type=int, help="number of output channels")  # 改为5
# ...existing code...

def main():
    # ...existing code...
    with torch.no_grad():
        dice_list_case = []
        for i, batch in enumerate(val_loader):
            val_inputs, val_labels = (batch["image"].cuda(), batch["label"].cuda())
            img_name = batch["image_meta_dict"]["filename_or_obj"][0].split("/")[-1]
            print("Inference on case {}".format(img_name))
            val_outputs = sliding_window_inference(val_inputs, (96, 96, 96), 4, model, overlap=args.infer_overlap)
            val_outputs = torch.softmax(val_outputs, 1).cpu().numpy()
            val_outputs = np.argmax(val_outputs, axis=1).astype(np.uint8)
            val_labels = val_labels.cpu().numpy()[:, 0, :, :, :]
            dice_list_sub = []
            for i in range(1, args.out_channels):  # 改为类别数
                organ_Dice = dice(val_outputs[0] == i, val_labels[0] == i)
                dice_list_sub.append(organ_Dice)
            mean_dice = np.mean(dice_list_sub)
            print("Mean Organ Dice: {}".format(mean_dice))
            dice_list_case.append(mean_dice)
        print("Overall Mean Dice: {}".format(np.mean(dice_list_case)))
# ...existing code...

也是修改通道数

roi_xyz参数修改

全部改到64比较安全,否则报错

GT0问题处理

import json

import nibabel as nib

import os

  

json_path = "/data/coding/research-contributions-main/UNETR/BTCV/dataset_monai.json"

  

with open(json_path, "r") as f:

    data = json.load(f)

  

# 兼容不同json结构

if isinstance(data, dict) and "training" in data:

    items = data["training"]

elif isinstance(data, list):

    items = data

else:

    raise RuntimeError("Unknown dataset_monai.json structure")

  

for item in items:

    label_path = item["label"] if isinstance(item, dict) else item["label"]

    if not os.path.isabs(label_path):

        label_path = os.path.join(os.path.dirname(json_path), label_path)

    if not os.path.exists(label_path):

        print(f"Label file not found: {label_path}")

        continue

    img = nib.load(label_path)

    arr = img.get_fdata()

    if arr.max() == 0:

        print(f"All-zero label: {label_path}")

把有问题的样本删了
预处理过后可能仍然产生全 0, 在 trainer改一下直接跳过不训练

def train_epoch(model, loader, optimizer, scaler, epoch, loss_func, args):
    model.train()
    start_time = time.time()
    run_loss = AverageMeter()
    for idx, batch_data in enumerate(loader):
        batch_start = time.time()
        try:
            if isinstance(batch_data, list):
                data, target = batch_data
            else:
                data, target = batch_data["image"], batch_data["label"]
            data, target = data.cuda(args.rank), target.cuda(args.rank)
            for param in model.parameters():
                param.grad = None
            with autocast(enabled=args.amp):
                logits = model(data)
                loss = loss_func(logits, target)
            if args.amp:
                scaler.scale(loss).backward()
                scaler.step(optimizer)
                scaler.update()
            else:
                loss.backward()
                optimizer.step()
            if args.distributed:
                loss_list = distributed_all_gather([loss], out_numpy=True, is_valid=idx < loader.sampler.valid_length)
                run_loss.update(
                    np.mean(np.mean(np.stack(loss_list, axis=0), axis=0), axis=0), n=args.batch_size * args.world_size
                )
            else:
                run_loss.update(loss.item(), n=args.batch_size)
            if args.rank == 0:
                print(
                    "Epoch {}/{} {}/{}".format(epoch, args.max_epochs, idx, len(loader)),
                    "loss: {:.4f}".format(run_loss.avg),
                    "time {:.2f}s".format(time.time() - start_time),
                )
            batch_end = time.time()
            print(f"Epoch {epoch} Batch {idx} time: {batch_end - batch_start:.2f}s")
            start_time = time.time()
        except RuntimeError as e:
            if "all zero" in str(e) or "high <= 0" in str(e):
                print(f"Skip batch {idx} due to all-zero label patch or sampling error.")
                continue
            else:
                raise e
    for param in model.parameters():
        param.grad = None
    return run_loss.avg

后面 1094002 又有问题, 有个 6 的标签值
写个脚本把他从 json 文件删了