1212from ..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+
1523def _fread3 (fobj ):
1624 """Read a 3-byte int from an open binary file object
1725
@@ -74,8 +82,10 @@ def _read_volume_info(fobj):
7482
7583
7684def _pack_rgba (rgba ):
77- """Used by :meth:`read_annot` and :meth:`write_annot` to pack an RGBA
78- sequence into a single integer.
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 Freesuerfer `.annot` file.
7989
8090 Parameters
8191 ----------
@@ -86,7 +96,7 @@ def _pack_rgba(rgba):
8696 Returns
8797 -------
8898
89- out : ndarray, shape (n, )
99+ out : ndarray, shape (n, 1 )
90100 Annotation values for each colour.
91101 """
92102 bitshifts = 2 ** np .array ([[0 ], [8 ], [16 ], [24 ]], dtype = rgba .dtype )
@@ -316,7 +326,20 @@ def write_morph_data(file_like, values, fnum=0):
316326
317327
318328def read_annot (filepath , orig_ids = False ):
319- """Read in a Freesurfer annotation from a .annot file.
329+ """Read in a Freesurfer annotation from a `.annot` file.
330+
331+ An `.annot` file contains a sequence of vertices with a label (also known
332+ as an "annotation value") associated with each vertex, and then a sequence
333+ of colours corresponding to each label.
334+
335+ The colour table itself may be stored in either an "old-style" format, or
336+ a "new-style" format - the :func:`_read_annot_ctab_old_format` and
337+ :func:`_read_annot_ctab_new_format` functions are respectively used to
338+ read in the colour table.
339+
340+ See:
341+ https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles#Annotation
342+ https://github.com/freesurfer/freesurfer/blob/dev/matlab/read_annotation.m
320343
321344 Parameters
322345 ----------
@@ -336,11 +359,10 @@ def read_annot(filepath, orig_ids=False):
336359 RGBA + label id colortable array.
337360 names : list of str
338361 The names of the labels. The length of the list is n_labels.
362+
339363 """
340364 with open (filepath , "rb" ) as fobj :
341- # all data (apart from strings) in an .annot file is stored as
342- # big-endian int32
343- dt = ">i4"
365+ dt = _ANNOT_DT
344366
345367 # number of vertices
346368 vnum = np .fromfile (fobj , dt , 1 )[0 ]
@@ -355,50 +377,16 @@ def read_annot(filepath, orig_ids=False):
355377 raise Exception ('Color table not found in annotation file' )
356378
357379 # in old-format files, the next field will contain the number of
358- # entries in the colour table. In new-format files, this will be
380+ # entries in the colour table. In new-format files, this must be
359381 # equal to -2
360382 n_entries = np .fromfile (fobj , dt , 1 )[0 ]
361383
362384 # We've got an old-format .annot file.
363385 if n_entries > 0 :
364-
365- # orig_tab string length + string
366- length = np .fromfile (fobj , dt , 1 )[0 ]
367- orig_tab = np .fromfile (fobj , '>c' , length )
368- orig_tab = orig_tab [:- 1 ]
369- names = list ()
370- ctab = np .zeros ((n_entries , 5 ), dt )
371- for i in xrange (n_entries ):
372- # structure name length + string
373- name_length = np .fromfile (fobj , dt , 1 )[0 ]
374- name = np .fromfile (fobj , "|S%d" % name_length , 1 )[0 ]
375- names .append (name )
376- # read RGBA for this entry
377- ctab [i , :4 ] = np .fromfile (fobj , dt , 4 )
386+ ctab , names = _read_annot_ctab_old_format (fobj , n_entries )
378387 # We've got a new-format .annot file
379388 else :
380- # file version number
381- ctab_version = - n_entries
382- if ctab_version != 2 :
383- raise Exception ('Color table version not supported' )
384- # maximum LUT index present in the file
385- max_index = np .fromfile (fobj , dt , 1 )[0 ]
386- ctab = np .zeros ((max_index , 5 ), dt )
387- # orig_tab string length + string
388- length = np .fromfile (fobj , dt , 1 )[0 ]
389- np .fromfile (fobj , "|S%d" % length , 1 )[0 ] # Orig table path
390- # number of LUT entries present in the file
391- entries_to_read = np .fromfile (fobj , dt , 1 )[0 ]
392- names = list ()
393- for _ in xrange (entries_to_read ):
394- # index of this entry
395- idx = np .fromfile (fobj , dt , 1 )[0 ]
396- # structure name length + string
397- name_length = np .fromfile (fobj , dt , 1 )[0 ]
398- name = np .fromfile (fobj , "|S%d" % name_length , 1 )[0 ]
399- names .append (name )
400- # RGBA
401- ctab [idx , :4 ] = np .fromfile (fobj , dt , 4 )
389+ ctab , names = _read_annot_ctab_new_format (fobj , - n_entries )
402390
403391 # generate annotation values for each LUT entry
404392 ctab [:, [4 ]] = _pack_rgba (ctab [:, :4 ])
@@ -414,11 +402,101 @@ def read_annot(filepath, orig_ids=False):
414402 return labels , ctab , names
415403
416404
405+ def _read_annot_ctab_old_format (fobj , n_entries ):
406+ """Read in an old-style Freesurfer colour table from `fobj`.
407+
408+ This function is used by :func:`read_annot`.
409+
410+ Parameters
411+ ----------
412+
413+ fobj : file-like
414+ Open file handle to a Freesurfer `.annot` file, with seek point
415+ at the beginning of the colour table data.
416+ n_entries : int
417+ Number of entries in the colour table.
418+
419+ Returns
420+ -------
421+
422+ ctab : ndarray, shape (n_entries, 5)
423+ RGBA colortable array - the last column contains all zeros.
424+ names : list of str
425+ The names of the labels. The length of the list is n_entries.
426+ """
427+ dt = _ANNOT_DT
428+
429+ # orig_tab string length + string
430+ length = np .fromfile (fobj , dt , 1 )[0 ]
431+ orig_tab = np .fromfile (fobj , '>c' , length )
432+ orig_tab = orig_tab [:- 1 ]
433+ names = list ()
434+ ctab = np .zeros ((n_entries , 5 ), dt )
435+ for i in xrange (n_entries ):
436+ # structure name length + string
437+ name_length = np .fromfile (fobj , dt , 1 )[0 ]
438+ name = np .fromfile (fobj , "|S%d" % name_length , 1 )[0 ]
439+ names .append (name )
440+ # read RGBA for this entry
441+ ctab [i , :4 ] = np .fromfile (fobj , dt , 4 )
442+
443+ return ctab , names
444+
445+
446+ def _read_annot_ctab_new_format (fobj , ctab_version ):
447+ """Read in a new-style Freesurfer colour table from `fobj`.
448+
449+ This function is used by :func:`read_annot`.
450+
451+ Parameters
452+ ----------
453+
454+ fobj : file-like
455+ Open file handle to a Freesurfer `.annot` file, with seek point
456+ at the beginning of the colour table data.
457+ ctab_version : int
458+ Colour table format version - must be equal to 2
459+
460+ Returns
461+ -------
462+
463+ ctab : ndarray, shape (n_labels, 5)
464+ RGBA colortable array - the last column contains all zeros.
465+ names : list of str
466+ The names of the labels. The length of the list is n_labels.
467+ """
468+ dt = _ANNOT_DT
469+ # This code works with a file version == 2, nothing else
470+ if ctab_version != 2 :
471+ raise Exception ('Unrecognised .annot file version (%i)' , ctab_version )
472+ # maximum LUT index present in the file
473+ max_index = np .fromfile (fobj , dt , 1 )[0 ]
474+ ctab = np .zeros ((max_index , 5 ), dt )
475+ # orig_tab string length + string
476+ length = np .fromfile (fobj , dt , 1 )[0 ]
477+ np .fromfile (fobj , "|S%d" % length , 1 )[0 ] # Orig table path
478+ # number of LUT entries present in the file
479+ entries_to_read = np .fromfile (fobj , dt , 1 )[0 ]
480+ names = list ()
481+ for _ in xrange (entries_to_read ):
482+ # index of this entry
483+ idx = np .fromfile (fobj , dt , 1 )[0 ]
484+ # structure name length + string
485+ name_length = np .fromfile (fobj , dt , 1 )[0 ]
486+ name = np .fromfile (fobj , "|S%d" % name_length , 1 )[0 ]
487+ names .append (name )
488+ # RGBA
489+ ctab [idx , :4 ] = np .fromfile (fobj , dt , 4 )
490+
491+ return ctab , names
492+
493+
417494def write_annot (filepath , labels , ctab , names , fill_ctab = True ):
418495 """Write out a Freesurfer annotation file.
419496
420497 See:
421498 https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles#Annotation
499+ https://github.com/freesurfer/freesurfer/blob/dev/matlab/write_annotation.m
422500
423501 Parameters
424502 ----------
@@ -430,14 +508,14 @@ def write_annot(filepath, labels, ctab, names, fill_ctab=True):
430508 RGBA + label id colortable array.
431509 names : list of str
432510 The names of the labels. The length of the list is n_labels.
433- fill_ctab : bool
511+ fill_ctab : {True, False} optional
434512 If True, the annotation values for each vertex are automatically
435513 generated. In this case, the provided `ctab` may have shape
436514 (n_labels, 4) or (n_labels, 5) - if the latter, the final column is
437515 ignored.
438516 """
439517 with open (filepath , "wb" ) as fobj :
440- dt = ">i4"
518+ dt = _ANNOT_DT
441519 vnum = len (labels )
442520
443521 def write (num , dtype = dt ):
0 commit comments