Browse Source

Label: fixed color codes; added property hex_color_code; added script freesurfer-annotation-labels

Fabian Peter Hammerle 5 years ago
parent
commit
c9481a48cd

+ 16 - 0
README.md

@@ -0,0 +1,16 @@
+## Usage
+
+### List Labels in Annotation Files
+
+```sh
+$ freesurfer-annotation-labels tests/subjects/fabian/label/lh.aparc.annot
+index	color	name
+0	#190519	unknown
+1	#196428	bankssts
+2	#7d64a0	caudalanteriorcingulate
+3	#641900	caudalmiddlefrontal
+...
+33	#4614aa	temporalpole
+34	#9696c8	transversetemporal
+35	#ffc020	insula
+```

+ 133 - 338
examples/annotation_stats.ipynb

@@ -76,6 +76,8 @@
        "      <th>green</th>\n",
        "      <th>blue</th>\n",
        "      <th>transparency</th>\n",
+       "      <th>hex_color_code</th>\n",
+       "      <th>color_code</th>\n",
        "    </tr>\n",
        "  </thead>\n",
        "  <tbody>\n",
@@ -83,332 +85,70 @@
        "      <th>0</th>\n",
        "      <td>unknown</td>\n",
        "      <td>25</td>\n",
-       "      <td>25</td>\n",
        "      <td>5</td>\n",
+       "      <td>25</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#190519</td>\n",
        "      <td>0</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>1</th>\n",
        "      <td>bankssts</td>\n",
        "      <td>25</td>\n",
-       "      <td>40</td>\n",
        "      <td>100</td>\n",
+       "      <td>40</td>\n",
        "      <td>0</td>\n",
+       "      <td>#196428</td>\n",
+       "      <td>2647065</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2</th>\n",
        "      <td>caudalanteriorcingulate</td>\n",
        "      <td>125</td>\n",
-       "      <td>160</td>\n",
        "      <td>100</td>\n",
+       "      <td>160</td>\n",
        "      <td>0</td>\n",
+       "      <td>#7d64a0</td>\n",
+       "      <td>10511485</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>3</th>\n",
        "      <td>caudalmiddlefrontal</td>\n",
        "      <td>100</td>\n",
-       "      <td>0</td>\n",
        "      <td>25</td>\n",
        "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#641900</td>\n",
+       "      <td>6500</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4</th>\n",
        "      <td>corpuscallosum</td>\n",
        "      <td>120</td>\n",
-       "      <td>50</td>\n",
        "      <td>70</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>5</th>\n",
-       "      <td>cuneus</td>\n",
-       "      <td>220</td>\n",
-       "      <td>100</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>6</th>\n",
-       "      <td>entorhinal</td>\n",
-       "      <td>220</td>\n",
-       "      <td>10</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>7</th>\n",
-       "      <td>fusiform</td>\n",
-       "      <td>180</td>\n",
-       "      <td>140</td>\n",
-       "      <td>220</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>8</th>\n",
-       "      <td>inferiorparietal</td>\n",
-       "      <td>220</td>\n",
-       "      <td>220</td>\n",
-       "      <td>60</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>9</th>\n",
-       "      <td>inferiortemporal</td>\n",
-       "      <td>180</td>\n",
-       "      <td>120</td>\n",
-       "      <td>40</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>10</th>\n",
-       "      <td>isthmuscingulate</td>\n",
-       "      <td>140</td>\n",
-       "      <td>140</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>11</th>\n",
-       "      <td>lateraloccipital</td>\n",
-       "      <td>20</td>\n",
-       "      <td>140</td>\n",
-       "      <td>30</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>12</th>\n",
-       "      <td>lateralorbitofrontal</td>\n",
-       "      <td>35</td>\n",
-       "      <td>50</td>\n",
-       "      <td>75</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>13</th>\n",
-       "      <td>lingual</td>\n",
-       "      <td>225</td>\n",
-       "      <td>140</td>\n",
-       "      <td>140</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>14</th>\n",
-       "      <td>medialorbitofrontal</td>\n",
-       "      <td>200</td>\n",
-       "      <td>75</td>\n",
-       "      <td>35</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>15</th>\n",
-       "      <td>middletemporal</td>\n",
-       "      <td>160</td>\n",
-       "      <td>50</td>\n",
-       "      <td>100</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>16</th>\n",
-       "      <td>parahippocampal</td>\n",
-       "      <td>20</td>\n",
-       "      <td>60</td>\n",
-       "      <td>220</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>17</th>\n",
-       "      <td>paracentral</td>\n",
-       "      <td>60</td>\n",
-       "      <td>60</td>\n",
-       "      <td>220</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>18</th>\n",
-       "      <td>parsopercularis</td>\n",
-       "      <td>220</td>\n",
-       "      <td>140</td>\n",
-       "      <td>180</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>19</th>\n",
-       "      <td>parsorbitalis</td>\n",
-       "      <td>20</td>\n",
        "      <td>50</td>\n",
