Skip to content

Commit 33767c9

Browse files
Merge pull request #592 from pauldmccarthy/fix_freesurfer_annot
MRG: Fix freesurfer `read_annot`, and enhance `write_annot` This PR comprises two sections, related to the nibabel.freesurfer.io.read_annot and write_annot functions. The first involves changes to read_annot, to fix some bugs, and the second involves enhancements to write_annot, to make saving .annot files a little easier.
2 parents 8312af6 + 60284ae commit 33767c9

File tree

2 files changed

+306
-42
lines changed

2 files changed

+306
-42
lines changed

nibabel/freesurfer/io.py

Lines changed: 169 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
from ..openers import Opener
1313

1414

15+
_ANNOT_DT = ">i4"
16+
"""Data type for Freesurfer `.annot` files.
17+
18+
Used by :func:`read_annot` and :func:`write_annot`. All data (apart from
19+
strings) in an `.annot` file is stored as big-endian int32.
20+
"""
21+
22+
1523
def _fread3(fobj):
1624
"""Read a 3-byte int from an open binary file object
1725
@@ -73,6 +81,26 @@ def _read_volume_info(fobj):
7381
return volume_info
7482

7583

84+
def _pack_rgba(rgba):
85+
"""Pack an RGBA sequence into a single integer.
86+
87+
Used by :func:`read_annot` and :func:`write_annot` to generate
88+
"annotation values" for a Freesurfer ``.annot`` file.
89+
90+
Parameters
91+
----------
92+
rgba : ndarray, shape (n, 4)
93+
RGBA colors
94+
95+
Returns
96+
-------
97+
out : ndarray, shape (n, 1)
98+
Annotation values for each color.
99+
"""
100+
bitshifts = 2 ** np.array([[0], [8], [16], [24]], dtype=rgba.dtype)
101+
return rgba.dot(bitshifts)
102+
103+
76104
def read_geometry(filepath, read_metadata=False, read_stamp=False):
77105
"""Read a triangular format Freesurfer surface mesh.
78106
@@ -296,7 +324,18 @@ def write_morph_data(file_like, values, fnum=0):
296324

297325

298326
def read_annot(filepath, orig_ids=False):
299-
"""Read in a Freesurfer annotation from a .annot file.
327+
"""Read in a Freesurfer annotation from a ``.annot`` file.
328+
329+
An ``.annot`` file contains a sequence of vertices with a label (also known
330+
as an "annotation value") associated with each vertex, and then a sequence
331+
of colors corresponding to each label.
332+
333+
Annotation file format versions 1 and 2 are supported, corresponding to
334+
the "old-style" and "new-style" color table layout.
335+
336+
See:
337+
* https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles#Annotation
338+
* https://github.com/freesurfer/freesurfer/blob/dev/matlab/read_annotation.m
300339
301340
Parameters
302341
----------
@@ -314,55 +353,38 @@ def read_annot(filepath, orig_ids=False):
314353
to any label and orig_ids=False, its id will be set to -1.
315354
ctab : ndarray, shape (n_labels, 5)
316355
RGBA + label id colortable array.
317-
names : list of str
356+
names : list of str (python 2), list of bytes (python 3)
318357
The names of the labels. The length of the list is n_labels.
319358
"""
320359
with open(filepath, "rb") as fobj:
321-
dt = ">i4"
360+
dt = _ANNOT_DT
361+
362+
# number of vertices
322363
vnum = np.fromfile(fobj, dt, 1)[0]
364+
365+
# vertex ids + annotation values
323366
data = np.fromfile(fobj, dt, vnum * 2).reshape(vnum, 2)
324367
labels = data[:, 1]
325368

369+
# is there a color table?
326370
ctab_exists = np.fromfile(fobj, dt, 1)[0]
327371
if not ctab_exists:
328372
raise Exception('Color table not found in annotation file')
373+
374+
# in old-format files, the next field will contain the number of
375+
# entries in the color table. In new-format files, this must be
376+
# equal to -2
329377
n_entries = np.fromfile(fobj, dt, 1)[0]
378+
379+
# We've got an old-format .annot file.
330380
if n_entries > 0:
331-
length = np.fromfile(fobj, dt, 1)[0]
332-
orig_tab = np.fromfile(fobj, '>c', length)
333-
orig_tab = orig_tab[:-1]
334-
335-
names = list()
336-
ctab = np.zeros((n_entries, 5), np.int)
337-
for i in xrange(n_entries):
338-
name_length = np.fromfile(fobj, dt, 1)[0]
339-
name = np.fromfile(fobj, "|S%d" % name_length, 1)[0]
340-
names.append(name)
341-
ctab[i, :4] = np.fromfile(fobj, dt, 4)
342-
ctab[i, 4] = (ctab[i, 0] + ctab[i, 1] * (2 ** 8) +
343-
ctab[i, 2] * (2 ** 16) +
344-
ctab[i, 3] * (2 ** 24))
381+
ctab, names = _read_annot_ctab_old_format(fobj, n_entries)
382+
# We've got a new-format .annot file
345383
else:
346-
ctab_version = -n_entries
347-
if ctab_version != 2:
348-
raise Exception('Color table version not supported')
349-
n_entries = np.fromfile(fobj, dt, 1)[0]
350-
ctab = np.zeros((n_entries, 5), np.int)
351-
length = np.fromfile(fobj, dt, 1)[0]
352-
np.fromfile(fobj, "|S%d" % length, 1)[0] # Orig table path
353-
entries_to_read = np.fromfile(fobj, dt, 1)[0]
354-
names = list()
355-
for i in xrange(entries_to_read):
356-
np.fromfile(fobj, dt, 1)[0] # Structure
357-
name_length = np.fromfile(fobj, dt, 1)[0]
358-
name = np.fromfile(fobj, "|S%d" % name_length, 1)[0]
359-
names.append(name)
360-
ctab[i, :4] = np.fromfile(fobj, dt, 4)
361-
ctab[i, 4] = (ctab[i, 0] + ctab[i, 1] * (2 ** 8) +
362-
ctab[i, 2] * (2 ** 16))
363-
ctab[:, 3] = 255
364-
365-
labels = labels.astype(np.int)
384+
ctab, names = _read_annot_ctab_new_format(fobj, -n_entries)
385+
386+
# generate annotation values for each LUT entry
387+
ctab[:, [4]] = _pack_rgba(ctab[:, :4])
366388

367389
if not orig_ids:
368390
ord = np.argsort(ctab[:, -1])
@@ -372,11 +394,104 @@ def read_annot(filepath, orig_ids=False):
372394
return labels, ctab, names
373395

374396

375-
def write_annot(filepath, labels, ctab, names):
376-
"""Write out a Freesurfer annotation file.
397+
def _read_annot_ctab_old_format(fobj, n_entries):
398+
"""Read in an old-style Freesurfer color table from `fobj`.
399+
400+
This function is used by :func:`read_annot`.
401+
402+
Parameters
403+
----------
404+
405+
fobj : file-like
406+
Open file handle to a Freesurfer `.annot` file, with seek point
407+
at the beginning of the color table data.
408+
n_entries : int
409+
Number of entries in the color table.
410+
411+
Returns
412+
-------
413+
414+
ctab : ndarray, shape (n_entries, 5)
415+
RGBA colortable array - the last column contains all zeros.
416+
names : list of str
417+
The names of the labels. The length of the list is n_entries.
418+
"""
419+
assert hasattr(fobj, 'read')
420+
421+
dt = _ANNOT_DT
422+
# orig_tab string length + string
423+
length = np.fromfile(fobj, dt, 1)[0]
424+
orig_tab = np.fromfile(fobj, '>c', length)
425+
orig_tab = orig_tab[:-1]
426+
names = list()
427+
ctab = np.zeros((n_entries, 5), dt)
428+
for i in xrange(n_entries):
429+
# structure name length + string
430+
name_length = np.fromfile(fobj, dt, 1)[0]
431+
name = np.fromfile(fobj, "|S%d" % name_length, 1)[0]
432+
names.append(name)
433+
# read RGBA for this entry
434+
ctab[i, :4] = np.fromfile(fobj, dt, 4)
435+
436+
return ctab, names
437+
438+
439+
def _read_annot_ctab_new_format(fobj, ctab_version):
440+
"""Read in a new-style Freesurfer color table from `fobj`.
441+
442+
This function is used by :func:`read_annot`.
443+
444+
Parameters
445+
----------
446+
447+
fobj : file-like
448+
Open file handle to a Freesurfer `.annot` file, with seek point
449+
at the beginning of the color table data.
450+
ctab_version : int
451+
Color table format version - must be equal to 2
452+
453+
Returns
454+
-------
455+
456+
ctab : ndarray, shape (n_labels, 5)
457+
RGBA colortable array - the last column contains all zeros.
458+
names : list of str
459+
The names of the labels. The length of the list is n_labels.
460+
"""
461+
assert hasattr(fobj, 'read')
462+
463+
dt = _ANNOT_DT
464+
# This code works with a file version == 2, nothing else
465+
if ctab_version != 2:
466+
raise Exception('Unrecognised .annot file version (%i)', ctab_version)
467+
# maximum LUT index present in the file
468+
max_index = np.fromfile(fobj, dt, 1)[0]
469+
ctab = np.zeros((max_index, 5), dt)
470+
# orig_tab string length + string
471+
length = np.fromfile(fobj, dt, 1)[0]
472+
np.fromfile(fobj, "|S%d" % length, 1)[0] # Orig table path
473+
# number of LUT entries present in the file
474+
entries_to_read = np.fromfile(fobj, dt, 1)[0]
475+
names = list()
476+
for _ in xrange(entries_to_read):
477+
# index of this entry
478+
idx = np.fromfile(fobj, dt, 1)[0]
479+
# structure name length + string
480+
name_length = np.fromfile(fobj, dt, 1)[0]
481+
name = np.fromfile(fobj, "|S%d" % name_length, 1)[0]
482+
names.append(name)
483+
# RGBA
484+
ctab[idx, :4] = np.fromfile(fobj, dt, 4)
485+
486+
return ctab, names
487+
488+
489+
def write_annot(filepath, labels, ctab, names, fill_ctab=True):
490+
"""Write out a "new-style" Freesurfer annotation file.
377491
378492
See:
379-
https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles#Annotation
493+
* https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles#Annotation
494+
* https://github.com/freesurfer/freesurfer/blob/dev/matlab/write_annotation.m
380495
381496
Parameters
382497
----------
@@ -388,18 +503,31 @@ def write_annot(filepath, labels, ctab, names):
388503
RGBA + label id colortable array.
389504
names : list of str
390505
The names of the labels. The length of the list is n_labels.
506+
fill_ctab : {True, False} optional
507+
If True, the annotation values for each vertex are automatically
508+
generated. In this case, the provided `ctab` may have shape
509+
(n_labels, 4) or (n_labels, 5) - if the latter, the final column is
510+
ignored.
391511
"""
392512
with open(filepath, "wb") as fobj:
393-
dt = ">i4"
513+
dt = _ANNOT_DT
394514
vnum = len(labels)
395515

396516
def write(num, dtype=dt):
397517
np.array([num]).astype(dtype).tofile(fobj)
398518

399519
def write_string(s):
520+
s = (s if isinstance(s, bytes) else s.encode()) + b'\x00'
400521
write(len(s))
401522
write(s, dtype='|S%d' % len(s))
402523

524+
# Generate annotation values for each ctab entry
525+
if fill_ctab:
526+
ctab = np.hstack((ctab[:, :4], _pack_rgba(ctab[:, :4])))
527+
elif not np.array_equal(ctab[:, [4]], _pack_rgba(ctab[:, :4])):
528+
warnings.warn('Annotation values in {} will be incorrect'.format(
529+
filepath))
530+
403531
# vtxct
404532
write(vnum)
405533

0 commit comments

Comments
 (0)