brkraw 0.5.3__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 -124
- brkraw/apps/loader/formatter.py +0 -2
- brkraw/apps/loader/helper.py +181 -68
- brkraw/apps/loader/info/scan.py +2 -2
- brkraw/apps/loader/info/transform.py +0 -1
- brkraw/apps/loader/types.py +35 -15
- 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 +56 -14
- brkraw/cli/commands/hook.py +1 -1
- 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/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/specs/meta/validator.py +0 -1
- brkraw/specs/rules/logic.py +1 -3
- {brkraw-0.5.3.dist-info → brkraw-0.5.5.dist-info}/METADATA +4 -4
- {brkraw-0.5.3.dist-info → brkraw-0.5.5.dist-info}/RECORD +36 -32
- {brkraw-0.5.3.dist-info → brkraw-0.5.5.dist-info}/entry_points.txt +1 -0
- {brkraw-0.5.3.dist-info → brkraw-0.5.5.dist-info}/WHEEL +0 -0
- {brkraw-0.5.3.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, ToFilename, ConvertType, GetDataobjType, GetAffineType
|
|
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)
|
|
@@ -474,13 +585,13 @@ def _apply_affine_post_transform(affines: AffineReturn, *, kwargs: Mapping[str,
|
|
|
474
585
|
def get_nifti1image(
|
|
475
586
|
self: Union["Scan", "ScanLoader"],
|
|
476
587
|
reco_id: int,
|
|
477
|
-
dataobjs: Tuple[
|
|
478
|
-
affines: Tuple[
|
|
588
|
+
dataobjs: Tuple[NDArray, ...],
|
|
589
|
+
affines: Tuple[NDArray, ...],
|
|
479
590
|
*,
|
|
480
591
|
xyz_units: XYZUNIT = "mm",
|
|
481
592
|
t_units: TUNIT = "sec",
|
|
482
593
|
override_header: Optional[Nifti1HeaderContents] = None,
|
|
483
|
-
) ->
|
|
594
|
+
) -> ConvertedObj:
|
|
484
595
|
"""Return NIfTI image(s) for a reco.
|
|
485
596
|
|
|
486
597
|
Args:
|
|
@@ -495,9 +606,12 @@ def get_nifti1image(
|
|
|
495
606
|
metadata is unavailable.
|
|
496
607
|
"""
|
|
497
608
|
self = cast(ScanLoader, self)
|
|
498
|
-
|
|
609
|
+
|
|
499
610
|
image_info = self.image_info.get(reco_id)
|
|
500
|
-
if
|
|
611
|
+
if image_info is None:
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
if dataobjs is None or affines is None:
|
|
501
615
|
return None
|
|
502
616
|
|
|
503
617
|
niiobjs = []
|
|
@@ -520,7 +634,7 @@ def get_nifti1image(
|
|
|
520
634
|
|
|
521
635
|
|
|
522
636
|
def convert(
|
|
523
|
-
self:
|
|
637
|
+
self: "ScanLoader",
|
|
524
638
|
reco_id: Optional[int] = None,
|
|
525
639
|
*,
|
|
526
640
|
space: AffineSpace = "subject_ras",
|
|
@@ -528,11 +642,9 @@ def convert(
|
|
|
528
642
|
override_subject_type: Optional[SubjectType] = None,
|
|
529
643
|
override_subject_pose: Optional[SubjectPose] = None,
|
|
530
644
|
flatten_fg: bool = False,
|
|
531
|
-
xyz_units: XYZUNIT = "mm",
|
|
532
|
-
t_units: TUNIT = "sec",
|
|
533
645
|
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
534
646
|
**kwargs: Any,
|
|
535
|
-
) ->
|
|
647
|
+
) -> ConvertedObj:
|
|
536
648
|
"""Convert a reco to output object(s).
|
|
537
649
|
|
|
538
650
|
Args:
|
|
@@ -541,12 +653,8 @@ def convert(
|
|
|
541
653
|
override_subject_type: Subject type override for subject-view wrapping.
|
|
542
654
|
override_subject_pose: Subject pose override for subject-view wrapping.
|
|
543
655
|
flatten_fg: If True, flatten foreground dimensions.
|
|
544
|
-
xyz_units: Spatial units for NIfTI header.
|
|
545
|
-
t_units: Temporal units for NIfTI header.
|
|
546
656
|
hook_args_by_name: Optional hook args mapping (split per helper signature).
|
|
547
657
|
flatten_fg: If True, flatten foreground dimensions.
|
|
548
|
-
xyz_units: Spatial units for NIfTI header.
|
|
549
|
-
t_units: Temporal units for NIfTI header.
|
|
550
658
|
Returns:
|
|
551
659
|
Single NIfTI image when one slice pack exists; otherwise a tuple of
|
|
552
660
|
images. Returns None when required metadata is unavailable.
|
|
@@ -555,9 +663,10 @@ def convert(
|
|
|
555
663
|
hasattr(self, attr) for attr in ["image_info", "affine_info", "get_dataobj", "get_affine"]
|
|
556
664
|
):
|
|
557
665
|
return None
|
|
558
|
-
|
|
666
|
+
|
|
559
667
|
self = cast(ScanLoader, self)
|
|
560
|
-
resolved_reco_id =
|
|
668
|
+
resolved_reco_id = resolve_reco_id(self, reco_id)
|
|
669
|
+
logger.debug("Resolved reco_id = %s", resolved_reco_id)
|
|
561
670
|
if resolved_reco_id is None:
|
|
562
671
|
return None
|
|
563
672
|
|
|
@@ -577,12 +686,17 @@ def convert(
|
|
|
577
686
|
)
|
|
578
687
|
|
|
579
688
|
hook_kwargs = _resolve_hook_kwargs(self, hook_args_by_name)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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}
|
|
586
700
|
if data_kwargs:
|
|
587
701
|
logger.debug(
|
|
588
702
|
"Calling get_dataobj for scan %s reco %s with args %s",
|
|
@@ -598,7 +712,8 @@ def convert(
|
|
|
598
712
|
resolved_reco_id,
|
|
599
713
|
)
|
|
600
714
|
dataobjs = self.get_dataobj(resolved_reco_id)
|
|
601
|
-
|
|
715
|
+
|
|
716
|
+
affine_kwargs = _filter_hook_kwargs(self.get_affine, merged_kwargs)
|
|
602
717
|
convert_kwargs = {
|
|
603
718
|
key: value
|
|
604
719
|
for key, value in convert_kwargs.items()
|
|
@@ -646,7 +761,7 @@ def convert(
|
|
|
646
761
|
flattened = int(np.prod(dataobj.shape[3:]))
|
|
647
762
|
dataobjs[i] = dataobj.reshape((*spatial_shape, flattened), order="A")
|
|
648
763
|
dataobjs = tuple(dataobjs)
|
|
649
|
-
|
|
764
|
+
|
|
650
765
|
converter_func = getattr(self, "converter_func", None)
|
|
651
766
|
if isinstance(converter_func, ConvertType):
|
|
652
767
|
hook_call_kwargs = _filter_hook_kwargs(converter_func, convert_kwargs)
|
|
@@ -663,8 +778,6 @@ def convert(
|
|
|
663
778
|
)
|
|
664
779
|
|
|
665
780
|
nifti1image_kwargs = {
|
|
666
|
-
"xyz_units": xyz_units,
|
|
667
|
-
"t_units": t_units,
|
|
668
781
|
"override_header": override_header,
|
|
669
782
|
**kwargs,
|
|
670
783
|
}
|
|
@@ -813,7 +926,7 @@ def get_metadata(
|
|
|
813
926
|
spec: Optional[Union[Mapping[str, Any], str, Path]] = None,
|
|
814
927
|
context_map: Optional[Union[str, Path]] = None,
|
|
815
928
|
return_spec: bool = False,
|
|
816
|
-
):
|
|
929
|
+
) -> Metadata:
|
|
817
930
|
"""Resolve metadata using a remapper spec.
|
|
818
931
|
|
|
819
932
|
Args:
|
|
@@ -828,7 +941,7 @@ def get_metadata(
|
|
|
828
941
|
return_spec is True, returns (metadata, spec_info).
|
|
829
942
|
"""
|
|
830
943
|
scan = cast(ScanLoader, self)
|
|
831
|
-
resolved_reco_id =
|
|
944
|
+
resolved_reco_id = resolve_reco_id(scan, reco_id)
|
|
832
945
|
if resolved_reco_id is None:
|
|
833
946
|
if return_spec:
|
|
834
947
|
return None, None
|
brkraw/apps/loader/info/scan.py
CHANGED
|
@@ -8,9 +8,9 @@ from ....specs.remapper.validator import validate_spec
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from ..types import ScanLoader
|
|
11
|
+
from ..types import ScanLoader
|
|
12
12
|
|
|
13
|
-
logger = logging.getLogger(
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def resolve(
|
brkraw/apps/loader/types.py
CHANGED
|
@@ -5,7 +5,7 @@ Last updated: 2025-12-30
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any, Union, Tuple, Dict, Optional, Protocol, Literal, Mapping,
|
|
8
|
+
from typing import Any, Union, Tuple, Dict, Optional, Protocol, Literal, Mapping, List, TYPE_CHECKING, runtime_checkable
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from typing_extensions import ParamSpec, TypeAlias
|
|
11
11
|
else:
|
|
@@ -16,21 +16,26 @@ else:
|
|
|
16
16
|
from ...dataclasses.study import Study
|
|
17
17
|
from ...dataclasses.scan import Scan
|
|
18
18
|
from ...dataclasses.reco import Reco
|
|
19
|
+
from ...resolver.affine import SubjectType, SubjectPose
|
|
19
20
|
import numpy as np
|
|
21
|
+
from numpy.typing import NDArray
|
|
20
22
|
|
|
21
23
|
if TYPE_CHECKING:
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
from ...core.parameters import Parameters
|
|
24
26
|
from ...resolver.image import ResolvedImage
|
|
25
|
-
from ...resolver.affine import ResolvedAffine
|
|
27
|
+
from ...resolver.affine import ResolvedAffine
|
|
26
28
|
from ...resolver.nifti import Nifti1HeaderContents, XYZUNIT, TUNIT
|
|
27
|
-
from nibabel.nifti1 import Nifti1Image
|
|
28
|
-
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
InfoScope = Literal['full', 'study', 'scan']
|
|
32
|
-
|
|
32
|
+
Dataobjs = Optional[Union[NDArray, Tuple[NDArray, ...]]]
|
|
33
|
+
Affines = Optional[Union[NDArray, Tuple[NDArray, ...]]]
|
|
33
34
|
AffineSpace = Literal["raw", "scanner", "subject_ras"]
|
|
35
|
+
ConvertedObj = Optional[Union["ToFilename", Tuple["ToFilename", ...]]]
|
|
36
|
+
Metadata = Optional[Union[Dict, Tuple[Optional[Dict], ...]]]
|
|
37
|
+
HookArgs = Optional[Mapping[str, Mapping[str, Any]]]
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
P = ParamSpec("P")
|
|
36
41
|
|
|
@@ -42,9 +47,11 @@ class GetDataobjType(Protocol[P]):
|
|
|
42
47
|
self,
|
|
43
48
|
scan: "Scan",
|
|
44
49
|
reco_id: Optional[int],
|
|
50
|
+
cycle_index: Optional[int],
|
|
51
|
+
cycle_count: Optional[int],
|
|
45
52
|
*args: P.args,
|
|
46
53
|
**kwargs: P.kwargs
|
|
47
|
-
) ->
|
|
54
|
+
) -> Dataobjs:
|
|
48
55
|
...
|
|
49
56
|
|
|
50
57
|
|
|
@@ -61,7 +68,7 @@ class GetAffineType(Protocol):
|
|
|
61
68
|
override_subject_pose: Optional[SubjectPose],
|
|
62
69
|
decimals: Optional[int] = None,
|
|
63
70
|
**kwargs: Any
|
|
64
|
-
) ->
|
|
71
|
+
) -> Affines:
|
|
65
72
|
...
|
|
66
73
|
|
|
67
74
|
|
|
@@ -74,7 +81,7 @@ class ConvertType(Protocol):
|
|
|
74
81
|
dataobj: Union[Tuple["np.ndarray", ...], "np.ndarray"],
|
|
75
82
|
affine: Union[Tuple["np.ndarray", ...], "np.ndarray"],
|
|
76
83
|
**kwargs: Any,
|
|
77
|
-
) ->
|
|
84
|
+
) -> ConvertedObj:
|
|
78
85
|
...
|
|
79
86
|
|
|
80
87
|
class ToFilename(Protocol):
|
|
@@ -108,6 +115,7 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
108
115
|
converter_func: Optional[ConvertType]
|
|
109
116
|
_converter_hook: Optional[ConverterHook]
|
|
110
117
|
_converter_hook_name: Optional[str]
|
|
118
|
+
_hook_resolved: bool = False
|
|
111
119
|
|
|
112
120
|
|
|
113
121
|
def get_fid(self,
|
|
@@ -119,8 +127,12 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
119
127
|
|
|
120
128
|
def get_dataobj(
|
|
121
129
|
self,
|
|
122
|
-
reco_id: Optional[int] = None
|
|
123
|
-
|
|
130
|
+
reco_id: Optional[int] = None,
|
|
131
|
+
*,
|
|
132
|
+
cycle_index: Optional[int] = None,
|
|
133
|
+
cycle_count: Optional[int] = None,
|
|
134
|
+
**kwargs: Any
|
|
135
|
+
) -> Dataobjs:
|
|
124
136
|
...
|
|
125
137
|
|
|
126
138
|
def get_affine(
|
|
@@ -132,7 +144,7 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
132
144
|
override_subject_pose: Optional[SubjectPose],
|
|
133
145
|
decimals: Optional[int] = None,
|
|
134
146
|
**kwargs: Any,
|
|
135
|
-
) ->
|
|
147
|
+
) -> Affines:
|
|
136
148
|
...
|
|
137
149
|
|
|
138
150
|
def get_nifti1image(
|
|
@@ -144,7 +156,7 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
144
156
|
override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
|
|
145
157
|
xyz_units: XYZUNIT,
|
|
146
158
|
t_units: TUNIT
|
|
147
|
-
) ->
|
|
159
|
+
) -> ConvertedObj:
|
|
148
160
|
...
|
|
149
161
|
|
|
150
162
|
def convert(
|
|
@@ -158,9 +170,9 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
158
170
|
flatten_fg: bool,
|
|
159
171
|
xyz_units: XYZUNIT,
|
|
160
172
|
t_units: TUNIT,
|
|
161
|
-
hook_args_by_name:
|
|
173
|
+
hook_args_by_name: HookArgs = None,
|
|
162
174
|
**kwargs: Any,
|
|
163
|
-
) ->
|
|
175
|
+
) -> ConvertedObj:
|
|
164
176
|
...
|
|
165
177
|
|
|
166
178
|
def get_metadata(
|
|
@@ -169,7 +181,7 @@ class ScanLoader(Scan, BaseLoader):
|
|
|
169
181
|
spec: Optional[Union[Mapping[str, Any], str, "Path"]] = None,
|
|
170
182
|
context_map: Optional[Union[str, "Path"]] = None,
|
|
171
183
|
return_spec: bool = False,
|
|
172
|
-
) ->
|
|
184
|
+
) -> Metadata:
|
|
173
185
|
...
|
|
174
186
|
|
|
175
187
|
|
|
@@ -191,6 +203,14 @@ __all__ = [
|
|
|
191
203
|
'StudyLoader',
|
|
192
204
|
'ScanLoader',
|
|
193
205
|
'RecoLoader',
|
|
206
|
+
'SubjectType',
|
|
207
|
+
'SubjectPose',
|
|
208
|
+
'Affines',
|
|
209
|
+
'Dataobjs',
|
|
210
|
+
'Metadata',
|
|
211
|
+
'ConvertedObj',
|
|
212
|
+
'HookArgs',
|
|
213
|
+
'AffineSpace',
|
|
194
214
|
]
|
|
195
215
|
|
|
196
216
|
def __dir__() -> List[str]:
|
brkraw/cli/commands/addon.py
CHANGED
|
@@ -10,7 +10,7 @@ from brkraw.core import config as config_core
|
|
|
10
10
|
from brkraw.core import formatter
|
|
11
11
|
from brkraw.apps import addon as addon_app
|
|
12
12
|
|
|
13
|
-
logger = logging.getLogger(
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def cmd_addon(args: argparse.Namespace) -> int:
|