-       "      <td>100</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>20</th>\n",
-       "      <td>parstriangularis</td>\n",
-       "      <td>220</td>\n",
-       "      <td>20</td>\n",
-       "      <td>60</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>21</th>\n",
-       "      <td>pericalcarine</td>\n",
-       "      <td>120</td>\n",
-       "      <td>60</td>\n",
-       "      <td>100</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>22</th>\n",
-       "      <td>postcentral</td>\n",
-       "      <td>220</td>\n",
-       "      <td>20</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>23</th>\n",
-       "      <td>posteriorcingulate</td>\n",
-       "      <td>220</td>\n",
-       "      <td>220</td>\n",
-       "      <td>180</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>24</th>\n",
-       "      <td>precentral</td>\n",
-       "      <td>60</td>\n",
-       "      <td>220</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>25</th>\n",
-       "      <td>precuneus</td>\n",
-       "      <td>160</td>\n",
-       "      <td>180</td>\n",
-       "      <td>140</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>26</th>\n",
-       "      <td>rostralanteriorcingulate</td>\n",
-       "      <td>80</td>\n",
-       "      <td>140</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>27</th>\n",
-       "      <td>rostralmiddlefrontal</td>\n",
-       "      <td>75</td>\n",
-       "      <td>125</td>\n",
-       "      <td>50</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>28</th>\n",
-       "      <td>superiorfrontal</td>\n",
-       "      <td>20</td>\n",
-       "      <td>160</td>\n",
-       "      <td>220</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>29</th>\n",
-       "      <td>superiorparietal</td>\n",
-       "      <td>20</td>\n",
-       "      <td>140</td>\n",
-       "      <td>180</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>30</th>\n",
-       "      <td>superiortemporal</td>\n",
-       "      <td>140</td>\n",
-       "      <td>220</td>\n",
-       "      <td>220</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>31</th>\n",
-       "      <td>supramarginal</td>\n",
-       "      <td>80</td>\n",
-       "      <td>20</td>\n",
-       "      <td>160</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>32</th>\n",
-       "      <td>frontalpole</td>\n",
-       "      <td>100</td>\n",
-       "      <td>100</td>\n",
-       "      <td>0</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>33</th>\n",
-       "      <td>temporalpole</td>\n",
-       "      <td>70</td>\n",
-       "      <td>170</td>\n",
-       "      <td>20</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>34</th>\n",
-       "      <td>transversetemporal</td>\n",
-       "      <td>150</td>\n",
-       "      <td>200</td>\n",
-       "      <td>150</td>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>35</th>\n",
-       "      <td>insula</td>\n",
-       "      <td>255</td>\n",
-       "      <td>32</td>\n",
-       "      <td>192</td>\n",
        "      <td>0</td>\n",
