Error creating a Freesurfer annotation file with nibabel

I’m hoping to create a binary annotation file using Nibabel’s write_annot function (version 3.2.1). However, I keep getting this bug. A reproducible example is below:


import numpy as np
import nibabel as nib

# create binary mask
n_vertices = 10242
indices = np.random.randint(0, n_vertices, (50,))
mask = np.zeros((n_vertices), dtype=int)
mask[indices] = 1

ctab = np.array([[255, 255, 255, 0]])
names = ['mask']

nib.freesurfer.write_annot('mask.annot', labels=mask, ctab=ctab, names=names)

This produces:

IndexError                                Traceback (most recent call last)
<ipython-input-91-36733fdfa0f8> in <module>
     10 names = ['mask']
     11 
---> 12 nib.freesurfer.write_annot('mask.annot', labels=mask, ctab=ctab, names=names)

~/miniconda3/lib/python3.8/site-packages/nibabel/freesurfer/io.py in write_annot(filepath, labels, ctab, names, fill_ctab)
    542 
    543         # convert labels into coded CLUT values
--> 544         clut_labels = ctab[:, -1][labels]
    545         clut_labels[np.where(labels == -1)] = 0
    546 

IndexError: index 1 is out of bounds for axis 0 with size 1

I get the same error when fill_ctab=False and len(ctab)=5 (i.e. added label id to ctab).

Based on the error, ctab is being indexed with the labels. Not sure if this is a bug or something I’m doing wrong.

Thoughts? Thanks!

Hey @danjgale!

Your ctab array needs to have two rows (and your names list also nees to have two entries—though that isn’t enforced on file creation, the file will not be malformed and you won’t be able to load it).

You need a marker for the background value (0, in your case) in addition to the mask value:

import numpy as np
import nibabel as nib

# create binary mask
n_vertices = 10242
indices = np.random.randint(0, n_vertices, (50,))
mask = np.zeros((n_vertices), dtype=int)
mask[indices] = 1

ctab = np.array([[25, 25, 25, 0], [255, 255, 255, 255]])
names = ['background', 'mask']

nib.freesurfer.write_annot('mask.annot', labels=mask, ctab=ctab, names=names)

(N.B. If you provide an entry in ctab that is all zeros ([0, 0, 0, 0]) the values for your label array will get saved as -1 and not 0! Make sure your “zero” entry has at least some positive values.)

2 Likes

Amazing, thank you!

RE: your note. Is there a reason why the values get saved as -1 rather than their original value?

From what I understand, it’s sort of a side effect of how annotation files are saved to disk. The full label array itself isn’t saved in the file, but rather the color table look-up values are (these are the values in the fifth column of the ctab array).

These look-up values are calculated via: (B * 256^2) + (G * 256) + (R), so when all of R, G, and B are 0, the look-up value is also 0. For some reason, when reading an annotation file that has a color table look-up value of 0, it gets interpreted as “no label” rather than “label = 0” (I am unclear if this is a FreeSurfer-specified thing or just a nibabel thing—I assume the former). Because of this, nibabel assigns it a value of -1 (see: nibabel/io.py at 62aea04248e70d7c4529954ca41685d7f75a0b1e · nipy/nibabel · GitHub).

Basically: I’m not too clear on exactly why, but it seems to be purposeful rather than accidental :sweat_smile:

1 Like
2 Likes

Hi @rmarkello & @danjgale & @effigies

I am actually having a similar issue.

import numpy as np
from nibabel import freesurfer
from glob import glob

lh_labels= glob('*lh*.label')

# initialize variables
lh_vertices = []
# initialize names list starting with 'unknown'
names=['background']

for ii in lh_labels:
    lh_vertices.append(freesurfer.read_labels(ii))
    names.append(os.path.basename(ii.split('.',1)[0]))


lh_labels_array=np.concatente(lh_vertices)

# load in long version of li 2016 parcellation including all 116 ROIs
ctab_long=np.loadtxt(path_to_rgb, dtype='int')

# create ctab with the appropriate length
names_len=len(names)

ctab=np.delete(ctab_long, slice(names_len,116),0)

print('writing LH annotation file')
freesurfer.write_annot(outpath + 'lh.li2019_parc.annot', labels=lh_labels_array, ctab=ctab, names=names)

However, I get the same error as @danjgale.

IndexError                                Traceback (most recent call last)
<ipython-input-31-dab2f8c108c2> in <module>
----> 1 freesurfer.write_annot('lh.li2019_parc.annot', labels=lh_labels_array, ctab=ctab, names=names)
c:\programdata\miniconda3\lib\site-packages\nibabel\freesurfer\io.py in write_annot(filepath, labels, ctab, names, fill_ctab)
    542
    543         # convert labels into coded CLUT values
--> 544         clut_labels = ctab[:, -1][labels]
    545         clut_labels[np.where(labels == -1)] = 0
    546

IndexError: index 8505 is out of bounds for axis 0 with size 51

If I print the variables and their shapes I get the following:

In [35]: len(names)
Out[35]: 51

In [27]: names
Out[27]:
['background',
 'Net_10_ROIs_1_lh_native_dil2',
 'Net_11_ROIs_1_lh_native_dil2',
 'Net_12_ROIs_1_lh_native_dil2',
 'Net_12_ROIs_2_lh_native_dil2',
 'Net_13_ROIs_1_lh_native_dil2',
 .....


In [36]: ctab.shape
Out[36]: (51, 4)

In [32]: ctab
Out[32]:
array([[255, 255, 255,   0],
       [ 25,   5,  25, 255],
       [ 25, 100,  40, 255],
       [ 70, 130, 180, 255],
       ....

Would really appreciate any advice or help.