brkraw 0.5.2__py3-none-any.whl → 0.5.5__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 +1 -1
- brkraw/api/__init__.py +122 -0
- brkraw/api/types.py +39 -0
- brkraw/apps/loader/__init__.py +3 -6
- brkraw/apps/loader/core.py +128 -132
- brkraw/apps/loader/formatter.py +0 -2
- brkraw/apps/loader/helper.py +334 -114
- brkraw/apps/loader/info/scan.py +2 -2
- brkraw/apps/loader/info/transform.py +0 -1
- brkraw/apps/loader/types.py +56 -59
- brkraw/cli/commands/addon.py +1 -1
- brkraw/cli/commands/cache.py +82 -0
- brkraw/cli/commands/config.py +2 -2
- brkraw/cli/commands/convert.py +61 -38
- brkraw/cli/commands/hook.py +1 -3
- brkraw/cli/commands/info.py +1 -1
- brkraw/cli/commands/init.py +1 -1
- brkraw/cli/commands/params.py +1 -1
- brkraw/cli/commands/prune.py +2 -2
- brkraw/cli/commands/session.py +1 -11
- brkraw/cli/main.py +51 -1
- brkraw/cli/utils.py +1 -1
- brkraw/core/cache.py +87 -0
- brkraw/core/config.py +18 -2
- brkraw/core/fs.py +26 -9
- brkraw/core/zip.py +46 -32
- brkraw/dataclasses/__init__.py +3 -2
- brkraw/dataclasses/study.py +73 -23
- brkraw/resolver/datatype.py +10 -2
- brkraw/resolver/image.py +140 -21
- brkraw/resolver/nifti.py +4 -12
- brkraw/schema/niftiheader.yaml +0 -2
- brkraw/specs/meta/validator.py +0 -1
- brkraw/specs/rules/logic.py +1 -3
- {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/METADATA +8 -9
- {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/RECORD +39 -35
- {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/entry_points.txt +1 -0
- {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/WHEEL +0 -0
- {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/licenses/LICENSE +0 -0
brkraw/apps/loader/helper.py
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
"""Internal helper functions for BrukerLoader.
|
|
2
|
-
|
|
3
|
-
Last updated: 2025-12-30
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
1
|
from __future__ import annotations
|
|
7
2
|
|
|
8
3
|
from types import MethodType
|
|
9
4
|
from functools import partial
|
|
10
5
|
import inspect
|
|
11
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
cast,
|
|
9
|
+
Optional,
|
|
10
|
+
Tuple,
|
|
11
|
+
Union,
|
|
12
|
+
Any,
|
|
13
|
+
Mapping,
|
|
14
|
+
List,
|
|
15
|
+
Dict
|
|
16
|
+
)
|
|
12
17
|
from pathlib import Path
|
|
13
18
|
from warnings import warn
|
|
14
19
|
import logging
|
|
15
20
|
|
|
16
21
|
import numpy as np
|
|
22
|
+
from numpy.typing import NDArray
|
|
17
23
|
from nibabel.nifti1 import Nifti1Image
|
|
18
24
|
|
|
19
25
|
from ...core.config import resolve_root
|
|
@@ -21,22 +27,38 @@ from ...core.parameters import Parameters
|
|
|
21
27
|
from ...specs.remapper import load_spec, map_parameters, load_context_map, apply_context_map
|
|
22
28
|
from ...specs.rules import load_rules, select_rule_use
|
|
23
29
|
from ...dataclasses import Reco, Scan, Study
|
|
24
|
-
from .types import ScanLoader
|
|
25
30
|
from ...specs import hook as converter_core
|
|
26
31
|
from ...resolver import affine as affine_resolver
|
|
27
32
|
from ...resolver import image as image_resolver
|
|
28
33
|
from ...resolver import fid as fid_resolver
|
|
29
34
|
from ...resolver import nifti as nifti_resolver
|
|
30
35
|
from ...resolver.helpers import get_file
|
|
31
|
-
|
|
36
|
+
from .types import (
|
|
37
|
+
ScanLoader,
|
|
38
|
+
ConvertType,
|
|
39
|
+
GetDataobjType,
|
|
40
|
+
GetAffineType
|
|
41
|
+
)
|
|
32
42
|
if TYPE_CHECKING:
|
|
33
43
|
from ...resolver.nifti import Nifti1HeaderContents
|
|
34
|
-
from .types import
|
|
44
|
+
from .types import (
|
|
45
|
+
SubjectType,
|
|
46
|
+
SubjectPose,
|
|
47
|
+
XYZUNIT,
|
|
48
|
+
TUNIT,
|
|
49
|
+
Dataobjs,
|
|
50
|
+
Affines,
|
|
51
|
+
AffineSpace,
|
|
52
|
+
ConvertedObj,
|
|
53
|
+
Metadata
|
|
54
|
+
)
|
|
35
55
|
|
|
36
|
-
logger = logging.getLogger(
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
37
57
|
|
|
38
58
|
__all__ = [
|
|
59
|
+
"resolve_reco_id",
|
|
39
60
|
"resolve_data_and_affine",
|
|
61
|
+
"resolve_converter_hook",
|
|
40
62
|
"search_parameters",
|
|
41
63
|
"get_dataobj",
|
|
42
64
|
"get_affine",
|
|
@@ -55,7 +77,7 @@ def make_dir(names: List[str]):
|
|
|
55
77
|
return _dir
|
|
56
78
|
|
|
57
79
|
|
|
58
|
-
def
|
|
80
|
+
def resolve_reco_id(
|
|
59
81
|
scan: Union["Scan", "ScanLoader"],
|
|
60
82
|
reco_id: Optional[int],
|
|
61
83
|
) -> Optional[int]:
|
|
@@ -79,7 +101,7 @@ def _resolve_reco_id(
|
|
|
79
101
|
|
|
80
102
|
|
|
81
103
|
def resolve_data_and_affine(
|
|
82
|
-
scan:
|
|
104
|
+
scan: "Scan",
|
|
83
105
|
reco_id: Optional[int] = None,
|
|
84
106
|
*,
|
|
85
107
|
affine_decimals: int = 6,
|
|
@@ -92,6 +114,9 @@ def resolve_data_and_affine(
|
|
|
92
114
|
affine_decimals: Decimal rounding applied to resolved affines.
|
|
93
115
|
"""
|
|
94
116
|
scan = cast(ScanLoader, scan)
|
|
117
|
+
scan.get_fid = MethodType(fid_resolver.resolve, scan)
|
|
118
|
+
scan.image_info = {}
|
|
119
|
+
scan.affine_info = {}
|
|
95
120
|
|
|
96
121
|
reco_ids = [reco_id] if reco_id is not None else list(scan.avail.keys())
|
|
97
122
|
if not reco_ids:
|
|
@@ -108,7 +133,7 @@ def resolve_data_and_affine(
|
|
|
108
133
|
)
|
|
109
134
|
continue
|
|
110
135
|
try:
|
|
111
|
-
image_info = image_resolver.resolve(scan, rid)
|
|
136
|
+
image_info = image_resolver.resolve(scan, rid, load_data=False)
|
|
112
137
|
except Exception as exc:
|
|
113
138
|
logger.warning(
|
|
114
139
|
"Failed to resolve image data for scan %s reco %s: %s",
|
|
@@ -122,6 +147,7 @@ def resolve_data_and_affine(
|
|
|
122
147
|
affine_info = affine_resolver.resolve(
|
|
123
148
|
scan, rid, decimals=affine_decimals, unwrap_pose=False,
|
|
124
149
|
)
|
|
150
|
+
|
|
125
151
|
except Exception as exc:
|
|
126
152
|
logger.warning(
|
|
127
153
|
"Failed to resolve affine for scan %s reco %s: %s",
|
|
@@ -131,15 +157,65 @@ def resolve_data_and_affine(
|
|
|
131
157
|
)
|
|
132
158
|
affine_info = None
|
|
133
159
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
scan.image_info[rid] = image_info
|
|
161
|
+
scan.affine_info[rid] = affine_info
|
|
162
|
+
|
|
163
|
+
def _load_rules(base):
|
|
164
|
+
try:
|
|
165
|
+
rules = load_rules(root=base, validate=False)
|
|
166
|
+
except Exception:
|
|
167
|
+
rules = {}
|
|
168
|
+
return rules
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def resolve_converter_hook(
|
|
172
|
+
scan: "Scan",
|
|
173
|
+
base: Path,
|
|
174
|
+
*,
|
|
175
|
+
affine_decimals: int = 6,
|
|
176
|
+
):
|
|
177
|
+
scan = cast(ScanLoader, scan)
|
|
178
|
+
rules = _load_rules(base)
|
|
179
|
+
if rules:
|
|
180
|
+
try:
|
|
181
|
+
hook_name = select_rule_use(
|
|
182
|
+
scan,
|
|
183
|
+
rules.get("converter_hook", []),
|
|
184
|
+
base=base,
|
|
185
|
+
resolve_paths=False,
|
|
186
|
+
)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.debug(
|
|
189
|
+
"Converter hook rule selection failed for scan %s: %s",
|
|
190
|
+
getattr(scan, "scan_id", "?"),
|
|
191
|
+
exc,
|
|
192
|
+
exc_info=True,
|
|
193
|
+
)
|
|
194
|
+
hook_name = None
|
|
195
|
+
|
|
196
|
+
if isinstance(hook_name, str):
|
|
197
|
+
try:
|
|
198
|
+
entry = converter_core.resolve_hook(hook_name)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.warning(
|
|
201
|
+
"Converter hook %r not available: %s",
|
|
202
|
+
hook_name,
|
|
203
|
+
exc,
|
|
204
|
+
)
|
|
205
|
+
entry = None
|
|
206
|
+
if entry:
|
|
207
|
+
logger.debug("Applying converter hook: %s", hook_name)
|
|
208
|
+
scan._converter_hook_name = hook_name
|
|
209
|
+
apply_converter_hook(
|
|
210
|
+
scan,
|
|
211
|
+
entry,
|
|
212
|
+
affine_decimals=affine_decimals,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
logger.debug("Converter hook %r resolved to no entry.", hook_name)
|
|
140
216
|
else:
|
|
141
|
-
|
|
142
|
-
scan.
|
|
217
|
+
logger.debug("No converter hook selected for scan %s.", getattr(scan, "scan_id", "?"))
|
|
218
|
+
scan._hook_resolved = True
|
|
143
219
|
|
|
144
220
|
|
|
145
221
|
def search_parameters(
|
|
@@ -242,7 +318,7 @@ def search_parameters(
|
|
|
242
318
|
if scan_id is None:
|
|
243
319
|
warn("To search from Study object, specifying <scan_id> is required.")
|
|
244
320
|
return None
|
|
245
|
-
scan = self.get_scan(scan_id)
|
|
321
|
+
scan = cast(ScanLoader, self.get_scan(scan_id))
|
|
246
322
|
scan_hits = search_node(scan)
|
|
247
323
|
if reco_id is None:
|
|
248
324
|
reco_hits = search_recos(scan)
|
|
@@ -281,10 +357,10 @@ def search_parameters(
|
|
|
281
357
|
|
|
282
358
|
|
|
283
359
|
def _finalize_affines(
|
|
284
|
-
affines:
|
|
360
|
+
affines: List[NDArray],
|
|
285
361
|
num_slice_packs: int,
|
|
286
362
|
decimals: Optional[int],
|
|
287
|
-
) ->
|
|
363
|
+
) -> Affines:
|
|
288
364
|
if num_slice_packs == 1:
|
|
289
365
|
affine = affines[0]
|
|
290
366
|
if decimals is not None:
|
|
@@ -298,38 +374,73 @@ def _finalize_affines(
|
|
|
298
374
|
|
|
299
375
|
|
|
300
376
|
def get_dataobj(
|
|
301
|
-
self:
|
|
377
|
+
self: "ScanLoader",
|
|
302
378
|
reco_id: Optional[int] = None,
|
|
303
|
-
**
|
|
304
|
-
) ->
|
|
379
|
+
**kwargs: Dict[str, Any]
|
|
380
|
+
) -> Dataobjs:
|
|
305
381
|
"""Return reconstructed data for a reco, split by slice pack if needed.
|
|
306
382
|
|
|
307
383
|
Args:
|
|
308
384
|
self: Scan or ScanLoader instance.
|
|
309
|
-
|
|
385
|
+
reco_id: Reco identifier to read (defaults to the first available).
|
|
386
|
+
cycle_index: Optional cycle start index (last axis), reads all cycles when None.
|
|
387
|
+
cycle_count: Optional number of cycles to read from cycle_index; reads to end when None.
|
|
388
|
+
Ignored when the dataset reports <= 1 total cycle.
|
|
310
389
|
|
|
311
390
|
Returns:
|
|
312
391
|
Single ndarray when one slice pack exists; otherwise a tuple of arrays.
|
|
313
392
|
Returns None when required metadata is unavailable.
|
|
314
393
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
resolved_reco_id = _resolve_reco_id(self, reco_id)
|
|
394
|
+
cycle_index = cast(Optional[int], kwargs.get('cycle_index'))
|
|
395
|
+
cycle_count = cast(Optional[int], kwargs.get('cycle_count'))
|
|
396
|
+
resolved_reco_id = resolve_reco_id(self, reco_id)
|
|
319
397
|
if resolved_reco_id is None:
|
|
320
398
|
return None
|
|
399
|
+
|
|
321
400
|
affine_info = self.affine_info.get(resolved_reco_id)
|
|
401
|
+
if affine_info is None:
|
|
402
|
+
logger.warning(
|
|
403
|
+
"affine_info is not available for scan %s",
|
|
404
|
+
getattr(self, "scan_id", "?")
|
|
405
|
+
)
|
|
406
|
+
return None
|
|
322
407
|
image_info = self.image_info.get(resolved_reco_id)
|
|
323
|
-
if
|
|
408
|
+
if image_info is None:
|
|
409
|
+
logger.warning(
|
|
410
|
+
"image_info is not available for scan %s",
|
|
411
|
+
getattr(self, "scan_id", "?")
|
|
412
|
+
)
|
|
324
413
|
return None
|
|
325
414
|
|
|
326
|
-
|
|
327
|
-
|
|
415
|
+
# Normalize cycle arguments if provided.
|
|
416
|
+
cycle_args_requested = cycle_index is not None or cycle_count is not None
|
|
417
|
+
if cycle_index is None and cycle_count is not None:
|
|
418
|
+
cycle_index = 0
|
|
419
|
+
|
|
420
|
+
# If the dataset has <= 1 cycle, ignore cycle slicing to avoid block reads.
|
|
421
|
+
if cycle_args_requested:
|
|
422
|
+
total_cycles = int(image_info["num_cycles"])
|
|
423
|
+
if total_cycles <= 1:
|
|
424
|
+
cycle_index = None
|
|
425
|
+
cycle_count = None
|
|
426
|
+
cycle_args_requested = False
|
|
427
|
+
|
|
428
|
+
if cycle_args_requested or image_info.get("dataobj") is None:
|
|
429
|
+
image_info = image_resolver.resolve(
|
|
430
|
+
self,
|
|
431
|
+
resolved_reco_id,
|
|
432
|
+
load_data=True,
|
|
433
|
+
cycle_index=cycle_index,
|
|
434
|
+
cycle_count=cycle_count,
|
|
435
|
+
)
|
|
436
|
+
self.image_info[resolved_reco_id] = image_info
|
|
328
437
|
|
|
438
|
+
num_slices = affine_info["num_slices"]
|
|
439
|
+
dataobj = cast(dict, image_info).get("dataobj")
|
|
329
440
|
slice_pack = []
|
|
330
441
|
slice_offset = 0
|
|
331
442
|
for _num_slices in num_slices:
|
|
332
|
-
_dataobj = dataobj[:, :, slice(slice_offset, slice_offset + _num_slices)]
|
|
443
|
+
_dataobj = cast(NDArray, dataobj)[:, :, slice(slice_offset, slice_offset + _num_slices)]
|
|
333
444
|
slice_offset += _num_slices
|
|
334
445
|
slice_pack.append(_dataobj)
|
|
335
446
|
|
|
@@ -339,7 +450,7 @@ def get_dataobj(
|
|
|
339
450
|
|
|
340
451
|
|
|
341
452
|
def get_affine(
|
|
342
|
-
self:
|
|
453
|
+
self: "ScanLoader",
|
|
343
454
|
reco_id: Optional[int] = None,
|
|
344
455
|
*,
|
|
345
456
|
space: AffineSpace = "subject_ras",
|
|
@@ -347,7 +458,7 @@ def get_affine(
|
|
|
347
458
|
override_subject_pose: Optional["SubjectPose"] = None,
|
|
348
459
|
decimals: Optional[int] = None,
|
|
349
460
|
**kwargs: Any,
|
|
350
|
-
) ->
|
|
461
|
+
) -> Affines:
|
|
351
462
|
"""
|
|
352
463
|
Return affine(s) for a reco in the requested coordinate space.
|
|
353
464
|
|
|
@@ -379,7 +490,7 @@ def get_affine(
|
|
|
379
490
|
return None
|
|
380
491
|
|
|
381
492
|
self = cast("ScanLoader", self)
|
|
382
|
-
resolved_reco_id =
|
|
493
|
+
resolved_reco_id = resolve_reco_id(self, reco_id)
|
|
383
494
|
if resolved_reco_id is None:
|
|
384
495
|
return None
|
|
385
496
|
|
|
@@ -427,7 +538,7 @@ def get_affine(
|
|
|
427
538
|
return _apply_affine_post_transform(result, kwargs=kwargs)
|
|
428
539
|
|
|
429
540
|
|
|
430
|
-
def _apply_affine_post_transform(affines:
|
|
541
|
+
def _apply_affine_post_transform(affines: Affines, *, kwargs: Mapping[str, Any]) -> Affines:
|
|
431
542
|
"""Apply optional flips/rotations to affines right before returning.
|
|
432
543
|
|
|
433
544
|
These transforms are applied in world space and do not depend on output
|
|
@@ -458,7 +569,7 @@ def _apply_affine_post_transform(affines: AffineReturn, *, kwargs: Mapping[str,
|
|
|
458
569
|
if not (flip_x or flip_y or flip_z or rad_x or rad_y or rad_z):
|
|
459
570
|
return affines
|
|
460
571
|
|
|
461
|
-
def apply_one(a:
|
|
572
|
+
def apply_one(a: NDArray) -> NDArray:
|
|
462
573
|
out = np.asarray(a, dtype=float)
|
|
463
574
|
if flip_x or flip_y or flip_z:
|
|
464
575
|
out = affine_resolver.flip_affine(out, flip_x=flip_x, flip_y=flip_y, flip_z=flip_z)
|
|
@@ -473,53 +584,148 @@ def _apply_affine_post_transform(affines: AffineReturn, *, kwargs: Mapping[str,
|
|
|
473
584
|
|
|
474
585
|
def get_nifti1image(
|
|
475
586
|
self: Union["Scan", "ScanLoader"],
|
|
587
|
+
reco_id: int,
|
|
588
|
+
dataobjs: Tuple[NDArray, ...],
|
|
589
|
+
affines: Tuple[NDArray, ...],
|
|
590
|
+
*,
|
|
591
|
+
xyz_units: XYZUNIT = "mm",
|
|
592
|
+
t_units: TUNIT = "sec",
|
|
593
|
+
override_header: Optional[Nifti1HeaderContents] = None,
|
|
594
|
+
) -> ConvertedObj:
|
|
595
|
+
"""Return NIfTI image(s) for a reco.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
self: Scan or ScanLoader instance.
|
|
599
|
+
reco_id: Reco identifier to read (defaults to the first available).
|
|
600
|
+
xyz_units: Spatial units for NIfTI header.
|
|
601
|
+
t_units: Temporal units for NIfTI header.
|
|
602
|
+
override_header: Optional header values to apply.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Output object(s) supporting to_filename(). Returns None when required
|
|
606
|
+
metadata is unavailable.
|
|
607
|
+
"""
|
|
608
|
+
self = cast(ScanLoader, self)
|
|
609
|
+
|
|
610
|
+
image_info = self.image_info.get(reco_id)
|
|
611
|
+
if image_info is None:
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
if dataobjs is None or affines is None:
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
niiobjs = []
|
|
618
|
+
for i, dataobj in enumerate(dataobjs):
|
|
619
|
+
affine = affines[i]
|
|
620
|
+
niiobj = Nifti1Image(dataobj, affine)
|
|
621
|
+
nifti1header_contents = nifti_resolver.resolve(
|
|
622
|
+
image_info, xyz_units=xyz_units, t_units=t_units
|
|
623
|
+
)
|
|
624
|
+
if override_header:
|
|
625
|
+
for key, value in override_header.items():
|
|
626
|
+
if value is not None:
|
|
627
|
+
nifti1header_contents[key] = value
|
|
628
|
+
niiobj = nifti_resolver.update(niiobj, nifti1header_contents)
|
|
629
|
+
niiobjs.append(niiobj)
|
|
630
|
+
|
|
631
|
+
if len(niiobjs) == 1:
|
|
632
|
+
return niiobjs[0]
|
|
633
|
+
return tuple(niiobjs)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def convert(
|
|
637
|
+
self: "ScanLoader",
|
|
476
638
|
reco_id: Optional[int] = None,
|
|
477
639
|
*,
|
|
478
640
|
space: AffineSpace = "subject_ras",
|
|
479
641
|
override_header: Optional[Nifti1HeaderContents] = None,
|
|
480
642
|
override_subject_type: Optional[SubjectType] = None,
|
|
481
643
|
override_subject_pose: Optional[SubjectPose] = None,
|
|
482
|
-
flip_x: bool = False,
|
|
483
644
|
flatten_fg: bool = False,
|
|
484
|
-
xyz_units: XYZUNIT = "mm",
|
|
485
|
-
t_units: TUNIT = "sec",
|
|
486
645
|
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
646
|
+
**kwargs: Any,
|
|
647
|
+
) -> ConvertedObj:
|
|
648
|
+
"""Convert a reco to output object(s).
|
|
649
|
+
|
|
490
650
|
Args:
|
|
491
|
-
self: Scan or ScanLoader instance.
|
|
492
|
-
reco_id: Reco identifier to read (defaults to the first available).
|
|
493
651
|
space: Output affine space ("raw", "scanner", "subject_ras").
|
|
494
652
|
override_header: Optional header values to apply.
|
|
495
653
|
override_subject_type: Subject type override for subject-view wrapping.
|
|
496
654
|
override_subject_pose: Subject pose override for subject-view wrapping.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
655
|
+
flatten_fg: If True, flatten foreground dimensions.
|
|
656
|
+
hook_args_by_name: Optional hook args mapping (split per helper signature).
|
|
657
|
+
flatten_fg: If True, flatten foreground dimensions.
|
|
501
658
|
Returns:
|
|
502
659
|
Single NIfTI image when one slice pack exists; otherwise a tuple of
|
|
503
660
|
images. Returns None when required metadata is unavailable.
|
|
504
661
|
"""
|
|
505
|
-
|
|
506
662
|
if not all(
|
|
507
663
|
hasattr(self, attr) for attr in ["image_info", "affine_info", "get_dataobj", "get_affine"]
|
|
508
664
|
):
|
|
509
665
|
return None
|
|
510
|
-
|
|
666
|
+
|
|
511
667
|
self = cast(ScanLoader, self)
|
|
512
|
-
resolved_reco_id =
|
|
668
|
+
resolved_reco_id = resolve_reco_id(self, reco_id)
|
|
669
|
+
logger.debug("Resolved reco_id = %s", resolved_reco_id)
|
|
513
670
|
if resolved_reco_id is None:
|
|
514
671
|
return None
|
|
672
|
+
|
|
673
|
+
hook_name = getattr(self, "_converter_hook_name", None)
|
|
674
|
+
if isinstance(hook_name, str) and hook_name:
|
|
675
|
+
logger.debug(
|
|
676
|
+
"Convert starting for scan %s reco %s with hook %s",
|
|
677
|
+
getattr(self, "scan_id", "?"),
|
|
678
|
+
resolved_reco_id,
|
|
679
|
+
hook_name,
|
|
680
|
+
)
|
|
681
|
+
else:
|
|
682
|
+
logger.debug(
|
|
683
|
+
"Convert starting for scan %s reco %s (no hook)",
|
|
684
|
+
getattr(self, "scan_id", "?"),
|
|
685
|
+
resolved_reco_id,
|
|
686
|
+
)
|
|
687
|
+
|
|
515
688
|
hook_kwargs = _resolve_hook_kwargs(self, hook_args_by_name)
|
|
516
|
-
|
|
689
|
+
|
|
690
|
+
# Merge explicit **kwargs (CLI/user) with hook kwargs. Explicit kwargs win.
|
|
691
|
+
merged_kwargs: Dict[str, Any] = dict(hook_kwargs) if hook_kwargs else {}
|
|
692
|
+
merged_kwargs.update(kwargs)
|
|
693
|
+
|
|
694
|
+
data_kwargs = _filter_hook_kwargs(self.get_dataobj, merged_kwargs)
|
|
695
|
+
# flip_* are affine-only options; never pass them to get_dataobj.
|
|
696
|
+
for key in ("flip_x", "flip_y", "flip_z"):
|
|
697
|
+
data_kwargs.pop(key, None)
|
|
698
|
+
|
|
699
|
+
convert_kwargs = {key: value for key, value in merged_kwargs.items() if key not in data_kwargs}
|
|
517
700
|
if data_kwargs:
|
|
701
|
+
logger.debug(
|
|
702
|
+
"Calling get_dataobj for scan %s reco %s with args %s",
|
|
703
|
+
getattr(self, "scan_id", "?"),
|
|
704
|
+
resolved_reco_id,
|
|
705
|
+
data_kwargs,
|
|
706
|
+
)
|
|
518
707
|
dataobjs = self.get_dataobj(resolved_reco_id, **data_kwargs)
|
|
519
708
|
else:
|
|
709
|
+
logger.debug(
|
|
710
|
+
"Calling get_dataobj for scan %s reco %s (no args)",
|
|
711
|
+
getattr(self, "scan_id", "?"),
|
|
712
|
+
resolved_reco_id,
|
|
713
|
+
)
|
|
520
714
|
dataobjs = self.get_dataobj(resolved_reco_id)
|
|
521
|
-
|
|
715
|
+
|
|
716
|
+
affine_kwargs = _filter_hook_kwargs(self.get_affine, merged_kwargs)
|
|
717
|
+
convert_kwargs = {
|
|
718
|
+
key: value
|
|
719
|
+
for key, value in convert_kwargs.items()
|
|
720
|
+
if key not in affine_kwargs
|
|
721
|
+
}
|
|
522
722
|
if affine_kwargs:
|
|
723
|
+
logger.debug(
|
|
724
|
+
"Calling get_affine for scan %s reco %s with args %s",
|
|
725
|
+
getattr(self, "scan_id", "?"),
|
|
726
|
+
resolved_reco_id,
|
|
727
|
+
affine_kwargs,
|
|
728
|
+
)
|
|
523
729
|
affines = self.get_affine(
|
|
524
730
|
resolved_reco_id,
|
|
525
731
|
space=space,
|
|
@@ -528,77 +734,62 @@ def get_nifti1image(
|
|
|
528
734
|
**affine_kwargs,
|
|
529
735
|
)
|
|
530
736
|
else:
|
|
737
|
+
logger.debug(
|
|
738
|
+
"Calling get_affine for scan %s reco %s (no args)",
|
|
739
|
+
getattr(self, "scan_id", "?"),
|
|
740
|
+
resolved_reco_id,
|
|
741
|
+
)
|
|
531
742
|
affines = self.get_affine(
|
|
532
743
|
resolved_reco_id,
|
|
533
744
|
space=space,
|
|
534
745
|
override_subject_type=override_subject_type,
|
|
535
746
|
override_subject_pose=override_subject_pose,
|
|
536
747
|
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if dataobjs is None or affines is None or image_info is None:
|
|
748
|
+
|
|
749
|
+
if dataobjs is None or affines is None:
|
|
540
750
|
return None
|
|
541
|
-
|
|
542
|
-
if not isinstance(dataobjs, tuple)
|
|
751
|
+
|
|
752
|
+
if not isinstance(dataobjs, tuple):
|
|
543
753
|
dataobjs = (dataobjs,)
|
|
754
|
+
if not isinstance(affines, tuple):
|
|
544
755
|
affines = (affines,)
|
|
545
|
-
|
|
546
|
-
|
|
756
|
+
|
|
757
|
+
dataobjs = list(dataobjs)
|
|
547
758
|
for i, dataobj in enumerate(dataobjs):
|
|
548
759
|
if flatten_fg and dataobj.ndim > 4:
|
|
549
760
|
spatial_shape = dataobj.shape[:3]
|
|
550
761
|
flattened = int(np.prod(dataobj.shape[3:]))
|
|
551
|
-
|
|
552
|
-
|
|
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)
|
|
762
|
+
dataobjs[i] = dataobj.reshape((*spatial_shape, flattened), order="A")
|
|
763
|
+
dataobjs = tuple(dataobjs)
|
|
567
764
|
|
|
765
|
+
converter_func = getattr(self, "converter_func", None)
|
|
766
|
+
if isinstance(converter_func, ConvertType):
|
|
767
|
+
hook_call_kwargs = _filter_hook_kwargs(converter_func, convert_kwargs)
|
|
768
|
+
logger.debug(
|
|
769
|
+
"Calling converter hook for scan %s reco %s with args %s",
|
|
770
|
+
getattr(self, "scan_id", "?"),
|
|
771
|
+
resolved_reco_id,
|
|
772
|
+
hook_call_kwargs,
|
|
773
|
+
)
|
|
774
|
+
return converter_func(
|
|
775
|
+
dataobj=dataobjs,
|
|
776
|
+
affine=affines,
|
|
777
|
+
**hook_call_kwargs,
|
|
778
|
+
)
|
|
568
779
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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}")
|
|
780
|
+
nifti1image_kwargs = {
|
|
781
|
+
"override_header": override_header,
|
|
782
|
+
**kwargs,
|
|
783
|
+
}
|
|
784
|
+
nifti1image_kwargs = _filter_hook_kwargs(get_nifti1image, nifti1image_kwargs)
|
|
587
785
|
return get_nifti1image(
|
|
588
786
|
self,
|
|
589
|
-
reco_id,
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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,
|
|
787
|
+
reco_id=resolved_reco_id,
|
|
788
|
+
dataobjs=dataobjs,
|
|
789
|
+
affines=affines,
|
|
790
|
+
**nifti1image_kwargs,
|
|
599
791
|
)
|
|
600
792
|
|
|
601
|
-
|
|
602
793
|
def _resolve_hook_kwargs(
|
|
603
794
|
scan: Union["Scan", "ScanLoader"],
|
|
604
795
|
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]],
|
|
@@ -608,6 +799,12 @@ def _resolve_hook_kwargs(
|
|
|
608
799
|
hook_name = getattr(scan, "_converter_hook_name", None)
|
|
609
800
|
if not isinstance(hook_name, str) or not hook_name:
|
|
610
801
|
return {}
|
|
802
|
+
logger.debug(
|
|
803
|
+
"Resolving hook args for scan %s hook %s (available: %s)",
|
|
804
|
+
getattr(scan, "scan_id", "?"),
|
|
805
|
+
hook_name,
|
|
806
|
+
sorted(hook_args_by_name.keys()),
|
|
807
|
+
)
|
|
611
808
|
values = hook_args_by_name.get(hook_name)
|
|
612
809
|
if values is None:
|
|
613
810
|
seen: set[str] = set()
|
|
@@ -643,7 +840,12 @@ def _resolve_hook_kwargs(
|
|
|
643
840
|
)
|
|
644
841
|
values = candidate_values
|
|
645
842
|
break
|
|
646
|
-
|
|
843
|
+
resolved = dict(values) if isinstance(values, Mapping) else {}
|
|
844
|
+
if resolved:
|
|
845
|
+
logger.debug("Resolved hook args for %s: %s", hook_name, resolved)
|
|
846
|
+
else:
|
|
847
|
+
logger.debug("No hook args resolved for %s.", hook_name)
|
|
848
|
+
return resolved
|
|
647
849
|
|
|
648
850
|
|
|
649
851
|
def _filter_hook_kwargs(func: Any, hook_kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
|
@@ -724,7 +926,7 @@ def get_metadata(
|
|
|
724
926
|
spec: Optional[Union[Mapping[str, Any], str, Path]] = None,
|
|
725
927
|
context_map: Optional[Union[str, Path]] = None,
|
|
726
928
|
return_spec: bool = False,
|
|
727
|
-
):
|
|
929
|
+
) -> Metadata:
|
|
728
930
|
"""Resolve metadata using a remapper spec.
|
|
729
931
|
|
|
730
932
|
Args:
|
|
@@ -739,7 +941,7 @@ def get_metadata(
|
|
|
739
941
|
return_spec is True, returns (metadata, spec_info).
|
|
740
942
|
"""
|
|
741
943
|
scan = cast(ScanLoader, self)
|
|
742
|
-
resolved_reco_id =
|
|
944
|
+
resolved_reco_id = resolve_reco_id(scan, reco_id)
|
|
743
945
|
if resolved_reco_id is None:
|
|
744
946
|
if return_spec:
|
|
745
947
|
return None, None
|
|
@@ -785,6 +987,17 @@ def apply_converter_hook(
|
|
|
785
987
|
"""Override scan conversion helpers using a converter hook."""
|
|
786
988
|
converter_core.validate_hook(converter_hook)
|
|
787
989
|
plugin = dict(converter_hook)
|
|
990
|
+
logger.debug(
|
|
991
|
+
"Binding converter hook for scan %s: %s",
|
|
992
|
+
getattr(scan, "scan_id", "?"),
|
|
993
|
+
sorted(plugin.keys()),
|
|
994
|
+
)
|
|
995
|
+
if "get_dataobj" in plugin and not isinstance(plugin["get_dataobj"], GetDataobjType):
|
|
996
|
+
raise TypeError("Converter hook 'get_dataobj' must match GetDataobjType.")
|
|
997
|
+
if "get_affine" in plugin and not isinstance(plugin["get_affine"], GetAffineType):
|
|
998
|
+
raise TypeError("Converter hook 'get_affine' must match GetAffineType.")
|
|
999
|
+
if "convert" in plugin and not isinstance(plugin["convert"], ConvertType):
|
|
1000
|
+
raise TypeError("Converter hook 'convert' must match ConvertType.")
|
|
788
1001
|
scan._converter_hook = plugin
|
|
789
1002
|
if "get_dataobj" in plugin:
|
|
790
1003
|
scan.get_dataobj = MethodType(plugin["get_dataobj"], scan)
|
|
@@ -794,4 +1007,11 @@ def apply_converter_hook(
|
|
|
794
1007
|
get_affine = partial(get_affine, decimals=affine_decimals)
|
|
795
1008
|
scan.get_affine = MethodType(get_affine, scan)
|
|
796
1009
|
if "convert" in plugin:
|
|
797
|
-
scan.
|
|
1010
|
+
scan.converter_func = MethodType(plugin["convert"], scan)
|
|
1011
|
+
else:
|
|
1012
|
+
scan.converter_func = None
|
|
1013
|
+
logger.debug(
|
|
1014
|
+
"Converter hook bound for scan %s (hook=%s)",
|
|
1015
|
+
getattr(scan, "scan_id", "?"),
|
|
1016
|
+
getattr(scan, "_converter_hook_name", None),
|
|
1017
|
+
)
|