From bd31b9d3f7861fdafa0e919d1bf0ed31f848550f Mon Sep 17 00:00:00 2001 From: mathiasg Date: Fri, 21 Oct 2022 17:38:52 -0400 Subject: [PATCH 1/6] Rework: GenerateCifti interface - Drops support for older, fsaverage/MNI152NLin2009cAsym style - Reworks metadata to be BIDS compliant - Adds typing hints --- niworkflows/interfaces/cifti.py | 284 +++++++++++++------------------- 1 file changed, 114 insertions(+), 170 deletions(-) diff --git a/niworkflows/interfaces/cifti.py b/niworkflows/interfaces/cifti.py index 4e39ed5030e..eadfdc7cd09 100644 --- a/niworkflows/interfaces/cifti.py +++ b/niworkflows/interfaces/cifti.py @@ -23,6 +23,7 @@ """Handling connectivity: combines FreeSurfer surfaces with subcortical volumes.""" from pathlib import Path import json +import typing import warnings import nibabel as nb @@ -42,8 +43,7 @@ from niworkflows.interfaces.nibabel import reorient_image -CIFTI_SURFACES = ("fsaverage5", "fsaverage6", "fsLR") -CIFTI_VOLUMES = ("MNI152NLin2009cAsym", "MNI152NLin6Asym") + CIFTI_STRUCT_WITH_LABELS = { # CITFI structures with corresponding labels # SURFACES "CIFTI_STRUCTURE_CORTEX_LEFT": None, @@ -56,8 +56,8 @@ "CIFTI_STRUCTURE_BRAIN_STEM": (16,), "CIFTI_STRUCTURE_CAUDATE_LEFT": (11,), "CIFTI_STRUCTURE_CAUDATE_RIGHT": (50,), - "CIFTI_STRUCTURE_CEREBELLUM_LEFT": (6, 8,), # DKT31 # HCP MNI152 - "CIFTI_STRUCTURE_CEREBELLUM_RIGHT": (45, 47,), # DKT31 # HCP MNI152 + "CIFTI_STRUCTURE_CEREBELLUM_LEFT": (8,), # HCP MNI152 + "CIFTI_STRUCTURE_CEREBELLUM_RIGHT": (47,), # HCP MNI152 "CIFTI_STRUCTURE_DIENCEPHALON_VENTRAL_LEFT": (28,), "CIFTI_STRUCTURE_DIENCEPHALON_VENTRAL_RIGHT": (60,), "CIFTI_STRUCTURE_HIPPOCAMPUS_LEFT": (17,), @@ -69,37 +69,29 @@ "CIFTI_STRUCTURE_THALAMUS_LEFT": (10,), "CIFTI_STRUCTURE_THALAMUS_RIGHT": (49,), } -CIFTI_VARIANTS = { - "HCP grayordinates": ("fsLR", "MNI152NLin6Asym"), - "fMRIPrep grayordinates": ("fsaverage", "MNI152NLin2009cAsym"), -} class _GenerateCiftiInputSpec(BaseInterfaceInputSpec): bold_file = File(mandatory=True, exists=True, desc="input BOLD file") volume_target = traits.Enum( "MNI152NLin6Asym", - "MNI152NLin2009cAsym", usedefault=True, desc="CIFTI volumetric output space", ) surface_target = traits.Enum( "fsLR", - "fsaverage5", - "fsaverage6", usedefault=True, desc="CIFTI surface target space", ) surface_density = traits.Enum( - "10k", "32k", "41k", "59k", desc="Surface vertices density." + "32k", "59k", desc="Surface vertices density." ) TR = traits.Float(mandatory=True, desc="Repetition time") surface_bolds = traits.List( File(exists=True), mandatory=True, - desc="list of surface BOLD GIFTI files" " (length 2 with order [L,R])", + desc="list of surface BOLD GIFTI files (length 2 with order [L,R])", ) - subjects_dir = Directory(mandatory=True, desc="FreeSurfer SUBJECTS_DIR") class _GenerateCiftiOutputSpec(TraitedSpec): @@ -111,47 +103,31 @@ class _GenerateCiftiOutputSpec(TraitedSpec): class GenerateCifti(SimpleInterface): """ - Generate CIFTI image from BOLD file in target spaces. - - Currently supports ``fsLR``, ``fsaverage5``, or ``fsaverage6`` for template surfaces and - ``MNI152NLin6Asym`` or ``MNI152NLin2009cAsym`` as template volumes. - + Generate a HCP-style CIFTI image from BOLD file in target spaces. """ - input_spec = _GenerateCiftiInputSpec output_spec = _GenerateCiftiOutputSpec def _run_interface(self, runtime): - annotation_files, label_file = _get_cifti_data( - self.inputs.surface_target, - self.inputs.volume_target, - self.inputs.subjects_dir, - self.inputs.surface_density, - ) - out_metadata, variant, density = _get_cifti_variant( - self.inputs.surface_target, - self.inputs.volume_target, - self.inputs.surface_density, - ) - self._results.update({"out_metadata": out_metadata, "variant": variant}) - if density: - self._results["density"] = density - + surface_labels, volume_labels, metadata = _prepare_cifti(self.inputs.surface_density) self._results["out_file"] = _create_cifti_image( self.inputs.bold_file, - label_file, + volume_labels, self.inputs.surface_bolds, - annotation_files, + surface_labels, self.inputs.TR, - (self.inputs.surface_target, self.inputs.volume_target), + metadata, ) + metadata_file = Path("bold.dtseries.json").absolute() + metadata_file.write_text(json.dumps(metadata, indent=2)) + self._results["out_metadata"] = str(metadata_file) return runtime class _CiftiNameSourceInputSpec(BaseInterfaceInputSpec): - variant = traits.Str( + space = traits.Str( mandatory=True, - desc="unique label of spaces used in combination to generate CIFTI file", + desc="the space identifier", ) density = traits.Str(desc="density label") @@ -167,7 +143,7 @@ class CiftiNameSource(SimpleInterface): Examples -------- >>> namer = CiftiNameSource() - >>> namer.inputs.variant = 'HCP grayordinates' + >>> namer.inputs.space = 'fsLR' >>> res = namer.run() >>> res.outputs.out_name 'space-fsLR_bold.dtseries' @@ -183,167 +159,134 @@ class CiftiNameSource(SimpleInterface): output_spec = _CiftiNameSourceOutputSpec def _run_interface(self, runtime): - suffix = "" - if "hcp" in self.inputs.variant.lower(): - suffix += "space-fsLR_" + entities = [('space', self.inputs.space)] if self.inputs.density: - suffix += "den-{}_".format(self.inputs.density) + entities.append(('den', self.inputs.density)) - suffix += "bold.dtseries" - self._results["out_name"] = suffix + out_name = '_'.join([f'{k}-{v}' for k, v in entities] + ['bold.dtseries']) + self._results["out_name"] = out_name return runtime -def _get_cifti_data(surface, volume, subjects_dir=None, density=None): +def _prepare_cifti(surface_density: str) -> typing.List[list, str, dict]: """ - Fetch surface and volumetric label files for CIFTI creation. + Fetch the required templates needed for CIFTI-2 generation, based on input surface density. Parameters ---------- - surface : str - Target surface space - volume : str - Target volume space - subjects_dir : str, optional - Path to FreeSurfer subjects directory (required `fsaverage5`/`fsaverage6` surfaces) - density : str, optional + surface_density : Surface density (required for `fsLR` surfaces) Returns ------- - annotation_files : list - Surface annotation files to allow removal of medial wall - label_file : str - Volumetric label file of subcortical structures + surface_labels + Surface label files for vertex inclusion/exclusion. + volume_label + Volumetric label file of subcortical structures. + metadata + Dictionary with BIDS metadata. Examples -------- - >>> annots, label = _get_cifti_data('fsLR', 'MNI152NLin6Asym', density='32k') - >>> annots # doctest: +ELLIPSIS + >>> surface_labels, volume_labels, metadata_file = _prepare_cifti('32k') + >>> surface_labels # doctest: +ELLIPSIS ['.../tpl-fsLR_hemi-L_den-32k_desc-nomedialwall_dparc.label.gii', \ '.../tpl-fsLR_hemi-R_den-32k_desc-nomedialwall_dparc.label.gii'] - >>> label # doctest: +ELLIPSIS + >>> volume_labels # doctest: +ELLIPSIS '.../tpl-MNI152NLin6Asym_res-02_atlas-HCP_dseg.nii.gz' + >>> metadata # doctest: +ELLIPSIS + {'Density': '91,282 grayordinates corresponding to all of the grey matter sampled at a \ +2mm average vertex spacing... 'SpatialReference': {'VolumeReference': ... """ - if surface not in CIFTI_SURFACES or volume not in CIFTI_VOLUMES: - raise NotImplementedError( - "Variant (surface: {0}, volume: {1}) is not supported".format( - surface, volume - ) - ) - - tpl_kwargs = {"suffix": "dseg"} - # fMRIPrep grayordinates - if volume == "MNI152NLin2009cAsym": - tpl_kwargs.update({"resolution": "2", "desc": "DKT31"}) - annotation_files = sorted( - (subjects_dir / surface / "label").glob("*h.aparc.annot") - ) - # HCP grayordinates - elif volume == "MNI152NLin6Asym": - # templateflow specific resolutions (2mm, 1.6mm) - res = {"32k": "2", "59k": "6"}[density] - tpl_kwargs.update({"atlas": "HCP", "resolution": res}) - annotation_files = [ - str(f) - for f in tf.get( - "fsLR", density=density, desc="nomedialwall", suffix="dparc" - ) - ] - - if len(annotation_files) != 2: - raise IOError("Invalid number of surface annotation files") - label_file = str(tf.get(volume, **tpl_kwargs)) - return annotation_files, label_file - - -def _get_cifti_variant(surface, volume, density=None): - """ - Identify CIFTI variant and return metadata. - Parameters - ---------- - surface : str - Target surface space - volume : str - Target volume space - density : str, optional - Surface density (required for `fsLR` surfaces) - - Returns - ------- - out_metadata : str - JSON file with variant metadata - variant : str - Name of CIFTI variant - - Examples - -------- - >>> metafile, variant, _ = _get_cifti_variant('fsaverage5', 'MNI152NLin2009cAsym') - >>> str(metafile) # doctest: +ELLIPSIS - '.../dtseries_variant.json' - >>> variant - 'fMRIPrep grayordinates' - - >>> _, variant, grayords = _get_cifti_variant('fsLR', 'MNI152NLin6Asym', density='59k') - >>> variant - 'HCP grayordinates' - >>> grayords - '170k' - - """ - if surface in ("fsaverage5", "fsaverage6"): - density = {"fsaverage5": "10k", "fsaverage6": "41k"}[surface] - surface = "fsaverage" - - for variant, targets in CIFTI_VARIANTS.items(): - if all(target in targets for target in (surface, volume)): - break - variant = None - if variant is None: - raise NotImplementedError( - "No corresponding variant for (surface: {0}, volume: {1})".format( - surface, volume + vertices_key = { + "32k": { + "tf-res": "02", + "grayords": "91,282", + "res-mm": "2mm" + }, + "59k": { + "tf-res": "06", + "grayords": "170,494", + "res-mm": "1.6mm" + } + } + if surface_density not in vertices_key: + raise NotImplementedError("Density {surface_density} is not supported.") + + tf_vol_res = vertices_key[surface_density]['tf-res'] + grayords = vertices_key[surface_density]['grayords'] + res_mm = vertices_key[surface_density]['res-mm'] + # Fetch templates + surface_labels = [ + str( + tf.get( + "fsLR", + density=surface_density, + hemi=hemi, + desc="nomedialwall", + suffix="dparc", + raise_empty=True, ) ) + for hemi in ("L", "R") + ] + volume_label = str( + tf.get( + "MNI152NLin6Asym", + suffix="dseg", + atlas="HCP", + resolution=tf_vol_res, + raise_empty=True + ) + ) - grayords = None - out_metadata = Path.cwd() / "dtseries_variant.json" - out_json = { - "space": variant, - "surface": surface, - "volume": volume, - "surface_density": density, + tf_url = "https://templateflow.s3.amazonaws.com" + volume_url = f"{tf_url}/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-{tf_vol_res}_T1w.nii.gz" + surfaces_url = ( # midthickness is the default, but varying levels of inflation are all valid + f"{tf_url}/tpl-fsLR/tpl-fsLR_den-{surface_density}_hemi-%s_midthickness.surf.gii" + ) + metadata = { + "Density": ( + f"{grayords} grayordinates corresponding to all of the grey matter sampled at a " + f"{res_mm} average vertex spacing on the surface and as {res_mm} voxels subcortically" + ), + "SpatialReference": { + "VolumeReference": volume_url, + "CIFTI_STRUCTURE_LEFT_CORTEX": surfaces_url % "L", + "CIFTI_STRUCTURE_RIGHT_CORTEX": surfaces_url % "R", + } } - if surface == "fsLR": - grayords = {"32k": "91k", "59k": "170k"}[density] - out_json["grayordinates"] = grayords - out_metadata.write_text(json.dumps(out_json, indent=2)) - return out_metadata, variant, grayords + return surface_labels, volume_label, metadata def _create_cifti_image( - bold_file, label_file, bold_surfs, annotation_files, tr, targets + bold_file: str, + volume_label: str, + bold_surfs: typing.List[str, str], + surface_labels: typing.List[str, str], + tr: float, + metadata: typing.Optional[dict] = None, ): """ Generate CIFTI image in target space. Parameters ---------- - bold_file : str + bold_file BOLD volumetric timeseries - label_file : str + volume_label Subcortical label file - bold_surfs : list + bold_surfs BOLD surface timeseries [L,R] - annotation_files : list + surface_labels Surface label files used to remove medial wall - tr : float + tr BOLD repetition time - targets : tuple or list - Surface and volumetric output spaces + metadata + Metadata to include in CIFTI header Returns ------- @@ -351,7 +294,7 @@ def _create_cifti_image( BOLD data saved as CIFTI dtseries """ bold_img = nb.load(bold_file) - label_img = nb.load(label_file) + label_img = nb.load(volume_label) if label_img.shape != bold_img.shape[:3]: warnings.warn("Resampling bold volume to match label dimensions") bold_img = resample_to_img(bold_img, label_img) @@ -377,12 +320,12 @@ def _create_cifti_image( # currently only supports L/R cortex surf_ts = nb.load(bold_surfs[hemi == "RIGHT"]) surf_verts = len(surf_ts.darrays[0].data) - if annotation_files[0].endswith(".annot"): - annot = nb.freesurfer.read_annot(annotation_files[hemi == "RIGHT"]) + if surface_labels[0].endswith(".annot"): + annot = nb.freesurfer.read_annot(surface_labels[hemi == "RIGHT"]) # remove medial wall medial = np.nonzero(annot[0] != annot[2].index(b"unknown"))[0] else: - annot = nb.load(annotation_files[hemi == "RIGHT"]) + annot = nb.load(surface_labels[hemi == "RIGHT"]) medial = np.nonzero(annot.darrays[0].data)[0] # extract values across volumes ts = np.array([tsarr.data[medial] for tsarr in surf_ts.darrays]) @@ -450,15 +393,16 @@ def _create_cifti_image( (1,), "CIFTI_INDEX_TYPE_BRAIN_MODELS", maps=brainmodels ) # provide some metadata to CIFTI matrix - meta = { - "surface": targets[0], - "volume": targets[1], - } + if not metadata: + metadata = { + "surface": "fsLR", + "volume": "MNI152NLin6Asym", + } # generate and save CIFTI image matrix = ci.Cifti2Matrix() matrix.append(series_map) matrix.append(geometry_map) - matrix.metadata = ci.Cifti2MetaData(meta) + matrix.metadata = ci.Cifti2MetaData(metadata) hdr = ci.Cifti2Header(matrix) img = ci.Cifti2Image(dataobj=bm_ts, header=hdr) img.set_data_dtype(bold_img.get_data_dtype()) From ea4a36efb08e177a9ebef1f3eede6902cbe5a724 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 24 Oct 2022 13:07:29 -0400 Subject: [PATCH 2/6] FIX: Fixed length typing hints --- niworkflows/interfaces/cifti.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/niworkflows/interfaces/cifti.py b/niworkflows/interfaces/cifti.py index eadfdc7cd09..e71973e27d2 100644 --- a/niworkflows/interfaces/cifti.py +++ b/niworkflows/interfaces/cifti.py @@ -37,7 +37,6 @@ File, traits, SimpleInterface, - Directory, ) import templateflow.api as tf @@ -168,7 +167,7 @@ def _run_interface(self, runtime): return runtime -def _prepare_cifti(surface_density: str) -> typing.List[list, str, dict]: +def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: """ Fetch the required templates needed for CIFTI-2 generation, based on input surface density. @@ -265,8 +264,8 @@ def _prepare_cifti(surface_density: str) -> typing.List[list, str, dict]: def _create_cifti_image( bold_file: str, volume_label: str, - bold_surfs: typing.List[str, str], - surface_labels: typing.List[str, str], + bold_surfs: typing.Tuple[str, str], + surface_labels: typing.Tuple[str, str], tr: float, metadata: typing.Optional[dict] = None, ): @@ -280,9 +279,9 @@ def _create_cifti_image( volume_label Subcortical label file bold_surfs - BOLD surface timeseries [L,R] + BOLD surface timeseries (L,R) surface_labels - Surface label files used to remove medial wall + Surface label files used to remove medial wall (L,R) tr BOLD repetition time metadata From 633d141b1d3607358976edcbae9ebfc212a57953 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 24 Oct 2022 13:27:41 -0400 Subject: [PATCH 3/6] TST: Fix doctest --- niworkflows/interfaces/cifti.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/cifti.py b/niworkflows/interfaces/cifti.py index e71973e27d2..ef63dd68105 100644 --- a/niworkflows/interfaces/cifti.py +++ b/niworkflows/interfaces/cifti.py @@ -187,7 +187,7 @@ def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: Examples -------- - >>> surface_labels, volume_labels, metadata_file = _prepare_cifti('32k') + >>> surface_labels, volume_labels, metadata = _prepare_cifti('32k') >>> surface_labels # doctest: +ELLIPSIS ['.../tpl-fsLR_hemi-L_den-32k_desc-nomedialwall_dparc.label.gii', \ '.../tpl-fsLR_hemi-R_den-32k_desc-nomedialwall_dparc.label.gii'] From b2c3eb644fc0c8c9c69011f73666167808ff5a6c Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 25 Oct 2022 11:35:14 -0400 Subject: [PATCH 4/6] TST: Add CIFTI test --- niworkflows/interfaces/tests/test_cifti.py | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 niworkflows/interfaces/tests/test_cifti.py diff --git a/niworkflows/interfaces/tests/test_cifti.py b/niworkflows/interfaces/tests/test_cifti.py new file mode 100644 index 00000000000..a8501cf1b20 --- /dev/null +++ b/niworkflows/interfaces/tests/test_cifti.py @@ -0,0 +1,57 @@ +import json +from pathlib import Path + +import nibabel as nb +import numpy as np +import pytest + +from ..cifti import GenerateCifti, CIFTI_STRUCT_WITH_LABELS + + +@pytest.fixture(scope="module") +def cifti_data(): + out = Path().absolute() + volume_file = str(out / "volume.nii.gz") + left_gii = str(out / "left.gii") + right_gii = str(out / "right.gii") + surface_data = [nb.gifti.GiftiDataArray(np.ones(32492)) for _ in range(4)] + vol = nb.Nifti1Image(np.ones((91, 109, 91, 4)), np.eye(4)) + gii = nb.GiftiImage(darrays=surface_data) + + vol.to_filename(volume_file) + gii.to_filename(left_gii) + gii.to_filename(right_gii) + yield volume_file, left_gii, right_gii + for f in (volume_file, left_gii, right_gii): + Path(f).unlink() + + +def test_GenerateCifti(tmpdir, cifti_data): + tmpdir.chdir() + + bold_volume = cifti_data[0] + bold_surfaces = list(cifti_data[1:]) + + gen = GenerateCifti( + bold_file=bold_volume, + surface_bolds=bold_surfaces, + surface_density='32k', + TR=1, + ) + res = gen.run().outputs + + cifti = nb.load(res.out_file) + assert cifti.shape == (4, 91282) + matrix = cifti.header.matrix + assert matrix.mapped_indices == [0, 1] + series_map = matrix.get_index_map(0) + bm_map = matrix.get_index_map(1) + assert series_map.indices_map_to_data_type == 'CIFTI_INDEX_TYPE_SERIES' + assert bm_map.indices_map_to_data_type == 'CIFTI_INDEX_TYPE_BRAIN_MODELS' + assert len(list(bm_map.brain_models)) == len(CIFTI_STRUCT_WITH_LABELS) + + metadata = json.loads(Path(res.out_metadata).read_text()) + assert 'Density' in metadata + assert 'SpatialReference' in metadata + for key in ('VolumeReference', 'CIFTI_STRUCTURE_LEFT_CORTEX', 'CIFTI_STRUCTURE_RIGHT_CORTEX'): + assert key in metadata['SpatialReference'] From e2827cf032651303cd4f3737240657362b7dd05f Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 25 Oct 2022 13:05:18 -0400 Subject: [PATCH 5/6] FIX: Create test data in tempdir --- niworkflows/interfaces/tests/test_cifti.py | 29 +++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/niworkflows/interfaces/tests/test_cifti.py b/niworkflows/interfaces/tests/test_cifti.py index a8501cf1b20..804630177dc 100644 --- a/niworkflows/interfaces/tests/test_cifti.py +++ b/niworkflows/interfaces/tests/test_cifti.py @@ -10,20 +10,21 @@ @pytest.fixture(scope="module") def cifti_data(): - out = Path().absolute() - volume_file = str(out / "volume.nii.gz") - left_gii = str(out / "left.gii") - right_gii = str(out / "right.gii") - surface_data = [nb.gifti.GiftiDataArray(np.ones(32492)) for _ in range(4)] - vol = nb.Nifti1Image(np.ones((91, 109, 91, 4)), np.eye(4)) - gii = nb.GiftiImage(darrays=surface_data) - - vol.to_filename(volume_file) - gii.to_filename(left_gii) - gii.to_filename(right_gii) - yield volume_file, left_gii, right_gii - for f in (volume_file, left_gii, right_gii): - Path(f).unlink() + import tempfile + + with tempfile.TemporaryDirectory('cifti-data') as tmp: + out = Path(tmp).absolute() + volume_file = str(out / "volume.nii.gz") + left_gii = str(out / "left.gii") + right_gii = str(out / "right.gii") + surface_data = [nb.gifti.GiftiDataArray(np.ones(32492)) for _ in range(4)] + vol = nb.Nifti1Image(np.ones((91, 109, 91, 4)), np.eye(4)) + gii = nb.GiftiImage(darrays=surface_data) + + vol.to_filename(volume_file) + gii.to_filename(left_gii) + gii.to_filename(right_gii) + yield volume_file, left_gii, right_gii def test_GenerateCifti(tmpdir, cifti_data): From 99a589c6cf7a1cd30f658ae0e2f6b38169b87dba Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 25 Oct 2022 15:12:54 -0400 Subject: [PATCH 6/6] REWORK: Drop surface density input, add `grayordinates` input --- niworkflows/interfaces/cifti.py | 52 ++++++++++------------ niworkflows/interfaces/tests/test_cifti.py | 2 +- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/niworkflows/interfaces/cifti.py b/niworkflows/interfaces/cifti.py index ef63dd68105..db3b677cfda 100644 --- a/niworkflows/interfaces/cifti.py +++ b/niworkflows/interfaces/cifti.py @@ -82,8 +82,8 @@ class _GenerateCiftiInputSpec(BaseInterfaceInputSpec): usedefault=True, desc="CIFTI surface target space", ) - surface_density = traits.Enum( - "32k", "59k", desc="Surface vertices density." + grayordinates = traits.Enum( + "91k", "170k", usedefault=True, desc="Final CIFTI grayordinates" ) TR = traits.Float(mandatory=True, desc="Repetition time") surface_bolds = traits.List( @@ -94,10 +94,8 @@ class _GenerateCiftiInputSpec(BaseInterfaceInputSpec): class _GenerateCiftiOutputSpec(TraitedSpec): - out_file = File(exists=True, desc="generated CIFTI file") - out_metadata = File(exists=True, desc="variant metadata JSON") - variant = traits.Str(desc="Name of variant space") - density = traits.Str(desc="Total number of grayordinates") + out_file = File(desc="generated CIFTI file") + out_metadata = File(desc="CIFTI metadata JSON") class GenerateCifti(SimpleInterface): @@ -108,7 +106,8 @@ class GenerateCifti(SimpleInterface): output_spec = _GenerateCiftiOutputSpec def _run_interface(self, runtime): - surface_labels, volume_labels, metadata = _prepare_cifti(self.inputs.surface_density) + + surface_labels, volume_labels, metadata = _prepare_cifti(self.inputs.grayordinates) self._results["out_file"] = _create_cifti_image( self.inputs.bold_file, volume_labels, @@ -167,14 +166,14 @@ def _run_interface(self, runtime): return runtime -def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: +def _prepare_cifti(grayordinates: str) -> typing.Tuple[list, str, dict]: """ Fetch the required templates needed for CIFTI-2 generation, based on input surface density. Parameters ---------- - surface_density : - Surface density (required for `fsLR` surfaces) + grayordinates : + Total CIFTI grayordinates (91k, 170k) Returns ------- @@ -187,7 +186,7 @@ def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: Examples -------- - >>> surface_labels, volume_labels, metadata = _prepare_cifti('32k') + >>> surface_labels, volume_labels, metadata = _prepare_cifti('91k') >>> surface_labels # doctest: +ELLIPSIS ['.../tpl-fsLR_hemi-L_den-32k_desc-nomedialwall_dparc.label.gii', \ '.../tpl-fsLR_hemi-R_den-32k_desc-nomedialwall_dparc.label.gii'] @@ -199,24 +198,27 @@ def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: """ - vertices_key = { - "32k": { + grayord_key = { + "91k": { + "surface-den": "32k", "tf-res": "02", "grayords": "91,282", "res-mm": "2mm" }, - "59k": { + "170k": { + "surface-den": "59k", "tf-res": "06", "grayords": "170,494", "res-mm": "1.6mm" } } - if surface_density not in vertices_key: - raise NotImplementedError("Density {surface_density} is not supported.") + if grayordinates not in grayord_key: + raise NotImplementedError("Grayordinates {grayordinates} is not supported.") - tf_vol_res = vertices_key[surface_density]['tf-res'] - grayords = vertices_key[surface_density]['grayords'] - res_mm = vertices_key[surface_density]['res-mm'] + tf_vol_res = grayord_key[grayordinates]['tf-res'] + total_grayords = grayord_key[grayordinates]['grayords'] + res_mm = grayord_key[grayordinates]['res-mm'] + surface_density = grayord_key[grayordinates]['surface-den'] # Fetch templates surface_labels = [ str( @@ -248,7 +250,7 @@ def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: ) metadata = { "Density": ( - f"{grayords} grayordinates corresponding to all of the grey matter sampled at a " + f"{total_grayords} grayordinates corresponding to all of the grey matter sampled at a " f"{res_mm} average vertex spacing on the surface and as {res_mm} voxels subcortically" ), "SpatialReference": { @@ -257,7 +259,6 @@ def _prepare_cifti(surface_density: str) -> typing.Tuple[list, str, dict]: "CIFTI_STRUCTURE_RIGHT_CORTEX": surfaces_url % "R", } } - return surface_labels, volume_label, metadata @@ -319,13 +320,8 @@ def _create_cifti_image( # currently only supports L/R cortex surf_ts = nb.load(bold_surfs[hemi == "RIGHT"]) surf_verts = len(surf_ts.darrays[0].data) - if surface_labels[0].endswith(".annot"): - annot = nb.freesurfer.read_annot(surface_labels[hemi == "RIGHT"]) - # remove medial wall - medial = np.nonzero(annot[0] != annot[2].index(b"unknown"))[0] - else: - annot = nb.load(surface_labels[hemi == "RIGHT"]) - medial = np.nonzero(annot.darrays[0].data)[0] + labels = nb.load(surface_labels[hemi == "RIGHT"]) + medial = np.nonzero(labels.darrays[0].data)[0] # extract values across volumes ts = np.array([tsarr.data[medial] for tsarr in surf_ts.darrays]) diff --git a/niworkflows/interfaces/tests/test_cifti.py b/niworkflows/interfaces/tests/test_cifti.py index 804630177dc..97a2a32decc 100644 --- a/niworkflows/interfaces/tests/test_cifti.py +++ b/niworkflows/interfaces/tests/test_cifti.py @@ -36,7 +36,7 @@ def test_GenerateCifti(tmpdir, cifti_data): gen = GenerateCifti( bold_file=bold_volume, surface_bolds=bold_surfaces, - surface_density='32k', + grayordinates="91k", TR=1, ) res = gen.run().outputs