Skip to content

Commit 691ee6f

Browse files
authored
FIX: Use the binarized output from the brain extraction (#246)
* FIX: Use the binarized output from the brain extraction * TST: Add helper script to verify outputs * CI: Add verification test to outputs * TST: Add leniancy to cortical / subcortical labels * TST: Ensure ITK transforms are properly loaded
1 parent 134a318 commit 691ee6f

File tree

3 files changed

+234
-4
lines changed

3 files changed

+234
-4
lines changed

.circleci/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,12 @@ jobs:
383383
diff /tmp/src/nibabies/.circleci/${CHECK_OUTPUTS_FILE} /tmp/${DATASET}/test/outputs.out
384384
rm -rf /tmp/${DATASET}/test
385385
exit $?
386+
- run:
387+
name: Verify outputs
388+
command: |
389+
pip install nibabel numpy nitransforms
390+
python /tmp/src/nibabies/scripts/check_outputs.py /tmp/${DATASET}/derivatives/nibabies
391+
exit $?
386392
- store_artifacts:
387393
path: /tmp/bcp/derivatives
388394

nibabies/workflows/bold/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
583583
# Native-space BOLD files
584584
(final_boldref_wf, final_boldref_mask, [('outputnode.epi_ref_file', 'in_file')]),
585585
(final_boldref_wf, bold_final, [('outputnode.epi_ref_file', 'boldref')]),
586-
(final_boldref_mask, bold_final, [('out_file', 'mask')]),
586+
(final_boldref_mask, bold_final, [('out_mask', 'mask')]),
587587
(bold_final, outputnode, [
588588
('bold', 'bold_native'),
589589
('boldref', 'bold_native_ref'),
@@ -999,7 +999,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
999999
] if not multiecho else [
10001000
(inputnode, initial_boldref_mask, [('bold_ref', 'in_file')]),
10011001
(initial_boldref_mask, bold_t2s_wf, [
1002-
("out_file", "inputnode.bold_mask"),
1002+
("out_mask", "inputnode.bold_mask"),
10031003
]),
10041004
(bold_bold_trans_wf, join_echos, [
10051005
("outputnode.bold", "bold_files"),
@@ -1075,7 +1075,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
10751075
(inputnode, coeff2epi_wf, [
10761076
("bold_ref", "inputnode.target_ref")]),
10771077
(initial_boldref_mask, coeff2epi_wf, [
1078-
("out_file", "inputnode.target_mask")]), # skull-stripped brain
1078+
("out_mask", "inputnode.target_mask")]), # skull-stripped brain
10791079
(coeff2epi_wf, unwarp_wf, [
10801080
("outputnode.fmap_coeff", "inputnode.fmap_coeff")]),
10811081
(inputnode, sdc_report, [("bold_ref", "before")]),
@@ -1084,7 +1084,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
10841084
(bold_split, unwarp_wf, [
10851085
("out_files", "inputnode.distorted")]),
10861086
(final_boldref_wf, sdc_report, [("outputnode.epi_ref_file", "after")]),
1087-
(final_boldref_mask, sdc_report, [("out_file", "wm_seg")]),
1087+
(final_boldref_mask, sdc_report, [("out_mask", "wm_seg")]),
10881088
(inputnode, ds_report_sdc, [("bold_file", "source_file")]),
10891089
(sdc_report, ds_report_sdc, [("out_report", "in_file")]),
10901090

scripts/check_outputs.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import argparse
2+
from collections import defaultdict
3+
import json
4+
from pathlib import Path
5+
import warnings
6+
warnings.simplefilter("ignore", UserWarning)
7+
8+
import nibabel as nb
9+
import nitransforms as nt
10+
import numpy as np
11+
12+
13+
DEFAULT_OUTPUT_SPACES = {"MNIInfant",}
14+
15+
16+
def get_parser():
17+
18+
# class AddSpaces(argparse.Action):
19+
# _spaces = {"MNIInfant", }
20+
21+
# def __call__(self, parser, args, values, option_string=None):
22+
# for value in values:
23+
# if "cohort" in value: # skip cohorts for now
24+
# continue
25+
# self._spaces.add(value)
26+
# setattr(args, self.dest, self._spaces)
27+
28+
parser = argparse.ArgumentParser(description="Script to verify NiBabies outputs")
29+
parser.add_argument("output_dir", type=Path, help="Directory containing nibabies outputs")
30+
return parser
31+
32+
33+
def main(argv=None):
34+
parser = get_parser()
35+
pargs = parser.parse_args(argv)
36+
test_output_layout(pargs.output_dir)
37+
for anats in pargs.output_dir.glob("sub-*/**/anat"):
38+
if not anats.is_dir():
39+
continue
40+
test_anatomicals(anats)
41+
42+
for funcs in pargs.output_dir.glob("sub-*/**/func"):
43+
if not funcs.is_dir():
44+
continue
45+
test_functionals(funcs)
46+
print(f"No errors found for output directory {str(pargs.output_dir)}")
47+
48+
49+
def test_output_layout(output_dir):
50+
assert output_dir.exists()
51+
dd = output_dir / 'dataset_description.json'
52+
assert dd.exists()
53+
assert 'NiBabies' in json.loads(dd.read_text())['Name']
54+
for ext in ('bib', 'html', 'md', 'tex'):
55+
assert (output_dir / "logs" / f"CITATION.{ext}").exists()
56+
57+
58+
def test_anatomicals(anat_dir):
59+
print(f"** Checking anatomical derivatives ({str(anat_dir)}):")
60+
outputs = defaultdict(list)
61+
for fl in anat_dir.glob("*"):
62+
outtype = None
63+
if 'desc-brain_mask' in fl.name:
64+
outtype = 'masks'
65+
elif 'desc-preproc_T1w' in fl.name:
66+
outtype = 'preprocs'
67+
elif '_dseg.' in fl.name or '_probseg.' in fl.name:
68+
outtype = 'segs'
69+
elif '_xfm.' in fl.name:
70+
outtype = 'xfms'
71+
elif 'surf.gii' in fl.name:
72+
outtype = 'surfs'
73+
74+
if outtype:
75+
outputs[outtype].append(fl)
76+
77+
_check_masks(outputs['masks'])
78+
_check_segs(outputs['segs'])
79+
_check_xfms(outputs['xfms'])
80+
_check_surfs(outputs['surfs'])
81+
_check_t1w_preprocs(outputs['preprocs'])
82+
83+
84+
def test_functionals(func_dir):
85+
print(f"** Checking functional derivatives ({str(func_dir)}):")
86+
outputs = defaultdict(list)
87+
for fl in func_dir.glob("*"):
88+
outtype = None
89+
if 'desc-brain_mask' in fl.name:
90+
outtype = 'masks'
91+
elif 'desc-preproc_bold' in fl.name or '_boldref.' in fl.name:
92+
outtype = 'preprocs'
93+
elif 'desc-aseg_dseg' in fl.name or '_probseg.' in fl.name:
94+
outtype = 'segs'
95+
elif '_xfm.' in fl.name:
96+
outtype = 'xfms'
97+
elif '.surf.gii' in fl.name:
98+
outtype = 'surfs'
99+
elif 'bold.dtseries' in fl.name:
100+
outtype = 'ciftis'
101+
102+
if outtype:
103+
outputs[outtype].append(fl)
104+
105+
_check_bold_preprocs(outputs['preprocs'])
106+
_check_masks(outputs['masks'])
107+
_check_segs(outputs['segs'])
108+
_check_xfms(outputs['xfms'])
109+
_check_surfs(outputs['surfs'])
110+
_check_ciftis(outputs['ciftis'])
111+
112+
113+
def _check_masks(masks):
114+
for mask in masks:
115+
print(str(mask), end='')
116+
if mask.name.endswith('.json'):
117+
metadata = json.loads(mask.read_text())
118+
assert metadata
119+
# assert metadata["Type"] == "Brain"
120+
else:
121+
img = nb.load(mask)
122+
assert img.dataobj.dtype == np.uint8
123+
assert np.all(np.unique(img.dataobj) == [0, 1])
124+
print(u' \u2713')
125+
126+
127+
def _check_segs(segs):
128+
for seg in segs:
129+
print(str(seg), end='')
130+
img = nb.load(seg)
131+
if 'desc-aseg' in seg.name:
132+
assert img.dataobj.dtype == np.int16
133+
labels = set(np.unique(img.dataobj))
134+
# check for common subcortical labels
135+
subcor = {26, 58, 18, 54, 16, 11, 50, 8, 47, 28, 60, 17, 53, 13, 52, 12, 51, 10, 49}
136+
missing = subcor.difference(labels)
137+
if missing:
138+
print(f"Missing labels {missing}")
139+
elif 'desc-aparcaseg' in seg.name:
140+
assert img.dataobj.dtype == np.int16
141+
labels = set(np.unique(img.dataobj))
142+
# check for common cortical labels
143+
cort = {1000, 1007, 1022, 2000, 2007, 2022}
144+
missing = cort.difference(labels)
145+
if missing:
146+
print(f"Missing labels {missing}")
147+
elif '_probseg.' in seg.name:
148+
assert img.dataobj.dtype == np.float32
149+
assert np.max(img.dataobj) == 1
150+
assert np.min(img.dataobj) == 0
151+
print(u' \u2713')
152+
153+
154+
def _check_xfms(xfms):
155+
for xfm in xfms:
156+
print(str(xfm), end='')
157+
if xfm.name.endswith(".txt"):
158+
assert nt.linear.load(xfm, fmt='itk')
159+
elif xfm.name.endswith('.h5'):
160+
assert nt.manip.load(xfm)
161+
else:
162+
raise NotImplementedError
163+
print(u' \u2713')
164+
165+
166+
def _check_surfs(surfs):
167+
for surf in surfs:
168+
print(str(surf), end='')
169+
if surf.name.endswith('.surf.gii'):
170+
img = nb.load(surf)
171+
assert img.numDA == 2
172+
da0, da1 = img.darrays
173+
assert da0.intent == 1008 # NIFTI_INTENT_POINTSET
174+
assert da1.intent == 1009 # NIFTI_INTENT_TRIANGLE
175+
print(u' \u2713')
176+
177+
178+
def _check_t1w_preprocs(preprocs):
179+
for preproc in preprocs:
180+
print(str(preproc), end='')
181+
if '.json' in preproc.name:
182+
metadata = json.loads(preproc.read_text())
183+
assert 'SkullStripped' in metadata
184+
else:
185+
img = nb.load(preproc)
186+
assert len(img.shape) == 3
187+
print(u' \u2713')
188+
189+
190+
def _check_bold_preprocs(preprocs):
191+
for preproc in preprocs:
192+
print(str(preproc), end='')
193+
if '.json' in preproc.name:
194+
metadata = json.loads(preproc.read_text())
195+
assert metadata['SkullStripped'] is False
196+
else:
197+
img = nb.load(preproc)
198+
if '_boldref.' in preproc.name:
199+
assert len(img.shape) == 3
200+
elif 'desc-preproc_bold' in preproc.name:
201+
assert len(img.shape) == 4
202+
print(u' \u2713')
203+
204+
205+
def _check_ciftis(ciftis):
206+
for cifti in ciftis:
207+
print(str(cifti), end='')
208+
if '.json' in cifti.name:
209+
metadata = json.loads(cifti.read_text())
210+
assert metadata
211+
else:
212+
img = nb.load(cifti)
213+
assert len(img.shape) == 2
214+
matrix = img.header.matrix
215+
assert matrix.mapped_indices == [0, 1]
216+
series_map = matrix.get_index_map(0)
217+
bm_map = matrix.get_index_map(1)
218+
assert series_map.indices_map_to_data_type == 'CIFTI_INDEX_TYPE_SERIES'
219+
assert bm_map.indices_map_to_data_type == 'CIFTI_INDEX_TYPE_BRAIN_MODELS'
220+
print(u' \u2713')
221+
222+
223+
if __name__ == "__main__":
224+
main()

0 commit comments

Comments
 (0)