+       "      <td>#784632</td>\n",
+       "      <td>3294840</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
        "</div>"
       ],
       "text/plain": [
-       "                        name  red  green  blue  transparency\n",
-       "0                    unknown   25     25     5             0\n",
-       "1                   bankssts   25     40   100             0\n",
-       "2    caudalanteriorcingulate  125    160   100             0\n",
-       "3        caudalmiddlefrontal  100      0    25             0\n",
-       "4             corpuscallosum  120     50    70             0\n",
-       "5                     cuneus  220    100    20             0\n",
-       "6                 entorhinal  220     10    20             0\n",
-       "7                   fusiform  180    140   220             0\n",
-       "8           inferiorparietal  220    220    60             0\n",
-       "9           inferiortemporal  180    120    40             0\n",
-       "10          isthmuscingulate  140    140    20             0\n",
-       "11          lateraloccipital   20    140    30             0\n",
-       "12      lateralorbitofrontal   35     50    75             0\n",
-       "13                   lingual  225    140   140             0\n",
-       "14       medialorbitofrontal  200     75    35             0\n",
-       "15            middletemporal  160     50   100             0\n",
-       "16           parahippocampal   20     60   220             0\n",
-       "17               paracentral   60     60   220             0\n",
-       "18           parsopercularis  220    140   180             0\n",
-       "19             parsorbitalis   20     50   100             0\n",
-       "20          parstriangularis  220     20    60             0\n",
-       "21             pericalcarine  120     60   100             0\n",
-       "22               postcentral  220     20    20             0\n",
-       "23        posteriorcingulate  220    220   180             0\n",
-       "24                precentral   60    220    20             0\n",
-       "25                 precuneus  160    180   140             0\n",
-       "26  rostralanteriorcingulate   80    140    20             0\n",
-       "27      rostralmiddlefrontal   75    125    50             0\n",
-       "28           superiorfrontal   20    160   220             0\n",
-       "29          superiorparietal   20    140   180             0\n",
-       "30          superiortemporal  140    220   220             0\n",
-       "31             supramarginal   80     20   160             0\n",
-       "32               frontalpole  100    100     0             0\n",
-       "33              temporalpole   70    170    20             0\n",
-       "34        transversetemporal  150    200   150             0\n",
-       "35                    insula  255     32   192             0"
+       "                      name  red  green  blue  transparency hex_color_code  \\\n",
+       "0                  unknown   25      5    25             0        #190519   \n",
+       "1                 bankssts   25    100    40             0        #196428   \n",
+       "2  caudalanteriorcingulate  125    100   160             0        #7d64a0   \n",
+       "3      caudalmiddlefrontal  100     25     0             0        #641900   \n",
+       "4           corpuscallosum  120     70    50             0        #784632   \n",
+       "\n",
+       "   color_code  \n",
+       "0           0  \n",
+       "1     2647065  \n",
+       "2    10511485  \n",
+       "3        6500  \n",
+       "4     3294840  "
       ]
      },
      "execution_count": 3,
