aicentreflip's picture
Publish the FLIP tutorial models
d4700c7 verified
Raw History Blame Contribute Delete
5.78 kB
"""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)