Skip to content

Commit 322d670

Browse files
mgxdeffigies
andauthored
ENH: Validate files passed with --derivatives (#182)
* ENH: Add derivatives validation during workflow construction * ENH: Copy T1w header to avoid precision errors * FIX: Ensure derivatives point to filenames * RF: Simplify physical space check Co-authored-by: Chris Markiewicz <[email protected]> * FIX: Reinsert `ValidateImage` for orientation safety * FIX: Remove unused import Co-authored-by: Chris Markiewicz <[email protected]>
1 parent ccdd8b7 commit 322d670

File tree

4 files changed

+70
-4
lines changed

4 files changed

+70
-4
lines changed

nibabies/utils/bids.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ def collect_precomputed_derivatives(layout, subject_id, derivatives_filters=None
337337
scope='derivatives',
338338
subject=subject_id,
339339
extension=['.nii', '.nii.gz'],
340+
return_type="filename",
340341
**query,
341342
)
342343
if not res:

nibabies/utils/validation.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import sys
2+
import nibabel as nb
3+
import numpy as np
4+
5+
6+
def validate_t1w_derivatives(t1w_template, *, anat_mask=None, anat_aseg=None, atol=1e-5):
7+
"""
8+
Validate anatomical derivatives.
9+
This function compares the input T1w's orientation and physical space to each derivative.
10+
11+
Parameters
12+
----------
13+
t1w_template : str
14+
T1w template
15+
anat_mask : str or None
16+
Precomputed anatomical brain mask
17+
anat_aseg : str or None
18+
Precomputed anatomical segmentations
19+
atol : float
20+
Absolute error tolerance between image origins
21+
22+
Returns
23+
-------
24+
validated : dict
25+
A dictionary composed of derivative keys and validated filename values.
26+
Derivatives that failed validation will not be included.
27+
"""
28+
29+
validated = {}
30+
# T1w information
31+
t1w = nb.load(t1w_template)
32+
expected_ort = nb.aff2axcodes(t1w.affine)
33+
34+
# Ensure orientation
35+
for name, deriv_fl in zip(('anat_mask', 'anat_aseg'), (anat_mask, anat_aseg)):
36+
if deriv_fl is None:
37+
continue
38+
img = nb.load(deriv_fl)
39+
if nb.aff2axcodes(img.affine) != expected_ort:
40+
print(
41+
f"Orientation mismatch between {name} <{deriv_fl}> and T1w <{t1w_template}>",
42+
file=sys.stderr,
43+
)
44+
continue
45+
if img.shape != t1w.shape or not np.allclose(t1w.affine, img.affine, atol=atol):
46+
print(
47+
f"Physical space mismatch between {name} <{deriv_fl}> and T1w <{t1w_template}>",
48+
file=sys.stderr,
49+
)
50+
continue
51+
validated[name] = deriv_fl
52+
return validated

nibabies/workflows/anatomical/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ def init_infant_anat_wf(
9393
precomp_mask = existing_derivatives.get("anat_mask")
9494
precomp_aseg = existing_derivatives.get("anat_aseg")
9595

96+
# verify derivatives are relatively similar to T1w
97+
if precomp_mask or precomp_aseg:
98+
from ...utils.validation import validate_t1w_derivatives
99+
100+
validated_derivatives = validate_t1w_derivatives( # compare derivatives to the first T1w
101+
t1w[0], anat_mask=precomp_mask, anat_aseg=precomp_aseg
102+
)
103+
precomp_mask = validated_derivatives.get("anat_mask")
104+
precomp_aseg = validated_derivatives.get("anat_aseg")
105+
96106
wf = Workflow(name=name)
97107
desc = f"""\n
98108
Anatomical data preprocessing

nibabies/workflows/anatomical/brain_extraction.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def init_precomputed_mask_wf(
272272
bspline_fitting_distance=200, omp_nthreads=None, name="precomputed_mask_wf"
273273
):
274274
from nipype.interfaces.ants import N4BiasFieldCorrection
275-
from niworkflows.interfaces.header import ValidateImage
275+
from niworkflows.interfaces.header import CopyXForm, ValidateImage
276276
from niworkflows.interfaces.nibabel import ApplyMask, IntensityClip
277277

278278
workflow = pe.Workflow(name=name)
@@ -283,6 +283,7 @@ def init_precomputed_mask_wf(
283283
)
284284

285285
validate_mask = pe.Node(ValidateImage(), name="validate_mask")
286+
fix_hdr = pe.Node(CopyXForm(), mem_gb=0.1, name="fix_hdr")
286287
final_n4 = pe.Node(
287288
N4BiasFieldCorrection(
288289
dimension=3,
@@ -303,12 +304,14 @@ def init_precomputed_mask_wf(
303304
# fmt:off
304305
workflow.connect([
305306
(inputnode, validate_mask, [("t1w_mask", "in_file")]),
307+
(validate_mask, fix_hdr, [("out_file", "in_file")]),
308+
(inputnode, fix_hdr, [("t1w", "hdr_file")]),
306309
(inputnode, final_n4, [("t1w", "input_image")]),
307-
(validate_mask, final_n4, [("out_file", "weight_image")]),
308-
(validate_mask, apply_mask, [("out_file", "in_mask")]),
310+
(fix_hdr, final_n4, [("out_file", "weight_image")]),
311+
(fix_hdr, apply_mask, [("out_file", "in_mask")]),
309312
(final_n4, apply_mask, [("output_image", "in_file")]),
310313
(final_n4, final_clip, [("output_image", "in_file")]),
311-
(validate_mask, outputnode, [("out_file", "t1w_mask")]),
314+
(fix_hdr, outputnode, [("out_file", "t1w_mask")]),
312315
(final_clip, outputnode, [("out_file", "t1w_preproc")]),
313316
(apply_mask, outputnode, [("out_file", "t1w_brain")]),
314317
])

0 commit comments

Comments
 (0)