101 lines
No EOL
4 KiB
Python
101 lines
No EOL
4 KiB
Python
"""MR->MR control for the synthmorph investigation.
|
|
|
|
The benchmark pairs are CT->T1c (out-of-domain for the MRI-trained
|
|
SynthMorph model). To separate the cross-modality effect from the other
|
|
suspects (extent truncation of the CT FOV, min-max normalization), this
|
|
registers two T1c scans of the SAME subject (m6_24V62QQM, 2023-02-23 and
|
|
2023-10-17 -- in-domain, MRI->MRI, same tightly-cropped geometry as the
|
|
benchmark's fixed image) and scores the result with the benchmark scoring
|
|
(MI over the head, ImpactLoss, skull-ring gap) plus the transform
|
|
decomposition against a SimpleITK reference.
|
|
|
|
Usage:
|
|
python scripts/investigate_synthmorph_mm.py [--out results/synthmorph_investigation]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, ROOT)
|
|
|
|
import numpy as np
|
|
import SimpleITK as sitk
|
|
|
|
from src.common import read_nii_img, head_mask_from_image
|
|
from src import registration_pipelines as RP
|
|
from scripts.investigate_synthmorph import (
|
|
_to_affine, _angle_between_rotations, head_centroid_world, BR, M6)
|
|
|
|
KEY_A = "m6_24V62QQM_20230223"
|
|
KEY_B = "m6_24V62QQM_20231017"
|
|
|
|
|
|
def score_pair(img_moving, img_fixed, T):
|
|
"""Benchmark scoring of the warped moving (in the fixed grid) ~ fixed."""
|
|
ha = sitk.GetArrayFromImage(img_fixed).astype("float32")
|
|
wc = sitk.GetArrayFromImage(sitk.Resample(img_moving, img_fixed, T,
|
|
sitk.sitkLinear)).astype("float32")
|
|
gap = M6._skull_ring_gap(wc, ha)
|
|
mi = BR._mi_masked(wc, ha, head_mask_from_image(ha))
|
|
il = BR._impact_loss_value(wc, ha)
|
|
return {"mi": mi, "impact_loss": il, "gap": gap}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", default=os.path.join(ROOT, "results",
|
|
"synthmorph_investigation"))
|
|
ap.add_argument("--gpu", type=int, default=0)
|
|
args = ap.parse_args()
|
|
os.makedirs(args.out, exist_ok=True)
|
|
|
|
labeled, _ = M6.discover()
|
|
bykey = {r["key"]: r for r in labeled}
|
|
a, b = bykey[KEY_A], bykey[KEY_B]
|
|
img_a = read_nii_img(a["img"])
|
|
img_b = read_nii_img(b["img"])
|
|
out = {"pair": [KEY_A, KEY_B], "note": ("same-subject T1c->T1c, 8 months "
|
|
"apart; in-domain control for the "
|
|
"CT->T1c benchmark pairs")}
|
|
|
|
import math
|
|
c_fix = head_centroid_world(img_b)
|
|
T_ref, meta_ref = RP.register_simpleitk(img_a, img_b, threads=16,
|
|
mode="benchmark")
|
|
sref = score_pair(img_a, img_b, T_ref)
|
|
R_ref, t_ref = _to_affine(T_ref)
|
|
th_ref = math.degrees(float(np.clip((np.trace(R_ref) - 1) / 2, -1, 1)))
|
|
out["reference"] = {
|
|
"tool": meta_ref["toolkit"], "R": R_ref.tolist(), "t": t_ref.tolist(),
|
|
"mi": sref["mi"], "impact_loss": sref["impact_loss"], "gap": sref["gap"],
|
|
"rotation_deg": round(th_ref, 2),
|
|
"head_centre_fix": [round(float(x), 1)
|
|
for x in (R_ref @ c_fix + t_ref)],
|
|
}
|
|
|
|
t0 = time.time()
|
|
T_sm, meta_sm = RP.register_synthmorph(img_a, img_b, model="rigid",
|
|
gpu=args.gpu, extent=256)
|
|
ssm = score_pair(img_a, img_b, T_sm)
|
|
R_sm, t_sm = _to_affine(T_sm)
|
|
out["synthmorph_rigid"] = {
|
|
"seconds": round(time.time() - t0, 1),
|
|
"auto_inverted": meta_sm["auto_inverted"],
|
|
"mi": ssm["mi"], "impact_loss": ssm["impact_loss"], "gap": ssm["gap"],
|
|
"rot_err_vs_ref_deg": round(_angle_between_rotations(R_ref, R_sm), 2),
|
|
"head_centre_err_mm": round(
|
|
float(np.linalg.norm((R_sm @ c_fix + t_sm) - (R_ref @ c_fix + t_ref))), 1),
|
|
}
|
|
# no-clip sanity: MR inputs are never clipped by the adapter
|
|
fp = os.path.join(args.out, "partE_mr_mr.json")
|
|
with open(fp, "w") as f:
|
|
json.dump(out, f, indent=1)
|
|
print(json.dumps(out, indent=1))
|
|
print("wrote", fp)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |