brkraw 0.5.0rc1__py3-none-any.whl → 0.5.1__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/apps/hook/core.py +58 -10
- brkraw/apps/loader/core.py +5 -1
- brkraw/apps/loader/helper.py +155 -14
- brkraw/apps/loader/info/scan.py +18 -5
- brkraw/apps/loader/types.py +6 -1
- brkraw/cli/commands/convert.py +201 -79
- brkraw/cli/commands/hook.py +146 -4
- brkraw/cli/commands/session.py +6 -1
- brkraw/cli/hook_args.py +80 -0
- brkraw/core/config.py +8 -0
- brkraw/core/layout.py +56 -11
- brkraw/default/rules/00_default.yaml +4 -0
- brkraw/default/specs/metadata_dicom.yaml +236 -0
- brkraw/default/specs/metadata_transforms.py +18 -0
- brkraw/resolver/affine.py +56 -32
- brkraw/schema/context_map.yaml +5 -0
- brkraw/schema/remapper.yaml +6 -0
- brkraw/specs/__init__.py +2 -2
- brkraw/specs/{converter → hook}/logic.py +1 -0
- brkraw/specs/{converter → hook}/validator.py +1 -0
- brkraw/specs/remapper/logic.py +83 -16
- brkraw/specs/remapper/validator.py +21 -5
- {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/METADATA +31 -4
- {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/RECORD +29 -33
- brkraw/default/rules/10-metadata.yaml +0 -42
- brkraw/default/rules/20-mrs.yaml +0 -14
- brkraw/default/specs/metadata_anat.yaml +0 -54
- brkraw/default/specs/metadata_common.yaml +0 -129
- brkraw/default/specs/metadata_func.yaml +0 -127
- brkraw/default/specs/mrs.yaml +0 -71
- brkraw/default/specs/mrs_transforms.py +0 -26
- brkraw/specs/{converter → hook}/__init__.py +1 -1
- {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/WHEEL +0 -0
- {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/entry_points.txt +0 -0
- {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/licenses/LICENSE +0 -0
brkraw/cli/hook_args.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Mapping
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def merge_hook_args(
|
|
11
|
+
base: Mapping[str, Mapping[str, Any]],
|
|
12
|
+
override: Mapping[str, Mapping[str, Any]],
|
|
13
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
14
|
+
merged: Dict[str, Dict[str, Any]] = {}
|
|
15
|
+
for hook_name, values in base.items():
|
|
16
|
+
if not isinstance(hook_name, str) or not hook_name:
|
|
17
|
+
continue
|
|
18
|
+
if not isinstance(values, Mapping):
|
|
19
|
+
continue
|
|
20
|
+
merged[hook_name] = dict(values)
|
|
21
|
+
for hook_name, values in override.items():
|
|
22
|
+
if not isinstance(hook_name, str) or not hook_name:
|
|
23
|
+
continue
|
|
24
|
+
if not isinstance(values, Mapping):
|
|
25
|
+
continue
|
|
26
|
+
merged.setdefault(hook_name, {}).update(dict(values))
|
|
27
|
+
return merged
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_hook_args_yaml(paths: List[str]) -> Dict[str, Dict[str, Any]]:
|
|
31
|
+
"""Load hook args mapping from YAML files.
|
|
32
|
+
|
|
33
|
+
Supported YAML formats:
|
|
34
|
+
- `{hooks: {hook_name: {key: value}}}`
|
|
35
|
+
- `{hook_name: {key: value}}`
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
merged: Dict[str, Dict[str, Any]] = {}
|
|
39
|
+
|
|
40
|
+
def normalize_doc(doc: Any, *, source: str) -> Dict[str, Dict[str, Any]]:
|
|
41
|
+
if doc is None:
|
|
42
|
+
return {}
|
|
43
|
+
hooks_obj = doc.get("hooks") if isinstance(doc, Mapping) else None
|
|
44
|
+
if hooks_obj is None and isinstance(doc, Mapping):
|
|
45
|
+
hooks_obj = doc
|
|
46
|
+
if not isinstance(hooks_obj, Mapping):
|
|
47
|
+
raise ValueError(f"Invalid hook args YAML in {source!r}: expected mapping.")
|
|
48
|
+
|
|
49
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
50
|
+
for hook_name, values in hooks_obj.items():
|
|
51
|
+
if not isinstance(hook_name, str) or not hook_name.strip():
|
|
52
|
+
continue
|
|
53
|
+
if values is None:
|
|
54
|
+
continue
|
|
55
|
+
if not isinstance(values, Mapping):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Invalid hook args YAML in {source!r}: hook {hook_name!r} must map to a dict."
|
|
58
|
+
)
|
|
59
|
+
out[hook_name.strip()] = dict(values)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
for raw in paths:
|
|
63
|
+
source = raw.strip()
|
|
64
|
+
if not source:
|
|
65
|
+
continue
|
|
66
|
+
if source == "-":
|
|
67
|
+
doc = yaml.safe_load(sys.stdin.read())
|
|
68
|
+
else:
|
|
69
|
+
path = Path(source).expanduser()
|
|
70
|
+
if not path.exists():
|
|
71
|
+
raise ValueError(f"Hook args YAML not found: {source}")
|
|
72
|
+
doc = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
73
|
+
parsed = normalize_doc(doc, source=source)
|
|
74
|
+
merged = merge_hook_args(merged, parsed)
|
|
75
|
+
|
|
76
|
+
return merged
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["load_hook_args_yaml", "merge_hook_args"]
|
|
80
|
+
|
brkraw/core/config.py
CHANGED
|
@@ -42,6 +42,14 @@ output:
|
|
|
42
42
|
slicepack_suffix: "_slpack{index}"
|
|
43
43
|
# float_decimals: 6
|
|
44
44
|
|
|
45
|
+
# Viewer settings for brkraw-viewer (optional GUI extension).
|
|
46
|
+
viewer:
|
|
47
|
+
cache:
|
|
48
|
+
# Cache loaded scan data in memory to speed up space/pose changes.
|
|
49
|
+
enabled: true
|
|
50
|
+
# Maximum number of scan/reco entries to keep (LRU). 0 disables caching.
|
|
51
|
+
max_items: 10
|
|
52
|
+
|
|
45
53
|
# rules_dir: rules
|
|
46
54
|
# specs_dir: specs
|
|
47
55
|
# pruner_specs_dir: pruner_specs
|
brkraw/core/layout.py
CHANGED
|
@@ -31,6 +31,7 @@ def render_layout(
|
|
|
31
31
|
context_map: Optional[Union[str, Path]] = None,
|
|
32
32
|
root: Optional[Union[str, Path]] = None,
|
|
33
33
|
reco_id: Optional[int] = None,
|
|
34
|
+
counter: Optional[int] = None,
|
|
34
35
|
override_info_spec: Optional[Union[str, Path]] = None,
|
|
35
36
|
override_metadata_spec: Optional[Union[str, Path]] = None,
|
|
36
37
|
) -> str:
|
|
@@ -50,8 +51,8 @@ def render_layout(
|
|
|
50
51
|
override_metadata_spec=override_metadata_spec,
|
|
51
52
|
)
|
|
52
53
|
if isinstance(layout_template, str) and layout_template:
|
|
53
|
-
return _render_layout_template(layout_template, info, scan_id)
|
|
54
|
-
return _render_fields(layout_entries, info, scan_id)
|
|
54
|
+
return _render_layout_template(layout_template, info, scan_id, reco_id=reco_id, counter=counter)
|
|
55
|
+
return _render_fields(layout_entries, info, scan_id, reco_id=reco_id, counter=counter)
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def load_layout_info(
|
|
@@ -111,6 +112,12 @@ def load_layout_info_parts(
|
|
|
111
112
|
if not isinstance(mapped, dict):
|
|
112
113
|
raise ValueError("override_info_spec must resolve to a mapping.")
|
|
113
114
|
info = mapped
|
|
115
|
+
study_info = info_resolver.study(loader) or {}
|
|
116
|
+
if isinstance(study_info, dict):
|
|
117
|
+
if "Study" in study_info and "Study" not in info:
|
|
118
|
+
info["Study"] = study_info["Study"]
|
|
119
|
+
if "Subject" in study_info and "Subject" not in info:
|
|
120
|
+
info["Subject"] = study_info["Subject"]
|
|
114
121
|
else:
|
|
115
122
|
study_info = info_resolver.study(loader) or {}
|
|
116
123
|
scan_info = info_resolver.scan(scan) or {}
|
|
@@ -152,19 +159,26 @@ def render_slicepack_suffixes(
|
|
|
152
159
|
*,
|
|
153
160
|
count: int,
|
|
154
161
|
template: str = "_slpack{index}",
|
|
162
|
+
counter: Optional[int] = None,
|
|
155
163
|
) -> List[str]:
|
|
156
164
|
suffixes: List[str] = []
|
|
157
165
|
for idx in range(count):
|
|
158
|
-
suffixes.append(_render_slicepack_suffix(template, info, idx))
|
|
166
|
+
suffixes.append(_render_slicepack_suffix(template, info, idx, counter=counter))
|
|
159
167
|
return suffixes
|
|
160
168
|
|
|
161
169
|
|
|
162
|
-
def _render_slicepack_suffix(
|
|
170
|
+
def _render_slicepack_suffix(
|
|
171
|
+
template: str,
|
|
172
|
+
info: Mapping[str, Any],
|
|
173
|
+
idx: int,
|
|
174
|
+
*,
|
|
175
|
+
counter: Optional[int],
|
|
176
|
+
) -> str:
|
|
163
177
|
def _replace(match: re.Match[str]) -> str:
|
|
164
178
|
tag = match.group(1)
|
|
165
179
|
if tag.lower() == "index":
|
|
166
180
|
return str(idx + 1)
|
|
167
|
-
value = _resolve_tag(tag, info, idx + 1)
|
|
181
|
+
value = _resolve_tag(tag, info, idx + 1, reco_id=None, counter=counter)
|
|
168
182
|
chosen = _select_indexed_value(value, idx)
|
|
169
183
|
if chosen is None:
|
|
170
184
|
return str(idx + 1)
|
|
@@ -214,6 +228,9 @@ def _render_fields(
|
|
|
214
228
|
fields: Optional[Iterable[Mapping[str, Any]]],
|
|
215
229
|
info: Mapping[str, Any],
|
|
216
230
|
scan_id: int,
|
|
231
|
+
*,
|
|
232
|
+
reco_id: Optional[int],
|
|
233
|
+
counter: Optional[int],
|
|
217
234
|
) -> str:
|
|
218
235
|
parts: List[str] = []
|
|
219
236
|
seps: List[Optional[str]] = []
|
|
@@ -250,7 +267,7 @@ def _render_fields(
|
|
|
250
267
|
entry_clean = key.replace(".", "").lower()
|
|
251
268
|
if not _ENTRY_PATTERN.match(entry_clean):
|
|
252
269
|
continue
|
|
253
|
-
value = _resolve_tag(key, info, scan_id)
|
|
270
|
+
value = _resolve_tag(key, info, scan_id, reco_id=reco_id, counter=counter)
|
|
254
271
|
value_str = _format_value_with_options(
|
|
255
272
|
value,
|
|
256
273
|
value_pattern=value_pattern,
|
|
@@ -297,25 +314,53 @@ def _render_fields(
|
|
|
297
314
|
return result
|
|
298
315
|
|
|
299
316
|
|
|
300
|
-
def _render_layout_template(
|
|
317
|
+
def _render_layout_template(
|
|
318
|
+
template: str,
|
|
319
|
+
info: Mapping[str, Any],
|
|
320
|
+
scan_id: int,
|
|
321
|
+
*,
|
|
322
|
+
reco_id: Optional[int],
|
|
323
|
+
counter: Optional[int],
|
|
324
|
+
) -> str:
|
|
301
325
|
if not _LAYOUT_TAG.search(template):
|
|
302
326
|
return template
|
|
303
|
-
rendered = _LAYOUT_TAG.sub(
|
|
327
|
+
rendered = _LAYOUT_TAG.sub(
|
|
328
|
+
lambda m: _resolve_layout_tag(m, info, scan_id, reco_id=reco_id, counter=counter),
|
|
329
|
+
template,
|
|
330
|
+
)
|
|
304
331
|
return rendered or template
|
|
305
332
|
|
|
306
333
|
|
|
307
|
-
def _resolve_layout_tag(
|
|
334
|
+
def _resolve_layout_tag(
|
|
335
|
+
match: re.Match[str],
|
|
336
|
+
info: Mapping[str, Any],
|
|
337
|
+
scan_id: int,
|
|
338
|
+
*,
|
|
339
|
+
reco_id: Optional[int],
|
|
340
|
+
counter: Optional[int],
|
|
341
|
+
) -> str:
|
|
308
342
|
tag = match.group(1) or ""
|
|
309
343
|
if not tag:
|
|
310
344
|
return ""
|
|
311
|
-
value = _resolve_tag(tag.strip(), info, scan_id)
|
|
345
|
+
value = _resolve_tag(tag.strip(), info, scan_id, reco_id=reco_id, counter=counter)
|
|
312
346
|
rendered = _format_value(value)
|
|
313
347
|
return rendered or ""
|
|
314
348
|
|
|
315
349
|
|
|
316
|
-
def _resolve_tag(
|
|
350
|
+
def _resolve_tag(
|
|
351
|
+
tag: str,
|
|
352
|
+
info: Mapping[str, Any],
|
|
353
|
+
scan_id: int,
|
|
354
|
+
*,
|
|
355
|
+
reco_id: Optional[int] = None,
|
|
356
|
+
counter: Optional[int] = None,
|
|
357
|
+
) -> Any:
|
|
317
358
|
if tag in {"ScanID", "scan_id", "scanid"}:
|
|
318
359
|
return scan_id
|
|
360
|
+
if tag in {"RecoID", "reco_id", "recoid"}:
|
|
361
|
+
return reco_id
|
|
362
|
+
if tag in {"Counter", "counter"}:
|
|
363
|
+
return counter
|
|
319
364
|
if "." in tag:
|
|
320
365
|
root_key, rest = tag.split(".", 1)
|
|
321
366
|
root_val = info.get(root_key)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
__meta__:
|
|
2
|
+
name: "metadata_dicom"
|
|
3
|
+
version: "0.0.1"
|
|
4
|
+
description: "DICOM metadata mapping for Bruker PvDataset."
|
|
5
|
+
category: "metadata_spec"
|
|
6
|
+
transforms_source: "metadata_transforms.py"
|
|
7
|
+
|
|
8
|
+
ImageType:
|
|
9
|
+
sources:
|
|
10
|
+
- file: visu_pars
|
|
11
|
+
key: VisuSeriesTypeId
|
|
12
|
+
transform: strip_jcamp_string
|
|
13
|
+
|
|
14
|
+
AcquisitionDateTime:
|
|
15
|
+
sources:
|
|
16
|
+
- file: visu_pars
|
|
17
|
+
key: VisuAcqDate
|
|
18
|
+
transform: strip_jcamp_string
|
|
19
|
+
|
|
20
|
+
MagneticFieldStrength:
|
|
21
|
+
sources:
|
|
22
|
+
- file: visu_pars
|
|
23
|
+
key: VisuMagneticFieldStrength
|
|
24
|
+
|
|
25
|
+
ScanningSequence:
|
|
26
|
+
sources:
|
|
27
|
+
- file: visu_pars
|
|
28
|
+
key: VisuAcqSequenceName
|
|
29
|
+
transform: strip_jcamp_string
|
|
30
|
+
|
|
31
|
+
SequenceVariant:
|
|
32
|
+
sources:
|
|
33
|
+
- file: visu_pars
|
|
34
|
+
key: VisuAcqSequenceName
|
|
35
|
+
transform: strip_jcamp_string
|
|
36
|
+
|
|
37
|
+
ScanOptions:
|
|
38
|
+
sources:
|
|
39
|
+
- file: visu_pars
|
|
40
|
+
key: VisuAcqSpectralSuppression
|
|
41
|
+
transform: strip_jcamp_string
|
|
42
|
+
|
|
43
|
+
RepetitionTime:
|
|
44
|
+
sources:
|
|
45
|
+
- file: visu_pars
|
|
46
|
+
key: VisuAcqRepetitionTime
|
|
47
|
+
|
|
48
|
+
EchoTime:
|
|
49
|
+
sources:
|
|
50
|
+
- file: visu_pars
|
|
51
|
+
key: VisuAcqEchoTime
|
|
52
|
+
|
|
53
|
+
InversionTime:
|
|
54
|
+
sources:
|
|
55
|
+
- file: visu_pars
|
|
56
|
+
key: VisuAcqInversionTime
|
|
57
|
+
|
|
58
|
+
FlipAngle:
|
|
59
|
+
sources:
|
|
60
|
+
- file: visu_pars
|
|
61
|
+
key: VisuAcqFlipAngle
|
|
62
|
+
|
|
63
|
+
SliceThickness:
|
|
64
|
+
sources:
|
|
65
|
+
- file: visu_pars
|
|
66
|
+
key: VisuCoreSliceThickness
|
|
67
|
+
|
|
68
|
+
PixelBandwidth:
|
|
69
|
+
sources:
|
|
70
|
+
- file: visu_pars
|
|
71
|
+
key: VisuAcqPixelBandwidth
|
|
72
|
+
|
|
73
|
+
InPlanePhaseEncodingDirection:
|
|
74
|
+
sources:
|
|
75
|
+
- file: visu_pars
|
|
76
|
+
key: VisuAcqGradEncoding
|
|
77
|
+
|
|
78
|
+
PercentSampling:
|
|
79
|
+
sources:
|
|
80
|
+
- file: visu_pars
|
|
81
|
+
key: VisuAcqPartialFourier
|
|
82
|
+
|
|
83
|
+
PercentPhaseFieldOfView:
|
|
84
|
+
sources:
|
|
85
|
+
- file: visu_pars
|
|
86
|
+
key: VisuCoreExtent
|
|
87
|
+
|
|
88
|
+
PixelSpacing:
|
|
89
|
+
inputs:
|
|
90
|
+
extent:
|
|
91
|
+
sources:
|
|
92
|
+
- file: visu_pars
|
|
93
|
+
key: VisuCoreExtent
|
|
94
|
+
size:
|
|
95
|
+
sources:
|
|
96
|
+
- file: visu_pars
|
|
97
|
+
key: VisuCoreSize
|
|
98
|
+
transform: pixel_spacing_from_extent
|
|
99
|
+
|
|
100
|
+
SliceThickness_FG:
|
|
101
|
+
sources:
|
|
102
|
+
- file: visu_pars
|
|
103
|
+
key: VisuCoreSliceThickness
|
|
104
|
+
|
|
105
|
+
ImagePositionPatient:
|
|
106
|
+
sources:
|
|
107
|
+
- file: visu_pars
|
|
108
|
+
key: VisuCorePosition
|
|
109
|
+
transform: as_list
|
|
110
|
+
|
|
111
|
+
ImageOrientationPatient:
|
|
112
|
+
sources:
|
|
113
|
+
- file: visu_pars
|
|
114
|
+
key: VisuCoreOrientation
|
|
115
|
+
transform: as_list
|
|
116
|
+
|
|
117
|
+
RescaleSlope_FG:
|
|
118
|
+
sources:
|
|
119
|
+
- file: visu_pars
|
|
120
|
+
key: VisuCoreDataSlope
|
|
121
|
+
|
|
122
|
+
RescaleIntercept_FG:
|
|
123
|
+
sources:
|
|
124
|
+
- file: visu_pars
|
|
125
|
+
key: VisuCoreDataOffs
|
|
126
|
+
|
|
127
|
+
RepetitionTime_FG:
|
|
128
|
+
sources:
|
|
129
|
+
- file: visu_pars
|
|
130
|
+
key: VisuAcqRepetitionTime
|
|
131
|
+
|
|
132
|
+
EchoTime_FG:
|
|
133
|
+
sources:
|
|
134
|
+
- file: visu_pars
|
|
135
|
+
key: VisuAcqEchoTime
|
|
136
|
+
|
|
137
|
+
FlipAngle_FG:
|
|
138
|
+
sources:
|
|
139
|
+
- file: visu_pars
|
|
140
|
+
key: VisuAcqFlipAngle
|
|
141
|
+
|
|
142
|
+
EchoTrainLength:
|
|
143
|
+
sources:
|
|
144
|
+
- file: visu_pars
|
|
145
|
+
key: VisuAcqEchoTrainLength
|
|
146
|
+
|
|
147
|
+
NumberOfAverages:
|
|
148
|
+
sources:
|
|
149
|
+
- file: visu_pars
|
|
150
|
+
key: VisuAcqNAverages
|
|
151
|
+
|
|
152
|
+
PartialFourier:
|
|
153
|
+
sources:
|
|
154
|
+
- file: visu_pars
|
|
155
|
+
key: VisuAcqPartialFourier
|
|
156
|
+
|
|
157
|
+
PhaseEncodingDirectionPositive:
|
|
158
|
+
sources:
|
|
159
|
+
- file: visu_pars
|
|
160
|
+
key: VisuAcqGradEncoding
|
|
161
|
+
|
|
162
|
+
DiffusionBValue_FG:
|
|
163
|
+
sources:
|
|
164
|
+
- file: visu_pars
|
|
165
|
+
key: VisuAcqDiffusionBMatrix
|
|
166
|
+
|
|
167
|
+
DiffusionGradientOrientation_FG:
|
|
168
|
+
sources:
|
|
169
|
+
- file: visu_pars
|
|
170
|
+
key: VisuAcqDiffusionGradOrient
|
|
171
|
+
transform: as_list
|
|
172
|
+
|
|
173
|
+
MRSpectroscopyAcquisitionType:
|
|
174
|
+
sources:
|
|
175
|
+
- file: visu_pars
|
|
176
|
+
key: VisuMrsAcquisitionType
|
|
177
|
+
transform: strip_jcamp_string
|
|
178
|
+
|
|
179
|
+
ResonantNucleus:
|
|
180
|
+
sources:
|
|
181
|
+
- file: visu_pars
|
|
182
|
+
key: VisuMrsResonantNuclei
|
|
183
|
+
transform: strip_jcamp_string
|
|
184
|
+
|
|
185
|
+
SpectralWidth_MRS:
|
|
186
|
+
sources:
|
|
187
|
+
- file: visu_pars
|
|
188
|
+
key: VisuMrsSpectralWidth
|
|
189
|
+
|
|
190
|
+
TransmitterFrequency_MRS:
|
|
191
|
+
sources:
|
|
192
|
+
- file: visu_pars
|
|
193
|
+
key: VisuMrsTransmitterFreq
|
|
194
|
+
|
|
195
|
+
ChemicalShiftReference:
|
|
196
|
+
sources:
|
|
197
|
+
- file: visu_pars
|
|
198
|
+
key: VisuMrsChemicalShiftRef
|
|
199
|
+
|
|
200
|
+
NumberOfZeroFills:
|
|
201
|
+
sources:
|
|
202
|
+
- file: visu_pars
|
|
203
|
+
key: VisuMrsZeroFill
|
|
204
|
+
|
|
205
|
+
KSpaceFiltering:
|
|
206
|
+
sources:
|
|
207
|
+
- file: visu_pars
|
|
208
|
+
key: VisuAcqKSpaceFiltering
|
|
209
|
+
|
|
210
|
+
PulseSequenceName_MRS:
|
|
211
|
+
sources:
|
|
212
|
+
- file: visu_pars
|
|
213
|
+
key: VisuAcqSequenceName
|
|
214
|
+
transform: strip_jcamp_string
|
|
215
|
+
|
|
216
|
+
EchoTime_MRS:
|
|
217
|
+
sources:
|
|
218
|
+
- file: visu_pars
|
|
219
|
+
key: VisuAcqEchoTime
|
|
220
|
+
|
|
221
|
+
RepetitionTime_MRS:
|
|
222
|
+
sources:
|
|
223
|
+
- file: visu_pars
|
|
224
|
+
key: VisuAcqRepetitionTime
|
|
225
|
+
|
|
226
|
+
WaterSuppression:
|
|
227
|
+
sources:
|
|
228
|
+
- file: visu_pars
|
|
229
|
+
key: VisuAcqSpectralSuppression
|
|
230
|
+
transform: strip_jcamp_string
|
|
231
|
+
|
|
232
|
+
VolumeLocalizationTechnique:
|
|
233
|
+
sources:
|
|
234
|
+
- file: visu_pars
|
|
235
|
+
key: VisuMrsLocalizationTechnique
|
|
236
|
+
transform: strip_jcamp_string
|
|
@@ -51,6 +51,24 @@ def as_list(value):
|
|
|
51
51
|
return [value]
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def pixel_spacing_from_extent(extent=None, size=None):
|
|
55
|
+
if extent is None or size is None:
|
|
56
|
+
return None
|
|
57
|
+
arr_extent = np.asarray(extent, dtype=float).ravel()
|
|
58
|
+
arr_size = np.asarray(size, dtype=float).ravel()
|
|
59
|
+
if arr_extent.size == 0 or arr_size.size == 0:
|
|
60
|
+
return None
|
|
61
|
+
if arr_extent.size != arr_size.size:
|
|
62
|
+
count = min(arr_extent.size, arr_size.size)
|
|
63
|
+
if count == 0:
|
|
64
|
+
return None
|
|
65
|
+
arr_extent = arr_extent[:count]
|
|
66
|
+
arr_size = arr_size[:count]
|
|
67
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
68
|
+
spacing = arr_extent / arr_size
|
|
69
|
+
return spacing.tolist()
|
|
70
|
+
|
|
71
|
+
|
|
54
72
|
def normalize_method(value: Optional[str]) -> str:
|
|
55
73
|
return strip_jcamp_string(value).upper()
|
|
56
74
|
|
brkraw/resolver/affine.py
CHANGED
|
@@ -328,42 +328,66 @@ def resolve_matvec_and_shape(visu_pars,
|
|
|
328
328
|
shape = np.asarray(visu_pars.get("VisuCoreSize"), dtype=float)
|
|
329
329
|
|
|
330
330
|
if dim == 2:
|
|
331
|
-
num_rotates = rotate.shape[0]
|
|
332
|
-
num_origins = origin.shape[0]
|
|
333
331
|
num_slicepack = len(num_slices)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
332
|
+
|
|
333
|
+
if spack_idx < 0 or spack_idx >= num_slicepack:
|
|
334
|
+
raise IndexError(f"spack_idx out of range: {spack_idx} (num packs: {num_slicepack})")
|
|
335
|
+
|
|
336
|
+
total_slices = int(np.sum(np.asarray(num_slices, dtype=int)))
|
|
337
|
+
spack_slice_start = int(np.sum(np.asarray(num_slices[:spack_idx], dtype=int)))
|
|
338
|
+
spack_slice_end = spack_slice_start + int(num_slices[spack_idx])
|
|
339
|
+
|
|
340
|
+
def _select_slice_entries(arr: np.ndarray, *, width: int, name: str) -> np.ndarray:
|
|
341
|
+
arr = np.asarray(arr, dtype=float)
|
|
342
|
+
if arr.ndim == 1:
|
|
343
|
+
if arr.size == width:
|
|
344
|
+
arr = arr.reshape((1, width))
|
|
345
|
+
else:
|
|
346
|
+
raise ValueError(f"{name} has shape {arr.shape}, expected (*, {width})")
|
|
347
|
+
if arr.ndim != 2 or arr.shape[1] != width:
|
|
348
|
+
raise ValueError(f"{name} has shape {arr.shape}, expected (*, {width})")
|
|
349
|
+
|
|
350
|
+
# Prefer per-slice entries (concatenated across slice packs).
|
|
351
|
+
if arr.shape[0] > total_slices:
|
|
352
|
+
if not np.allclose(arr[:total_slices], arr[0], atol=0, rtol=0):
|
|
353
|
+
logger.warning(
|
|
354
|
+
"%s has %s entries but expected %s; using the first %s entries.",
|
|
355
|
+
name,
|
|
356
|
+
arr.shape[0],
|
|
357
|
+
total_slices,
|
|
358
|
+
total_slices,
|
|
359
|
+
)
|
|
360
|
+
arr = arr[:total_slices, :]
|
|
361
|
+
|
|
362
|
+
if arr.shape[0] == total_slices:
|
|
363
|
+
return arr[spack_slice_start:spack_slice_end, :]
|
|
364
|
+
|
|
365
|
+
# Fallback: per-pack entries (one entry per slice pack).
|
|
366
|
+
if arr.shape[0] == num_slicepack:
|
|
367
|
+
if int(num_slices[spack_idx]) != 1:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"{name} provides one entry per slice pack ({num_slicepack}) "
|
|
370
|
+
f"but pack {spack_idx} has {num_slices[spack_idx]} slices; "
|
|
371
|
+
"per-slice entries are required to resolve slice positions."
|
|
372
|
+
)
|
|
373
|
+
return arr[spack_idx:spack_idx + 1, :]
|
|
374
|
+
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"{name} has {arr.shape[0]} entries, expected {total_slices} (per-slice) "
|
|
377
|
+
f"or {num_slicepack} (per-pack); method num_slices={num_slices}."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
_rotate = _select_slice_entries(rotate, width=9, name="VisuCoreOrientation")
|
|
381
|
+
_origin = _select_slice_entries(origin, width=3, name="VisuCorePosition")
|
|
364
382
|
_num_slices = num_slices[spack_idx]
|
|
365
383
|
_slice_thickness = slice_thickness[spack_idx]
|
|
366
384
|
|
|
385
|
+
if _rotate.shape[0] > 1 and not np.allclose(_rotate, _rotate[0], atol=0, rtol=0):
|
|
386
|
+
logger.warning(
|
|
387
|
+
"VisuCoreOrientation varies across slices in pack %s; using the first slice orientation.",
|
|
388
|
+
spack_idx,
|
|
389
|
+
)
|
|
390
|
+
|
|
367
391
|
row = _rotate[0, 0:3]
|
|
368
392
|
col = _rotate[0, 3:6]
|
|
369
393
|
slc = _rotate[0, 6:9]
|
brkraw/schema/context_map.yaml
CHANGED
brkraw/schema/remapper.yaml
CHANGED
|
@@ -41,6 +41,10 @@ $defs:
|
|
|
41
41
|
type: object
|
|
42
42
|
additionalProperties: { $ref: "#/$defs/Input" }
|
|
43
43
|
description: "Named inputs that are passed to a transform or returned as-is."
|
|
44
|
+
const: {}
|
|
45
|
+
ref:
|
|
46
|
+
type: string
|
|
47
|
+
description: "Dot path to a previously resolved output value."
|
|
44
48
|
transform:
|
|
45
49
|
anyOf:
|
|
46
50
|
- type: string
|
|
@@ -52,6 +56,8 @@ $defs:
|
|
|
52
56
|
oneOf:
|
|
53
57
|
- required: ["sources"]
|
|
54
58
|
- required: ["inputs"]
|
|
59
|
+
- required: ["const"]
|
|
60
|
+
- required: ["ref"]
|
|
55
61
|
|
|
56
62
|
Source:
|
|
57
63
|
type: object
|
brkraw/specs/__init__.py
CHANGED