{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Converting masks back to annotations\n", "\n", "**Overview:**\n", "\n", "![masks_to_annotations](https://user-images.githubusercontent.com/22067552/80078415-e2de0100-851c-11ea-81ce-3b2d74ee6246.png)\n", "\n", "Most segmentation algorithms produce outputs in an image format. Visualizing these outputs in HistomicsUI requires conversion from mask images to an annotation document containing (x,y) coordinates in the whole-slide image coordinate frame. This notebook demonstrates this conversion process in two steps:\n", "\n", "- Converting a mask image into contours (coordinates in the mask frame)\n", "\n", "- Placing contours data into a format following the annotation document schema that can be pushed to DSA for visualization in HistomicsUI.\n", "\n", "This notebook is based on work described in Amgad et al, 2019:\n", "\n", "_Mohamed Amgad, Habiba Elfandy, Hagar Hussein, ..., Jonathan Beezley, Deepak R Chittajallu, David Manthey, David A Gutman, Lee A D Cooper, Structured crowdsourcing enables convolutional segmentation of histology images, Bioinformatics, 2019, btz083_\n", "\n", "**Where to look?**\n", "\n", "```\n", "|_ histomicstk/\n", " |_annotations_and_masks/\n", " | |_masks_to_annotations_handler.py\n", " |_tests/\n", " |_test_masks_to_annotations_handler.py\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "CWD = os.getcwd()\n", "import girder_client\n", "from pandas import read_csv\n", "from imageio import imread\n", "from histomicstk.annotations_and_masks.masks_to_annotations_handler import (\n", " get_contours_from_mask,\n", " get_single_annotation_document_from_contours,\n", " get_annotation_documents_from_contours)\n", "\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "plt.rcParams['figure.figsize'] = 7, 7" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Connect girder client and set parameters\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": "{'_accessLevel': 2,\n '_id': '59bc677892ca9a0017c2e855',\n '_modelType': 'user',\n 'admin': True,\n 'created': '2017-09-15T23:51:20.203000+00:00',\n 'email': 'mtageld@emory.edu',\n 'emailVerified': False,\n 'firstName': 'Mohamed',\n 'groupInvites': [],\n 'groups': ['59f7713a92ca9a0017a29765',\n '5c607488e62914004d0ff4a6',\n '5e44a2e0ddda5f8398785304',\n '5e76b3f3ddda5f83982beb9a'],\n 'lastName': 'Tageldin',\n 'login': 'kheffah',\n 'otp': False,\n 'public': True,\n 'size': 0,\n 'status': 'enabled'}" }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# APIURL = 'http://demo.kitware.com/histomicstk/api/v1/'\n", "# SAMPLE_SLIDE_ID = '5bbdee92e629140048d01b5d'\n", "APIURL = 'http://candygram.neurology.emory.edu:8080/api/v1/'\n", "SAMPLE_SLIDE_ID = '5d586d76bd4404c6b1f286ae'\n", "\n", "# Connect to girder client\n", "gc = girder_client.GirderClient(apiUrl=APIURL)\n", "gc.authenticate(interactive=True)\n", "# gc.authenticate(apiKey='kri19nTIGOkWH01TbzRqfohaaDWb6kPecRqGmemb')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Let's inspect the ground truth codes file\n", "\n", "This contains the ground truth codes and information dataframe. This is a dataframe that is indexed by the annotation group name and has the following columns:\n", "\n", "- ``group``: group name of annotation (string), eg. \"mostly_tumor\"\n", "- ``GT_code``: int, desired ground truth code (in the mask) Pixels of this value belong to corresponding group (class)\n", "- ``color``: str, rgb format. eg. rgb(255,0,0).\n", "\n", "**NOTE:**\n", "\n", "Zero pixels have special meaning and do not encode specific ground truth class. Instead, they simply mean 'Outside ROI' and should be ignored during model training or evaluation." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# read GTCodes dataframe\n", "GTCODE_PATH = os.path.join(\n", " CWD, '..', '..', 'tests', 'test_files', 'sample_GTcodes.csv')\n", "GTCodes_df = read_csv(GTCODE_PATH)\n", "GTCodes_df.index = GTCodes_df.loc[:, 'group']" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
groupoverlay_orderGT_codeis_roiis_background_classcolorcomments
group
roiroi025410rgb(200,0,150)NaN
evaluation_roievaluation_roi025310rgb(255,0,0)NaN
mostly_tumormostly_tumor1100rgb(255,0,0)core class
mostly_stromamostly_stroma2201rgb(255,125,0)core class
mostly_lymphocytic_infiltratemostly_lymphocytic_infiltrate1300rgb(0,0,255)core class
\n
", "text/plain": " group overlay_order \\\ngroup \nroi roi 0 \nevaluation_roi evaluation_roi 0 \nmostly_tumor mostly_tumor 1 \nmostly_stroma mostly_stroma 2 \nmostly_lymphocytic_infiltrate mostly_lymphocytic_infiltrate 1 \n\n GT_code is_roi is_background_class \\\ngroup \nroi 254 1 0 \nevaluation_roi 253 1 0 \nmostly_tumor 1 0 0 \nmostly_stroma 2 0 1 \nmostly_lymphocytic_infiltrate 3 0 0 \n\n color comments \ngroup \nroi rgb(200,0,150) NaN \nevaluation_roi rgb(255,0,0) NaN \nmostly_tumor rgb(255,0,0) core class \nmostly_stroma rgb(255,125,0) core class \nmostly_lymphocytic_infiltrate rgb(0,0,255) core class " }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "GTCodes_df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Read and visualize mask" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# read mask\n", "X_OFFSET = 59206\n", "Y_OFFSET = 33505\n", "MASKNAME = 'TCGA-A2-A0YE-01Z-00-DX1.8A2E3094-5755-42BC-969D-7F0A2ECA0F39' + \\\n", " '_left-%d_top-%d_mag-BASE.png' % (X_OFFSET, Y_OFFSET)\n", "MASKPATH = os.path.join(CWD, '..', '..', 'tests', 'test_files', 'annotations_and_masks', MASKNAME)\n", "MASK = imread(MASKPATH)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": "
" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plt.figure(figsize=(7,7))\n", "plt.imshow(MASK)\n", "plt.title(MASKNAME[:23])\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Get contours from mask" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function ``get_contours_from_mask()`` generates contours from a mask image. There are many parameters that can be set but most have defaults set for the most common use cases. The only required parameters you must provide are ``MASK`` and ``GTCodes_df``, but you may want to consider setting the following parameters based on your specific needs: ``get_roi_contour``, ``roi_group``, ``discard_nonenclosed_background``, ``background_group``, that control behaviour regarding region of interest (ROI) boundary and background pixel class (e.g. stroma)." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Parse ground truth mask and gets countours for annotations.\n", "\n", " Parameters\n", " -----------\n", " MASK : nd array\n", " ground truth mask (m,n) where pixel values encode group membership.\n", " GTCodes_df : pandas Dataframe\n", " the ground truth codes and information dataframe.\n", " This is a dataframe that is indexed by the annotation group name and\n", " has the following columns.\n", "\n", " group: str\n", " group name of annotation, eg. mostly_tumor.\n", " GT_code: int\n", " desired ground truth code (in the mask). Pixels of this value\n", " belong to corresponding group (class).\n", " color: str\n", " rgb format. eg. rgb(255,0,0).\n", " groups_to_get : None\n", " if None (default) then all groups (ground truth labels) will be\n", " extracted. Otherwise pass a list fo strings like ['mostly_tumor',].\n", " MIN_SIZE : int\n", " minimum bounding box size of contour\n", " MAX_SIZE : None\n", " if not None, int. Maximum bounding box size of contour. Sometimes\n", " very large contours cause segmentation faults that originate from\n", " opencv and are not caught by python, causing the python process\n", " to unexpectedly hault. If you would like to set a maximum size to\n", " defend against this, a suggested maximum would be 15000.\n", " get_roi_contour : bool\n", " whether to get contour for boundary of region of interest (ROI). This\n", " is most relevant when dealing with multiple ROIs per slide and with\n", " rotated rectangular or polygonal ROIs.\n", " roi_group : str\n", " name of roi group in the GT_Codes dataframe (eg roi)\n", " discard_nonenclosed_background : bool\n", " If a background group contour is NOT fully enclosed, discard it.\n", " This is a purely aesthetic method, makes sure that the background group\n", " contours (eg stroma) are discarded by default to avoid cluttering the\n", " field when posted to DSA for viewing online. The only exception is\n", " if they are enclosed within something else (eg tumor), in which case\n", " they are kept since they represent holes. This is related to\n", " https://github.com/DigitalSlideArchive/HistomicsTK/issues/675\n", " WARNING - This is a bit slower since the contours will have to be\n", " converted to shapely polygons. It is not noticeable for hundreds of\n", " contours, but you will notice the speed difference if you are parsing\n", " thousands of contours. Default, for this reason, is False.\n", " background_group : str\n", " name of background group in the GT_codes dataframe (eg mostly_stroma)\n", " verbose : bool\n", " Print progress to screen?\n", " monitorPrefix : str\n", " text to prepend to printed statements\n", "\n", " Returns\n", " --------\n", " pandas DataFrame\n", " contours extracted from input mask. The following columns are output.\n", "\n", " group : str\n", " annotation group (ground truth label).\n", " color : str\n", " annotation color if it were to be posted to DSA.\n", " is_roi : bool\n", " whether this annotation is a region of interest boundary\n", " ymin : int\n", " minimun y coordinate\n", " ymax : int\n", " maximum y coordinate\n", " xmin : int\n", " minimum x coordinate\n", " xmax : int\n", " maximum x coordinate\n", " has_holes : bool\n", " whether this contour has holes\n", " touches_edge-top : bool\n", " whether this contour touches top mask edge\n", " touches_edge-bottom : bool\n", " whether this contour touches bottom mask edge\n", " touches_edge-left : bool\n", " whether this contour touches left mask edge\n", " touches_edge-right : bool\n", " whether this contour touches right mask edge\n", " coords_x : str\n", " vertix x coordinates comma-separated values\n", " coords_y\n", " vertix y coordinated comma-separated values\n", "\n", " \n" ] } ], "source": [ "print(get_contours_from_mask.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Extract contours" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "TCGA-A2-A0YE: getting contours: non-roi: roi: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: evaluation_roi: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_tumor: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_tumor: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_stroma: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_stroma: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 1 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 2 of 11: TOO SIMPLE (2 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 3 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 4 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 5 of 11: TOO SMALL (10 x 18 pixels) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 6 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 8 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 9 of 11: TOO SIMPLE (1 coordinates) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_lymphocytic_infiltrate: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_lymphocytic_infiltrate: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: nest 5 of 14: TOO SMALL (23 x 74 pixels) -- IGNORED\n", "TCGA-A2-A0YE: getting contours: non-roi: necrosis_or_debris: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: glandular_secretions: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_blood: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: exclude: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: exclude: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: metaplasia_NOS: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_fat: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_plasma_cells: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: other_immune_infiltrate: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_mucoid_material: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: normal_acinus_or_duct: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: normal_acinus_or_duct: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: lymphatics: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: undetermined: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: nerve: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: skin_adnexia: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: blood_vessel: getting contours\n", "TCGA-A2-A0YE: getting contours: non-roi: blood_vessel: adding contours\n", "TCGA-A2-A0YE: getting contours: non-roi: angioinvasion: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: mostly_dcis: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: non-roi: other: NO OBJECTS!!\n", "TCGA-A2-A0YE: getting contours: discarding backgrnd: discarded 3 contours\n", "TCGA-A2-A0YE: getting contours: roi: roi: getting contours\n", "TCGA-A2-A0YE: getting contours: roi: roi: adding contours\n" ] } ], "source": [ "# Let's extract all contours from a mask, including ROI boundary. We will\n", "# be discarding any stromal contours that are not fully enclosed within a\n", "# non-stromal contour since we already know that stroma is the background\n", "# group. This is so things look uncluttered when posted to DSA.\n", "groups_to_get = None\n", "contours_df = get_contours_from_mask(\n", " MASK=MASK, GTCodes_df=GTCodes_df, groups_to_get=groups_to_get,\n", " get_roi_contour=True, roi_group='roi',\n", " discard_nonenclosed_background=True,\n", " background_group='mostly_stroma',\n", " MIN_SIZE=30, MAX_SIZE=None, verbose=True,\n", " monitorPrefix=MASKNAME[:12] + ': getting contours')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Let's inspect the contours dataframe\n", "\n", "The columns that really matter here are ``group``, ``color``, ``coords_x``, and ``coords_y``." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
groupcoloryminymaxxminxmaxhas_holestouches_edge-toptouches_edge-lefttouches_edge-bottomtouches_edge-rightcoords_xcoords_y
0roirgb(200,0,150)0.04593.00.04541.00.01.01.01.01.02835,2834,2833,2832,2831,2830,2829,2827,2826,2...0,1,1,2,2,3,3,5,5,6,6,8,8,9,9,10,10,12,12,13,1...
1mostly_tumorrgb(255,0,0)4269.04560.01639.02039.01.00.00.00.00.01673,1672,1668,1667,1662,1661,1659,1658,1658,1...4269,4270,4270,4271,4271,4272,4272,4273,4274,4...
2mostly_tumorrgb(255,0,0)3764.04282.01607.02187.00.00.00.00.00.01770,1769,1768,1767,1765,1764,1762,1761,1760,1...3764,3765,3765,3766,3766,3767,3767,3768,3768,3...
3mostly_tumorrgb(255,0,0)3712.04051.01201.01411.00.00.00.00.00.01214,1213,1211,1210,1208,1207,1206,1205,1203,1...3712,3713,3713,3714,3714,3715,3715,3716,3716,3...
4mostly_tumorrgb(255,0,0)3356.03748.03108.03540.00.00.00.00.00.03342,3341,3337,3336,3332,3331,3328,3327,3326,3...3356,3357,3357,3358,3358,3359,3359,3360,3360,3...
\n
", "text/plain": " group color ymin ymax xmin xmax has_holes \\\n0 roi rgb(200,0,150) 0.0 4593.0 0.0 4541.0 0.0 \n1 mostly_tumor rgb(255,0,0) 4269.0 4560.0 1639.0 2039.0 1.0 \n2 mostly_tumor rgb(255,0,0) 3764.0 4282.0 1607.0 2187.0 0.0 \n3 mostly_tumor rgb(255,0,0) 3712.0 4051.0 1201.0 1411.0 0.0 \n4 mostly_tumor rgb(255,0,0) 3356.0 3748.0 3108.0 3540.0 0.0 \n\n touches_edge-top touches_edge-left touches_edge-bottom \\\n0 1.0 1.0 1.0 \n1 0.0 0.0 0.0 \n2 0.0 0.0 0.0 \n3 0.0 0.0 0.0 \n4 0.0 0.0 0.0 \n\n touches_edge-right coords_x \\\n0 1.0 2835,2834,2833,2832,2831,2830,2829,2827,2826,2... \n1 0.0 1673,1672,1668,1667,1662,1661,1659,1658,1658,1... \n2 0.0 1770,1769,1768,1767,1765,1764,1762,1761,1760,1... \n3 0.0 1214,1213,1211,1210,1208,1207,1206,1205,1203,1... \n4 0.0 3342,3341,3337,3336,3332,3331,3328,3327,3326,3... \n\n coords_y \n0 0,1,1,2,2,3,3,5,5,6,6,8,8,9,9,10,10,12,12,13,1... \n1 4269,4270,4270,4271,4271,4272,4272,4273,4274,4... \n2 3764,3765,3765,3766,3766,3767,3767,3768,3768,3... \n3 3712,3713,3713,3714,3714,3715,3715,3716,3716,3... \n4 3356,3357,3357,3358,3358,3359,3359,3360,3360,3... " }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "contours_df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Get annotation documents from contours\n", "\n", "This method ``get_annotation_documents_from_contours()`` generates formatted annotation documents from contours that can be posted to the DSA server." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Given dataframe of contours, get list of annotation documents.\n", "\n", " This method parses a dataframe of contours to a list of dictionaries, each\n", " of which represents and large_image style annotation. This is a wrapper\n", " that extends the functionality of the method\n", " get_single_annotation_document_from_contours(), whose docstring should\n", " be referenced for implementation details and further explanation.\n", "\n", " Parameters\n", " -----------\n", " contours_df : pandas DataFrame\n", " WARNING - This is modified inside the function, so pass a copy.\n", " This dataframe includes data on contours extracted from input mask\n", " using get_contours_from_mask(). If you have contours using some other\n", " method, just make sure the dataframe follows the same schema as the\n", " output from get_contours_from_mask(). You may find a sample dataframe\n", " in thie repo at ./tests/test_files/annotations_and_masks/sample_contours_df.tsv\n", " The following columns are relevant for this method.\n", "\n", " group : str\n", " annotation group (ground truth label).\n", " color : str\n", " annotation color if it were to be posted to DSA.\n", " coords_x : str\n", " vertix x coordinates comma-separated values\n", " coords_y\n", " vertix y coordinated comma-separated values\n", " separate_docs_by_group : bool\n", " if set to True, you get one or more annotation documents (dicts)\n", " for each group (eg tumor) independently.\n", " annots_per_doc : int\n", " maximum number of annotation elements (polygons) per dict. The smaller\n", " this number, the more numerous the annotation documents, but the more\n", " seamless it is to post this data to the DSA server or to view using the\n", " HistomicsTK interface since you will be loading smaller chunks of data\n", " at a time.\n", " annprops : dict\n", " properties of annotation elements. Contains the following keys\n", " F, X_OFFSET, Y_OFFSET, opacity, lineWidth. Refer to\n", " get_single_annotation_document_from_contours() for details.\n", " docnamePrefix : str\n", " test to prepend to annotation document name\n", " verbose : bool\n", " Print progress to screen?\n", " monitorPrefix : str\n", " text to prepend to printed statements\n", "\n", " Returns\n", " --------\n", " list of dicts\n", " DSA-style annotation document.\n", "\n", " \n" ] } ], "source": [ "print(get_annotation_documents_from_contours.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As mentioned in the docs, this function wraps ``get_single_annotation_document_from_contours()``" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Given dataframe of contours, get annotation document.\n", "\n", " This uses the large_image annotation schema to create an annotation\n", " document that maybe posted to DSA for viewing using something like:\n", " resp = gc.post(\"/annotation?itemId=\" + slide_id, json=annotation_doc)\n", " The annotation schema can be found at:\n", " github.com/girder/large_image/blob/master/docs/annotations.md .\n", "\n", " Parameters\n", " -----------\n", " contours_df_slice : pandas DataFrame\n", " The following columns are of relevance and must be contained.\n", "\n", " group : str\n", " annotation group (ground truth label).\n", " color : str\n", " annotation color if it were to be posted to DSA.\n", " coords_x : str\n", " vertix x coordinates comma-separated values\n", " coords_y\n", " vertix y coordinated comma-separated values\n", " docname : str\n", " annotation document name\n", " F : float\n", " how much smaller is the mask where the contours come from is relative\n", " to the slide scan magnification. For example, if the mask is at 10x\n", " whereas the slide scan magnification is 20x, then F would be 2.0.\n", " X_OFFSET : int\n", " x offset to add to contours at BASE (SCAN) magnification\n", " Y_OFFSET : int\n", " y offset to add to contours at BASE (SCAN) magnification\n", " opacity : float\n", " opacity of annotation elements (in the range [0, 1])\n", " lineWidth : float\n", " width of boarders of annotation elements\n", " verbose : bool\n", " Print progress to screen?\n", " monitorPrefix : str\n", " text to prepend to printed statements\n", "\n", " Returns\n", " --------\n", " dict\n", " DSA-style annotation document ready to be post for viewing.\n", "\n", " \n" ] } ], "source": [ "print(get_single_annotation_document_from_contours.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's get a list of annotation documents (each is a dictionary). For the purpose of this tutorial, \n", "we separate the documents by group (i.e. each document is composed of polygons from the same\n", "style/group). You could decide to allow heterogeneous groups in the same annotation document by\n", "setting ``separate_docs_by_group`` to ``False``. We place 10 polygons in each document for this demo\n", "for illustration purposes. Realistically you would want each document to contain several hundred depending on their complexity. Placing too many polygons in each document can lead to performance issues when rendering in HistomicsUI." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get annotation documents" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 1 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 2 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 3 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 4 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 5 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 6 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 7 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 8 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 9 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 10 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 11 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 12 of 13\n", "TCGA-A2-A0YE: annotation docs: mostly_lymphocytic_infiltrate: doc 1 of 1: contour 13 of 13\n", "TCGA-A2-A0YE: annotation docs: exclude: doc 1 of 1: contour 1 of 1\n", "TCGA-A2-A0YE: annotation docs: blood_vessel: doc 1 of 1: contour 1 of 3\n", "TCGA-A2-A0YE: annotation docs: blood_vessel: doc 1 of 1: contour 2 of 3\n", "TCGA-A2-A0YE: annotation docs: blood_vessel: doc 1 of 1: contour 3 of 3\n", "TCGA-A2-A0YE: annotation docs: roi: doc 1 of 1: contour 1 of 1\n", "TCGA-A2-A0YE: annotation docs: normal_acinus_or_duct: doc 1 of 1: contour 1 of 2\n", "TCGA-A2-A0YE: annotation docs: normal_acinus_or_duct: doc 1 of 1: contour 2 of 2\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 1 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 2 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 3 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 4 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 5 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 6 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 7 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 8 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 9 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 1 of 2: contour 10 of 10\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 1 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 2 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 3 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 4 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 5 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 6 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 7 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 8 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 9 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 10 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 11 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 12 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 13 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 14 of 15\n", "TCGA-A2-A0YE: annotation docs: mostly_tumor: doc 2 of 2: contour 15 of 15\n" ] } ], "source": [ "# get list of annotation documents\n", "annprops = {\n", " 'X_OFFSET': X_OFFSET,\n", " 'Y_OFFSET': Y_OFFSET,\n", " 'opacity': 0.2,\n", " 'lineWidth': 4.0,\n", "}\n", "annotation_docs = get_annotation_documents_from_contours(\n", " contours_df.copy(), separate_docs_by_group=True, annots_per_doc=10,\n", " docnamePrefix='demo', annprops=annprops,\n", " verbose=True, monitorPrefix=MASKNAME[:12] + ': annotation docs')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Let's examine one of the documents. \n", "\n", "Limit display to the first two elements (polygons) and cap the vertices for clarity." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "ann_doc = annotation_docs[0].copy()\n", "ann_doc['elements'] = ann_doc['elements'][:2]\n", "for i in range(2):\n", " ann_doc['elements'][i]['points'] = ann_doc['elements'][i]['points'][:5]" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": "{'name': 'demo_mostly_lymphocytic_infiltrate-0',\n 'description': '',\n 'elements': [{'group': 'mostly_lymphocytic_infiltrate',\n 'type': 'polyline',\n 'lineColor': 'rgb(0,0,255)',\n 'lineWidth': 4.0,\n 'closed': True,\n 'points': [[61974.0, 37427.0, 0.0],\n [61975.0, 37428.0, 0.0],\n [61975.0, 37429.0, 0.0],\n [61976.0, 37430.0, 0.0],\n [61976.0, 37431.0, 0.0]],\n 'label': {'value': 'mostly_lymphocytic_infiltrate'},\n 'fillColor': 'rgba(0,0,255,0.2)'},\n {'group': 'mostly_lymphocytic_infiltrate',\n 'type': 'polyline',\n 'lineColor': 'rgb(0,0,255)',\n 'lineWidth': 4.0,\n 'closed': True,\n 'points': [[60531.0, 37045.0, 0.0],\n [60528.0, 37048.0, 0.0],\n [60527.0, 37048.0, 0.0],\n [60522.0, 37053.0, 0.0],\n [60522.0, 37054.0, 0.0]],\n 'label': {'value': 'mostly_lymphocytic_infiltrate'},\n 'fillColor': 'rgba(0,0,255,0.2)'}]}" }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ann_doc" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Post the annotation to the correct item/slide in DSA" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "# deleting existing annotations in target slide (if any)\n", "existing_annotations = gc.get('/annotation/item/' + SAMPLE_SLIDE_ID)\n", "for ann in existing_annotations:\n", " gc.delete('/annotation/%s' % ann['_id'])\n", "\n", "# post the annotation documents you created\n", "for annotation_doc in annotation_docs:\n", " resp = gc.post(\n", " '/annotation?itemId=' + SAMPLE_SLIDE_ID, json=annotation_doc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now you can go to HistomicsUI and confirm that the posted annotations make\n", "sense and correspond to tissue boundaries and expected labels." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 2 }