|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +Modelled on FSL's label2surf command, allows transforming CIFTI or GIFTI label |
| 5 | +files to FSL-compatible GIFTI files combining surface and label structures. |
| 6 | +These are, for example, suitable for input to probtrackx. |
| 7 | +
|
| 8 | +Arguments |
| 9 | +--------- |
| 10 | +-s|--surf |
| 11 | + Path to input surface GIFTI (probably white, midthickness, or pial surface) |
| 12 | +-o|--out |
| 13 | + Path to output surface+label GIFTI |
| 14 | +-l|--label |
| 15 | + Path to CIFTI/GIFTI label file (can be .dlabel.nii, .dscalar.nii, |
| 16 | + .label.gii, or .shape.gii). Can specify flag more than once to supply |
| 17 | + multiple label files - in this case, the script will take the union over |
| 18 | + all labels. |
| 19 | +--hemi |
| 20 | + Surface hemisphere. Choose from L, R, lh, or rh. Required for CIFTI labels, |
| 21 | + but ignored for GIFTI labels. |
| 22 | +--label-ids |
| 23 | + Name (for CIFTI dlabels only) or numeric value of specific label to use |
| 24 | + if multiple labels are encoded in same file. If multiple values given, |
| 25 | + will take union of all corresponding labels. If omitted (default), take |
| 26 | + union of all labels. Can also specify flag multiple times to apply |
| 27 | + different selection for each input label. |
| 28 | +-m|--map |
| 29 | + Index of map/darray to select within CIFTI/GIFTI file (zero-indexed). |
| 30 | + The default is 0. Can also specify flag multiple times to apply different |
| 31 | + index to each input label. |
| 32 | +-h|--help |
| 33 | + Print help and exit |
| 34 | +
|
| 35 | +""" |
| 36 | + |
| 37 | +import sys, argparse |
| 38 | +import numpy as np |
| 39 | +import nibabel as nib |
| 40 | + |
| 41 | + |
| 42 | +### Custom funcs ### |
| 43 | + |
| 44 | +def get_struct_info(struct, brain_models_axis): |
| 45 | + for this_struct, slice_, model in brain_models_axis.iter_structures(): |
| 46 | + if this_struct == struct: |
| 47 | + return slice_, model |
| 48 | + raise Exception(f"No information for structure '{struct}'") |
| 49 | + |
| 50 | + |
| 51 | +### Parse args ### |
| 52 | + |
| 53 | +parser = argparse.ArgumentParser( |
| 54 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter, usage=__doc__ |
| 55 | + ) |
| 56 | + |
| 57 | +parser.add_argument('-s', '--surf', required=True, help='Input surface') |
| 58 | +parser.add_argument('-o', '--out', required=True, help='Output surface') |
| 59 | +parser.add_argument('-l', '--label', required=True, action='append', |
| 60 | + help='CIFTI label file(s)') |
| 61 | +parser.add_argument('--hemi', choices=['L','R','lh','rh'], |
| 62 | + help='Hemisphere to extract label from') |
| 63 | +parser.add_argument('--label-ids', action='append', nargs='+', |
| 64 | + help='Numeric ID or name of label') |
| 65 | +parser.add_argument('-m', '--map', action='append', type=int, |
| 66 | + help='Index of map to extract label from') |
| 67 | + |
| 68 | +if len(sys.argv) < 2: |
| 69 | + parser.print_help() |
| 70 | + sys.exit() |
| 71 | + |
| 72 | +args = parser.parse_args() |
| 73 | +surf_file = args.surf |
| 74 | +outfile = args.out |
| 75 | +label_files = args.label |
| 76 | +hemi = args.hemi |
| 77 | +label_ids = args.label_ids |
| 78 | +mapNs = args.map |
| 79 | + |
| 80 | +if hemi is None: |
| 81 | + hemi_struct = None |
| 82 | +elif hemi in ['L','lh']: |
| 83 | + hemi_struct = 'CIFTI_STRUCTURE_CORTEX_LEFT' |
| 84 | +elif hemi in ['R','rh']: |
| 85 | + hemi_struct = 'CIFTI_STRUCTURE_CORTEX_RIGHT' |
| 86 | + |
| 87 | +if label_ids is None: |
| 88 | + label_ids = [None] * len(label_files) |
| 89 | +elif len(label_ids) == 1: |
| 90 | + label_ids *= len(label_files) |
| 91 | + |
| 92 | +if mapNs is None: |
| 93 | + mapNs = [0] * len(label_files) |
| 94 | +else: |
| 95 | + mapNs *= len(label_files) |
| 96 | + |
| 97 | + |
| 98 | +### Begin ### |
| 99 | + |
| 100 | +# Read surface |
| 101 | +surf_gii = nib.load(surf_file) |
| 102 | + |
| 103 | +coords_darray = surf_gii.get_arrays_from_intent('NIFTI_INTENT_POINTSET') |
| 104 | +if len(coords_darray) != 1: |
| 105 | + raise ValueError('Surface must contain exactly one pointset array') |
| 106 | +coords_darray = coords_darray[0] |
| 107 | +nVtcs_surf = coords_darray.dims[0] |
| 108 | + |
| 109 | +triangle_darray = surf_gii.get_arrays_from_intent('NIFTI_INTENT_TRIANGLE') |
| 110 | +if len(triangle_darray) != 1: |
| 111 | + raise ValueError('Surface must contain exactly one triangle array') |
| 112 | +triangle_darray = triangle_darray[0] |
| 113 | +triangle_darray.coordsys = None # only valid for pointset arrays |
| 114 | + |
| 115 | +# Pre-allocate array for padded labels |
| 116 | +pad_label_data = np.zeros(nVtcs_surf, dtype=bool) |
| 117 | + |
| 118 | +# Loop labels |
| 119 | +for i, label_file in enumerate(label_files): |
| 120 | + these_label_ids = label_ids[i] |
| 121 | + mapN = mapNs[i] |
| 122 | + |
| 123 | + # Open handle to label |
| 124 | + label_img = nib.load(label_file) |
| 125 | + |
| 126 | + # Handle CIFTI labels |
| 127 | + if isinstance(label_img, nib.Cifti2Image): |
| 128 | + # Need hemisphere for CIFTIs |
| 129 | + if hemi is None: |
| 130 | + raise ValueError('Must specify hemisphere for CIFTI labels') |
| 131 | + |
| 132 | + # Read label & data |
| 133 | + label_data = label_img.get_fdata()[mapN] |
| 134 | + |
| 135 | + # Convert label ID to numeric if necessary |
| 136 | + if these_label_ids is not None: |
| 137 | + for j, ID in enumerate(these_label_ids): |
| 138 | + try: |
| 139 | + these_label_ids[j] = int(ID) |
| 140 | + except ValueError: |
| 141 | + ax0 = label_img.header.get_axis(0) |
| 142 | + if not isinstance(ax0, nib.cifti2.LabelAxis): |
| 143 | + raise ValueError( |
| 144 | + 'Label ID names are only supported for ' \ |
| 145 | + 'CIFTI dlabels. Use integer value instead.' |
| 146 | + ) |
| 147 | + matches = [k for k,v in ax0.label[mapN].items() if v[0] == ID] |
| 148 | + if len(matches) != 1: |
| 149 | + raise ValueError(f"No matches in {label_file} for label '{ID}'") |
| 150 | + these_label_ids[j] = matches[0] |
| 151 | + |
| 152 | + # Get brain models info for hemisphere |
| 153 | + ax1 = label_img.header.get_axis(1) |
| 154 | + slice_, model = get_struct_info(hemi_struct, ax1) |
| 155 | + nVtcs_label = ax1.nvertices[hemi_struct] |
| 156 | + if nVtcs_surf != nVtcs_label: |
| 157 | + raise Exception(('Number of vertices in label ({}) does not ' \ |
| 158 | + + 'match number of vertices on surface ({})') \ |
| 159 | + .format(nVtcs_label, nVtcs_surf)) |
| 160 | + |
| 161 | + # Pad label to full set of vertices, add to array |
| 162 | + if these_label_ids is None: |
| 163 | + isLabel = label_data[slice_].astype(bool) |
| 164 | + else: |
| 165 | + isLabel = np.isin(label_data[slice_], these_label_ids) |
| 166 | + pad_label_data[model.vertex] |= isLabel |
| 167 | + |
| 168 | + # Handle GIFTI labels |
| 169 | + elif isinstance(label_img, nib.GiftiImage): |
| 170 | + # Get label data |
| 171 | + label_data = label_img.darrays[mapN].data |
| 172 | + nVtcs_label = len(label_data) |
| 173 | + if nVtcs_surf != nVtcs_label: |
| 174 | + raise Exception(('Number of vertices in label ({}) does not ' \ |
| 175 | + + 'match number of vertices on surface ({})') \ |
| 176 | + .format(nVtcs_label, nVtcs_surf)) |
| 177 | + |
| 178 | + # Check label IDs |
| 179 | + for j, ID in enumerate(these_label_ids): |
| 180 | + try: |
| 181 | + these_label_ids[j] = int(ID) |
| 182 | + except ValueError: |
| 183 | + raise ValueError( |
| 184 | + 'Label ID names are only supported for ' \ |
| 185 | + 'CIFTI dlabels. Use integer value instead.' |
| 186 | + ) |
| 187 | + |
| 188 | + # Add to array |
| 189 | + pad_label_data |= np.isin(label_data[slice_], these_label_ids) |
| 190 | + |
| 191 | + else: |
| 192 | + raise TypeError('Label must be CIFTI or GIFTI') |
| 193 | + |
| 194 | +# Convert to darray |
| 195 | +label_darray = nib.gifti.GiftiDataArray( |
| 196 | + pad_label_data.astype(np.float32), intent='NIFTI_INTENT_SHAPE', |
| 197 | + datatype='NIFTI_TYPE_FLOAT32' |
| 198 | + ) |
| 199 | +label_darray.coordsys = None # only valid for pointset arrays |
| 200 | + |
| 201 | +# Create new GIFTI & save |
| 202 | +new_gii = nib.GiftiImage( |
| 203 | + header=surf_gii.header, extra=surf_gii.extra, meta=surf_gii.meta, |
| 204 | + darrays=[coords_darray, triangle_darray, label_darray] |
| 205 | + ) |
| 206 | +new_gii.to_filename(outfile) |
0 commit comments