12
12
from ..openers import Opener
13
13
14
14
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
+
15
23
def _fread3 (fobj ):
16
24
"""Read a 3-byte int from an open binary file object
17
25
@@ -73,6 +81,26 @@ def _read_volume_info(fobj):
73
81
return volume_info
74
82
75
83
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
+
76
104
def read_geometry (filepath , read_metadata = False , read_stamp = False ):
77
105
"""Read a triangular format Freesurfer surface mesh.
78
106
@@ -296,7 +324,18 @@ def write_morph_data(file_like, values, fnum=0):
296
324
297
325
298
326
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
300
339
301
340
Parameters
302
341
----------
@@ -314,55 +353,38 @@ def read_annot(filepath, orig_ids=False):
314
353
to any label and orig_ids=False, its id will be set to -1.
315
354
ctab : ndarray, shape (n_labels, 5)
316
355
RGBA + label id colortable array.
317
- names : list of str
356
+ names : list of str (python 2), list of bytes (python 3)
318
357
The names of the labels. The length of the list is n_labels.
319
358
"""
320
359
with open (filepath , "rb" ) as fobj :
321
- dt = ">i4"
360
+ dt = _ANNOT_DT
361
+
362
+ # number of vertices
322
363
vnum = np .fromfile (fobj , dt , 1 )[0 ]
364
+
365
+ # vertex ids + annotation values
323
366
data = np .fromfile (fobj , dt , vnum * 2 ).reshape (vnum , 2 )
324
367
labels = data [:, 1 ]
325
368
369
+ # is there a color table?
326
370
ctab_exists = np .fromfile (fobj , dt , 1 )[0 ]
327
371
if not ctab_exists :
328
372
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
329
377
n_entries = np .fromfile (fobj , dt , 1 )[0 ]
378
+
379
+ # We've got an old-format .annot file.
330
380
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
345
383
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 ])
366
388
367
389
if not orig_ids :
368
390
ord = np .argsort (ctab [:, - 1 ])
@@ -372,11 +394,104 @@ def read_annot(filepath, orig_ids=False):
372
394
return labels , ctab , names
373
395
374
396
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.
377
491
378
492
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
380
495
381
496
Parameters
382
497
----------
@@ -388,18 +503,31 @@ def write_annot(filepath, labels, ctab, names):
388
503
RGBA + label id colortable array.
389
504
names : list of str
390
505
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.
391
511
"""
392
512
with open (filepath , "wb" ) as fobj :
393
- dt = ">i4"
513
+ dt = _ANNOT_DT
394
514
vnum = len (labels )
395
515
396
516
def write (num , dtype = dt ):
397
517
np .array ([num ]).astype (dtype ).tofile (fobj )
398
518
399
519
def write_string (s ):
520
+ s = (s if isinstance (s , bytes ) else s .encode ()) + b'\x00 '
400
521
write (len (s ))
401
522
write (s , dtype = '|S%d' % len (s ))
402
523
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
+
403
531
# vtxct
404
532
write (vnum )
405
533
0 commit comments