brkraw 0.3.11__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brkraw/__init__.py +9 -3
- brkraw/apps/__init__.py +12 -0
- brkraw/apps/addon/__init__.py +30 -0
- brkraw/apps/addon/core.py +35 -0
- brkraw/apps/addon/dependencies.py +402 -0
- brkraw/apps/addon/installation.py +500 -0
- brkraw/apps/addon/io.py +21 -0
- brkraw/apps/hook/__init__.py +25 -0
- brkraw/apps/hook/core.py +636 -0
- brkraw/apps/loader/__init__.py +10 -0
- brkraw/apps/loader/core.py +622 -0
- brkraw/apps/loader/formatter.py +288 -0
- brkraw/apps/loader/helper.py +797 -0
- brkraw/apps/loader/info/__init__.py +11 -0
- brkraw/apps/loader/info/scan.py +85 -0
- brkraw/apps/loader/info/scan.yaml +90 -0
- brkraw/apps/loader/info/study.py +69 -0
- brkraw/apps/loader/info/study.yaml +156 -0
- brkraw/apps/loader/info/transform.py +92 -0
- brkraw/apps/loader/types.py +220 -0
- brkraw/cli/__init__.py +5 -0
- brkraw/cli/commands/__init__.py +2 -0
- brkraw/cli/commands/addon.py +327 -0
- brkraw/cli/commands/config.py +205 -0
- brkraw/cli/commands/convert.py +903 -0
- brkraw/cli/commands/hook.py +348 -0
- brkraw/cli/commands/info.py +74 -0
- brkraw/cli/commands/init.py +214 -0
- brkraw/cli/commands/params.py +106 -0
- brkraw/cli/commands/prune.py +288 -0
- brkraw/cli/commands/session.py +371 -0
- brkraw/cli/hook_args.py +80 -0
- brkraw/cli/main.py +83 -0
- brkraw/cli/utils.py +60 -0
- brkraw/core/__init__.py +13 -0
- brkraw/core/config.py +380 -0
- brkraw/core/entrypoints.py +25 -0
- brkraw/core/formatter.py +367 -0
- brkraw/core/fs.py +495 -0
- brkraw/core/jcamp.py +600 -0
- brkraw/core/layout.py +451 -0
- brkraw/core/parameters.py +781 -0
- brkraw/core/zip.py +1121 -0
- brkraw/dataclasses/__init__.py +14 -0
- brkraw/dataclasses/node.py +139 -0
- brkraw/dataclasses/reco.py +33 -0
- brkraw/dataclasses/scan.py +61 -0
- brkraw/dataclasses/study.py +131 -0
- brkraw/default/__init__.py +3 -0
- brkraw/default/pruner_specs/deid4share.yaml +42 -0
- brkraw/default/rules/00_default.yaml +4 -0
- brkraw/default/specs/metadata_dicom.yaml +236 -0
- brkraw/default/specs/metadata_transforms.py +92 -0
- brkraw/resolver/__init__.py +7 -0
- brkraw/resolver/affine.py +539 -0
- brkraw/resolver/datatype.py +69 -0
- brkraw/resolver/fid.py +90 -0
- brkraw/resolver/helpers.py +36 -0
- brkraw/resolver/image.py +188 -0
- brkraw/resolver/nifti.py +370 -0
- brkraw/resolver/shape.py +235 -0
- brkraw/schema/__init__.py +3 -0
- brkraw/schema/context_map.yaml +62 -0
- brkraw/schema/meta.yaml +57 -0
- brkraw/schema/niftiheader.yaml +95 -0
- brkraw/schema/pruner.yaml +55 -0
- brkraw/schema/remapper.yaml +128 -0
- brkraw/schema/rules.yaml +154 -0
- brkraw/specs/__init__.py +10 -0
- brkraw/specs/hook/__init__.py +12 -0
- brkraw/specs/hook/logic.py +31 -0
- brkraw/specs/hook/validator.py +22 -0
- brkraw/specs/meta/__init__.py +5 -0
- brkraw/specs/meta/validator.py +156 -0
- brkraw/specs/pruner/__init__.py +15 -0
- brkraw/specs/pruner/logic.py +361 -0
- brkraw/specs/pruner/validator.py +119 -0
- brkraw/specs/remapper/__init__.py +27 -0
- brkraw/specs/remapper/logic.py +924 -0
- brkraw/specs/remapper/validator.py +314 -0
- brkraw/specs/rules/__init__.py +6 -0
- brkraw/specs/rules/logic.py +263 -0
- brkraw/specs/rules/validator.py +103 -0
- brkraw-0.5.0.dist-info/METADATA +81 -0
- brkraw-0.5.0.dist-info/RECORD +88 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
- brkraw-0.5.0.dist-info/entry_points.txt +13 -0
- brkraw/lib/__init__.py +0 -4
- brkraw/lib/backup.py +0 -641
- brkraw/lib/bids.py +0 -0
- brkraw/lib/errors.py +0 -125
- brkraw/lib/loader.py +0 -1220
- brkraw/lib/orient.py +0 -194
- brkraw/lib/parser.py +0 -48
- brkraw/lib/pvobj.py +0 -301
- brkraw/lib/reference.py +0 -245
- brkraw/lib/utils.py +0 -471
- brkraw/scripts/__init__.py +0 -0
- brkraw/scripts/brk_backup.py +0 -106
- brkraw/scripts/brkraw.py +0 -744
- brkraw/ui/__init__.py +0 -0
- brkraw/ui/config.py +0 -17
- brkraw/ui/main_win.py +0 -214
- brkraw/ui/previewer.py +0 -225
- brkraw/ui/scan_info.py +0 -72
- brkraw/ui/scan_list.py +0 -73
- brkraw/ui/subj_info.py +0 -128
- brkraw-0.3.11.dist-info/METADATA +0 -25
- brkraw-0.3.11.dist-info/RECORD +0 -28
- brkraw-0.3.11.dist-info/entry_points.txt +0 -3
- brkraw-0.3.11.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
"""Internal helper functions for BrukerLoader.
|
|
2
|
+
|
|
3
|
+
Last updated: 2025-12-30
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from types import MethodType
|
|
9
|
+
from functools import partial
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union, Any, Mapping, cast, List, Dict, Literal
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from warnings import warn
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from nibabel.nifti1 import Nifti1Image
|
|
18
|
+
|
|
19
|
+
from ...core.config import resolve_root
|
|
20
|
+
from ...core.parameters import Parameters
|
|
21
|
+
from ...specs.remapper import load_spec, map_parameters, load_context_map, apply_context_map
|
|
22
|
+
from ...specs.rules import load_rules, select_rule_use
|
|
23
|
+
from ...dataclasses import Reco, Scan, Study
|
|
24
|
+
from .types import ScanLoader
|
|
25
|
+
from ...specs import hook as converter_core
|
|
26
|
+
from ...resolver import affine as affine_resolver
|
|
27
|
+
from ...resolver import image as image_resolver
|
|
28
|
+
from ...resolver import fid as fid_resolver
|
|
29
|
+
from ...resolver import nifti as nifti_resolver
|
|
30
|
+
from ...resolver.helpers import get_file
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ...resolver.nifti import Nifti1HeaderContents
|
|
34
|
+
from .types import SubjectType, SubjectPose, XYZUNIT, TUNIT, AffineReturn, AffineSpace
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("brkraw")
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"resolve_data_and_affine",
|
|
40
|
+
"search_parameters",
|
|
41
|
+
"get_dataobj",
|
|
42
|
+
"get_affine",
|
|
43
|
+
"get_nifti1image",
|
|
44
|
+
"convert",
|
|
45
|
+
"get_metadata",
|
|
46
|
+
"apply_converter_hook",
|
|
47
|
+
"make_dir",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def make_dir(names: List[str]):
|
|
52
|
+
"""Return a stable __dir__ function for a module."""
|
|
53
|
+
def _dir() -> List[str]:
|
|
54
|
+
return sorted(names)
|
|
55
|
+
return _dir
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_reco_id(
|
|
59
|
+
scan: Union["Scan", "ScanLoader"],
|
|
60
|
+
reco_id: Optional[int],
|
|
61
|
+
) -> Optional[int]:
|
|
62
|
+
"""Resolve a reco id, defaulting to the first available when None."""
|
|
63
|
+
scan = cast(ScanLoader, scan)
|
|
64
|
+
available = list(scan.avail.keys())
|
|
65
|
+
if not available:
|
|
66
|
+
logger.warning("No reco ids available for scan %s", getattr(scan, "scan_id", "?"))
|
|
67
|
+
return None
|
|
68
|
+
if reco_id is None:
|
|
69
|
+
return available[0]
|
|
70
|
+
if reco_id not in scan.avail:
|
|
71
|
+
logger.warning(
|
|
72
|
+
"Reco id %s not available for scan %s (available: %s)",
|
|
73
|
+
reco_id,
|
|
74
|
+
getattr(scan, "scan_id", "?"),
|
|
75
|
+
available,
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
return reco_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_data_and_affine(
|
|
82
|
+
scan: Union["Scan", "ScanLoader"],
|
|
83
|
+
reco_id: Optional[int] = None,
|
|
84
|
+
*,
|
|
85
|
+
affine_decimals: int = 6,
|
|
86
|
+
):
|
|
87
|
+
"""Populate per-reco image/affine resolver outputs on a scan.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
scan: Scan node to attach image/affine info.
|
|
91
|
+
reco_id: Reco identifier to resolve (default: 1).
|
|
92
|
+
affine_decimals: Decimal rounding applied to resolved affines.
|
|
93
|
+
"""
|
|
94
|
+
scan = cast(ScanLoader, scan)
|
|
95
|
+
|
|
96
|
+
reco_ids = [reco_id] if reco_id is not None else list(scan.avail.keys())
|
|
97
|
+
if not reco_ids:
|
|
98
|
+
logger.warning("No reco ids available to resolve for scan %s", getattr(scan, "scan_id", "?"))
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
for rid in reco_ids:
|
|
102
|
+
if rid not in scan.avail:
|
|
103
|
+
logger.warning(
|
|
104
|
+
"Reco id %s not available for scan %s (available: %s)",
|
|
105
|
+
rid,
|
|
106
|
+
getattr(scan, "scan_id", "?"),
|
|
107
|
+
list(scan.avail.keys()),
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
image_info = image_resolver.resolve(scan, rid)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"Failed to resolve image data for scan %s reco %s: %s",
|
|
115
|
+
getattr(scan, "scan_id", "?"),
|
|
116
|
+
rid,
|
|
117
|
+
exc,
|
|
118
|
+
)
|
|
119
|
+
image_info = None
|
|
120
|
+
try:
|
|
121
|
+
# store subject-view affines (scanner unwrap happens in get_affine)
|
|
122
|
+
affine_info = affine_resolver.resolve(
|
|
123
|
+
scan, rid, decimals=affine_decimals, unwrap_pose=False,
|
|
124
|
+
)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Failed to resolve affine for scan %s reco %s: %s",
|
|
128
|
+
getattr(scan, "scan_id", "?"),
|
|
129
|
+
rid,
|
|
130
|
+
exc,
|
|
131
|
+
)
|
|
132
|
+
affine_info = None
|
|
133
|
+
|
|
134
|
+
if hasattr(scan, "image_info"):
|
|
135
|
+
scan.image_info[rid] = image_info
|
|
136
|
+
else:
|
|
137
|
+
setattr(scan, "image_info", {rid: image_info})
|
|
138
|
+
if hasattr(scan, "affine_info"):
|
|
139
|
+
scan.affine_info[rid] = affine_info
|
|
140
|
+
else:
|
|
141
|
+
setattr(scan, "affine_info", {rid: affine_info})
|
|
142
|
+
scan.get_fid = MethodType(fid_resolver.resolve, scan)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def search_parameters(
|
|
146
|
+
self: Union[Study, Scan, Reco],
|
|
147
|
+
key: str,
|
|
148
|
+
file: Optional[Union[str, List[str]]] = None,
|
|
149
|
+
scan_id: Optional[int] = None,
|
|
150
|
+
reco_id: Optional[int] = None,
|
|
151
|
+
) -> Optional[dict]:
|
|
152
|
+
"""Search parameter files for keys on Study/Scan/Reco objects.
|
|
153
|
+
|
|
154
|
+
Results are grouped by filename. When searching a Study/Scan without
|
|
155
|
+
reco_id, scan and reco hits are merged as
|
|
156
|
+
`{filename: {"scan": {...}, "reco_<id>": {...}}}`. With a specific reco_id
|
|
157
|
+
(or Reco), results stay flat as `{filename: {matched_key: value}}`.
|
|
158
|
+
Missing files are ignored; non-parameter files raise TypeError.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
self: Study, Scan, or Reco instance.
|
|
162
|
+
key: Parameter key to search for.
|
|
163
|
+
file: Filename or list of filenames to search (default: common set).
|
|
164
|
+
scan_id: Scan id (required when searching from Study).
|
|
165
|
+
reco_id: Reco id (optional; flattens results for that reco).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Mapping of filename to found values, or None if no hits.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
files = ["method", "acqp", "visu_pars", "reco"] if file is None else file
|
|
172
|
+
files = [files] if isinstance(files, str) else list(files)
|
|
173
|
+
|
|
174
|
+
def load_parameters(obj: Union[Study, Scan, Reco], filename: str) -> Optional[Parameters]:
|
|
175
|
+
try:
|
|
176
|
+
params = get_file(obj, filename)
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(params, Parameters):
|
|
180
|
+
raise TypeError(f"Not a Paravision parameter file: {filename}")
|
|
181
|
+
return params
|
|
182
|
+
|
|
183
|
+
def flatten_matches(matches: List[dict]) -> dict:
|
|
184
|
+
flat: dict = {}
|
|
185
|
+
for entry in matches:
|
|
186
|
+
flat.update(entry)
|
|
187
|
+
return flat
|
|
188
|
+
|
|
189
|
+
def search_node(node: Union[Study, Scan, Reco]) -> Dict[str, dict]:
|
|
190
|
+
hits: Dict[str, dict] = {}
|
|
191
|
+
for fname in files:
|
|
192
|
+
params = load_parameters(node, fname)
|
|
193
|
+
if params is None:
|
|
194
|
+
continue
|
|
195
|
+
matches = params.search_keys(key)
|
|
196
|
+
if matches:
|
|
197
|
+
hits[fname] = flatten_matches(matches)
|
|
198
|
+
return hits
|
|
199
|
+
|
|
200
|
+
def search_recos(scan_obj: Scan) -> Dict[int, Dict[str, dict]]:
|
|
201
|
+
reco_hits: Dict[int, Dict[str, dict]] = {}
|
|
202
|
+
for rid, reco in scan_obj.avail.items():
|
|
203
|
+
hits = search_node(reco)
|
|
204
|
+
if hits:
|
|
205
|
+
reco_hits[rid] = hits
|
|
206
|
+
return reco_hits
|
|
207
|
+
|
|
208
|
+
def merge_scan_and_recos(
|
|
209
|
+
scan_hits: Dict[str, dict], reco_hits: Dict[int, Dict[str, dict]]
|
|
210
|
+
) -> Dict[str, Union[Dict[str, dict], dict]]:
|
|
211
|
+
"""Merge scan/reco hits by filename.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
scan_hits: Per-filename hits from the scan object.
|
|
215
|
+
reco_hits: Per-reco hits keyed by reco id.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Merged mapping keyed by filename.
|
|
219
|
+
"""
|
|
220
|
+
if not scan_hits and not reco_hits:
|
|
221
|
+
return {}
|
|
222
|
+
|
|
223
|
+
merged: Dict[str, Union[Dict[str, dict], dict]] = {}
|
|
224
|
+
all_fnames = set(scan_hits) | {fname for rh in reco_hits.values() for fname in rh}
|
|
225
|
+
for fname in all_fnames:
|
|
226
|
+
scan_hit = scan_hits.get(fname)
|
|
227
|
+
reco_for_fname = {
|
|
228
|
+
f"reco_{rid}": rhits[fname]
|
|
229
|
+
for rid, rhits in reco_hits.items()
|
|
230
|
+
if fname in rhits
|
|
231
|
+
}
|
|
232
|
+
if reco_for_fname:
|
|
233
|
+
merged[fname] = {}
|
|
234
|
+
if scan_hit:
|
|
235
|
+
merged[fname]["scan"] = scan_hit
|
|
236
|
+
merged[fname].update(reco_for_fname)
|
|
237
|
+
elif scan_hit:
|
|
238
|
+
merged[fname] = scan_hit
|
|
239
|
+
return merged
|
|
240
|
+
|
|
241
|
+
if isinstance(self, Study):
|
|
242
|
+
if scan_id is None:
|
|
243
|
+
warn("To search from Study object, specifying <scan_id> is required.")
|
|
244
|
+
return None
|
|
245
|
+
scan = self.get_scan(scan_id)
|
|
246
|
+
scan_hits = search_node(scan)
|
|
247
|
+
if reco_id is None:
|
|
248
|
+
reco_hits = search_recos(scan)
|
|
249
|
+
merged = merge_scan_and_recos(scan_hits, reco_hits)
|
|
250
|
+
return merged or None
|
|
251
|
+
# specific reco: keep flat
|
|
252
|
+
result: Dict[str, dict] = {}
|
|
253
|
+
if scan_hits:
|
|
254
|
+
result.update(scan_hits)
|
|
255
|
+
reco = scan.get_reco(reco_id)
|
|
256
|
+
reco_hits = search_node(reco)
|
|
257
|
+
if reco_hits:
|
|
258
|
+
result.update(reco_hits)
|
|
259
|
+
return result or None
|
|
260
|
+
|
|
261
|
+
if isinstance(self, Scan):
|
|
262
|
+
scan_hits = search_node(self)
|
|
263
|
+
if reco_id is None:
|
|
264
|
+
reco_hits = search_recos(self)
|
|
265
|
+
merged = merge_scan_and_recos(scan_hits, reco_hits)
|
|
266
|
+
return merged or None
|
|
267
|
+
# specific reco: keep flat
|
|
268
|
+
result: Dict[str, dict] = {}
|
|
269
|
+
if scan_hits:
|
|
270
|
+
result.update(scan_hits)
|
|
271
|
+
reco_hits = search_node(self.get_reco(reco_id))
|
|
272
|
+
if reco_hits:
|
|
273
|
+
result.update(reco_hits)
|
|
274
|
+
return result or None
|
|
275
|
+
|
|
276
|
+
if isinstance(self, Reco):
|
|
277
|
+
reco_hits = search_node(self)
|
|
278
|
+
return reco_hits or None
|
|
279
|
+
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _finalize_affines(
|
|
284
|
+
affines: list[np.ndarray],
|
|
285
|
+
num_slice_packs: int,
|
|
286
|
+
decimals: Optional[int],
|
|
287
|
+
) -> AffineReturn:
|
|
288
|
+
if num_slice_packs == 1:
|
|
289
|
+
affine = affines[0]
|
|
290
|
+
if decimals is not None:
|
|
291
|
+
affine = np.round(affine, decimals=decimals)
|
|
292
|
+
return affine
|
|
293
|
+
|
|
294
|
+
if decimals is not None:
|
|
295
|
+
return tuple(np.round(a, decimals=decimals) for a in affines)
|
|
296
|
+
|
|
297
|
+
return tuple(affines)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_dataobj(
|
|
301
|
+
self: Union["Scan", "ScanLoader"],
|
|
302
|
+
reco_id: Optional[int] = None,
|
|
303
|
+
**_: Any,
|
|
304
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
305
|
+
"""Return reconstructed data for a reco, split by slice pack if needed.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
self: Scan or ScanLoader instance.
|
|
309
|
+
reco_id: Reco identifier to read (defaults to the first available).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Single ndarray when one slice pack exists; otherwise a tuple of arrays.
|
|
313
|
+
Returns None when required metadata is unavailable.
|
|
314
|
+
"""
|
|
315
|
+
if not hasattr(self, "image_info") or not hasattr(self, "affine_info"):
|
|
316
|
+
return None
|
|
317
|
+
self = cast(ScanLoader, self)
|
|
318
|
+
resolved_reco_id = _resolve_reco_id(self, reco_id)
|
|
319
|
+
if resolved_reco_id is None:
|
|
320
|
+
return None
|
|
321
|
+
affine_info = self.affine_info.get(resolved_reco_id)
|
|
322
|
+
image_info = self.image_info.get(resolved_reco_id)
|
|
323
|
+
if affine_info is None or image_info is None:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
num_slices = affine_info["num_slices"]
|
|
327
|
+
dataobj = image_info["dataobj"]
|
|
328
|
+
|
|
329
|
+
slice_pack = []
|
|
330
|
+
slice_offset = 0
|
|
331
|
+
for _num_slices in num_slices:
|
|
332
|
+
_dataobj = dataobj[:, :, slice(slice_offset, slice_offset + _num_slices)]
|
|
333
|
+
slice_offset += _num_slices
|
|
334
|
+
slice_pack.append(_dataobj)
|
|
335
|
+
|
|
336
|
+
if len(slice_pack) == 1:
|
|
337
|
+
return slice_pack[0]
|
|
338
|
+
return tuple(slice_pack)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_affine(
|
|
342
|
+
self: Union["Scan", "ScanLoader"],
|
|
343
|
+
reco_id: Optional[int] = None,
|
|
344
|
+
*,
|
|
345
|
+
space: AffineSpace = "subject_ras",
|
|
346
|
+
override_subject_type: Optional["SubjectType"] = None,
|
|
347
|
+
override_subject_pose: Optional["SubjectPose"] = None,
|
|
348
|
+
decimals: Optional[int] = None,
|
|
349
|
+
**kwargs: Any,
|
|
350
|
+
) -> AffineReturn:
|
|
351
|
+
"""
|
|
352
|
+
Return affine(s) for a reco in the requested coordinate space.
|
|
353
|
+
|
|
354
|
+
Spaces:
|
|
355
|
+
- "raw": Return the affine(s) as stored (no transforms applied).
|
|
356
|
+
- "scanner": Return affine(s) in scanner XYZ (unwrapped).
|
|
357
|
+
- "subject_ras": Return affine(s) in subject-view RAS (wrap to subject pose/type).
|
|
358
|
+
|
|
359
|
+
Overrides:
|
|
360
|
+
- override_subject_type and override_subject_pose are only valid when space="subject_ras".
|
|
361
|
+
Overrides are applied during wrapping to subject RAS.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
self: Scan or ScanLoader instance.
|
|
365
|
+
reco_id: Reco identifier to read (defaults to the first available).
|
|
366
|
+
space: Output space: "raw", "scanner", or "subject_ras" (default: "subject_ras").
|
|
367
|
+
override_subject_type: Optional subject type override (only for "subject_ras").
|
|
368
|
+
override_subject_pose: Optional subject pose override (only for "subject_ras").
|
|
369
|
+
decimals: Optional decimal rounding applied to returned affines.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Single affine matrix when one slice pack exists; otherwise a tuple of affines.
|
|
373
|
+
Returns None when affine info is unavailable.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: If overrides are provided when space is not "subject_ras".
|
|
377
|
+
"""
|
|
378
|
+
if not hasattr(self, "affine_info"):
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
self = cast("ScanLoader", self)
|
|
382
|
+
resolved_reco_id = _resolve_reco_id(self, reco_id)
|
|
383
|
+
if resolved_reco_id is None:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
affine_info = self.affine_info.get(resolved_reco_id)
|
|
387
|
+
if affine_info is None:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
num_slice_packs = affine_info["num_slice_packs"]
|
|
391
|
+
affines = list(affine_info["affines"]) # make a copy-like list
|
|
392
|
+
|
|
393
|
+
is_override = (override_subject_type is not None) or (override_subject_pose is not None)
|
|
394
|
+
if is_override and space != "subject_ras":
|
|
395
|
+
raise ValueError(
|
|
396
|
+
"override_subject_type/override_subject_pose is only supported when space='subject_ras'."
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# "raw" does not need subject info
|
|
400
|
+
if space == "raw":
|
|
401
|
+
result = _finalize_affines(affines, num_slice_packs, decimals)
|
|
402
|
+
return _apply_affine_post_transform(result, kwargs=kwargs)
|
|
403
|
+
|
|
404
|
+
# Need subject type/pose for unwrap and wrap
|
|
405
|
+
visu_pars = get_file(self.avail[resolved_reco_id], "visu_pars")
|
|
406
|
+
subj_type, subj_pose = affine_resolver.get_subject_type_and_position(visu_pars)
|
|
407
|
+
|
|
408
|
+
# Step 1: unwrap to scanner XYZ
|
|
409
|
+
affines_scanner = [
|
|
410
|
+
affine_resolver.unwrap_to_scanner_xyz(affine, subj_type, subj_pose)
|
|
411
|
+
for affine in affines
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
if space == "scanner":
|
|
415
|
+
result = _finalize_affines(affines_scanner, num_slice_packs, decimals)
|
|
416
|
+
return _apply_affine_post_transform(result, kwargs=kwargs)
|
|
417
|
+
|
|
418
|
+
# Step 2: wrap to subject RAS (optionally with override)
|
|
419
|
+
use_type = override_subject_type or subj_type
|
|
420
|
+
use_pose = override_subject_pose or subj_pose
|
|
421
|
+
|
|
422
|
+
affines_subject_ras = [
|
|
423
|
+
affine_resolver.wrap_to_subject_ras(affine, use_type, use_pose)
|
|
424
|
+
for affine in affines_scanner
|
|
425
|
+
]
|
|
426
|
+
result = _finalize_affines(affines_subject_ras, num_slice_packs, decimals)
|
|
427
|
+
return _apply_affine_post_transform(result, kwargs=kwargs)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _apply_affine_post_transform(affines: AffineReturn, *, kwargs: Mapping[str, Any]) -> AffineReturn:
|
|
431
|
+
"""Apply optional flips/rotations to affines right before returning.
|
|
432
|
+
|
|
433
|
+
These transforms are applied in world space and do not depend on output
|
|
434
|
+
`space`. They are controlled via extra kwargs (intentionally not strict):
|
|
435
|
+
|
|
436
|
+
- flip_x / flip_y / flip_z: bool-like
|
|
437
|
+
- rad_x / rad_y / rad_z: radians (float-like)
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
def as_bool(value: Any) -> bool:
|
|
441
|
+
if isinstance(value, str):
|
|
442
|
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
443
|
+
return bool(value)
|
|
444
|
+
|
|
445
|
+
def as_float(value: Any) -> float:
|
|
446
|
+
try:
|
|
447
|
+
return float(value)
|
|
448
|
+
except (TypeError, ValueError):
|
|
449
|
+
return 0.0
|
|
450
|
+
|
|
451
|
+
flip_x = as_bool(kwargs.get("flip_x", False))
|
|
452
|
+
flip_y = as_bool(kwargs.get("flip_y", False))
|
|
453
|
+
flip_z = as_bool(kwargs.get("flip_z", False))
|
|
454
|
+
rad_x = as_float(kwargs.get("rad_x", 0.0))
|
|
455
|
+
rad_y = as_float(kwargs.get("rad_y", 0.0))
|
|
456
|
+
rad_z = as_float(kwargs.get("rad_z", 0.0))
|
|
457
|
+
|
|
458
|
+
if not (flip_x or flip_y or flip_z or rad_x or rad_y or rad_z):
|
|
459
|
+
return affines
|
|
460
|
+
|
|
461
|
+
def apply_one(a: np.ndarray) -> np.ndarray:
|
|
462
|
+
out = np.asarray(a, dtype=float)
|
|
463
|
+
if flip_x or flip_y or flip_z:
|
|
464
|
+
out = affine_resolver.flip_affine(out, flip_x=flip_x, flip_y=flip_y, flip_z=flip_z)
|
|
465
|
+
if rad_x or rad_y or rad_z:
|
|
466
|
+
out = affine_resolver.rotate_affine(out, rad_x=rad_x, rad_y=rad_y, rad_z=rad_z)
|
|
467
|
+
return np.asarray(out, dtype=float)
|
|
468
|
+
|
|
469
|
+
if isinstance(affines, tuple):
|
|
470
|
+
return tuple(apply_one(np.asarray(a)) for a in affines)
|
|
471
|
+
return apply_one(np.asarray(affines))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_nifti1image(
|
|
475
|
+
self: Union["Scan", "ScanLoader"],
|
|
476
|
+
reco_id: Optional[int] = None,
|
|
477
|
+
*,
|
|
478
|
+
space: AffineSpace = "subject_ras",
|
|
479
|
+
override_header: Optional[Nifti1HeaderContents] = None,
|
|
480
|
+
override_subject_type: Optional[SubjectType] = None,
|
|
481
|
+
override_subject_pose: Optional[SubjectPose] = None,
|
|
482
|
+
flip_x: bool = False,
|
|
483
|
+
flatten_fg: bool = False,
|
|
484
|
+
xyz_units: XYZUNIT = "mm",
|
|
485
|
+
t_units: TUNIT = "sec",
|
|
486
|
+
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
487
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
488
|
+
"""Return NIfTI image(s) for a reco.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
self: Scan or ScanLoader instance.
|
|
492
|
+
reco_id: Reco identifier to read (defaults to the first available).
|
|
493
|
+
space: Output affine space ("raw", "scanner", "subject_ras").
|
|
494
|
+
override_header: Optional header values to apply.
|
|
495
|
+
override_subject_type: Subject type override for subject-view wrapping.
|
|
496
|
+
override_subject_pose: Subject pose override for subject-view wrapping.
|
|
497
|
+
flip_x: If True, set NIfTI header x-flip flag.
|
|
498
|
+
xyz_units: Spatial units for NIfTI header.
|
|
499
|
+
t_units: Temporal units for NIfTI header.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Single NIfTI image when one slice pack exists; otherwise a tuple of
|
|
503
|
+
images. Returns None when required metadata is unavailable.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
if not all(
|
|
507
|
+
hasattr(self, attr) for attr in ["image_info", "affine_info", "get_dataobj", "get_affine"]
|
|
508
|
+
):
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
self = cast(ScanLoader, self)
|
|
512
|
+
resolved_reco_id = _resolve_reco_id(self, reco_id)
|
|
513
|
+
if resolved_reco_id is None:
|
|
514
|
+
return None
|
|
515
|
+
hook_kwargs = _resolve_hook_kwargs(self, hook_args_by_name)
|
|
516
|
+
data_kwargs = _filter_hook_kwargs(self.get_dataobj, hook_kwargs)
|
|
517
|
+
if data_kwargs:
|
|
518
|
+
dataobjs = self.get_dataobj(resolved_reco_id, **data_kwargs)
|
|
519
|
+
else:
|
|
520
|
+
dataobjs = self.get_dataobj(resolved_reco_id)
|
|
521
|
+
affine_kwargs = _filter_hook_kwargs(self.get_affine, hook_kwargs)
|
|
522
|
+
if affine_kwargs:
|
|
523
|
+
affines = self.get_affine(
|
|
524
|
+
resolved_reco_id,
|
|
525
|
+
space=space,
|
|
526
|
+
override_subject_type=override_subject_type,
|
|
527
|
+
override_subject_pose=override_subject_pose,
|
|
528
|
+
**affine_kwargs,
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
affines = self.get_affine(
|
|
532
|
+
resolved_reco_id,
|
|
533
|
+
space=space,
|
|
534
|
+
override_subject_type=override_subject_type,
|
|
535
|
+
override_subject_pose=override_subject_pose,
|
|
536
|
+
)
|
|
537
|
+
image_info = self.image_info.get(resolved_reco_id)
|
|
538
|
+
|
|
539
|
+
if dataobjs is None or affines is None or image_info is None:
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
if not isinstance(dataobjs, tuple) and not isinstance(affines, tuple):
|
|
543
|
+
dataobjs = (dataobjs,)
|
|
544
|
+
affines = (affines,)
|
|
545
|
+
|
|
546
|
+
niiobjs = []
|
|
547
|
+
for i, dataobj in enumerate(dataobjs):
|
|
548
|
+
if flatten_fg and dataobj.ndim > 4:
|
|
549
|
+
spatial_shape = dataobj.shape[:3]
|
|
550
|
+
flattened = int(np.prod(dataobj.shape[3:]))
|
|
551
|
+
dataobj = dataobj.reshape((*spatial_shape, flattened), order="A")
|
|
552
|
+
affine = affines[i]
|
|
553
|
+
niiobj = Nifti1Image(dataobj, affine)
|
|
554
|
+
nifti1header_contents = nifti_resolver.resolve(
|
|
555
|
+
image_info, flip_x=flip_x, xyz_units=xyz_units, t_units=t_units
|
|
556
|
+
)
|
|
557
|
+
if override_header:
|
|
558
|
+
for key, value in override_header.items():
|
|
559
|
+
if value is not None:
|
|
560
|
+
nifti1header_contents[key] = value
|
|
561
|
+
niiobj = nifti_resolver.update(niiobj, nifti1header_contents)
|
|
562
|
+
niiobjs.append(niiobj)
|
|
563
|
+
|
|
564
|
+
if len(niiobjs) == 1:
|
|
565
|
+
return niiobjs[0]
|
|
566
|
+
return tuple(niiobjs)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def convert(
|
|
570
|
+
self: Union["Scan", "ScanLoader"],
|
|
571
|
+
reco_id: Optional[int] = None,
|
|
572
|
+
*,
|
|
573
|
+
format: Literal["nifti", "nifti1"] = "nifti",
|
|
574
|
+
space: AffineSpace = "subject_ras",
|
|
575
|
+
override_header: Optional[Nifti1HeaderContents] = None,
|
|
576
|
+
override_subject_type: Optional[SubjectType] = None,
|
|
577
|
+
override_subject_pose: Optional[SubjectPose] = None,
|
|
578
|
+
flip_x: bool = False,
|
|
579
|
+
flatten_fg: bool = False,
|
|
580
|
+
xyz_units: XYZUNIT = "mm",
|
|
581
|
+
t_units: TUNIT = "sec",
|
|
582
|
+
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
583
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
584
|
+
"""Convert a reco to a selected output format."""
|
|
585
|
+
if format not in {"nifti", "nifti1"}:
|
|
586
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
587
|
+
return get_nifti1image(
|
|
588
|
+
self,
|
|
589
|
+
reco_id,
|
|
590
|
+
space=space,
|
|
591
|
+
override_header=override_header,
|
|
592
|
+
override_subject_type=override_subject_type,
|
|
593
|
+
override_subject_pose=override_subject_pose,
|
|
594
|
+
flip_x=flip_x,
|
|
595
|
+
flatten_fg=flatten_fg,
|
|
596
|
+
xyz_units=xyz_units,
|
|
597
|
+
t_units=t_units,
|
|
598
|
+
hook_args_by_name=hook_args_by_name,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _resolve_hook_kwargs(
|
|
603
|
+
scan: Union["Scan", "ScanLoader"],
|
|
604
|
+
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]],
|
|
605
|
+
) -> Dict[str, Any]:
|
|
606
|
+
if not hook_args_by_name:
|
|
607
|
+
return {}
|
|
608
|
+
hook_name = getattr(scan, "_converter_hook_name", None)
|
|
609
|
+
if not isinstance(hook_name, str) or not hook_name:
|
|
610
|
+
return {}
|
|
611
|
+
values = hook_args_by_name.get(hook_name)
|
|
612
|
+
if values is None:
|
|
613
|
+
seen: set[str] = set()
|
|
614
|
+
|
|
615
|
+
def _add(candidate: str) -> None:
|
|
616
|
+
cand = candidate.strip()
|
|
617
|
+
if not cand or cand in seen:
|
|
618
|
+
return
|
|
619
|
+
seen.add(cand)
|
|
620
|
+
|
|
621
|
+
_add(hook_name)
|
|
622
|
+
_add(hook_name.lower())
|
|
623
|
+
_add(hook_name.replace("_", "-"))
|
|
624
|
+
_add(hook_name.replace("-", "_"))
|
|
625
|
+
_add(hook_name.lower().replace("_", "-"))
|
|
626
|
+
_add(hook_name.lower().replace("-", "_"))
|
|
627
|
+
_add(f"brkraw-{hook_name}")
|
|
628
|
+
_add(f"brkraw_{hook_name}")
|
|
629
|
+
_add(f"brkraw-{hook_name.lower()}")
|
|
630
|
+
_add(f"brkraw_{hook_name.lower()}")
|
|
631
|
+
_add(f"brkraw-{hook_name.lower().replace('_', '-')}")
|
|
632
|
+
_add(f"brkraw_{hook_name.lower().replace('-', '_')}")
|
|
633
|
+
|
|
634
|
+
for candidate in sorted(seen):
|
|
635
|
+
if candidate == hook_name:
|
|
636
|
+
continue
|
|
637
|
+
candidate_values = hook_args_by_name.get(candidate)
|
|
638
|
+
if candidate_values is not None:
|
|
639
|
+
logger.debug(
|
|
640
|
+
"Using hook args for %r from alias %r.",
|
|
641
|
+
hook_name,
|
|
642
|
+
candidate,
|
|
643
|
+
)
|
|
644
|
+
values = candidate_values
|
|
645
|
+
break
|
|
646
|
+
return dict(values) if isinstance(values, Mapping) else {}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _filter_hook_kwargs(func: Any, hook_kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
|
650
|
+
"""Drop unsupported hook kwargs for a callable.
|
|
651
|
+
|
|
652
|
+
This keeps YAML/CLI presets safe when converter hooks do not accept
|
|
653
|
+
arbitrary kwargs.
|
|
654
|
+
"""
|
|
655
|
+
if not hook_kwargs:
|
|
656
|
+
return {}
|
|
657
|
+
try:
|
|
658
|
+
sig = inspect.signature(func)
|
|
659
|
+
except (TypeError, ValueError):
|
|
660
|
+
return dict(hook_kwargs)
|
|
661
|
+
for param in sig.parameters.values():
|
|
662
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
663
|
+
return dict(hook_kwargs)
|
|
664
|
+
allowed = {
|
|
665
|
+
param.name
|
|
666
|
+
for param in sig.parameters.values()
|
|
667
|
+
if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
668
|
+
and param.name != "self"
|
|
669
|
+
}
|
|
670
|
+
filtered = {key: value for key, value in hook_kwargs.items() if key in allowed}
|
|
671
|
+
dropped = [key for key in hook_kwargs.keys() if key not in allowed]
|
|
672
|
+
if dropped:
|
|
673
|
+
logger.debug(
|
|
674
|
+
"Ignoring unsupported hook args for %s: %s",
|
|
675
|
+
getattr(func, "__name__", "<callable>"),
|
|
676
|
+
", ".join(sorted(dropped)),
|
|
677
|
+
)
|
|
678
|
+
return filtered
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _resolve_metadata_spec(
|
|
682
|
+
scan: "ScanLoader",
|
|
683
|
+
spec: Optional[Union[Mapping[str, Any], str, Path]],
|
|
684
|
+
*,
|
|
685
|
+
base: Path,
|
|
686
|
+
) -> Optional[Tuple[Mapping[str, Any], Dict[str, Any], Optional[Path]]]:
|
|
687
|
+
"""Resolve a metadata spec and its transforms for a scan.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
scan: Scan instance to evaluate rules against.
|
|
691
|
+
spec: Optional spec mapping or spec path override.
|
|
692
|
+
base: Config root directory for rule resolution.
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
Tuple of (spec, transforms, spec_path) or None when no spec matches.
|
|
696
|
+
"""
|
|
697
|
+
if spec is None:
|
|
698
|
+
try:
|
|
699
|
+
rules = load_rules(root=base, validate=False)
|
|
700
|
+
except Exception:
|
|
701
|
+
return None
|
|
702
|
+
spec_path = select_rule_use(
|
|
703
|
+
scan,
|
|
704
|
+
rules.get("metadata_spec", []),
|
|
705
|
+
base=base,
|
|
706
|
+
resolve_paths=True,
|
|
707
|
+
)
|
|
708
|
+
if not isinstance(spec_path, Path) or not spec_path.exists():
|
|
709
|
+
return None
|
|
710
|
+
spec_data, transforms = load_spec(spec_path, validate=False)
|
|
711
|
+
return spec_data, transforms, spec_path
|
|
712
|
+
if isinstance(spec, (str, Path)):
|
|
713
|
+
spec_path = Path(spec)
|
|
714
|
+
spec_data, transforms = load_spec(spec_path, validate=False)
|
|
715
|
+
return spec_data, transforms, spec_path
|
|
716
|
+
if isinstance(spec, Mapping):
|
|
717
|
+
return spec, {}, None
|
|
718
|
+
raise TypeError(f"Unsupported spec type: {type(spec)!r}")
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def get_metadata(
|
|
722
|
+
self,
|
|
723
|
+
reco_id: Optional[int] = None,
|
|
724
|
+
spec: Optional[Union[Mapping[str, Any], str, Path]] = None,
|
|
725
|
+
context_map: Optional[Union[str, Path]] = None,
|
|
726
|
+
return_spec: bool = False,
|
|
727
|
+
):
|
|
728
|
+
"""Resolve metadata using a remapper spec.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
self: Scan instance.
|
|
732
|
+
reco_id: Reco identifier (defaults to the first available).
|
|
733
|
+
spec: Optional spec mapping or spec file path.
|
|
734
|
+
context_map: Optional context map override.
|
|
735
|
+
return_spec: If True, return spec info alongside metadata.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
Mapping of metadata fields, or None when no spec matches. When
|
|
739
|
+
return_spec is True, returns (metadata, spec_info).
|
|
740
|
+
"""
|
|
741
|
+
scan = cast(ScanLoader, self)
|
|
742
|
+
resolved_reco_id = _resolve_reco_id(scan, reco_id)
|
|
743
|
+
if resolved_reco_id is None:
|
|
744
|
+
if return_spec:
|
|
745
|
+
return None, None
|
|
746
|
+
return None
|
|
747
|
+
base = resolve_root(None)
|
|
748
|
+
resolved = _resolve_metadata_spec(scan, spec, base=base)
|
|
749
|
+
if resolved is None:
|
|
750
|
+
if return_spec:
|
|
751
|
+
return None, None
|
|
752
|
+
return None
|
|
753
|
+
spec_data, transforms, spec_path = resolved
|
|
754
|
+
metadata = map_parameters(
|
|
755
|
+
scan,
|
|
756
|
+
spec_data,
|
|
757
|
+
transforms,
|
|
758
|
+
validate=False,
|
|
759
|
+
context_map=None,
|
|
760
|
+
context={"scan_id": getattr(scan, "scan_id", None), "reco_id": resolved_reco_id},
|
|
761
|
+
)
|
|
762
|
+
if context_map:
|
|
763
|
+
map_data = load_context_map(context_map)
|
|
764
|
+
metadata = apply_context_map(
|
|
765
|
+
metadata,
|
|
766
|
+
map_data,
|
|
767
|
+
target="metadata_spec",
|
|
768
|
+
context={"scan_id": getattr(scan, "scan_id", None), "reco_id": resolved_reco_id},
|
|
769
|
+
)
|
|
770
|
+
if not return_spec:
|
|
771
|
+
return metadata
|
|
772
|
+
meta = spec_data.get("__meta__")
|
|
773
|
+
name = meta.get("name") if isinstance(meta, dict) else None
|
|
774
|
+
version = meta.get("version") if isinstance(meta, dict) else None
|
|
775
|
+
spec_info = {"path": spec_path, "name": name, "version": version}
|
|
776
|
+
return metadata, spec_info
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def apply_converter_hook(
|
|
780
|
+
scan: "ScanLoader",
|
|
781
|
+
converter_hook: Mapping[str, Any],
|
|
782
|
+
*,
|
|
783
|
+
affine_decimals: Optional[int] = None,
|
|
784
|
+
) -> None:
|
|
785
|
+
"""Override scan conversion helpers using a converter hook."""
|
|
786
|
+
converter_core.validate_hook(converter_hook)
|
|
787
|
+
plugin = dict(converter_hook)
|
|
788
|
+
scan._converter_hook = plugin
|
|
789
|
+
if "get_dataobj" in plugin:
|
|
790
|
+
scan.get_dataobj = MethodType(plugin["get_dataobj"], scan)
|
|
791
|
+
if "get_affine" in plugin:
|
|
792
|
+
get_affine = plugin["get_affine"]
|
|
793
|
+
if affine_decimals is not None:
|
|
794
|
+
get_affine = partial(get_affine, decimals=affine_decimals)
|
|
795
|
+
scan.get_affine = MethodType(get_affine, scan)
|
|
796
|
+
if "convert" in plugin:
|
|
797
|
+
scan.convert = MethodType(plugin["convert"], scan)
|