File size: 5,776 Bytes
d4700c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""Same held-out cases through several autoencoders, compared where the SSIM scores are: the original scan.

Each model runs in the geometry it was trained with; its reconstruction is de-normalised and mapped back
onto the scan's own 1 mm grid with MONAI's inverse transforms (as compare_autoencoders.py scores it). One
row per case; for each plane the original, then each model, cut through the tumour centre and shown in
one intensity window per row. Each model's foreground SSIM at 1 mm is printed on its axial panel.

python render_same_cases.py <autoencoder app_files> <images dir> <out.png> CASE:SEQUENCE[,CASE:SEQUENCE...] \
    NAME:GEOMETRY:CHECKPOINT:CONFIG_YAML [...]
"""

import os
import sys
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402
import monai.transforms as mt  # noqa: E402
import numpy as np  # noqa: E402
import torch  # noqa: E402
import yaml  # noqa: E402
from monai.data import MetaTensor  # noqa: E402
from monai.networks.nets import AutoencoderKL  # noqa: E402

app, images, out = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3])
cases = [tuple(item.split(":")) for item in sys.argv[4].split(",")]
specs = [spec.split(":") for spec in sys.argv[5:]]
sys.path.insert(0, str(app))
from trainer import foreground_ssim  # noqa: E402

device = torch.device(os.environ.get("DEVICE", "cpu"))
PLANES = (("axial", 2, False), ("coronal", 1, False), ("sagittal", 0, True))
to_ras = mt.Orientation(axcodes="RAS")


def geometry_chain(geometry: str, document: dict) -> mt.Compose:
    shape = document["training"]["spatial_shape"]
    steps = [mt.LoadImaged(keys=["image"], image_only=True), mt.EnsureChannelFirstd(keys=["image"], channel_dim="no_channel"),
             mt.Orientationd(keys=["image"], axcodes="RAS")]
    if geometry == "squash":
        steps.append(mt.Resized(keys=["image"], spatial_size=shape))
    else:
        steps += [mt.Spacingd(keys=["image"], pixdim=document["training"]["voxel_spacing"], mode="bilinear"),
                  mt.ResizeWithPadOrCropd(keys=["image"], spatial_size=shape)]
    return mt.Compose(steps)


def load_model(checkpoint: str, document: dict) -> AutoencoderKL:
    model = AutoencoderKL(**document["autoencoder"]).to(device).eval()
    state = torch.load(checkpoint, map_location="cpu", weights_only=True)
    model.load_state_dict({k.removeprefix("autoencoder."): v for k, v in state.items()})
    return model


def back_to_original(path: Path, chain: mt.Compose, model: AutoencoderKL) -> torch.Tensor:
    data = chain({"image": str(path)})
    grid = data["image"]
    mean, std = grid.mean(), grid.std()
    with torch.no_grad():
        reconstruction = model(((grid - mean) / std).unsqueeze(0).to(device))[0].float().cpu()
    data["image"] = grid.clone().copy_(reconstruction[0] * std + mean)
    return torch.as_tensor(chain.inverse(data)["image"]).float()


def cut(volume: np.ndarray, axis: int, index: int, flip_lr: bool) -> np.ndarray:
    plane = np.rot90(np.take(volume, index, axis=axis))
    return np.fliplr(plane) if flip_lr else plane


models = []
for name, geometry, checkpoint, config in specs:
    document = yaml.safe_load(Path(config).read_text())
    models.append((name, geometry_chain(geometry, document), load_model(checkpoint, document)))

columns = ["original"] + [name for name, _, _ in models]
figure, axes = plt.subplots(len(cases), len(PLANES) * len(columns),
                            figsize=(1.9 * len(PLANES) * len(columns), 2.1 * len(cases)), squeeze=False)
figure.patch.set_facecolor("black")
for row, (case, sequence) in enumerate(cases):
    folder = next(p.parent for p in images.glob(f"*/scans/label_{case}.nii.gz"))
    path = folder / f"input_{sequence}_{case}.nii.gz"
    truth = mt.LoadImage(image_only=True)(str(path))[None]
    label = mt.LoadImage(image_only=True)(str(folder / f"label_{case}.nii.gz"))[None]
    volumes, scores = [to_ras(truth)[0].numpy()], [None]
    for name, chain, model in models:
        back = back_to_original(path, chain, model)
        scores.append(foreground_ssim((back * (truth > 0))[None].float(), torch.as_tensor(truth)[None].float()))
        volumes.append(to_ras(MetaTensor(back, affine=truth.affine))[0].numpy())
        print(f"{case} {sequence} {name}: SSIM at 1 mm {scores[-1]:.3f}", flush=True)
    tumour = to_ras(label)[0].numpy() > 0
    centre = [int(round(c.mean())) for c in np.nonzero(tumour)]
    brain = volumes[0] > 0
    vmax = float(np.percentile(volumes[0][brain], 99.5))
    for p, (view, axis, flip_lr) in enumerate(PLANES):
        mask = cut(brain, axis, centre[axis], flip_lr)
        rows_, cols_ = np.nonzero(mask)
        box = (slice(max(rows_.min() - 4, 0), rows_.max() + 5), slice(max(cols_.min() - 4, 0), cols_.max() + 5))
        for c, (title, volume, score) in enumerate(zip(columns, volumes, scores)):
            handle = axes[row][p * len(columns) + c]
            handle.imshow(cut(volume, axis, centre[axis], flip_lr)[box], cmap="gray", vmin=0, vmax=vmax)
            handle.set_xticks([])
            handle.set_yticks([])
            for spine in handle.spines.values():
                spine.set_visible(False)
            if row == 0:
                handle.set_title(f"{title}\n{view}", color="0.85", fontsize=10)
            if p == 0 and score is not None:
                handle.text(0.03, 0.03, f"SSIM {score:.3f}", transform=handle.transAxes, color="yellow", fontsize=9,
                            bbox={"facecolor": "black", "edgecolor": "none", "alpha": 0.75, "pad": 2})
            if p == 0 and c == 0:
                handle.set_ylabel(f"{case}\n{sequence}", color="0.85", fontsize=10)
figure.tight_layout()
figure.savefig(out, facecolor=figure.get_facecolor(), dpi=110)
print("wrote", out)