Valentin Boussot commited on
Commit ·
1e7ef81
1
Parent(s): a45bf82
Remove the CBCT_CT_TS preset
Browse files- CBCT_CT_TS/Evaluation_with_fid.yml +0 -22
- CBCT_CT_TS/Evaluation_with_images.yml +0 -35
- CBCT_CT_TS/Evaluation_with_seg.yml +0 -29
- CBCT_CT_TS/Model.py +0 -301
- CBCT_CT_TS/ParameterMap_CBCT_generic_TS.txt +0 -152
- CBCT_CT_TS/Prediction.yml +0 -118
- CBCT_CT_TS/Uncertainty.yml +0 -24
- CBCT_CT_TS/app.json +0 -96
- CBCT_CT_TS/elastix_engine.py +0 -386
- CBCT_CT_TS/install.py +0 -325
- CBCT_CT_TS/model.pt +0 -3
- README.md +0 -1
CBCT_CT_TS/Evaluation_with_fid.yml
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
Evaluator:
|
| 2 |
-
metrics:
|
| 3 |
-
FixedFid:
|
| 4 |
-
targets_criterions:
|
| 5 |
-
MovingFid:
|
| 6 |
-
criterions_loader:
|
| 7 |
-
TRE: {}
|
| 8 |
-
Dataset:
|
| 9 |
-
groups_src:
|
| 10 |
-
Volume_0:
|
| 11 |
-
groups_dest:
|
| 12 |
-
FixedFid:
|
| 13 |
-
transforms: None
|
| 14 |
-
Reference_0:
|
| 15 |
-
groups_dest:
|
| 16 |
-
MovingFid:
|
| 17 |
-
transforms: None
|
| 18 |
-
subset: None
|
| 19 |
-
dataset_filenames:
|
| 20 |
-
- ./Dataset:mha
|
| 21 |
-
validation: None
|
| 22 |
-
train_name: ImpactReg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/Evaluation_with_images.yml
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
Evaluator:
|
| 2 |
-
metrics:
|
| 3 |
-
FixedImage:
|
| 4 |
-
targets_criterions:
|
| 5 |
-
MovingImage;Mask:
|
| 6 |
-
criterions_loader:
|
| 7 |
-
MAESaveMap:
|
| 8 |
-
reduction: mean
|
| 9 |
-
dataset: ./Evaluations/ImpactReg/Output:mha
|
| 10 |
-
group: MAE_map
|
| 11 |
-
Dataset:
|
| 12 |
-
groups_src:
|
| 13 |
-
Volume_0:
|
| 14 |
-
groups_dest:
|
| 15 |
-
FixedImage:
|
| 16 |
-
transforms:
|
| 17 |
-
TensorCast:
|
| 18 |
-
dtype: float32
|
| 19 |
-
Reference_0:
|
| 20 |
-
groups_dest:
|
| 21 |
-
MovingImage:
|
| 22 |
-
transforms:
|
| 23 |
-
TensorCast:
|
| 24 |
-
dtype: float32
|
| 25 |
-
Mask_0:
|
| 26 |
-
groups_dest:
|
| 27 |
-
Mask:
|
| 28 |
-
transforms:
|
| 29 |
-
TensorCast:
|
| 30 |
-
dtype: uint8
|
| 31 |
-
subset: None
|
| 32 |
-
dataset_filenames:
|
| 33 |
-
- ./Dataset:mha
|
| 34 |
-
validation: None
|
| 35 |
-
train_name: ImpactReg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/Evaluation_with_seg.yml
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
Evaluator:
|
| 2 |
-
metrics:
|
| 3 |
-
FixedSeg:
|
| 4 |
-
targets_criterions:
|
| 5 |
-
MovingSeg:
|
| 6 |
-
criterions_loader:
|
| 7 |
-
DiceSaveMap:
|
| 8 |
-
labels: None
|
| 9 |
-
dataset: ./Evaluations/ImpactReg/Output:mha
|
| 10 |
-
group: Seg_MAE_map
|
| 11 |
-
Dataset:
|
| 12 |
-
groups_src:
|
| 13 |
-
Volume_0:
|
| 14 |
-
groups_dest:
|
| 15 |
-
FixedSeg:
|
| 16 |
-
transforms:
|
| 17 |
-
TensorCast:
|
| 18 |
-
dtype: uint8
|
| 19 |
-
Reference_0:
|
| 20 |
-
groups_dest:
|
| 21 |
-
MovingSeg:
|
| 22 |
-
transforms:
|
| 23 |
-
TensorCast:
|
| 24 |
-
dtype: uint8
|
| 25 |
-
subset: None
|
| 26 |
-
dataset_filenames:
|
| 27 |
-
- ./Dataset:mha
|
| 28 |
-
validation: None
|
| 29 |
-
train_name: ImpactReg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/Model.py
DELETED
|
@@ -1,301 +0,0 @@
|
|
| 1 |
-
# Copyright (c) 2025 Valentin Boussot
|
| 2 |
-
#
|
| 3 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
-
# you may not use this file except in compliance with the License.
|
| 5 |
-
# You may obtain a copy of the License at
|
| 6 |
-
#
|
| 7 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
-
#
|
| 9 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
-
# See the License for the specific language governing permissions and
|
| 13 |
-
# limitations under the License.
|
| 14 |
-
#
|
| 15 |
-
# SPDX-License-Identifier: Apache-2.0
|
| 16 |
-
|
| 17 |
-
"""Registration as a KonfAI model: the config -> elastix parameter-map mapping + the ``add_module`` graph.
|
| 18 |
-
|
| 19 |
-
``RegistrationNet`` wires ``ElastixRegistration`` (fixed = branch 0, moving = branch 1, fixed/moving masks =
|
| 20 |
-
2/3) and splits its output into ``MovedImage`` / ``DisplacementField`` on the fixed grid. This module owns
|
| 21 |
-
the MAPPING — the per-resolution model matrix (``resolutions``) turned into IMPACT parameter-map lines, and
|
| 22 |
-
the config schema (``ModelSpec`` / ``ResolutionSpec``). The elastix RUNTIME (binary install, model download,
|
| 23 |
-
subprocess, progress) lives in ``elastix_engine.py`` and is imported only when the graph is built.
|
| 24 |
-
|
| 25 |
-
A UI reads the tuning knobs straight from the TYPES below: ``Literal`` (a fixed set),
|
| 26 |
-
``Annotated[.., Range]`` (numeric bounds), ``Annotated[str, Choices(...)]`` (a resolver the app owns).
|
| 27 |
-
|
| 28 |
-
NOTE: do NOT add ``from __future__ import annotations`` — KonfAI's config engine reads runtime annotations
|
| 29 |
-
(``get_origin``); PEP 563 stringized annotations break arg resolution.
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
import json
|
| 33 |
-
import os
|
| 34 |
-
import re
|
| 35 |
-
from dataclasses import dataclass, field
|
| 36 |
-
from pathlib import Path
|
| 37 |
-
from typing import Annotated, Literal
|
| 38 |
-
|
| 39 |
-
import torch
|
| 40 |
-
from huggingface_hub import hf_hub_download
|
| 41 |
-
from konfai.network import network
|
| 42 |
-
from konfai.utils.config import Choices, Range
|
| 43 |
-
|
| 44 |
-
# IMPACT field docs: https://github.com/vboussot/ImpactLoss/tree/main/ParameterMaps
|
| 45 |
-
# A model's FIXED props (dimension / channels / FOV formula) come from the registry (models.json on
|
| 46 |
-
# VBoussot/impact-torchscript-models); the config carries the FREE knobs (models per resolution, voxel size,
|
| 47 |
-
# iterations, per-model weights/mask/subset/pca/distance) and the global ``mode``.
|
| 48 |
-
_IMPACT_MODELS_REGISTRY = "VBoussot/impact-torchscript-models:models.json"
|
| 49 |
-
|
| 50 |
-
# ``2^l+3`` plateaus: segmenter layers 7-8 share layer 6's receptive field. Deeper configs should run
|
| 51 |
-
# Static anyway; in Jacobian we clamp ``l`` to this plateau.
|
| 52 |
-
_FOV_RAMP_MAX_LAYER = 6
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def registry_choices() -> list[str]:
|
| 56 |
-
"""The ``ref`` picker's values — model refs (``repo:path``) from the registry the engine already fetches
|
| 57 |
-
(offline-first). A user may still point ``ref`` at a local model."""
|
| 58 |
-
repo = _IMPACT_MODELS_REGISTRY.split(":", 1)[0]
|
| 59 |
-
return [f"{repo}:{key}" for key in load_models_registry()]
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def _num(x: object) -> str:
|
| 63 |
-
"""Format a number the elastix way: no trailing '.0' (6.0 -> '6', 0.2 -> '0.2')."""
|
| 64 |
-
return "%g" % float(x)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
@dataclass
|
| 68 |
-
class ModelSpec:
|
| 69 |
-
"""One feature model at one resolution (several may share a resolution). ``ref`` picks the model; the
|
| 70 |
-
rest are its per-(resolution, model) knobs. Dimension / channels / FOV are intrinsic — from the registry
|
| 71 |
-
(``models.json``) keyed by ``ref`` — never tuned."""
|
| 72 |
-
|
| 73 |
-
ref: Annotated[str, Choices(registry_choices)]
|
| 74 |
-
voxel_size: list[float] = field(default_factory=list)
|
| 75 |
-
layers_weight: list[float] = field(default_factory=lambda: [1.0])
|
| 76 |
-
subset_features: Annotated[int, Range(0, 1000)] = 0
|
| 77 |
-
pca: Annotated[int, Range(0, 100)] = 0
|
| 78 |
-
distance: Literal["L1", "L2", "Dice", "Cosine", "NCC"] = "L1"
|
| 79 |
-
layers_mask: str = ""
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
@dataclass
|
| 83 |
-
class ResolutionSpec:
|
| 84 |
-
"""One elastix resolution level: its iteration budget and the (self-configured) models compared there."""
|
| 85 |
-
|
| 86 |
-
max_iterations: Annotated[int, Range(1, 100000)]
|
| 87 |
-
models: dict[str, ModelSpec]
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def _sorted_specs(mapping: dict) -> list:
|
| 91 |
-
"""dict keyed by string indices ('0','1',...) -> values in numeric order."""
|
| 92 |
-
return [mapping[k] for k in sorted(mapping, key=lambda key: int(key))]
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def load_models_registry(ref: str = _IMPACT_MODELS_REGISTRY) -> dict:
|
| 96 |
-
"""Load models.json (the fixed params per model) from the model repo on Hugging Face.
|
| 97 |
-
|
| 98 |
-
The registry is NOT bundled with the preset. ``KONFAI_IMPACT_MODELS_REGISTRY`` (a local path) wins for
|
| 99 |
-
dev/offline; otherwise ``ref`` must be a ``repo:file`` Hugging Face reference.
|
| 100 |
-
"""
|
| 101 |
-
local = os.environ.get("KONFAI_IMPACT_MODELS_REGISTRY", "")
|
| 102 |
-
if local:
|
| 103 |
-
path = Path(local)
|
| 104 |
-
elif ":" in ref:
|
| 105 |
-
repo, filename = ref.split(":", 1)
|
| 106 |
-
path = Path(hf_hub_download(repo_id=repo, filename=filename, repo_type="model")) # nosec B615
|
| 107 |
-
else:
|
| 108 |
-
raise ValueError(
|
| 109 |
-
f"models_registry '{ref}' must be a 'repo:file' Hugging Face reference (the registry is fetched "
|
| 110 |
-
f"from HF, not bundled) — or set KONFAI_IMPACT_MODELS_REGISTRY to a local file for offline use."
|
| 111 |
-
)
|
| 112 |
-
return json.loads(path.read_text(encoding="utf-8"))
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def _model_key(ref: str) -> str:
|
| 116 |
-
"""Registry key / staged relative path = the model file within the repo (strip a 'repo:' prefix)."""
|
| 117 |
-
return ref.split(":", 1)[1] if ":" in ref else ref
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def _deepest_active_layer(layers_mask: str) -> int:
|
| 121 |
-
"""Deepest (largest-FOV) layer selected by ``layers_mask``, as a 0-based index.
|
| 122 |
-
|
| 123 |
-
A model returns its layers shallow->deep; ``layers_mask`` has one char per returned layer, position ``i``
|
| 124 |
-
== ``layer_i``, ``'1'`` = selected. In Jacobian the patch must cover the DEEPEST selected layer's
|
| 125 |
-
receptive field, so the FOV is governed by the rightmost ``'1'``.
|
| 126 |
-
"""
|
| 127 |
-
mask = layers_mask.strip().strip('"')
|
| 128 |
-
active = [i for i, char in enumerate(mask) if char == "1"]
|
| 129 |
-
if not active:
|
| 130 |
-
raise ValueError(f"LayersMask '{layers_mask}' selects no layer; cannot derive the model FOV.")
|
| 131 |
-
return max(active)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def _fov_value(fov: dict, layers_mask: str) -> int:
|
| 135 |
-
"""Evaluate a model's field-of-view (in voxels) from its registry ``fov`` spec.
|
| 136 |
-
|
| 137 |
-
Formulas (model repo, https://huggingface.co/VBoussot/impact-torchscript-models):
|
| 138 |
-
``2*r*d+1`` MIND, from radius ``r`` / dilation ``d`` (R1D2 -> 5);
|
| 139 |
-
``2^l+3`` TotalSegmentator / MRSegmentator, ``l`` = deepest layer picked by ``layers_mask``, clamped
|
| 140 |
-
to the receptive-field plateau ``_FOV_RAMP_MAX_LAYER`` (layers 7-8 -> layer 6);
|
| 141 |
-
a bare int a fixed FOV (SAM2.1 -> 29, DINOv2 -> 14);
|
| 142 |
-
``Global`` Anatomix — whole-image only (Static); no finite Jacobian patch -> error.
|
| 143 |
-
An explicit ``value`` in the spec is honoured as a precomputed shortcut.
|
| 144 |
-
"""
|
| 145 |
-
formula = str(fov.get("formula", "")).strip()
|
| 146 |
-
key = re.sub(r"\s+", "", formula).lower()
|
| 147 |
-
if key.isdigit():
|
| 148 |
-
return int(key)
|
| 149 |
-
if key == "2*r*d+1":
|
| 150 |
-
return 2 * int(fov["r"]) * int(fov["d"]) + 1
|
| 151 |
-
if key == "2^l+3":
|
| 152 |
-
return 2 ** min(_deepest_active_layer(layers_mask), _FOV_RAMP_MAX_LAYER) + 3
|
| 153 |
-
if "global" in key:
|
| 154 |
-
raise ValueError(f"model FOV '{formula}' is whole-image only (Static); it has no Jacobian patch size.")
|
| 155 |
-
if fov.get("value") is not None:
|
| 156 |
-
return int(fov["value"])
|
| 157 |
-
raise ValueError(f"cannot evaluate model FOV formula '{formula}'.")
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def _patch_size(mode: str, entry: dict, layers_mask: str) -> str:
|
| 161 |
-
"""PatchSize from the model FOV, one token per model axis (2D -> 2 tokens, 3D -> 3): Static -> whole
|
| 162 |
-
image (all zeros); Jacobian -> the evaluated FOV per axis. A 2D+3D mix at a resolution concatenates,
|
| 163 |
-
e.g. ``29 29 11 11 11`` (SAM 2D + TS 3D), matching IMPACT."""
|
| 164 |
-
dim = int(entry.get("dimension", 3))
|
| 165 |
-
if mode.strip().strip('"').lower() != "jacobian":
|
| 166 |
-
return " ".join(["0"] * dim)
|
| 167 |
-
fov = _fov_value(entry.get("fov", {}), layers_mask)
|
| 168 |
-
return " ".join([str(fov)] * dim)
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def generate_impact_parameter_map(template_text: str, resolutions: dict, registry: dict, mode: str = "Static") -> str:
|
| 172 |
-
"""Rewrite the resolution-dependent lines of ``template_text`` from the model matrix ``resolutions``.
|
| 173 |
-
|
| 174 |
-
Regenerated: MaximumNumberOfIterations, NumberOfResolutions, Fixed/MovingImagePyramidRescaleSchedule,
|
| 175 |
-
ImpactMode, and the whole ImpactXxxK block; every other line is kept verbatim. N (number of resolutions)
|
| 176 |
-
is deduced from the config. ``mode`` drives PatchSize: Static -> ``0 0 0``; Jacobian -> the per-model FOV
|
| 177 |
-
from the registry formula and the cell's ``layers_mask``.
|
| 178 |
-
"""
|
| 179 |
-
res = _sorted_specs(resolutions)
|
| 180 |
-
n = len(res)
|
| 181 |
-
mode_clean = mode.strip().strip('"') or "Static"
|
| 182 |
-
|
| 183 |
-
impact: list[str] = []
|
| 184 |
-
for k, r in enumerate(res):
|
| 185 |
-
models = _sorted_specs(r.models)
|
| 186 |
-
entries = [registry[_model_key(m.ref)] for m in models]
|
| 187 |
-
|
| 188 |
-
def row(stem: str, values: list[str]) -> None:
|
| 189 |
-
impact.append(f"(Impact{stem}{k} " + " ".join(values) + ")")
|
| 190 |
-
|
| 191 |
-
# From the registry ONLY the 3 truly model-fixed props (Dimension, NumberOfChannels, PatchSize = the
|
| 192 |
-
# model FOV); everything else is a per-model knob taken straight from the cell.
|
| 193 |
-
row("ModelsPath", [f'"{_model_key(m.ref)}"' for m in models])
|
| 194 |
-
row("Dimension", [e["dimension"] for e in entries])
|
| 195 |
-
row("NumberOfChannels", [e["numberofchannels"] for e in entries])
|
| 196 |
-
row("PatchSize", [_patch_size(mode_clean, e, m.layers_mask) for e, m in zip(entries, models)])
|
| 197 |
-
row("VoxelSize", [" ".join(_num(v) for v in m.voxel_size) for m in models])
|
| 198 |
-
row("LayersMask", [f'"{m.layers_mask}"' for m in models])
|
| 199 |
-
row("SubsetFeatures", [str(m.subset_features) for m in models])
|
| 200 |
-
row("PCA", [str(m.pca) for m in models])
|
| 201 |
-
row("Distance", [f'"{m.distance}"' for m in models])
|
| 202 |
-
row("LayersWeight", [" ".join(_num(w) for w in m.layers_weight) for m in models])
|
| 203 |
-
impact.append("") # blank line between resolutions, mirroring the reference maps
|
| 204 |
-
|
| 205 |
-
# The per-resolution block is the contiguous span from the first to the last ``Impact<name><k>`` line
|
| 206 |
-
# (inner blanks fall inside it). Replace the whole span at its first line so reference blanks aren't kept.
|
| 207 |
-
lines = template_text.splitlines()
|
| 208 |
-
indexed = [(re.match(r"^\s*\((\S+?)\s+(.*?)\)\s*$", ln), ln) for ln in lines]
|
| 209 |
-
block_rows = [i for i, (m, _) in enumerate(indexed) if m and re.match(r"^Impact[A-Za-z]+\d+$", m.group(1))]
|
| 210 |
-
block_lo, block_hi = (block_rows[0], block_rows[-1]) if block_rows else (-1, -2)
|
| 211 |
-
|
| 212 |
-
out: list[str] = []
|
| 213 |
-
for i, (m, line) in enumerate(indexed):
|
| 214 |
-
key = m.group(1) if m else None
|
| 215 |
-
if block_lo <= i <= block_hi:
|
| 216 |
-
if i == block_lo: # replace the whole span at its first line, drop the rest (incl. inner blanks)
|
| 217 |
-
out.extend(impact[:-1])
|
| 218 |
-
elif key == "MaximumNumberOfIterations":
|
| 219 |
-
out.append("(MaximumNumberOfIterations " + " ".join(_num(r.max_iterations) for r in res) + ")")
|
| 220 |
-
elif key == "NumberOfResolutions":
|
| 221 |
-
out.append(f"(NumberOfResolutions {n})")
|
| 222 |
-
elif key in ("FixedImagePyramidRescaleSchedule", "MovingImagePyramidRescaleSchedule"):
|
| 223 |
-
out.append(f"({key} " + " ".join(["1"] * 3 * n) + ")")
|
| 224 |
-
elif key == "ImpactMode":
|
| 225 |
-
out.append(f'(ImpactMode "{mode_clean}")')
|
| 226 |
-
else:
|
| 227 |
-
out.append(line)
|
| 228 |
-
return "\n".join(out)
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
class ChannelSelect(torch.nn.Module):
|
| 232 |
-
"""Select a channel slice ``[start:stop]`` (splits the registration output into moved / DVF)."""
|
| 233 |
-
|
| 234 |
-
def __init__(self, start: int, stop: int) -> None:
|
| 235 |
-
super().__init__()
|
| 236 |
-
self._start = start
|
| 237 |
-
self._stop = stop
|
| 238 |
-
|
| 239 |
-
def forward(self, tensor: torch.Tensor) -> torch.Tensor:
|
| 240 |
-
return tensor[:, self._start : self._stop]
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
class RegistrationNet(network.Network):
|
| 244 |
-
"""Pairwise registration as an ``add_module`` graph (fixed = branch 0, moving = branch 1, fixed mask = 2,
|
| 245 |
-
moving mask = 3; masks restrict the metric, whole-image = no restriction).
|
| 246 |
-
|
| 247 |
-
Outputs (both on the fixed grid): ``MovedImage`` (moving resampled onto fixed) and ``DisplacementField``
|
| 248 |
-
(the dim-component displacement field, mm). ``ElastixRegistration`` produces both channel-stacked; two
|
| 249 |
-
``ChannelSelect`` modules split them. Output geometry is attached by the predictor via
|
| 250 |
-
``same_as_group: Volume_0:Fixed``.
|
| 251 |
-
"""
|
| 252 |
-
|
| 253 |
-
def __init__(
|
| 254 |
-
self,
|
| 255 |
-
optimizer: network.OptimizerLoader = network.OptimizerLoader(),
|
| 256 |
-
schedulers: dict[str, network.LRSchedulersLoader] = {
|
| 257 |
-
"default:ReduceLROnPlateau": network.LRSchedulersLoader(0)
|
| 258 |
-
},
|
| 259 |
-
outputs_criterions: dict[str, network.TargetCriterionsLoader] = {"default": network.TargetCriterionsLoader()},
|
| 260 |
-
engine: str = "elastix",
|
| 261 |
-
parameter_maps: list[str] = [],
|
| 262 |
-
max_iterations: Annotated[int, Range(0, 100000)] = 0,
|
| 263 |
-
final_grid_spacing: Annotated[float, Range(0.0, 100.0)] = 0.0,
|
| 264 |
-
subset_features: Annotated[int, Range(0, 1000)] = 0,
|
| 265 |
-
spatial_samples: Annotated[int, Range(0, 100000)] = 0,
|
| 266 |
-
parameter_overrides: list[str] = [],
|
| 267 |
-
resolutions: dict[str, ResolutionSpec] = {},
|
| 268 |
-
mode: Literal["Static", "Jacobian"] = "Static",
|
| 269 |
-
) -> None:
|
| 270 |
-
# The registration is fully described by ``resolutions`` (config = source of truth): each resolution
|
| 271 |
-
# lists its self-configured models; the download list is derived from the cells. Global knobs override
|
| 272 |
-
# the generated map (final_grid_spacing -> FinalGridSpacingInPhysicalUnits mm, spatial_samples ->
|
| 273 |
-
# NumberOfSpatialSamples, parameter_overrides 'Key=value'). Empty ``resolutions`` = an intensity-only
|
| 274 |
-
# preset (fixed maps + overrides). The elastix runtime is imported here (heavy: torch/sitk/subprocess).
|
| 275 |
-
from elastix_engine import ElastixRegistration
|
| 276 |
-
|
| 277 |
-
super().__init__(
|
| 278 |
-
in_channels=1,
|
| 279 |
-
optimizer=optimizer,
|
| 280 |
-
schedulers=schedulers,
|
| 281 |
-
outputs_criterions=outputs_criterions,
|
| 282 |
-
dim=3,
|
| 283 |
-
)
|
| 284 |
-
self.add_module(
|
| 285 |
-
"Registration",
|
| 286 |
-
ElastixRegistration(
|
| 287 |
-
engine,
|
| 288 |
-
parameter_maps,
|
| 289 |
-
max_iterations,
|
| 290 |
-
final_grid_spacing,
|
| 291 |
-
subset_features,
|
| 292 |
-
spatial_samples,
|
| 293 |
-
parameter_overrides,
|
| 294 |
-
resolutions,
|
| 295 |
-
mode,
|
| 296 |
-
),
|
| 297 |
-
in_branch=[0, 1, 2, 3],
|
| 298 |
-
out_branch=["registration"],
|
| 299 |
-
)
|
| 300 |
-
self.add_module("MovedImage", ChannelSelect(0, 1), in_branch=["registration"], out_branch=["moved"])
|
| 301 |
-
self.add_module("DisplacementField", ChannelSelect(1, 4), in_branch=["registration"], out_branch=["dvf"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/ParameterMap_CBCT_generic_TS.txt
DELETED
|
@@ -1,152 +0,0 @@
|
|
| 1 |
-
(MaximumNumberOfIterations 300 300 250 200)
|
| 2 |
-
(NumberOfSpatialSamples 2000)
|
| 3 |
-
(Transform "RecursiveBSplineTransform")
|
| 4 |
-
(NumberOfResolutions 4)
|
| 5 |
-
(FinalGridSpacingInPhysicalUnits 14)
|
| 6 |
-
(FixedImagePyramid "FixedGenericImagePyramid")
|
| 7 |
-
(MovingImagePyramid "MovingGenericImagePyramid")
|
| 8 |
-
(FixedImagePyramidRescaleSchedule 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)
|
| 9 |
-
(MovingImagePyramidRescaleSchedule 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)
|
| 10 |
-
// (GridSpacingSchedule 10.000000 5.000000 2.000000 1.000000)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
(ImpactModelsPath0 "TS/M852.pt")
|
| 15 |
-
(ImpactDimension0 3)
|
| 16 |
-
(ImpactNumberOfChannels0 1)
|
| 17 |
-
(ImpactPatchSize0 0 0 0)
|
| 18 |
-
(ImpactVoxelSize0 3 3 3)
|
| 19 |
-
(ImpactLayersMask0 "0000001")
|
| 20 |
-
(ImpactSubsetFeatures0 64)
|
| 21 |
-
(ImpactPCA0 0)
|
| 22 |
-
(ImpactDistance0 "Dice")
|
| 23 |
-
(ImpactLayersWeight0 1)
|
| 24 |
-
|
| 25 |
-
(ImpactModelsPath1 "TS/M850.pt")
|
| 26 |
-
(ImpactDimension1 3)
|
| 27 |
-
(ImpactNumberOfChannels1 1)
|
| 28 |
-
(ImpactPatchSize1 0 0 0)
|
| 29 |
-
(ImpactVoxelSize1 3 3 3)
|
| 30 |
-
(ImpactLayersMask1 "00000001")
|
| 31 |
-
(ImpactSubsetFeatures1 64)
|
| 32 |
-
(ImpactPCA1 0)
|
| 33 |
-
(ImpactDistance1 "Dice")
|
| 34 |
-
(ImpactLayersWeight1 1)
|
| 35 |
-
|
| 36 |
-
(ImpactModelsPath2 "TS/M850.pt")
|
| 37 |
-
(ImpactDimension2 3)
|
| 38 |
-
(ImpactNumberOfChannels2 1)
|
| 39 |
-
(ImpactPatchSize2 0 0 0)
|
| 40 |
-
(ImpactVoxelSize2 2 2 3)
|
| 41 |
-
(ImpactLayersMask2 "01000001")
|
| 42 |
-
(ImpactSubsetFeatures2 64 64)
|
| 43 |
-
(ImpactPCA2 0 0)
|
| 44 |
-
(ImpactDistance2 "L1" "Dice")
|
| 45 |
-
(ImpactLayersWeight2 0.3 0.7)
|
| 46 |
-
|
| 47 |
-
(ImpactModelsPath3 "TS/M850.pt")
|
| 48 |
-
(ImpactDimension3 3)
|
| 49 |
-
(ImpactNumberOfChannels3 1)
|
| 50 |
-
(ImpactPatchSize3 0 0 0)
|
| 51 |
-
(ImpactVoxelSize3 2 2 3)
|
| 52 |
-
(ImpactLayersMask3 "01000001")
|
| 53 |
-
(ImpactSubsetFeatures3 64 64)
|
| 54 |
-
(ImpactPCA3 0 0)
|
| 55 |
-
(ImpactDistance3 "L1" "Dice")
|
| 56 |
-
(ImpactLayersWeight3 0.5 0.5)
|
| 57 |
-
|
| 58 |
-
(ImpactModelsPath4 "TS/M850.pt")
|
| 59 |
-
(ImpactDimension4 3)
|
| 60 |
-
(ImpactNumberOfChannels4 1)
|
| 61 |
-
(ImpactPatchSize4 0 0 0)
|
| 62 |
-
(ImpactVoxelSize4 2 2 3)
|
| 63 |
-
(ImpactLayersMask4 "01000000")
|
| 64 |
-
(ImpactSubsetFeatures4 64)
|
| 65 |
-
(ImpactPCA4 0)
|
| 66 |
-
(ImpactDistance4 "L1")
|
| 67 |
-
(ImpactLayersWeight4 1)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
(ImpactUseMixedPrecision "true")
|
| 71 |
-
(ImpactFeaturesMapUpdateInterval -1)
|
| 72 |
-
(ImpactWriteFeatureMaps "false")
|
| 73 |
-
(ImpactMode "Static")
|
| 74 |
-
(ImpactGPU 0)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
(Metric "Impact" "AdvancedMattesMutualInformation" "TransformBendingEnergyPenalty")
|
| 78 |
-
(Metric0Weight 1)
|
| 79 |
-
(Metric1Weight 0.4)
|
| 80 |
-
(Metric2Weight 10)
|
| 81 |
-
|
| 82 |
-
// imageTypes
|
| 83 |
-
(FixedInternalImagePixelType "float")
|
| 84 |
-
(MovingInternalImagePixelType "float")
|
| 85 |
-
(UseDirectionCosines "true")
|
| 86 |
-
|
| 87 |
-
// components
|
| 88 |
-
(Registration "MultiMetricMultiResolutionRegistration")
|
| 89 |
-
(BSplineTransformSplineOrder 3)
|
| 90 |
-
(UseCyclicTransform "false")
|
| 91 |
-
|
| 92 |
-
// transform
|
| 93 |
-
(AutomaticTransformInitialization "false")
|
| 94 |
-
(AutomaticTransformInitializationMethod "GeometricalCenter")
|
| 95 |
-
(AutomaticScalesEstimation "true")
|
| 96 |
-
(HowToCombineTransforms "Compose")
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
// optimizer
|
| 100 |
-
(Optimizer "AdaptiveStochasticGradientDescent")
|
| 101 |
-
(MaximumNumberOfSamplingAttempts 8)
|
| 102 |
-
(UseAdaptiveStepSizes "true")
|
| 103 |
-
(UseMultiThreadingForMetrics "true")
|
| 104 |
-
(ASGDParameterEstimationMethod "DisplacementDistribution")
|
| 105 |
-
//(MaximumStepLength 0.6602)
|
| 106 |
-
(SigmoidInitialTime 0.0)
|
| 107 |
-
(NoiseCompensation "true")
|
| 108 |
-
(NumberOfSamplesForExactGradient 4096)
|
| 109 |
-
|
| 110 |
-
// automatic
|
| 111 |
-
(AutomaticParameterEstimation "true")
|
| 112 |
-
//(SP_alpha 1)
|
| 113 |
-
//(SP_A 20.0)
|
| 114 |
-
//(SP a 400)
|
| 115 |
-
//(SigmoidMax 1.0)
|
| 116 |
-
//(SigmoidMin -0.8)
|
| 117 |
-
//(SigmoidScale 0.00000001)
|
| 118 |
-
//(NumberOfGradientMeasurements 10)
|
| 119 |
-
//(NumberOfJacobianMeasurements 1000)
|
| 120 |
-
|
| 121 |
-
(FixedKernelBSplineOrder 3)
|
| 122 |
-
(MovingKernelBSplineOrder 3)
|
| 123 |
-
(CheckNumberOfSamples "true")
|
| 124 |
-
(UseRelativeWeights "false")
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
// several
|
| 128 |
-
(WriteTransformParametersEachIteration "false")
|
| 129 |
-
(WriteTransformParametersEachResolution "false")
|
| 130 |
-
(ShowExactMetricValue "false")
|
| 131 |
-
(ErodeFixedMask "false")
|
| 132 |
-
(ErodeMovingMask "false")
|
| 133 |
-
(UseBinaryFormatForTransformationParameters "false")
|
| 134 |
-
|
| 135 |
-
// imageSampler
|
| 136 |
-
(Interpolator "BSplineInterpolator")
|
| 137 |
-
(ImageSampler "RandomCoordinate")
|
| 138 |
-
(NewSamplesEveryIteration "true")
|
| 139 |
-
(UseRandomSampleRegion "false")
|
| 140 |
-
|
| 141 |
-
// interpolator and resampler
|
| 142 |
-
(ResampleInterpolator "FinalBSplineInterpolator")
|
| 143 |
-
(FinalBSplineInterpolationOrder 3)
|
| 144 |
-
(BSplineInterpolationOrder 3)
|
| 145 |
-
(Resampler "DefaultResampler")
|
| 146 |
-
(WriteIterationInfo "false")
|
| 147 |
-
(WriteResultImage "false")
|
| 148 |
-
(DefaultPixelValue -1024)
|
| 149 |
-
(ResultImageFormat "mha")
|
| 150 |
-
|
| 151 |
-
(ITKTransformOutputFileNameExtension "itk.txt")
|
| 152 |
-
(WriteITKCompositeTransform "true")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/Prediction.yml
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 1 |
-
Predictor:
|
| 2 |
-
Model:
|
| 3 |
-
classpath: Model:RegistrationNet
|
| 4 |
-
RegistrationNet:
|
| 5 |
-
engine: elastix
|
| 6 |
-
parameter_maps:
|
| 7 |
-
- ParameterMap_CBCT_generic_TS.txt
|
| 8 |
-
outputs_criterions: None
|
| 9 |
-
max_iterations: 0
|
| 10 |
-
final_grid_spacing: 14.0
|
| 11 |
-
subset_features: 0
|
| 12 |
-
spatial_samples: 2000
|
| 13 |
-
parameter_overrides: []
|
| 14 |
-
Dataset:
|
| 15 |
-
groups_src:
|
| 16 |
-
Volume_0:
|
| 17 |
-
groups_dest:
|
| 18 |
-
Fixed:
|
| 19 |
-
transforms:
|
| 20 |
-
TensorCast:
|
| 21 |
-
dtype: float32
|
| 22 |
-
inverse: false
|
| 23 |
-
patch_transforms: None
|
| 24 |
-
is_input: true
|
| 25 |
-
Volume_1:
|
| 26 |
-
groups_dest:
|
| 27 |
-
Moving:
|
| 28 |
-
transforms:
|
| 29 |
-
TensorCast:
|
| 30 |
-
dtype: float32
|
| 31 |
-
inverse: false
|
| 32 |
-
patch_transforms: None
|
| 33 |
-
is_input: true
|
| 34 |
-
Volume_2:
|
| 35 |
-
groups_dest:
|
| 36 |
-
FixedMask:
|
| 37 |
-
transforms:
|
| 38 |
-
TensorCast:
|
| 39 |
-
dtype: float32
|
| 40 |
-
inverse: false
|
| 41 |
-
patch_transforms: None
|
| 42 |
-
is_input: true
|
| 43 |
-
Volume_3:
|
| 44 |
-
groups_dest:
|
| 45 |
-
MovingMask:
|
| 46 |
-
transforms:
|
| 47 |
-
TensorCast:
|
| 48 |
-
dtype: float32
|
| 49 |
-
inverse: false
|
| 50 |
-
patch_transforms: None
|
| 51 |
-
is_input: true
|
| 52 |
-
augmentations:
|
| 53 |
-
DataAugmentation_0:
|
| 54 |
-
data_augmentations:
|
| 55 |
-
Flip:
|
| 56 |
-
f_prob:
|
| 57 |
-
- 0
|
| 58 |
-
- 0.5
|
| 59 |
-
- 0.5
|
| 60 |
-
vector_field: true
|
| 61 |
-
prob: 1
|
| 62 |
-
nb: 2
|
| 63 |
-
Patch:
|
| 64 |
-
patch_size: None
|
| 65 |
-
overlap: None
|
| 66 |
-
mask: None
|
| 67 |
-
pad_value: None
|
| 68 |
-
extend_slice: 0
|
| 69 |
-
subset: None
|
| 70 |
-
filter: None
|
| 71 |
-
dataset_filenames:
|
| 72 |
-
- ./Dataset/:mha
|
| 73 |
-
use_cache: false
|
| 74 |
-
batch_size: 1
|
| 75 |
-
num_workers: None
|
| 76 |
-
pin_memory: false
|
| 77 |
-
prefetch_factor: None
|
| 78 |
-
persistent_workers: None
|
| 79 |
-
outputs_dataset:
|
| 80 |
-
MovedImage:
|
| 81 |
-
OutputDataset:
|
| 82 |
-
name_class: OutSameAsGroupDataset
|
| 83 |
-
before_reduction_transforms: None
|
| 84 |
-
after_reduction_transforms: None
|
| 85 |
-
final_transforms:
|
| 86 |
-
TensorCast:
|
| 87 |
-
dtype: float32
|
| 88 |
-
inverse: false
|
| 89 |
-
dataset_filename: Moved:mha
|
| 90 |
-
group: Moved
|
| 91 |
-
same_as_group: Volume_0:Fixed
|
| 92 |
-
patch_combine: None
|
| 93 |
-
inverse_transform: false
|
| 94 |
-
reduction: Mean
|
| 95 |
-
Mean: {}
|
| 96 |
-
DisplacementField:
|
| 97 |
-
OutputDataset:
|
| 98 |
-
name_class: OutSameAsGroupDataset
|
| 99 |
-
before_reduction_transforms: None
|
| 100 |
-
after_reduction_transforms: None
|
| 101 |
-
final_transforms:
|
| 102 |
-
TensorCast:
|
| 103 |
-
dtype: float32
|
| 104 |
-
inverse: false
|
| 105 |
-
dataset_filename: DVF:mha
|
| 106 |
-
group: DVF
|
| 107 |
-
same_as_group: Volume_0:Fixed
|
| 108 |
-
patch_combine: None
|
| 109 |
-
inverse_transform: false
|
| 110 |
-
reduction: Mean
|
| 111 |
-
Mean: {}
|
| 112 |
-
train_name: ImpactReg-CBCT-CT-TS
|
| 113 |
-
manual_seed: 42
|
| 114 |
-
gpu_checkpoints: None
|
| 115 |
-
images_log: None
|
| 116 |
-
combine: Mean
|
| 117 |
-
autocast: false
|
| 118 |
-
data_log: None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/Uncertainty.yml
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
Evaluator:
|
| 2 |
-
metrics:
|
| 3 |
-
Uncertainty:
|
| 4 |
-
targets_criterions:
|
| 5 |
-
None:
|
| 6 |
-
criterions_loader:
|
| 7 |
-
Mean:
|
| 8 |
-
name: Uncertainty
|
| 9 |
-
Dataset:
|
| 10 |
-
groups_src:
|
| 11 |
-
Volume_0:
|
| 12 |
-
groups_dest:
|
| 13 |
-
Uncertainty:
|
| 14 |
-
transforms:
|
| 15 |
-
Norm: {}
|
| 16 |
-
StandardDeviation: {}
|
| 17 |
-
Save:
|
| 18 |
-
dataset: ./Uncertainties/ImpactReg/Output:mha
|
| 19 |
-
group: None
|
| 20 |
-
subset: None
|
| 21 |
-
dataset_filenames:
|
| 22 |
-
- ./Dataset:mha
|
| 23 |
-
validation: None
|
| 24 |
-
train_name: ImpactReg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/app.json
DELETED
|
@@ -1,96 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"display_name": "CBCT/CT preset with TotalSegmentator",
|
| 3 |
-
"short_description": "Generic CBCT/CT deformable registration using TotalSegmentator features",
|
| 4 |
-
"description": "A four-level recursive B-spline deformable registration optimized for generic CBCT/CT alignment, driven by the IMPACT metric using semantic features extracted from pretrained TotalSegmentator TorchScript models. The optimization follows a multi-resolution ASGD scheme with up to 300, 300, 250, and 200 iterations using 2000 random spatial samples per level. Features are extracted at progressively finer voxel scales (3 mm, 3 mm, 2\u00d72\u00d73 mm, 2\u00d72\u00d73 mm), starting with Dice-based overlap on segmentation outputs and progressively integrating feature-level alignment via L1 distances on selected internal layers (0.3/0.7 then 0.5/0.5 L1/Dice), ending with a final purely feature-based stage. A composite objective (IMPACT + mutual information + bending energy penalty) ensures robust cross-modality alignment while enforcing smooth, physically plausible deformations.",
|
| 5 |
-
"task": "registration",
|
| 6 |
-
"tta": 0,
|
| 7 |
-
"mc_dropout": 0,
|
| 8 |
-
"models": [
|
| 9 |
-
"model.pt"
|
| 10 |
-
],
|
| 11 |
-
"inputs": {
|
| 12 |
-
"Fixed": {
|
| 13 |
-
"display_name": "Fixed image",
|
| 14 |
-
"volume_type": "VOLUME",
|
| 15 |
-
"required": true
|
| 16 |
-
},
|
| 17 |
-
"Moving": {
|
| 18 |
-
"display_name": "Moving image",
|
| 19 |
-
"volume_type": "VOLUME",
|
| 20 |
-
"required": true
|
| 21 |
-
},
|
| 22 |
-
"FixedMask": {
|
| 23 |
-
"display_name": "Fixed mask (optional)",
|
| 24 |
-
"volume_type": "SEGMENTATION",
|
| 25 |
-
"required": false,
|
| 26 |
-
"default": "ones"
|
| 27 |
-
},
|
| 28 |
-
"MovingMask": {
|
| 29 |
-
"display_name": "Moving mask (optional)",
|
| 30 |
-
"volume_type": "SEGMENTATION",
|
| 31 |
-
"required": false,
|
| 32 |
-
"default": "ones"
|
| 33 |
-
}
|
| 34 |
-
},
|
| 35 |
-
"outputs": {
|
| 36 |
-
"MovedImage": {
|
| 37 |
-
"display_name": "Moved image",
|
| 38 |
-
"volume_type": "VOLUME",
|
| 39 |
-
"required": true
|
| 40 |
-
},
|
| 41 |
-
"DisplacementField": {
|
| 42 |
-
"display_name": "Displacement field",
|
| 43 |
-
"volume_type": "VOLUME",
|
| 44 |
-
"required": false
|
| 45 |
-
}
|
| 46 |
-
},
|
| 47 |
-
"inputs_evaluations": {
|
| 48 |
-
"Image": {
|
| 49 |
-
"Evaluation_with_images.yml": {
|
| 50 |
-
"FixedImage": {
|
| 51 |
-
"display_name": "Fixed image",
|
| 52 |
-
"volume_type": "VOLUME",
|
| 53 |
-
"required": true
|
| 54 |
-
},
|
| 55 |
-
"MovingImage": {
|
| 56 |
-
"display_name": "Moving image",
|
| 57 |
-
"volume_type": "VOLUME",
|
| 58 |
-
"required": true
|
| 59 |
-
},
|
| 60 |
-
"Mask": {
|
| 61 |
-
"display_name": "Evaluation mask",
|
| 62 |
-
"volume_type": "SEGMENTATION",
|
| 63 |
-
"required": false
|
| 64 |
-
}
|
| 65 |
-
}
|
| 66 |
-
},
|
| 67 |
-
"Segmentation": {
|
| 68 |
-
"Evaluation_with_seg.yml": {
|
| 69 |
-
"FixedSeg": {
|
| 70 |
-
"display_name": "Fixed segmentation",
|
| 71 |
-
"volume_type": "SEGMENTATION",
|
| 72 |
-
"required": true
|
| 73 |
-
},
|
| 74 |
-
"MovingSeg": {
|
| 75 |
-
"display_name": "Moving segmentation",
|
| 76 |
-
"volume_type": "SEGMENTATION",
|
| 77 |
-
"required": true
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
},
|
| 81 |
-
"Landmarks": {
|
| 82 |
-
"Evaluation_with_fid.yml": {
|
| 83 |
-
"FixedFid": {
|
| 84 |
-
"display_name": "Fixed landmarks",
|
| 85 |
-
"volume_type": "FIDUCIALS",
|
| 86 |
-
"required": true
|
| 87 |
-
},
|
| 88 |
-
"MovingFid": {
|
| 89 |
-
"display_name": "Moving landmarks",
|
| 90 |
-
"volume_type": "FIDUCIALS",
|
| 91 |
-
"required": true
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/elastix_engine.py
DELETED
|
@@ -1,386 +0,0 @@
|
|
| 1 |
-
# Copyright (c) 2025 Valentin Boussot
|
| 2 |
-
#
|
| 3 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
-
# you may not use this file except in compliance with the License.
|
| 5 |
-
# You may obtain a copy of the License at
|
| 6 |
-
#
|
| 7 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
-
#
|
| 9 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
-
# See the License for the specific language governing permissions and
|
| 13 |
-
# limitations under the License.
|
| 14 |
-
#
|
| 15 |
-
# SPDX-License-Identifier: Apache-2.0
|
| 16 |
-
|
| 17 |
-
"""Elastix-IMPACT runtime for the registration bundle.
|
| 18 |
-
|
| 19 |
-
``ElastixEngine`` installs the elastix-IMPACT binary, downloads the TorchScript feature models, stages the
|
| 20 |
-
parameter maps (generated from the model matrix or copied + overridden), runs the subprocess, and resamples.
|
| 21 |
-
``ElastixRegistration`` is the graph module ``RegistrationNet`` wires — it bridges KonfAI tensors <-> SITK
|
| 22 |
-
images. The config -> parameter-map MAPPING lives in ``Model.py`` and is imported here.
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
import os
|
| 26 |
-
import re
|
| 27 |
-
import shutil
|
| 28 |
-
import subprocess # nosec B404
|
| 29 |
-
import tempfile
|
| 30 |
-
from pathlib import Path
|
| 31 |
-
|
| 32 |
-
import numpy as np
|
| 33 |
-
import SimpleITK as sitk
|
| 34 |
-
import torch
|
| 35 |
-
import tqdm
|
| 36 |
-
from huggingface_hub import hf_hub_download
|
| 37 |
-
from install import get_elastix_bin, install_elastix_impact, try_elastix
|
| 38 |
-
from konfai.utils.dataset import Attribute, data_to_image, image_to_data
|
| 39 |
-
|
| 40 |
-
from Model import _sorted_specs, generate_impact_parameter_map, load_models_registry
|
| 41 |
-
|
| 42 |
-
# Elastix + IMPACT binary is cached once here (heavy: binary + LibTorch) and reused across runs.
|
| 43 |
-
# Set KONFAI_ELASTIX_DIR to point at an existing install and skip the download.
|
| 44 |
-
ELASTIX_CACHE = Path.home() / ".cache" / "konfai" / "elastix-impact"
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _is_partial_mask(mask: "sitk.Image | None") -> bool:
|
| 48 |
-
"""True only for a mask that actually restricts the metric region — some voxels in, some out. An
|
| 49 |
-
absent optional mask arrives as a whole-image (all-ones) default from KonfAI, and an all-zero mask
|
| 50 |
-
is degenerate; both are treated as no mask, so elastix runs without ``-fMask`` / ``-mMask`` (i.e.
|
| 51 |
-
the whole image) instead of paying for a mask that restricts nothing."""
|
| 52 |
-
if mask is None:
|
| 53 |
-
return False
|
| 54 |
-
arr = sitk.GetArrayViewFromImage(mask)
|
| 55 |
-
return bool((arr > 0).any()) and bool((arr == 0).any())
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
class ElastixEngine:
|
| 59 |
-
"""Run the elastix-IMPACT binary on a fixed/moving pair; return (moved, dvf) on the fixed grid.
|
| 60 |
-
|
| 61 |
-
NOTE: the elastix-IMPACT metric lives only in the custom ``elastix-impact`` binary (SimpleElastix does
|
| 62 |
-
NOT ship it), so registration is a subprocess call, not ``sitk.ElastixImageFilter``.
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
def __init__(
|
| 66 |
-
self,
|
| 67 |
-
parameter_maps: list[str],
|
| 68 |
-
max_iterations: int = 0,
|
| 69 |
-
final_grid_spacing: float = 0.0,
|
| 70 |
-
subset_features: int = 0,
|
| 71 |
-
spatial_samples: int = 0,
|
| 72 |
-
parameter_overrides: list[str] = [],
|
| 73 |
-
resolutions: dict = {},
|
| 74 |
-
mode: str = "Static",
|
| 75 |
-
) -> None:
|
| 76 |
-
self._bundle_dir = Path(__file__).resolve().parent
|
| 77 |
-
self._parameter_maps = [self._bundle_dir / Path(p).name for p in parameter_maps]
|
| 78 |
-
self._max_iterations = max_iterations
|
| 79 |
-
self._final_grid_spacing = final_grid_spacing
|
| 80 |
-
self._subset_features = subset_features
|
| 81 |
-
self._spatial_samples = spatial_samples
|
| 82 |
-
self._parameter_overrides = list(parameter_overrides)
|
| 83 |
-
# ImpactMode: Static computes features once per level (PatchSize 0 0 0 = whole image); Jacobian
|
| 84 |
-
# samples random FOV-sized patches each iteration. One mode per preset.
|
| 85 |
-
self._mode = mode
|
| 86 |
-
# Matrix mode: with ``resolutions`` the map is GENERATED from it. Empty ``resolutions`` = an
|
| 87 |
-
# intensity preset (no IMPACT models): the fixed maps are staged with only the global overrides.
|
| 88 |
-
self._resolutions = resolutions
|
| 89 |
-
self._registry = load_models_registry() if resolutions else {}
|
| 90 |
-
# Feature models are DERIVED — the unique refs across the matrix cells (no flat ``models`` param).
|
| 91 |
-
models: list[str] = []
|
| 92 |
-
for res in _sorted_specs(resolutions):
|
| 93 |
-
for model in _sorted_specs(res.models):
|
| 94 |
-
if model.ref not in models:
|
| 95 |
-
models.append(model.ref)
|
| 96 |
-
self._models = models
|
| 97 |
-
# ``iterations`` (the progress-bar total) is DERIVED: the sum of per-resolution iteration budgets.
|
| 98 |
-
self._iterations = self._total_iterations()
|
| 99 |
-
self._elastix_bin = self._ensure_binary()
|
| 100 |
-
self._local_models = self._download_models()
|
| 101 |
-
|
| 102 |
-
def _total_iterations(self) -> int:
|
| 103 |
-
"""Total iterations across resolutions — the progress-bar budget, from the config (or the maps)."""
|
| 104 |
-
if self._resolutions:
|
| 105 |
-
return sum(int(res.max_iterations) for res in _sorted_specs(self._resolutions))
|
| 106 |
-
total = 0
|
| 107 |
-
for src in self._parameter_maps:
|
| 108 |
-
match = re.search(r"\(MaximumNumberOfIterations\s+([^)]*)\)", src.read_text(encoding="utf-8"))
|
| 109 |
-
if match:
|
| 110 |
-
total += sum(int(token) for token in match.group(1).split())
|
| 111 |
-
return total
|
| 112 |
-
|
| 113 |
-
def _ensure_binary(self) -> Path:
|
| 114 |
-
# Optional override: point at an existing elastix-IMPACT install (skips the download).
|
| 115 |
-
override = os.environ.get("KONFAI_ELASTIX_DIR", "")
|
| 116 |
-
if override:
|
| 117 |
-
try_elastix(Path(override))
|
| 118 |
-
return get_elastix_bin(Path(override)).resolve()
|
| 119 |
-
ELASTIX_CACHE.mkdir(parents=True, exist_ok=True)
|
| 120 |
-
try:
|
| 121 |
-
try_elastix(ELASTIX_CACHE)
|
| 122 |
-
except Exception:
|
| 123 |
-
install_elastix_impact(ELASTIX_CACHE, force_cuda=False, force_cpu=False)
|
| 124 |
-
try_elastix(ELASTIX_CACHE)
|
| 125 |
-
return get_elastix_bin(ELASTIX_CACHE).resolve()
|
| 126 |
-
|
| 127 |
-
def _download_models(self) -> list[tuple[str, Path]]:
|
| 128 |
-
"""Fetch the TorchScript feature models (``repo:filename``); keep (relative_name, local_path)."""
|
| 129 |
-
models = []
|
| 130 |
-
for ref in self._models:
|
| 131 |
-
repo, filename = ref.split(":", 1)
|
| 132 |
-
local = Path(hf_hub_download(repo_id=repo, filename=filename, repo_type="model")) # nosec B615
|
| 133 |
-
models.append((filename, local))
|
| 134 |
-
return models
|
| 135 |
-
|
| 136 |
-
def _parameter_map_overrides(self, global_only: bool = False) -> tuple[dict[str, str], list[tuple[str, str]]]:
|
| 137 |
-
"""The tuned knobs as parameter-map overrides: ``(per_token, exact)``.
|
| 138 |
-
|
| 139 |
-
``per_token`` maps an elastix key (or the ``ImpactSubsetFeatures`` prefix) to a value replacing
|
| 140 |
-
**each** existing token, preserving per-resolution / per-model multiplicity. ``exact`` entries (from
|
| 141 |
-
``parameter_overrides``, ``Key=value text``) replace the whole value verbatim and win over the named
|
| 142 |
-
knobs. Overrides only REPLACE keys already present — never inject. ``global_only`` (matrix mode) drops
|
| 143 |
-
``max_iterations`` / ``subset_features`` (the matrix already sets those per cell).
|
| 144 |
-
"""
|
| 145 |
-
per_token: dict[str, str] = {}
|
| 146 |
-
if not global_only and self._max_iterations > 0:
|
| 147 |
-
per_token["MaximumNumberOfIterations"] = str(int(self._max_iterations))
|
| 148 |
-
if self._final_grid_spacing > 0:
|
| 149 |
-
per_token["FinalGridSpacingInPhysicalUnits"] = str(float(self._final_grid_spacing))
|
| 150 |
-
if not global_only and self._subset_features > 0:
|
| 151 |
-
per_token["ImpactSubsetFeatures"] = str(int(self._subset_features)) # prefix: indexed per metric
|
| 152 |
-
if self._spatial_samples > 0:
|
| 153 |
-
per_token["NumberOfSpatialSamples"] = str(int(self._spatial_samples))
|
| 154 |
-
exact: list[tuple[str, str]] = []
|
| 155 |
-
for entry in self._parameter_overrides:
|
| 156 |
-
key, sep, value = entry.partition("=")
|
| 157 |
-
if not sep or not key.strip():
|
| 158 |
-
raise ValueError(f"Invalid parameter_overrides entry '{entry}': expected 'Key=value text'.")
|
| 159 |
-
exact.append((key.strip(), value.strip()))
|
| 160 |
-
return per_token, exact
|
| 161 |
-
|
| 162 |
-
@staticmethod
|
| 163 |
-
def _apply_map_overrides(
|
| 164 |
-
text: str, per_token: dict[str, str], exact: list[tuple[str, str]], device_index: int
|
| 165 |
-
) -> str:
|
| 166 |
-
"""Patch a parameter map: set ImpactGPU to the device, apply exact key overrides, replace each token
|
| 167 |
-
of a per-token knob (preserving multiplicity), and warn for a requested key absent from the map.
|
| 168 |
-
"""
|
| 169 |
-
entry_pattern = re.compile(r"^(\s*)\((\S+)((?:\s+[^)]*)?)\)\s*$")
|
| 170 |
-
requested = set(per_token) | {key for key, _ in exact}
|
| 171 |
-
seen: set[str] = set()
|
| 172 |
-
lines = []
|
| 173 |
-
for line in text.splitlines():
|
| 174 |
-
match = entry_pattern.match(line)
|
| 175 |
-
if match:
|
| 176 |
-
indent, key, values = match.group(1), match.group(2), match.group(3)
|
| 177 |
-
if key == "ImpactGPU":
|
| 178 |
-
line = f"{indent}(ImpactGPU {device_index})"
|
| 179 |
-
else:
|
| 180 |
-
exact_value = next((value for k, value in exact if k == key), None)
|
| 181 |
-
if exact_value is not None:
|
| 182 |
-
seen.add(key)
|
| 183 |
-
line = f"{indent}({key} {exact_value})"
|
| 184 |
-
else:
|
| 185 |
-
token_key = "ImpactSubsetFeatures" if key.startswith("ImpactSubsetFeatures") else key
|
| 186 |
-
if token_key in per_token:
|
| 187 |
-
seen.add(token_key)
|
| 188 |
-
replaced = " ".join(per_token[token_key] for _ in values.split())
|
| 189 |
-
line = f"{indent}({key} {replaced})"
|
| 190 |
-
lines.append(line)
|
| 191 |
-
# Overrides never inject keys, so a knob set for a key absent from every map silently does nothing —
|
| 192 |
-
# surface it (e.g. final_grid_spacing on a rigid-only preset).
|
| 193 |
-
for key in sorted(requested - seen):
|
| 194 |
-
print(f"[ImpactReg] note: override '{key}' matched no entry in the preset's parameter maps.")
|
| 195 |
-
return "\n".join(lines)
|
| 196 |
-
|
| 197 |
-
def _stage_parameter_maps(self, work: Path, device_index: int) -> list[Path]:
|
| 198 |
-
"""Stage the parameter maps into ``work``.
|
| 199 |
-
|
| 200 |
-
Matrix mode GENERATES each map from ``resolutions`` + the registry, then applies only the map-wide
|
| 201 |
-
knobs (the matrix already sets iterations/features per cell). Legacy mode copies the preset's maps and
|
| 202 |
-
applies every per-token / exact override. Both set the ImpactGPU device.
|
| 203 |
-
"""
|
| 204 |
-
staged = []
|
| 205 |
-
for src in self._parameter_maps:
|
| 206 |
-
if self._resolutions:
|
| 207 |
-
text = generate_impact_parameter_map(
|
| 208 |
-
src.read_text(encoding="utf-8"), self._resolutions, self._registry, self._mode
|
| 209 |
-
)
|
| 210 |
-
per_token, exact = self._parameter_map_overrides(global_only=True)
|
| 211 |
-
else:
|
| 212 |
-
text = src.read_text(encoding="utf-8")
|
| 213 |
-
per_token, exact = self._parameter_map_overrides()
|
| 214 |
-
text = self._apply_map_overrides(text, per_token, exact, device_index)
|
| 215 |
-
dst = work / src.name
|
| 216 |
-
dst.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8")
|
| 217 |
-
staged.append(dst)
|
| 218 |
-
return staged
|
| 219 |
-
|
| 220 |
-
def register(
|
| 221 |
-
self,
|
| 222 |
-
fixed: sitk.Image,
|
| 223 |
-
moving: sitk.Image,
|
| 224 |
-
device_index: int,
|
| 225 |
-
fixed_mask: sitk.Image | None = None,
|
| 226 |
-
moving_mask: sitk.Image | None = None,
|
| 227 |
-
) -> tuple[np.ndarray, np.ndarray]:
|
| 228 |
-
"""Register ``moving`` onto ``fixed``; return (moved, dvf) as channel-first arrays on the fixed grid.
|
| 229 |
-
|
| 230 |
-
Optional ``fixed_mask`` / ``moving_mask`` restrict the similarity metric to a region (elastix
|
| 231 |
-
``-fMask`` / ``-mMask``); a mask covering the whole image is equivalent to passing none.
|
| 232 |
-
"""
|
| 233 |
-
work = Path(tempfile.mkdtemp(prefix="konfai_reg_"))
|
| 234 |
-
try:
|
| 235 |
-
fixed_path, moving_path = work / "Fixed.mha", work / "Moving.mha"
|
| 236 |
-
sitk.WriteImage(fixed, str(fixed_path))
|
| 237 |
-
sitk.WriteImage(moving, str(moving_path))
|
| 238 |
-
|
| 239 |
-
# Stage the feature models at the relative path the maps reference (e.g. ImpactModelsPath0
|
| 240 |
-
# "MIND/R1D2_3D.pt"), resolved from the elastix working directory.
|
| 241 |
-
for rel_name, model_path in self._local_models:
|
| 242 |
-
dst = work / rel_name
|
| 243 |
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 244 |
-
if not dst.exists():
|
| 245 |
-
dst.symlink_to(model_path)
|
| 246 |
-
|
| 247 |
-
args = [str(self._elastix_bin), "-f", str(fixed_path), "-m", str(moving_path)]
|
| 248 |
-
for flag, mask, name in (("-fMask", fixed_mask, "FixedMask.mha"), ("-mMask", moving_mask, "MovingMask.mha")):
|
| 249 |
-
if _is_partial_mask(mask):
|
| 250 |
-
mask_path = work / name
|
| 251 |
-
sitk.WriteImage(sitk.Cast(mask, sitk.sitkUInt8), str(mask_path))
|
| 252 |
-
args += [flag, str(mask_path)]
|
| 253 |
-
args += ["-out", str(work)]
|
| 254 |
-
for pmap in self._stage_parameter_maps(work, device_index):
|
| 255 |
-
args += ["-p", str(pmap)]
|
| 256 |
-
|
| 257 |
-
# Make the elastix binary's bundled libs (libtorch under <install>/lib) and any extra
|
| 258 |
-
# libtorch/CUDA dirs (KONFAI_ELASTIX_EXTRA_LIB) findable so the IMPACT metric plugin loads.
|
| 259 |
-
env = os.environ.copy()
|
| 260 |
-
extra_libs = [str(self._elastix_bin.parent.parent / "lib"), os.environ.get("KONFAI_ELASTIX_EXTRA_LIB", "")]
|
| 261 |
-
env["LD_LIBRARY_PATH"] = os.pathsep.join(p for p in [*extra_libs, env.get("LD_LIBRARY_PATH", "")] if p)
|
| 262 |
-
proc = subprocess.Popen( # nosec B603
|
| 263 |
-
args,
|
| 264 |
-
cwd=str(work),
|
| 265 |
-
stdout=subprocess.PIPE,
|
| 266 |
-
stderr=subprocess.STDOUT,
|
| 267 |
-
text=True,
|
| 268 |
-
bufsize=1,
|
| 269 |
-
env=env,
|
| 270 |
-
)
|
| 271 |
-
# Drive a tqdm bar over elastix's iteration lines so SlicerKonfAI (which parses the "N% done"
|
| 272 |
-
# progress line) shows real progress. A tuned max_iterations makes the declared budget stale ->
|
| 273 |
-
# open-ended bar. The description mirrors KonfAI's bars: resolution level + the metric value.
|
| 274 |
-
captured: list[str] = []
|
| 275 |
-
iteration_line = re.compile(r"^\d+\s")
|
| 276 |
-
budget = None if self._max_iterations > 0 else (self._iterations or None)
|
| 277 |
-
progress = tqdm.tqdm(total=budget, desc="Registration", ncols=0, leave=True)
|
| 278 |
-
assert proc.stdout is not None
|
| 279 |
-
resolution = 0
|
| 280 |
-
for line in proc.stdout:
|
| 281 |
-
captured.append(line)
|
| 282 |
-
stripped = line.strip()
|
| 283 |
-
if stripped.startswith("Resolution:"):
|
| 284 |
-
try:
|
| 285 |
-
resolution = int(stripped.split(":", 1)[1])
|
| 286 |
-
except ValueError:
|
| 287 |
-
pass
|
| 288 |
-
elif iteration_line.match(line):
|
| 289 |
-
progress.update(1)
|
| 290 |
-
columns = line.split() # column 2 is the metric (header "1:ItNr 2:Metric ...")
|
| 291 |
-
if len(columns) > 1:
|
| 292 |
-
try:
|
| 293 |
-
progress.set_description(
|
| 294 |
-
f"Registration : res {resolution} | metric {float(columns[1]):.4f}"
|
| 295 |
-
)
|
| 296 |
-
except ValueError:
|
| 297 |
-
pass
|
| 298 |
-
progress.close()
|
| 299 |
-
returncode = proc.wait()
|
| 300 |
-
if returncode != 0:
|
| 301 |
-
raise RuntimeError(f"elastix failed (code {returncode}):\n{''.join(captured[-40:])}")
|
| 302 |
-
|
| 303 |
-
transforms = sorted(
|
| 304 |
-
work.glob("TransformParameters.*-Composite.itk.txt"),
|
| 305 |
-
key=lambda p: int(p.name.split(".")[1].split("-")[0]),
|
| 306 |
-
)
|
| 307 |
-
if not transforms:
|
| 308 |
-
raise FileNotFoundError("elastix produced no composite transform file.")
|
| 309 |
-
transform = sitk.ReadTransform(str(transforms[-1]))
|
| 310 |
-
|
| 311 |
-
moved = sitk.Resample(moving, fixed, transform, sitk.sitkLinear, 0.0, moving.GetPixelID())
|
| 312 |
-
dvf = sitk.TransformToDisplacementField(
|
| 313 |
-
transform,
|
| 314 |
-
sitk.sitkVectorFloat64,
|
| 315 |
-
fixed.GetSize(),
|
| 316 |
-
fixed.GetOrigin(),
|
| 317 |
-
fixed.GetSpacing(),
|
| 318 |
-
fixed.GetDirection(),
|
| 319 |
-
)
|
| 320 |
-
moved_np, _ = image_to_data(moved)
|
| 321 |
-
dvf_np, _ = image_to_data(dvf)
|
| 322 |
-
return moved_np, dvf_np
|
| 323 |
-
finally:
|
| 324 |
-
shutil.rmtree(work, ignore_errors=True)
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
class ElastixRegistration(torch.nn.Module):
|
| 328 |
-
"""Custom graph module: (fixed, moving) tensors + their geometry -> moved image on the fixed grid.
|
| 329 |
-
|
| 330 |
-
``accepts_attributes = True`` opts this module into receiving, from the KonfAI graph, the per-branch
|
| 331 |
-
``Attribute`` list alongside the tensors (same convention as ``CriterionWithAttribute``). elastix needs
|
| 332 |
-
the physical geometry (Origin/Spacing/Direction), which raw tensors do not carry.
|
| 333 |
-
"""
|
| 334 |
-
|
| 335 |
-
accepts_attributes = True
|
| 336 |
-
|
| 337 |
-
def __init__(
|
| 338 |
-
self,
|
| 339 |
-
engine: str,
|
| 340 |
-
parameter_maps: list[str],
|
| 341 |
-
max_iterations: int = 0,
|
| 342 |
-
final_grid_spacing: float = 0.0,
|
| 343 |
-
subset_features: int = 0,
|
| 344 |
-
spatial_samples: int = 0,
|
| 345 |
-
parameter_overrides: list[str] = [],
|
| 346 |
-
resolutions: dict = {},
|
| 347 |
-
mode: str = "Static",
|
| 348 |
-
) -> None:
|
| 349 |
-
super().__init__()
|
| 350 |
-
if engine != "elastix":
|
| 351 |
-
raise NotImplementedError(f"ElastixRegistration engine '{engine}' is not implemented yet.")
|
| 352 |
-
self._engine = ElastixEngine(
|
| 353 |
-
parameter_maps,
|
| 354 |
-
max_iterations,
|
| 355 |
-
final_grid_spacing,
|
| 356 |
-
subset_features,
|
| 357 |
-
spatial_samples,
|
| 358 |
-
parameter_overrides,
|
| 359 |
-
resolutions,
|
| 360 |
-
mode,
|
| 361 |
-
)
|
| 362 |
-
|
| 363 |
-
def forward(
|
| 364 |
-
self,
|
| 365 |
-
fixed: torch.Tensor,
|
| 366 |
-
moving: torch.Tensor,
|
| 367 |
-
fixed_mask: torch.Tensor,
|
| 368 |
-
moving_mask: torch.Tensor,
|
| 369 |
-
attributes: list[list[Attribute]],
|
| 370 |
-
) -> torch.Tensor:
|
| 371 |
-
# attributes = [fixed, moving, fixed_mask, moving_mask] branch attrs; each a list[Attribute] over the
|
| 372 |
-
# batch. Returns, per sample, the moved image (1 channel) stacked with the DVF (dim channels), both on
|
| 373 |
-
# the fixed grid; downstream ChannelSelect splits them. A whole-image mask (the default) restricts nothing.
|
| 374 |
-
fixed_attrs, moving_attrs, fmask_attrs, mmask_attrs = attributes
|
| 375 |
-
device_index = fixed.device.index if fixed.device.type == "cuda" else -1
|
| 376 |
-
combined = []
|
| 377 |
-
for b in range(fixed.shape[0]):
|
| 378 |
-
fixed_img = data_to_image(fixed[b].detach().cpu().numpy(), fixed_attrs[b])
|
| 379 |
-
moving_img = data_to_image(moving[b].detach().cpu().numpy(), moving_attrs[b])
|
| 380 |
-
fixed_mask_img = data_to_image(fixed_mask[b].detach().cpu().numpy(), fmask_attrs[b])
|
| 381 |
-
moving_mask_img = data_to_image(moving_mask[b].detach().cpu().numpy(), mmask_attrs[b])
|
| 382 |
-
moved_np, dvf_np = self._engine.register(
|
| 383 |
-
fixed_img, moving_img, device_index, fixed_mask_img, moving_mask_img
|
| 384 |
-
)
|
| 385 |
-
combined.append(torch.from_numpy(np.concatenate([moved_np, dvf_np], axis=0)))
|
| 386 |
-
return torch.stack(combined, dim=0).to(fixed.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/install.py
DELETED
|
@@ -1,325 +0,0 @@
|
|
| 1 |
-
# Copyright (c) 2025 Valentin Boussot
|
| 2 |
-
#
|
| 3 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
-
# you may not use this file except in compliance with the License.
|
| 5 |
-
# You may obtain a copy of the License at
|
| 6 |
-
#
|
| 7 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
-
#
|
| 9 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
-
# See the License for the specific language governing permissions and
|
| 13 |
-
# limitations under the License.
|
| 14 |
-
#
|
| 15 |
-
# SPDX-License-Identifier: Apache-2.0
|
| 16 |
-
|
| 17 |
-
import argparse
|
| 18 |
-
import platform
|
| 19 |
-
import re
|
| 20 |
-
import shutil
|
| 21 |
-
import stat
|
| 22 |
-
import subprocess # nosec B404
|
| 23 |
-
import zipfile
|
| 24 |
-
from pathlib import Path
|
| 25 |
-
|
| 26 |
-
import requests
|
| 27 |
-
from tqdm import tqdm
|
| 28 |
-
|
| 29 |
-
# -----------------------------------------------------------------------------
|
| 30 |
-
# Elastix + IMPACT binary assets hosted on GitHub Releases.
|
| 31 |
-
#
|
| 32 |
-
# Key format: (OS, ARCH, FLAVOR)
|
| 33 |
-
# - OS : platform.system() -> "Linux", "Windows", "Darwin"
|
| 34 |
-
# - ARCH : normalized architecture -> "x86_64"
|
| 35 |
-
# - FLAVOR : "cpu" or "cu128"
|
| 36 |
-
#
|
| 37 |
-
# CPU assets are standalone (LibTorch CPU bundled).
|
| 38 |
-
# CUDA assets do NOT bundle LibTorch (too large for GitHub limits).
|
| 39 |
-
# -----------------------------------------------------------------------------
|
| 40 |
-
ELX_ASSET_TEMPLATE = {
|
| 41 |
-
("Linux", "x86_64", "cpu"): "elastix-impact-linux-x86_64-cpu.zip",
|
| 42 |
-
("Linux", "x86_64", "cu128"): "elastix-impact-linux-x86_64-cu128.zip",
|
| 43 |
-
("Windows", "x86_64", "cpu"): "elastix-impact-windows-x86_64-cpu.zip",
|
| 44 |
-
("Windows", "x86_64", "cu128"): "elastix-impact-windows-x86_64-cu128.zip",
|
| 45 |
-
("Darwin", "x86_64", "cpu"): "elastix-impact-macos-14-x86_64-cpu.zip",
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
# -----------------------------------------------------------------------------
|
| 49 |
-
# Official LibTorch downloads (PyTorch).
|
| 50 |
-
#
|
| 51 |
-
# IMPORTANT:
|
| 52 |
-
# - Version MUST match the one used at build time (ABI compatibility).
|
| 53 |
-
# - Using "shared-with-deps" ensures CUDA runtime libraries are included
|
| 54 |
-
# (except the NVIDIA driver, which must be installed system-wide).
|
| 55 |
-
# -----------------------------------------------------------------------------
|
| 56 |
-
|
| 57 |
-
LIBTORCH_URL = {
|
| 58 |
-
(
|
| 59 |
-
"Linux",
|
| 60 |
-
"x86_64",
|
| 61 |
-
"cpu",
|
| 62 |
-
): "https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-2.8.0%2Bcpu.zip",
|
| 63 |
-
(
|
| 64 |
-
"Windows",
|
| 65 |
-
"x86_64",
|
| 66 |
-
"cpu",
|
| 67 |
-
): "https://download.pytorch.org/libtorch/cpu/libtorch-win-shared-with-deps-2.8.0%2Bcpu.zip",
|
| 68 |
-
(
|
| 69 |
-
"Darwin",
|
| 70 |
-
"arm64",
|
| 71 |
-
"cpu",
|
| 72 |
-
): "https://download.pytorch.org/libtorch/cpu/libtorch-win-shared-with-deps-2.8.0%2Bcpu.zip",
|
| 73 |
-
(
|
| 74 |
-
"Linux",
|
| 75 |
-
"x86_64",
|
| 76 |
-
"cu128",
|
| 77 |
-
): "https://download.pytorch.org/libtorch/cu128/libtorch-shared-with-deps-2.8.0%2Bcu128.zip",
|
| 78 |
-
(
|
| 79 |
-
"Windows",
|
| 80 |
-
"x86_64",
|
| 81 |
-
"cu128",
|
| 82 |
-
): "https://download.pytorch.org/libtorch/cu128/libtorch-win-shared-with-deps-2.8.0%2Bcu128.zip",
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
# -----------------------------------------------------------------------------
|
| 86 |
-
# Minimum NVIDIA driver versions required for CUDA 12.8.
|
| 87 |
-
#
|
| 88 |
-
# The CUDA Toolkit itself is NOT required.
|
| 89 |
-
# Only a sufficiently recent NVIDIA driver must be installed.
|
| 90 |
-
# -----------------------------------------------------------------------------
|
| 91 |
-
CUDA128_MIN_DRIVER_LINUX = (570, 26)
|
| 92 |
-
CUDA128_MIN_DRIVER_WINDOWS = (570, 65)
|
| 93 |
-
|
| 94 |
-
GITHUB_OWNER = "vboussot"
|
| 95 |
-
GITHUB_REPO = "ImpactElastix"
|
| 96 |
-
GITHUB_TAG = "1.0.0"
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
DEFAULT_PREFIX = Path.cwd() / "elastix-impact"
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def run_cmd(cmd: list[str]) -> str:
|
| 103 |
-
return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8", errors="replace") # nosec B603
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def detect_nvidia_driver() -> tuple[bool, tuple[int, int] | None]:
|
| 107 |
-
"""
|
| 108 |
-
Detect presence of an NVIDIA GPU and extract the driver version.
|
| 109 |
-
Returns:
|
| 110 |
-
(True, (major, minor)) if detected
|
| 111 |
-
(False, None) if nvidia-smi is not available
|
| 112 |
-
"""
|
| 113 |
-
try:
|
| 114 |
-
nvidia_smi = shutil.which("nvidia-smi")
|
| 115 |
-
if nvidia_smi is None:
|
| 116 |
-
raise RuntimeError("nvidia-smi not found in PATH")
|
| 117 |
-
|
| 118 |
-
out = (
|
| 119 |
-
subprocess.check_output(
|
| 120 |
-
[nvidia_smi, "--query-gpu=driver_version", "--format=csv,noheader"], stderr=subprocess.DEVNULL
|
| 121 |
-
) # nosec B603
|
| 122 |
-
.decode("utf-8")
|
| 123 |
-
.strip()
|
| 124 |
-
)
|
| 125 |
-
except Exception:
|
| 126 |
-
return (False, None)
|
| 127 |
-
|
| 128 |
-
# Exemple: "575.64"
|
| 129 |
-
m = re.match(r"([0-9]+)\.([0-9]+)", out)
|
| 130 |
-
if not m:
|
| 131 |
-
return (True, None)
|
| 132 |
-
|
| 133 |
-
return (True, (int(m.group(1)), int(m.group(2))))
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def driver_ok_for_cuda(os_name: str, drv: tuple[int, int] | None) -> bool:
|
| 137 |
-
"""
|
| 138 |
-
Check whether the detected NVIDIA driver satisfies the minimum
|
| 139 |
-
requirement for CUDA 12.8 on the given operating system.
|
| 140 |
-
"""
|
| 141 |
-
if drv is None:
|
| 142 |
-
return False
|
| 143 |
-
if os_name == "Linux":
|
| 144 |
-
return drv >= CUDA128_MIN_DRIVER_LINUX
|
| 145 |
-
if os_name == "Windows":
|
| 146 |
-
return drv >= CUDA128_MIN_DRIVER_WINDOWS
|
| 147 |
-
return False
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def normalize_arch(machine: str) -> str:
|
| 151 |
-
"""
|
| 152 |
-
Normalize platform.machine() output across operating systems.
|
| 153 |
-
|
| 154 |
-
Examples:
|
| 155 |
-
AMD64 -> x86_64
|
| 156 |
-
aarch64 -> arm64
|
| 157 |
-
"""
|
| 158 |
-
m = machine.lower()
|
| 159 |
-
if m in ("x86_64", "amd64"):
|
| 160 |
-
return "x86_64"
|
| 161 |
-
if m in ("aarch64", "arm64"):
|
| 162 |
-
return "arm64"
|
| 163 |
-
return machine
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def download_file(url: str, dst: Path) -> None:
|
| 167 |
-
"""
|
| 168 |
-
Download a file from the given URL with a progress indicator.
|
| 169 |
-
"""
|
| 170 |
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 171 |
-
print(f"Downloading: {url}", flush=True)
|
| 172 |
-
print(f" to: {dst}", flush=True)
|
| 173 |
-
|
| 174 |
-
try:
|
| 175 |
-
with requests.get(url, stream=True, timeout=60) as r:
|
| 176 |
-
r.raise_for_status()
|
| 177 |
-
total = int(r.headers.get("content-length", 0))
|
| 178 |
-
with open(dst, "wb") as f:
|
| 179 |
-
with tqdm(
|
| 180 |
-
total=total,
|
| 181 |
-
unit="B",
|
| 182 |
-
unit_scale=True,
|
| 183 |
-
desc=f"Downloading {dst.name}",
|
| 184 |
-
) as pbar:
|
| 185 |
-
for chunk in r.iter_content(chunk_size=8192):
|
| 186 |
-
f.write(chunk)
|
| 187 |
-
pbar.update(len(chunk))
|
| 188 |
-
except Exception as e:
|
| 189 |
-
raise e
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def extract_archive(archive: Path, dst_dir: Path) -> None:
|
| 193 |
-
"""
|
| 194 |
-
Extract a ZIP archive to the destination directory.
|
| 195 |
-
"""
|
| 196 |
-
dst_dir.mkdir(parents=True, exist_ok=True)
|
| 197 |
-
print(f"Extracting: {archive} -> {dst_dir}", flush=True)
|
| 198 |
-
with zipfile.ZipFile(archive, "r") as z:
|
| 199 |
-
z.extractall(dst_dir)
|
| 200 |
-
archive.unlink()
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
def install_elastix_impact(install_path: Path, force_cuda: bool, force_cpu: bool):
|
| 204 |
-
os_name = platform.system()
|
| 205 |
-
arch = normalize_arch(platform.machine())
|
| 206 |
-
has_nvidia, drv = detect_nvidia_driver()
|
| 207 |
-
|
| 208 |
-
if os_name not in ("Linux", "Windows", "Darwin"):
|
| 209 |
-
raise NameError(f"Unsupported OS: {os_name}")
|
| 210 |
-
|
| 211 |
-
if arch not in ("x86_64", "arm64"):
|
| 212 |
-
raise NameError(f"Unsupported arch: {arch} (expected x86_64, arm64)")
|
| 213 |
-
|
| 214 |
-
flavor = "cpu"
|
| 215 |
-
if force_cuda:
|
| 216 |
-
if not has_nvidia or not driver_ok_for_cuda(os_name, drv):
|
| 217 |
-
raise NameError(
|
| 218 |
-
"CUDA forced but NVIDIA driver/GPU not suitable. Detected: has_nvidia={has_nvidia}, driver={drv}"
|
| 219 |
-
)
|
| 220 |
-
|
| 221 |
-
flavor = "cu128"
|
| 222 |
-
elif not force_cpu:
|
| 223 |
-
if has_nvidia and driver_ok_for_cuda(os_name, drv):
|
| 224 |
-
flavor = "cu128"
|
| 225 |
-
else:
|
| 226 |
-
flavor = "cpu"
|
| 227 |
-
|
| 228 |
-
print(f"System: {os_name} {arch}", flush=True)
|
| 229 |
-
print(f"NVIDIA: {has_nvidia}, driver={drv}", flush=True)
|
| 230 |
-
print(f"Selected flavor: {flavor}", flush=True)
|
| 231 |
-
|
| 232 |
-
key = (os_name, arch, flavor)
|
| 233 |
-
if key not in ELX_ASSET_TEMPLATE:
|
| 234 |
-
raise NameError(f"No elastix asset configured for {key}")
|
| 235 |
-
if flavor != "cpu" and key not in LIBTORCH_URL:
|
| 236 |
-
raise NameError(f"No libtorch url configured for {key}")
|
| 237 |
-
|
| 238 |
-
install_path = install_path.resolve()
|
| 239 |
-
install_path.mkdir(parents=True, exist_ok=True)
|
| 240 |
-
|
| 241 |
-
elx_asset = ELX_ASSET_TEMPLATE[key]
|
| 242 |
-
elx_url = f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/download/{GITHUB_TAG}/{elx_asset}"
|
| 243 |
-
elx_archive = install_path / elx_asset
|
| 244 |
-
download_file(elx_url, elx_archive)
|
| 245 |
-
extract_archive(elx_archive, install_path)
|
| 246 |
-
|
| 247 |
-
# -------------------------------------------------------------------------
|
| 248 |
-
# ZIP archives may drop executable permissions.
|
| 249 |
-
# Ensure elastix and transformix are executable on Unix platforms.
|
| 250 |
-
# -------------------------------------------------------------------------
|
| 251 |
-
if os_name in ("Linux", "Darwin"):
|
| 252 |
-
for exe in ("elastix", "transformix"):
|
| 253 |
-
p = install_path / "bin" / exe
|
| 254 |
-
if p.exists():
|
| 255 |
-
p.chmod(p.stat().st_mode | stat.S_IEXEC)
|
| 256 |
-
|
| 257 |
-
# ---------------------------------------------------------------------
|
| 258 |
-
# - Download matching LibTorch 12.8 with or without CUDA 12.8
|
| 259 |
-
# - Extract it
|
| 260 |
-
# - Move runtime libraries to a location visible to the dynamic loader
|
| 261 |
-
#
|
| 262 |
-
# Linux / macOS : prefix/lib
|
| 263 |
-
# Windows : prefix (next to elastix.exe)
|
| 264 |
-
# ---------------------------------------------------------------------
|
| 265 |
-
lt_url = LIBTORCH_URL[key]
|
| 266 |
-
lt_archive = install_path / Path(lt_url).name
|
| 267 |
-
download_file(lt_url, lt_archive)
|
| 268 |
-
extract_archive(lt_archive, install_path)
|
| 269 |
-
for p in (install_path / "libtorch" / "lib").iterdir():
|
| 270 |
-
if ".so" in p.name or ".dll" in p.name or ".dylib" in p.name:
|
| 271 |
-
shutil.move(p, (install_path if os_name == "Windows" else install_path / "lib") / p.name)
|
| 272 |
-
|
| 273 |
-
shutil.rmtree(install_path / "libtorch")
|
| 274 |
-
if os_name == "Linux" and flavor == "cu128":
|
| 275 |
-
shutil.copy(install_path / "lib" / "libcudart-c3a75b33.so.12", install_path / "lib" / "libcudart.so.12")
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def get_elastix_bin(install_path: Path) -> Path:
|
| 279 |
-
return install_path / ("elastix.exe" if platform.system() == "Windows" else (Path("bin") / "elastix"))
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
def try_elastix(install_path: Path) -> None:
|
| 283 |
-
try:
|
| 284 |
-
subprocess.run(
|
| 285 |
-
[str(get_elastix_bin(install_path)), "-h"],
|
| 286 |
-
capture_output=True,
|
| 287 |
-
text=True,
|
| 288 |
-
check=True,
|
| 289 |
-
) # nosec B603
|
| 290 |
-
except subprocess.CalledProcessError as e:
|
| 291 |
-
msg = "Elastix execution failed.\n\n"
|
| 292 |
-
|
| 293 |
-
msg += f"Command:\n{' '.join(e.cmd)}\n"
|
| 294 |
-
msg += f"Return code: {e.returncode}\n\n"
|
| 295 |
-
|
| 296 |
-
if e.stderr:
|
| 297 |
-
msg += "Error output:\n"
|
| 298 |
-
msg += e.stderr.strip()
|
| 299 |
-
raise NameError(msg)
|
| 300 |
-
|
| 301 |
-
except OSError as e:
|
| 302 |
-
msg = (
|
| 303 |
-
"Elastix could not be started.\n\n"
|
| 304 |
-
"This is usually caused by missing shared libraries "
|
| 305 |
-
"(e.g. LibTorch or CUDA runtime).\n\n"
|
| 306 |
-
f"System error:\n{str(e)}"
|
| 307 |
-
)
|
| 308 |
-
|
| 309 |
-
raise NameError(msg)
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
def main() -> None:
|
| 313 |
-
ap = argparse.ArgumentParser()
|
| 314 |
-
ap.add_argument(
|
| 315 |
-
"--install-path", type=Path, default=DEFAULT_PREFIX, help="Install directory (default: ./elastix-impact)"
|
| 316 |
-
)
|
| 317 |
-
group = ap.add_mutually_exclusive_group()
|
| 318 |
-
group.add_argument("--force-cpu", action="store_true", help="Force CPU install even if NVIDIA present")
|
| 319 |
-
group.add_argument("--force-cuda", action="store_true", help="Force CUDA install (fails if no suitable driver)")
|
| 320 |
-
args = vars(ap.parse_args())
|
| 321 |
-
install_elastix_impact(**args)
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
if __name__ == "__main__":
|
| 325 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CBCT_CT_TS/model.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:de99fbc36331ce674639acc774f52b4a2d0027f2f312d9d28669e831a0c4fd7e
|
| 3 |
-
size 1249
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -35,7 +35,6 @@ resampled onto the fixed image (**`MovedImage`**) and the **`DisplacementField`*
|
|
| 35 |
| `MR_CT_TS` | MR/CT | elastix + IMPACT | MR/CT with MIND + TotalSegmentator features |
|
| 36 |
| `MR_CT_MRSeg` | MR/CT | elastix + IMPACT | MR/CT with MIND + MRSegmentator features |
|
| 37 |
| `CBCT_CT_HeadNeck` | CBCT/CT | elastix + IMPACT | CBCT/CT head & neck preset |
|
| 38 |
-
| `CBCT_CT_TS` | CBCT/CT | elastix + IMPACT | CBCT/CT with TotalSegmentator features |
|
| 39 |
| `CBCT_CT_MRSeg` | CBCT/CT | elastix + IMPACT | CBCT/CT with MRSegmentator features |
|
| 40 |
| `ConvexAdam_Coarse` | any | itk-impact (native) | Global coarse coupled-convex init (IMPACT/MIND) |
|
| 41 |
| `ConvexAdam_Fine` | any | itk-impact (native) | Adam instance-optimisation (tileable; expects a pre-aligned start) |
|
|
|
|
| 35 |
| `MR_CT_TS` | MR/CT | elastix + IMPACT | MR/CT with MIND + TotalSegmentator features |
|
| 36 |
| `MR_CT_MRSeg` | MR/CT | elastix + IMPACT | MR/CT with MIND + MRSegmentator features |
|
| 37 |
| `CBCT_CT_HeadNeck` | CBCT/CT | elastix + IMPACT | CBCT/CT head & neck preset |
|
|
|
|
| 38 |
| `CBCT_CT_MRSeg` | CBCT/CT | elastix + IMPACT | CBCT/CT with MRSegmentator features |
|
| 39 |
| `ConvexAdam_Coarse` | any | itk-impact (native) | Global coarse coupled-convex init (IMPACT/MIND) |
|
| 40 |
| `ConvexAdam_Fine` | any | itk-impact (native) | Adam instance-optimisation (tileable; expects a pre-aligned start) |
|