|
| 1 | +Labels |
| 2 | +====== |
| 3 | + |
| 4 | +Surfa provides utilities for working with image label mappings. Label mappings |
| 5 | +store information to correlate integer values in the image with a label name and |
| 6 | +RGBA color representation in an image viewer. The classes that surfa provides |
| 7 | +for working with labels are primarily built on top of dictionaries. |
| 8 | + |
| 9 | +**LabelLookup** objects hold the label information in an ordered dictionary, |
| 10 | +where each key is the int corresponding to the label value in the image, and each value is a `LabelElement`. |
| 11 | + |
| 12 | +**LabelElement** objects are mutable objects with two fields, `name` and `color` |
| 13 | +- `name` is a string representing the label name |
| 14 | +- `color` is a 4 element list of integer values representing the RGBA values |
| 15 | + |
| 16 | +## Label Basics |
| 17 | + |
| 18 | +`LabelLookup` objects can be instantiated by loading a FreeSurfer color lookup |
| 19 | +table file or defined manually by the user. There are also some utilities for |
| 20 | +creating `LabelLookup` objects representing label mapping commonly used in |
| 21 | +FreeSurfer. |
| 22 | + |
| 23 | +```python |
| 24 | +>>> import surfa as sf |
| 25 | + |
| 26 | +# instantiate `LabelLookup` from a file |
| 27 | +>>> labels_from_file = sf.load_label_lookup('/usr/local/freesurfer/7.4.1/luts/FreeSurferColorLUT.txt') |
| 28 | + |
| 29 | +# instantiate from a commonly used label mapping (Destrieux cortical atlas) |
| 30 | +>>> destrieux_labels = sf.freesurfer.destrieux() |
| 31 | + |
| 32 | +# instantiate manually |
| 33 | +>>> manual_labels = sf.LabelLookup() |
| 34 | +>>> manual_labels[0] = ('Unknown', [0,0,0,0]) |
| 35 | +>>> manual_labels[24] = ('CSF', [255,255,255,0]) |
| 36 | +>>> manual_labels[100] = ('CSF-Ventricle', [128,128,128,0]) |
| 37 | + |
| 38 | +>>> manual_labels[24].name |
| 39 | +'CSF' |
| 40 | +>>> manual_labels[24].color |
| 41 | +array([255., 255., 255., 0.]) |
| 42 | +``` |
| 43 | + |
| 44 | +Labels can be accessed by indexing the label value in the `LabelLookup`. The |
| 45 | +index of a label can be searched for by passing a string to the `search` method. |
| 46 | +By default the search is greedy and not case sensitive, this can be changed to |
| 47 | +an exact match via the `exact` arg. |
| 48 | + |
| 49 | +```python |
| 50 | +# greedy search |
| 51 | +>>> manual_labels.search('CSF') |
| 52 | +[24, 100] |
| 53 | + |
| 54 | +# exact search |
| 55 | +>>> manual_labels.search('CSF',exact=True) |
| 56 | +24 |
| 57 | +``` |
| 58 | + |
| 59 | +Subsets of a `LabelLookup` can be extracted into `LabelLookup` objects if |
| 60 | +working with the full label mapping contains lots of unused labels. |
| 61 | + |
| 62 | +```python |
| 63 | +>>> import numpy as np |
| 64 | + |
| 65 | +# load an aseg, find unique labels |
| 66 | +>>> aseg = sf.load_volume('aseg.mgz') |
| 67 | +>>> aseg_label_indices = np.unique(aseg.data) |
| 68 | +>>> aseg_label_indices |
| 69 | +array([ 0, 2, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, |
| 70 | + 16, 17, 18, 24, 26, 28, 30, 31, 41, 42, 43, 44, 46, |
| 71 | + 47, 49, 50, 51, 52, 53, 54, 58, 60, 62, 63, 72, 77, |
| 72 | + 85, 251, 252, 253, 254, 255], dtype=int32) |
| 73 | + |
| 74 | +>>> aseg_label_lookup = sf.freesurfer.labels().extract(aseg_label_indices) |
| 75 | + |
| 76 | +# sanity check |
| 77 | +>>> np.array([*aseg_label_lookup.keys()], dtype='int32') == aseg_label_indices |
| 78 | +array([ True, True, True, True, True, True, True, True, True, |
| 79 | + True, True, True, True, True, True, True, True, True, |
| 80 | + True, True, True, True, True, True, True, True, True, |
| 81 | + True, True, True, True, True, True, True, True, True, |
| 82 | + True, True, True, True, True, True, True, True, True]) |
| 83 | + |
| 84 | +>>> aseg_label_lookup |
| 85 | +0 Unknown 0, 0, 0, 1.00 |
| 86 | +2 Left-Cerebral-White-Matter 245, 245, 245, 1.00 |
| 87 | +3 Left-Cerebral-Cortex 205, 62, 78, 1.00 |
| 88 | +4 Left-Lateral-Ventricle 120, 18, 134, 1.00 |
| 89 | +5 Left-Inf-Lat-Vent 196, 58, 250, 1.00 |
| 90 | +... |
| 91 | +85 Optic-Chiasm 234, 169, 30, 1.00 |
| 92 | +251 CC_Posterior 0, 0, 64, 1.00 |
| 93 | +252 CC_Mid_Posterior 0, 0, 112, 1.00 |
| 94 | +253 CC_Central 0, 0, 160, 1.00 |
| 95 | +254 CC_Mid_Anterior 0, 0, 208, 1.00 |
| 96 | +255 CC_Anterior 0, 0, 255, 1.00 |
| 97 | +``` |
| 98 | + |
| 99 | +## Label Recoding |
| 100 | + |
| 101 | +Often times, label indices do not match up between label lookups, especially in |
| 102 | +the case we are recoding labels to merge classes. The `LabelRecoder` class |
| 103 | +provides the functionality to do this. |
| 104 | + |
| 105 | +The `LabelRecoder` class has two fields: |
| 106 | + |
| 107 | +- `mapping` a dictionary where the keys are the indices of the labels in the |
| 108 | +current `LabelLookup`, and the values are the index to recode it to |
| 109 | +- `target_labels` a `LabelLookup` that defines the target label names/colors. |
| 110 | +Can be `None` |
| 111 | + |
| 112 | +> [!NOTE] |
| 113 | +> The `target_labels` field will be assigned to the `labels` field of a label |
| 114 | +> volume that is recoded using a `LabelRecoder`. If this field is `None`, then |
| 115 | +> the recoded segmentation volume will have no label mapping, however, that can |
| 116 | +> be assigned to after the recoded segmentation volume is created |
| 117 | +
|
| 118 | +Some predefined `LabelLookup` and `LabelRecoder` objects can be found under |
| 119 | +`surfa.freesurfer` |
| 120 | + |
| 121 | +```python |
| 122 | +# recode the aseg volume labels to the tissue type |
| 123 | +>>> recoder = sf.freesurfer.tissue_type_recoder() |
| 124 | +>>> seg_tissue_types = sf.labels.recode(aseg,recoder) |
| 125 | +>>> seg_tissue_types.labels |
| 126 | +0 Unknown 0, 0, 0, 1.00 |
| 127 | +1 Cortex 205, 62, 78, 1.00 |
| 128 | +2 Subcortical-Gray-Matter 230, 148, 34, 1.00 |
| 129 | +3 White-Matter 245, 245, 245, 1.00 |
| 130 | +4 CSF 120, 18, 134, 1.00 |
| 131 | +5 Head 150, 150, 200, 1.00 |
| 132 | +6 Lesion 255, 165, 0, 1.00 |
| 133 | +``` |
| 134 | + |
| 135 | +## Label Metrics |
| 136 | + |
| 137 | +There are also some utilities for calculating the dice score and jaccard |
| 138 | +coefficients for two sets of label volumes. By default both will compute the |
| 139 | +metric for all unique labels found in the label volumes, or the user can specify |
| 140 | +a list of label indices to compute the metrics for. |
| 141 | + |
| 142 | +```python |
| 143 | +# load another segmentation to compare to the aseg |
| 144 | +>>> seg = sf.load_volume('aseg_2.mgz') |
| 145 | + |
| 146 | +>>> target_labels = [2,3,4,255] |
| 147 | +>>> sf.labels.dice(aseg,seg, target_labels) |
| 148 | +{2: 0.5299304443534139, 3: 0.42780355492851396, 4: 0.29737467850730337, 255: 0.21085134539038378} |
| 149 | +>>> sf.labels.jaccard(aseg,seg, target_labels) |
| 150 | +{2: 0.3604798441801161, 3: 0.2721056622851365, 4: 0.1746565581713504, 255: 0.1178500986193294} |
| 151 | +``` |
0 commit comments