@@ -419,9 +159,14 @@
    "source": [
     "import pandas\n",
     "\n",
-    "labels_frame = pandas.DataFrame((vars(label) for label in surface.annotation.labels),\n",
-    "                                columns=['index', 'name', 'red', 'green', 'blue', 'transparency'])\n",
-    "labels_frame.drop(columns=['index'])"
+    "label_frame = pandas.DataFrame((dict(color_code=label.color_code,\n",
+    "                                     hex_color_code=label.hex_color_code,\n",
+    "                                     **vars(label))\n",
+    "                                for label in surface.annotation.labels),\n",
+    "                               columns=['index', 'name', \n",
+    "                                        'red', 'green', 'blue', 'transparency', \n",
+    "                                        'hex_color_code', 'color_code'])\n",
+    "label_frame.drop(columns=['index']).head()"
    ]
   },
   {
@@ -450,8 +195,15 @@
        "  <thead>\n",
        "    <tr style=\"text-align: right;\">\n",
        "      <th></th>\n",
-       "      <th>annotation_value</th>\n",
+       "      <th>color_code</th>\n",
        "      <th>vertex_index</th>\n",
+       "      <th>index</th>\n",
+       "      <th>name</th>\n",
+       "      <th>red</th>\n",
+       "      <th>green</th>\n",
+       "      <th>blue</th>\n",
+       "      <th>transparency</th>\n",
+       "      <th>hex_color_code</th>\n",
        "    </tr>\n",
        "  </thead>\n",
        "  <tbody>\n",
@@ -459,38 +211,80 @@
        "      <th>0</th>\n",
        "      <td>6558940</td>\n",
        "      <td>0</td>\n",
+       "      <td>5</td>\n",
+       "      <td>cuneus</td>\n",
+       "      <td>220</td>\n",
+       "      <td>20</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#dc1464</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>1</th>\n",
        "      <td>6558940</td>\n",
        "      <td>1</td>\n",
+       "      <td>5</td>\n",
+       "      <td>cuneus</td>\n",
+       "      <td>220</td>\n",
+       "      <td>20</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#dc1464</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2</th>\n",
        "      <td>6558940</td>\n",
        "      <td>2</td>\n",
+       "      <td>5</td>\n",
+       "      <td>cuneus</td>\n",
+       "      <td>220</td>\n",
+       "      <td>20</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#dc1464</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>3</th>\n",
        "      <td>6558940</td>\n",
        "      <td>3</td>\n",
+       "      <td>5</td>\n",
+       "      <td>cuneus</td>\n",
+       "      <td>220</td>\n",
+       "      <td>20</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#dc1464</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4</th>\n",
        "      <td>6558940</td>\n",
        "      <td>4</td>\n",
+       "      <td>5</td>\n",
+       "      <td>cuneus</td>\n",
+       "      <td>220</td>\n",
+       "      <td>20</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>#dc1464</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
        "</div>"
       ],
       "text/plain": [
-       "   annotation_value  vertex_index\n",
-       "0           6558940             0\n",
-       "1           6558940             1\n",
-       "2           6558940             2\n",
-       "3           6558940             3\n",
-       "4           6558940             4"
+       "   color_code  vertex_index  index    name  red  green  blue  transparency  \\\n",
+       "0     6558940             0      5  cuneus  220     20   100             0   \n",
+       "1     6558940             1      5  cuneus  220     20   100             0   \n",
+       "2     6558940             2      5  cuneus  220     20   100             0   \n",
+       "3     6558940             3      5  cuneus  220     20   100             0   \n",
+       "4     6558940             4      5  cuneus  220     20   100             0   \n",
+       "\n",
+       "  hex_color_code  \n",
+       "0        #dc1464  \n",
+       "1        #dc1464  \n",
+       "2        #dc1464  \n",
+       "3        #dc1464  \n",
+       "4        #dc1464  "
       ]
      },
      "execution_count": 4,
@@ -501,9 +295,10 @@
    "source": [
     "import pandas\n",
     "\n",
-    "vertex_frame = pandas.DataFrame({'vertex_index': vertex_index, 'annotation_value': annotation_value}\n",
+    "vertex_frame = pandas.DataFrame({'vertex_index': vertex_index, 'color_code': annotation_value}\n",
     "                                for vertex_index, annotation_value in surface.annotation.vertex_values.items())\n",
-    "vertex_frame.head()"
+    "vertex_label_frame = vertex_frame.merge(label_frame, on='color_code')\n",
+    "vertex_label_frame.head()"
    ]
   },
   {
@@ -514,42 +309,42 @@
     {
      "data": {
       "text/plain": [
-       "10542100    12532\n",
-       "9221140      9883\n",
-       "9182740      9792\n",
-       "8204875      9484\n",
-       "1351760      8156\n",
-       "0            8062\n",
-       "14423100     7944\n",
-       "1316060      7095\n",
-       "14474380     7006\n",
-       "14433500     6737\n",
-       "9211105      5636\n",
-       "11832480     5560\n",
-       "3302560      5207\n",
-       "9231540      5003\n",
-       "7874740      4756\n",
-       "3296035      4383\n",
-       "6500         4128\n",
-       "2146559      4117\n",
-       "6558940      3033\n",
-       "4924360      3017\n",
-       "9221340      2839\n",
-       "3957880      2734\n",
-       "3988540      2507\n",
-       "1326300      2460\n",
-       "14464220     2352\n",
-       "9180300      1861\n",
-       "9180240      1711\n",
-       "10511485     1362\n",
-       "2647065      1322\n",
-       "3302420      1078\n",
-       "3988500       991\n",
-       "13145750      990\n",
-       "660700        789\n",
-       "11146310      727\n",
-       "6553700       368\n",
-       "Name: annotation_value, dtype: int64"
+       "superiorfrontal             12532\n",
+       "superiorparietal             9883\n",
+       "lateraloccipital             9792\n",
+       "rostralmiddlefrontal         9484\n",
+       "supramarginal                8156\n",
+       "unknown                      8062\n",
+       "precentral                   7944\n",
+       "postcentral                  7095\n",
+       "superiortemporal             7006\n",
+       "inferiorparietal             6737\n",
+       "lingual                      5636\n",
+       "precuneus                    5560\n",
+       "middletemporal               5207\n",
+       "fusiform                     5003\n",
+       "inferiortemporal             4756\n",
+       "lateralorbitofrontal         4383\n",
+       "caudalmiddlefrontal          4128\n",
+       "insula                       4117\n",
+       "cuneus                       3033\n",
+       "medialorbitofrontal          3017\n",
+       "parsopercularis              2839\n",
+       "pericalcarine                2734\n",
+       "paracentral                  2507\n",
+       "parstriangularis             2460\n",
+       "posteriorcingulate           2352\n",
+       "isthmuscingulate             1861\n",
+       "rostralanteriorcingulate     1711\n",
+       "caudalanteriorcingulate      1362\n",
+       "bankssts                     1322\n",
+       "parsorbitalis                1078\n",
+       "parahippocampal               991\n",
+       "transversetemporal            990\n",
+       "entorhinal                    789\n",
+       "temporalpole                  727\n",
+       "frontalpole                   368\n",
+       "Name: name, dtype: int64"
       ]
      },
      "execution_count": 5,
@@ -558,7 +353,7 @@
     }
    ],
    "source": [
-    "vertex_frame['annotation_value'].value_counts()"
+    "vertex_label_frame['name'].value_counts()"
    ]
   }
  ],

