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
@@ -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+
76104def 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
298326def 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