|
| 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