+ 19 - 10
freesurfer_surface/__init__.py

@@ -62,14 +62,20 @@ class Label:
     index: int
     name: str
     red: int
-    blue: int
     green: int
+    blue: int
     transparency: int
 
     @property
-    def color_code(self):
-        return int.from_bytes((self.transparency, self.red, self.green, self.blue),
-                              byteorder='big', signed=False)
+    def color_code(self) -> int:
+        if self.index == 0: # unknown
+            return 0
+        return int.from_bytes((self.red, self.green, self.blue, self.transparency),
+                              byteorder='little', signed=False)
+
+    @property
+    def hex_color_code(self) -> str:
+        return '#{:02x}{:02x}{:02x}'.format(self.red, self.green, self.blue)
 
 
 class Annotation:
@@ -78,6 +84,8 @@ class Annotation:
 
     _TAG_OLD_COLORTABLE = b'\0\0\0\x01'
 
+    # TODO rename vertex_color_codes
+    # TODO replace with vertex_label_index
     vertex_values: typing.Dict[int, int] = {}
     colortable_path: typing.Optional[bytes] = None
     # TODO dict
@@ -86,10 +94,10 @@ class Annotation:
     @staticmethod
     def _read_label(stream: typing.BinaryIO) -> Label:
         label = Label()
-        label.index, name_length = struct.unpack('>II', stream.read(4*2))
+        label.index, name_length = struct.unpack('>II', stream.read(4 * 2))
         label.name = stream.read(name_length - 1).decode()
         assert stream.read(1) == b'\0'
-        label.red, label.blue, label.green, label.transparency \
+        label.red, label.green, label.blue, label.transparency \
             = struct.unpack('>IIII', stream.read(4 * 4))
         return label
 
@@ -98,10 +106,8 @@ class Annotation:
         annotations_num, = struct.unpack('>I', stream.read(4))
         annotations = (struct.unpack('>II', stream.read(4 * 2))
                        for _ in range(annotations_num))
-        self.vertex_values = {vertex_index: annotation_value
-                              for vertex_index, annotation_value in annotations}
-        assert all((annotation_value >> (8 * 3)) == 0
-                   for annotation_value in self.vertex_values.values())
+        self.vertex_values = {vertex_index: color_code
+                              for vertex_index, color_code in annotations}
         assert stream.read(4) == self._TAG_OLD_COLORTABLE
         colortable_version, _, filename_length = struct.unpack('>III', stream.read(4 * 3))
         assert colortable_version > 0  # new version
