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 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) |