@@ -109,6 +115,9 @@ class Annotation:
         assert stream.read(1) == b'\0'
         labels_num, = struct.unpack('>I', stream.read(4))
         self.labels = [self._read_label(stream) for _ in range(labels_num)]
+        label_color_codes = set(l.color_code for l in self.labels)
+        assert all(vertex_color_code in label_color_codes
+                   for vertex_color_code in self.vertex_values.values())
         assert not stream.read(1)
 
     @classmethod

+ 20 - 0
freesurfer_surface/__main__.py

@@ -0,0 +1,20 @@
+import argparse
+import csv
+import sys
+
+from freesurfer_surface import Annotation
+
+
+def annotation_labels():
+    """
+    List Labels Stored in Freesurfer's Annotation File
+    (i.e., label/lh.aparc.annot)
+    """
+    argparser = argparse.ArgumentParser(description=annotation_labels.__doc__.strip())
+    argparser.add_argument('--delimiter', default='\t', help='default: %(default)r')
+    argparser.add_argument('annotation_file_path')
+    args = argparser.parse_args()
+    annotation = Annotation.read(args.annotation_file_path)
+    csv_writer = csv.writer(sys.stdout, delimiter=args.delimiter)
+    csv_writer.writerow(('index', 'color', 'name'))
+    csv_writer.writerows((l.index, l.hex_color_code, l.name,) for l in annotation.labels)

+ 5 - 0
setup.py

@@ -44,6 +44,11 @@ setuptools.setup(
         'Topic :: Utilities',
     ],
     packages=setuptools.find_packages(),
+    entry_points={
+        'console_scripts': [
+            'freesurfer-annotation-labels = freesurfer_surface.__main__:annotation_labels',
+        ],
+    },
     install_requires=[],
     setup_requires=[
         'setuptools_scm',

+ 2 - 0
tests/conftest.py

@@ -1,3 +1,5 @@
 import os
 
 SUBJECTS_DIR = os.path.join(os.path.dirname(__file__), 'subjects')
+
+ANNOTATION_FILE_PATH = os.path.join(SUBJECTS_DIR, 'fabian', 'label', 'lh.aparc.annot')

+ 18 - 6
tests/test_annotation.py

@@ -8,13 +8,25 @@ from conftest import SUBJECTS_DIR
 def test_load_annotation():
     annotation = Annotation.read(os.path.join(SUBJECTS_DIR, 'fabian', 'label', 'lh.aparc.annot'))
     assert len(annotation.vertex_values) == 155622
-    assert annotation.vertex_values[0] == (((100 << 8) + 20) << 8) + 220
-    assert annotation.vertex_values[1] == (((100 << 8) + 20) << 8) + 220
-    assert annotation.vertex_values[42] == (((140 << 8) + 30) << 8) + 20
+    assert annotation.vertex_values[64290] == 1316060
+    assert annotation.vertex_values[72160] == 1316060
+    assert annotation.vertex_values[84028] == 14423100
+    assert annotation.vertex_values[97356] == 14423100
+    assert annotation.vertex_values[123173] == 8204875
+    assert annotation.vertex_values[140727] == 8204875
+    assert annotation.vertex_values[93859] == 10542100
+    assert annotation.vertex_values[78572] == 0
+    assert annotation.vertex_values[120377] == 0
     assert annotation.colortable_path == b'/autofs/space/tanha_002/users/greve' \
                                          b'/fsdev.build/average/colortable_desikan_killiany.txt'
     assert len(annotation.labels) == 36
     assert vars(annotation.labels[0]) == {'index': 0, 'name': 'unknown',
-                                          'red': 25, 'blue': 5, 'green': 25, 'transparency': 0}
-    assert vars(annotation.labels[35]) == {'index': 35, 'name': 'insula',
-                                           'red': 255, 'blue': 192, 'green': 32, 'transparency': 0}
+                                          'red': 25, 'green': 5, 'blue': 25, 'transparency': 0}
+    precentral, = filter(lambda l: l.name == 'precentral', annotation.labels)
+    postcentral, = filter(lambda l: l.name == 'postcentral', annotation.labels)
+    assert vars(precentral) == {'index': 24, 'name': 'precentral',
+                                'red': 60, 'green': 20, 'blue': 220, 'transparency': 0}
+    assert vars(postcentral) == {'index': 22, 'name': 'postcentral',
+                                 'red': 220, 'green': 20, 'blue': 20, 'transparency': 0}
+    superiorfrontal, = filter(lambda l: l.color_code == 10542100, annotation.labels)
+    assert superiorfrontal.name == 'superiorfrontal'

+ 34 - 5
tests/test_label.py

@@ -5,16 +5,45 @@ from freesurfer_surface import Label
 
 @pytest.mark.parametrize(('red', 'green', 'blue', 'transparency', 'color_code'), [
     # pylint: disable=bad-whitespace
-    (100,  20, 220,   0, 6558940),
-    (140,  30,  20,   0, 9182740),
-    (140,  30,  20,   1, 9182740 + (1 << (8 * 3))),
-    (140,  30,  20,   7, 9182740 + (7 << (8 * 3))),
-    (140,  30,  20, 123, 2072780308),
+    (220,  20,  20,   0,  1316060),
+    ( 60,  20, 220,   0, 14423100),
+    ( 75,  50, 125,   0,  8204875),
+    ( 20, 220, 160,   0, 10542100),
 ])
 def test_color_code(red, green, blue, transparency, color_code):
     label = Label()
+    label.index = 21
     label.red = red
     label.green = green
     label.blue = blue
     label.transparency = transparency
     assert color_code == label.color_code
+
+def test_color_code_unknown():
+    label = Label()
+    label.index = 0
+    label.name = 'unknown'
+    label.red = 21
+    label.green = 21
+    label.blue = 21
+    assert label.color_code == 0
+
+
+@pytest.mark.parametrize(('red', 'green', 'blue', 'hex_color_code'), [
+    # pylint: disable=bad-whitespace
+    (  0,   0,   0, '#000000'),
+    (255, 255, 255, '#ffffff'),
+    (255,   0,   0, '#ff0000'),
+    (  0, 255,   0, '#00ff00'),
+    (  0,   0, 255, '#0000ff'),
+    (  1,   2,   3, '#010203'),
+    ( 17,  18,  19, '#111213'),
+    (128, 192, 255, '#80c0ff'),
+    ( 20, 220, 160, '#14dca0'),
+])
+def test_hex_color_code(red, green, blue, hex_color_code):
+    label = Label()
+    label.red = red
+    label.green = green
+    label.blue = blue
+    assert hex_color_code == label.hex_color_code.lower()

+ 42 - 0
tests/test_main.py

@@ -0,0 +1,42 @@
+import csv
+import io
+import subprocess
+import typing
+import unittest.mock
+
+import pytest
+
+from freesurfer_surface.__main__ import annotation_labels
+
+from conftest import ANNOTATION_FILE_PATH
+
+
+def check_rows(csv_rows: typing.List[str]):
+    assert len(csv_rows) == 36 + 1
+    assert csv_rows[0] == ['index', 'color', 'name']
+    assert csv_rows[1] == ['0', '#190519', 'unknown']
+    assert csv_rows[23] == ['22', '#dc1414', 'postcentral']
+    assert csv_rows[25] == ['24', '#3c14dc', 'precentral']
+
+
+def test_annotation_labels_function(capsys):
+    with unittest.mock.patch('sys.argv', ['', ANNOTATION_FILE_PATH]):
+        annotation_labels()
+    out, err = capsys.readouterr()
+    assert not err
+    check_rows(list(csv.reader(io.StringIO(out), delimiter='\t')))
+
+
+def test_annotation_labels_function_delimiter(capsys):
+    with unittest.mock.patch('sys.argv', ['', '--delimiter', ',', ANNOTATION_FILE_PATH]):
+        annotation_labels()
+    out, err = capsys.readouterr()
+    assert not err
+    check_rows(list(csv.reader(io.StringIO(out), delimiter=',')))
+
+
+def test_annotation_labels_script():
+    proc_info = subprocess.run(['freesurfer-annotation-labels', ANNOTATION_FILE_PATH],
+                               check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    assert not proc_info.stderr
+    check_rows(list(csv.reader(io.StringIO(proc_info.stdout.decode()), delimiter='\t')))