diff --git a/promort/VERSION b/promort/VERSION index 5eef0f10..d9df1bbc 100644 --- a/promort/VERSION +++ b/promort/VERSION @@ -1 +1 @@ -0.10.2 +0.11.0 diff --git a/promort/clinical_annotations_manager/management/commands/get_cores_clinical_data.py b/promort/clinical_annotations_manager/management/commands/get_cores_clinical_data.py index 86bfa65e..076588fa 100644 --- a/promort/clinical_annotations_manager/management/commands/get_cores_clinical_data.py +++ b/promort/clinical_annotations_manager/management/commands/get_cores_clinical_data.py @@ -25,7 +25,7 @@ import logging -logger = logging.getLogger('promort_commands') +logger = logging.getLogger("promort_commands") class Command(BaseCommand): @@ -34,64 +34,118 @@ class Command(BaseCommand): """ def add_arguments(self, parser): - parser.add_argument('--output_file', dest='output', type=str, required=True, - help='path of the output CSV file') - parser.add_argument('--page_size', dest='page_size', type=int, default=0, - help='the number of records retrieved for each page (this will enable pagination)') + parser.add_argument( + "--output_file", + dest="output", + type=str, + required=True, + help="path of the output CSV file", + ) + parser.add_argument( + "--page_size", + dest="page_size", + type=int, + default=0, + help="the number of records retrieved for each page (this will enable pagination)", + ) def _dump_data(self, page_size, csv_writer): if page_size > 0: - logger.info('Pagination enabled (%d records for page)', page_size) - ca_qs = CoreAnnotation.objects.get_queryset().order_by('creation_date') + logger.info("Pagination enabled (%d records for page)", page_size) + ca_qs = CoreAnnotation.objects.get_queryset().order_by("creation_date") paginator = Paginator(ca_qs, page_size) for x in paginator.page_range: - logger.info('-- page %d --', x) + logger.info("-- page %d --", x) page = paginator.page(x) for ca in page.object_list: self._dump_row(ca, csv_writer) else: - logger.info('Loading full batch') + logger.info("Loading full batch") core_annotations = CoreAnnotation.objects.all() for ca in core_annotations: self._dump_row(ca, csv_writer) def _dump_row(self, core_annotation, csv_writer): try: - action_start_time = core_annotation.action_start_time.strftime('%Y-%m-%d %H:%M:%S') + action_start_time = core_annotation.action_start_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) except AttributeError: action_start_time = None try: - action_complete_time = core_annotation.action_complete_time.strftime('%Y-%m-%d %H:%M:%S') + action_complete_time = core_annotation.action_complete_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) except AttributeError: action_complete_time = None csv_writer.writerow( { - 'case_id': core_annotation.core.slice.slide.case.id, - 'slide_id': core_annotation.core.slice.slide.id, - 'rois_review_step_id': core_annotation.annotation_step.rois_review_step.label, - 'clinical_review_step_id': core_annotation.annotation_step.label, - 'reviewer': core_annotation.author.username, - 'core_id': core_annotation.core.id, - 'core_label': core_annotation.core.label, - 'action_start_time': action_start_time, - 'action_complete_time': action_complete_time, - 'creation_date': core_annotation.creation_date.strftime('%Y-%m-%d %H:%M:%S'), - 'primary_gleason': core_annotation.primary_gleason, - 'secondary_gleason': core_annotation.secondary_gleason, - 'gleason_group_who_16': core_annotation.get_grade_group_text() + "case_id": core_annotation.core.slice.slide.case.id, + "slide_id": core_annotation.core.slice.slide.id, + "rois_review_step_id": core_annotation.annotation_step.rois_review_step.label, + "clinical_review_step_id": core_annotation.annotation_step.label, + "reviewer": core_annotation.author.username, + "core_id": core_annotation.core.id, + "core_label": core_annotation.core.label, + "action_start_time": action_start_time, + "action_complete_time": action_complete_time, + "creation_date": core_annotation.creation_date.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "primary_gleason": core_annotation.get_primary_gleason(), + "secondary_gleason": core_annotation.get_secondary_gleason(), + "gleason_group_who_16": core_annotation.get_grade_group_text(), + "nuclear_grade_size": core_annotation.nuclear_grade_size, + "intraluminal_acinar_differentiation_grade": core_annotation.intraluminal_acinar_differentiation_grade, + "intraluminal_secretions": core_annotation.intraluminal_secretions, + "central_maturation": core_annotation.central_maturation, + "extra_cribriform_gleason_score": core_annotation.extra_cribriform_gleason_score, + "predominant_rsg": core_annotation.predominant_rsg, + "highest_rsg": core_annotation.highest_rsg, + "rsg_within_highest_grade_area": core_annotation.rsg_within_highest_grade_area, + "perineural_invasion": core_annotation.perineural_invasion, + "perineural_growth_with_cribriform_patterns": core_annotation.perineural_growth_with_cribriform_patterns, + "extrapostatic_extension": core_annotation.extraprostatic_extension, + "largest_confluent_sheet": core_annotation.get_largest_confluent_sheet(), + "total_cribriform_area": core_annotation.get_total_cribriform_area(), } ) def _export_data(self, out_file, page_size): - header = ['case_id', 'slide_id', 'rois_review_step_id', 'clinical_review_step_id', 'reviewer', - 'core_id', 'core_label', 'action_start_time', 'action_complete_time', 'creation_date', - 'primary_gleason', 'secondary_gleason', 'gleason_group_who_16'] - with open(out_file, 'w') as ofile: - writer = DictWriter(ofile, delimiter=',', fieldnames=header) + header = [ + "case_id", + "slide_id", + "rois_review_step_id", + "clinical_review_step_id", + "reviewer", + "core_id", + "core_label", + "action_start_time", + "action_complete_time", + "creation_date", + "primary_gleason", + "secondary_gleason", + "gleason_group_who_16", + "nuclear_grade_size", + "intraluminal_acinar_differentiation_grade", + "intraluminal_secretions", + "central_maturation", + "extra_cribriform_gleason_score", + "predominant_rsg", + "highest_rsg", + "rsg_within_highest_grade_area", + "perineural_invasion", + "perineural_growth_with_cribriform_patterns", + "extrapostatic_extension", + "largest_confluent_sheet", + "total_cribriform_area", + ] + with open(out_file, "w") as ofile: + writer = DictWriter(ofile, delimiter=",", fieldnames=header) writer.writeheader() self._dump_data(page_size, writer) def handle(self, *args, **opts): - logger.info('=== Starting export job ===') - self._export_data(opts['output'], opts['page_size']) - logger.info('=== Data saved to %s ===', opts['output']) + logger.info("=== Starting export job ===") + self._export_data(opts["output"], opts["page_size"]) + logger.info("=== Data saved to %s ===", opts["output"]) diff --git a/promort/clinical_annotations_manager/management/commands/get_focus_regions_clinical_data.py b/promort/clinical_annotations_manager/management/commands/get_focus_regions_clinical_data.py index 73962436..7e77e15b 100644 --- a/promort/clinical_annotations_manager/management/commands/get_focus_regions_clinical_data.py +++ b/promort/clinical_annotations_manager/management/commands/get_focus_regions_clinical_data.py @@ -25,7 +25,7 @@ import logging -logger = logging.getLogger('promort_commands') +logger = logging.getLogger("promort_commands") class Command(BaseCommand): @@ -34,18 +34,30 @@ class Command(BaseCommand): """ def add_arguments(self, parser): - parser.add_argument('--output_file', dest='output', type=str, required=True, - help='path of the output CSV file') - parser.add_argument('--page_size', dest='page_size', type=int, default=0, - help='the number of records retrieved for each page (this will enable pagination)') + parser.add_argument( + "--output_file", + dest="output", + type=str, + required=True, + help="path of the output CSV file", + ) + parser.add_argument( + "--page_size", + dest="page_size", + type=int, + default=0, + help="the number of records retrieved for each page (this will enable pagination)", + ) def _dump_data(self, page_size, csv_writer): if page_size > 0: - logger.info('Pagination enabled (%d records for page)', page_size) - fra_qs = FocusRegionAnnotation.objects.get_queryset().order_by('creation_date') + logger.info("Pagination enabled (%d records for page)", page_size) + fra_qs = FocusRegionAnnotation.objects.get_queryset().order_by( + "creation_date" + ) paginator = Paginator(fra_qs, page_size) for x in paginator.page_range: - logger.info('-- page %d --', x) + logger.info("-- page %d --", x) page = paginator.page(x) for fra in page.object_list: self._dump_row(fra, csv_writer) @@ -56,53 +68,101 @@ def _dump_data(self, page_size, csv_writer): def _dump_row(self, focus_region_annotation, csv_writer): try: - action_start_time = focus_region_annotation.action_start_time.strftime('%Y-%m-%d %H:%M:%S') + action_start_time = focus_region_annotation.action_start_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) except AttributeError: action_start_time = None try: - action_complete_time = focus_region_annotation.action_complete_time.strftime('%Y-%m-%d %H:%M:%S') + action_complete_time = ( + focus_region_annotation.action_complete_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + ) except AttributeError: action_complete_time = None csv_writer.writerow( { - 'case_id': focus_region_annotation.focus_region.core.slice.slide.case.id, - 'slide_id': focus_region_annotation.focus_region.core.slice.slide.id, - 'rois_review_step_id': focus_region_annotation.annotation_step.rois_review_step.label, - 'clinical_review_step_id': focus_region_annotation.annotation_step.label, - 'reviewer': focus_region_annotation.author.username, - 'focus_region_id': focus_region_annotation.focus_region.id, - 'focus_region_label': focus_region_annotation.focus_region.label, - 'core_id': focus_region_annotation.focus_region.core.id, - 'core_label': focus_region_annotation.focus_region.core.label, - 'action_start_time': action_start_time, - 'action_complete_time': action_complete_time, - 'creation_date': focus_region_annotation.creation_date.strftime('%Y-%m-%d %H:%M:%S'), - 'perineural_involvement': focus_region_annotation.perineural_involvement, - 'intraductal_carcinoma': focus_region_annotation.intraductal_carcinoma, - 'ductal_carcinoma': focus_region_annotation.ductal_carcinoma, - 'poorly_formed_glands': focus_region_annotation.poorly_formed_glands, - 'cribriform_pattern': focus_region_annotation.cribriform_pattern, - 'small_cell_signet_ring': focus_region_annotation.small_cell_signet_ring, - 'hypernephroid_pattern': focus_region_annotation.hypernephroid_pattern, - 'mucinous': focus_region_annotation.mucinous, - 'comedo_necrosis': focus_region_annotation.comedo_necrosis, - 'total_gleason_4_area': focus_region_annotation.get_total_gleason_4_area(), - 'gleason_4_percentage': focus_region_annotation.get_gleason_4_percentage() + "case_id": focus_region_annotation.focus_region.core.slice.slide.case.id, + "slide_id": focus_region_annotation.focus_region.core.slice.slide.id, + "rois_review_step_id": focus_region_annotation.annotation_step.rois_review_step.label, + "clinical_review_step_id": focus_region_annotation.annotation_step.label, + "reviewer": focus_region_annotation.author.username, + "focus_region_id": focus_region_annotation.focus_region.id, + "focus_region_label": focus_region_annotation.focus_region.label, + "core_id": focus_region_annotation.focus_region.core.id, + "core_label": focus_region_annotation.focus_region.core.label, + "action_start_time": action_start_time, + "action_complete_time": action_complete_time, + "creation_date": focus_region_annotation.creation_date.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "perineural_involvement": focus_region_annotation.perineural_involvement, + "intraductal_carcinoma": focus_region_annotation.intraductal_carcinoma, + "ductal_carcinoma": focus_region_annotation.ductal_carcinoma, + "poorly_formed_glands": focus_region_annotation.poorly_formed_glands, + "cribriform_pattern": focus_region_annotation.cribriform_pattern, + "small_cell_signet_ring": focus_region_annotation.small_cell_signet_ring, + "hypernephroid_pattern": focus_region_annotation.hypernephroid_pattern, + "mucinous": focus_region_annotation.mucinous, + "comedo_necrosis": focus_region_annotation.comedo_necrosis, + "total_gleason_3_area": focus_region_annotation.get_total_gleason_area( + "G3" + ), + "total_gleason_4_area": focus_region_annotation.get_total_gleason_area( + "G4" + ), + "total_gleason_5_area": focus_region_annotation.get_total_gleason_area( + "G5" + ), + "gleason_3_percentage": focus_region_annotation.get_gleason_percentage( + "G3" + ), + "gleason_4_percentage": focus_region_annotation.get_gleason_percentage( + "G4" + ), + "gleason_5_percentage": focus_region_annotation.get_gleason_percentage( + "G5" + ), } ) def _export_data(self, out_file, page_size): - header = ['case_id', 'slide_id', 'rois_review_step_id', 'clinical_review_step_id', 'reviewer', - 'focus_region_id', 'focus_region_label', 'core_id', 'core_label', 'action_start_time', - 'action_complete_time', 'creation_date', 'perineural_involvement', 'intraductal_carcinoma', - 'ductal_carcinoma', 'poorly_formed_glands', 'cribriform_pattern', 'small_cell_signet_ring', - 'hypernephroid_pattern', 'mucinous', 'comedo_necrosis', 'total_gleason_4_area', 'gleason_4_percentage'] - with open(out_file, 'w') as ofile: - writer = DictWriter(ofile, delimiter=',', fieldnames=header) + header = [ + "case_id", + "slide_id", + "rois_review_step_id", + "clinical_review_step_id", + "reviewer", + "focus_region_id", + "focus_region_label", + "core_id", + "core_label", + "action_start_time", + "action_complete_time", + "creation_date", + "perineural_involvement", + "intraductal_carcinoma", + "ductal_carcinoma", + "poorly_formed_glands", + "cribriform_pattern", + "small_cell_signet_ring", + "hypernephroid_pattern", + "mucinous", + "comedo_necrosis", + "total_gleason_3_area", + "gleason_3_percentage", + "total_gleason_4_area", + "gleason_4_percentage", + "total_gleason_5_area", + "gleason_5_percentage", + ] + with open(out_file, "w") as ofile: + writer = DictWriter(ofile, delimiter=",", fieldnames=header) writer.writeheader() self._dump_data(page_size, writer) def handle(self, *args, **opts): - logger.info('=== Starting export job ===') - self._export_data(opts['output'], opts['page_size']) - logger.info('=== Data saved to %s ===', opts['output']) + logger.info("=== Starting export job ===") + self._export_data(opts["output"], opts["page_size"]) + logger.info("=== Data saved to %s ===", opts["output"]) diff --git a/promort/clinical_annotations_manager/management/commands/get_gleason_data.py b/promort/clinical_annotations_manager/management/commands/get_gleason_data.py deleted file mode 100644 index 7829875f..00000000 --- a/promort/clinical_annotations_manager/management/commands/get_gleason_data.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) 2021, CRS4 -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from django.core.management.base import BaseCommand -from django.core.paginator import Paginator -from clinical_annotations_manager.models import GleasonElement - -from csv import DictWriter - -import logging - -logger = logging.getLogger('promort_commands') - - -class Command(BaseCommand): - help = """ - Export existing Gleason items data to CSV. - No ROIs are exported, only metadata; to export ROIs use the extract_gleason_elements command. - """ - - def add_arguments(self, parser): - parser.add_argument('--output_file', dest='output', type=str, required=True, - help='path of the output CSV file') - parser.add_argument('--page_size', dest='page_size', type=int, default=0, - help='the number of records retrieved for each page (this will enable pagination)') - - def _dump_row(self, gleason_element, csv_writer): - try: - creation_start_date = gleason_element.creation_start_date.strftime('%Y-%m-%d %H:%M:%S') - except AttributeError: - creation_start_date = None - fr_ann = gleason_element.focus_region_annotation - csv_writer.writerow( - { - 'case_id': fr_ann.focus_region.core.slice.slide.case.id, - 'slide_id': fr_ann.focus_region.core.slice.slide.id, - 'rois_review_step_id': fr_ann.annotation_step.rois_review_step.label, - 'clinical_review_step_id': fr_ann.annotation_step.label, - 'reviewer': fr_ann.author.username, - 'focus_region_id': fr_ann.focus_region.id, - 'focus_region_label': fr_ann.focus_region.label, - 'core_id': fr_ann.focus_region.core.id, - 'core_label': fr_ann.focus_region.core.label, - 'gleason_element_id': gleason_element.id, - 'gleason_type': gleason_element.gleason_type, - 'creation_start_date': creation_start_date, - 'creation_date': gleason_element.creation_date.strftime('%Y-%m-%d %H:%M:%S') - } - ) - - def _dump_data(self, page_size, csv_writer): - if page_size > 0: - logger.info('Pagination enabled (%d records for page)', page_size) - ge_qs = GleasonElement.objects.get_queryset().order_by('creation_date') - paginator = Paginator(ge_qs, page_size) - for x in paginator.page_range: - logger.info(f'-- page {x} --') - page = paginator.page(x) - for ge in page.object_list: - self._dump_row(ge, csv_writer) - else: - logger.info('Loading full batch') - gleason_elements = GleasonElement.objects.all() - for ge in gleason_elements: - self._dump_row(ge, csv_writer) - - def _export_data(self, out_file, page_size): - header = ['case_id', 'slide_id', 'rois_review_step_id', 'clinical_review_step_id', 'reviewer', 'focus_region_id', - 'focus_region_label', 'core_id', 'core_label', 'gleason_element_id', 'gleason_type', - 'creation_start_date', 'creation_date'] - with open(out_file, 'w') as ofile: - writer = DictWriter(ofile, delimiter=',', fieldnames=header) - writer.writeheader() - self._dump_data(page_size, writer) - - def handle(self, *args, **opts): - logger.info('=== Starting export job ===') - self._export_data(opts['output'], opts['page_size']) - logger.info('=== Data saved to {0} ==='.format(opts['output'])) diff --git a/promort/clinical_annotations_manager/management/commands/get_gleason_patterns_data.py b/promort/clinical_annotations_manager/management/commands/get_gleason_patterns_data.py new file mode 100644 index 00000000..ba90853b --- /dev/null +++ b/promort/clinical_annotations_manager/management/commands/get_gleason_patterns_data.py @@ -0,0 +1,127 @@ +# Copyright (c) 2023, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from django.core.management.base import BaseCommand +from django.core.paginator import Paginator +from clinical_annotations_manager.models import GleasonPattern + +from csv import DictWriter + +import logging + +logger = logging.getLogger("promort_commands") + + +class Command(BaseCommand): + help = """ + Export existing Gleason Pattern data to a CSV + """ + + def add_arguments(self, parser): + parser.add_argument( + "--output_file", + dest="output", + type=str, + required=True, + help="path of the output CSV file", + ) + parser.add_argument( + "--page_size", + dest="page_size", + type=int, + default=0, + help="the number of records retrieved for each page (this will enable pagination)", + ) + + def _dump_row(self, gleason_pattern, csv_writer): + try: + action_start_time = gleason_pattern.action_start_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + except AttributeError: + action_start_time = None + try: + action_complete_time = gleason_pattern.action_complete_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + except AttributeError: + action_complete_time = None + csv_writer.writerow( + { + "case_id": gleason_pattern.annotation_step.slide.case.id, + "slide_id": gleason_pattern.annotation_step.slide.id, + "clinical_review_step_id": gleason_pattern.annotation_step.label, + "reviewer": gleason_pattern.author.username, + "focus_region_id": gleason_pattern.focus_region.id, + "focus_region_label": gleason_pattern.focus_region.label, + "action_start_time": action_start_time, + "action_completion_time": action_complete_time, + "creation_date": gleason_pattern.creation_date.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "gleason_pattern_id": gleason_pattern.id, + "gleason_pattern_label": gleason_pattern.label, + "gleason_type": gleason_pattern.gleason_type, + "area": gleason_pattern.area, + "subregions_count": gleason_pattern.subregions.count(), + } + ) + + def _dump_data(self, page_size, csv_writer): + if page_size > 0: + logger.info(f"Pagination enabled ({page_size} records for page)") + g_el_qs = GleasonPattern.objects.get_queryset().order_by("creation_date") + paginator = Paginator(g_el_qs, page_size) + for x in paginator.page_range: + logger.info(f"-- page {x} --") + page = paginator.page(x) + for g_el in page.object_list: + self._dump_row(g_el, csv_writer) + else: + logger.info("Loading full batch") + gleason_patterns = GleasonPattern.objects.all() + for g_el in gleason_patterns: + self._dump_row(g_el, csv_writer) + + def _export_data(self, out_file, page_size): + header = [ + "case_id", + "slide_id", + "clinical_review_step_id", + "reviewer", + "focus_region_id", + "focus_region_label", + "gleason_pattern_id", + "gleason_pattern_label", + "action_start_time", + "action_completion_time", + "creation_date", + "gleason_type", + "area", + "subregions_count", + ] + with open(out_file, "w") as ofile: + writer = DictWriter(ofile, delimiter=",", fieldnames=header) + writer.writeheader() + self._dump_data(page_size, writer) + + def handle(self, *args, **opts): + logger.info("=== Starting export job ===") + self._export_data(opts["output"], opts["page_size"]) + logger.info("=== Data saved to %s ===", opts["output"]) diff --git a/promort/clinical_annotations_manager/management/commands/get_gleason_subregions_data.py b/promort/clinical_annotations_manager/management/commands/get_gleason_subregions_data.py new file mode 100644 index 00000000..0b9152f3 --- /dev/null +++ b/promort/clinical_annotations_manager/management/commands/get_gleason_subregions_data.py @@ -0,0 +1,114 @@ +# Copyright (c) 2021, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from django.core.management.base import BaseCommand +from django.core.paginator import Paginator +from clinical_annotations_manager.models import GleasonPatternSubregion + +from csv import DictWriter + +import logging, json + +logger = logging.getLogger("promort_commands") + + +class Command(BaseCommand): + help = """ + Export existing Gleason Pattern subregions data to a CSV + """ + + def add_arguments(self, parser): + parser.add_argument( + "--output_file", + dest="output", + type=str, + required=True, + help="path of the output CSV file", + ) + parser.add_argument( + "--page_size", + dest="page_size", + type=int, + default=0, + help="the number of records retrieved for each page (this will enable pagination)", + ) + + def _dump_row(self, gleason_subregion, csv_writer): + gp = gleason_subregion.gleason_pattern + csv_writer.writerow( + { + "case_id": gp.annotation_step.slide.case.id, + "slide_id": gp.annotation_step.slide.id, + "clinical_review_step_id": gp.annotation_step.label, + "reviewer": gp.author.username, + "focus_region_id": gp.focus_region.id, + "gleason_pattern_id": gp.id, + "gleason_pattern_label": gp.label, + "creation_date": gleason_subregion.creation_date.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "subregion_id": gleason_subregion.id, + "subregion_label": gleason_subregion.label, + "type": json.loads(gleason_subregion.details_json)["type"], + "area": gleason_subregion.area, + } + ) + + def _dump_data(self, page_size, csv_writer): + if page_size > 0: + logger.info(f"Pagination enabled ({page_size} records for page)") + g_el_qs = GleasonPatternSubregion.objects.get_queryset().order_by( + "creation_date" + ) + paginator = Paginator(g_el_qs, page_size) + for x in paginator.page_range: + logger.info(f"-- page {x} --") + page = paginator.page(x) + for g_el in page.object_list: + self._dump_row(g_el, csv_writer) + else: + logger.info("Loading full batch") + gleason_patterns = GleasonPatternSubregion.objects.all() + for g_el in gleason_patterns: + self._dump_row(g_el, csv_writer) + + def _export_data(self, out_file, page_size): + header = [ + "case_id", + "slide_id", + "clinical_review_step_id", + "reviewer", + "focus_region_id", + "gleason_pattern_id", + "gleason_pattern_label", + "creation_date", + "subregion_id", + "subregion_label", + "type", + "area", + ] + with open(out_file, "w") as ofile: + writer = DictWriter(ofile, delimiter=",", fieldnames=header) + writer.writeheader() + self._dump_data(page_size, writer) + + def handle(self, *args, **opts): + logger.info("=== Starting export job ===") + self._export_data(opts["output"], opts["page_size"]) + logger.info(f"=== Data saved to {opts['output']}===") diff --git a/promort/clinical_annotations_manager/management/commands/get_slices_clinical_data.py b/promort/clinical_annotations_manager/management/commands/get_slices_clinical_data.py index f048dece..c316fcf1 100644 --- a/promort/clinical_annotations_manager/management/commands/get_slices_clinical_data.py +++ b/promort/clinical_annotations_manager/management/commands/get_slices_clinical_data.py @@ -25,7 +25,7 @@ import logging -logger = logging.getLogger('promort_commands') +logger = logging.getLogger("promort_commands") class Command(BaseCommand): @@ -34,69 +34,100 @@ class Command(BaseCommand): """ def add_arguments(self, parser): - parser.add_argument('--output_file', dest='output', type=str, required=True, - help='path of the output CSV file') - parser.add_argument('--page_size', dest='page_size', type=int, default=0, - help='the number of records retrieved for each page (this will enable pagination)') + parser.add_argument( + "--output_file", + dest="output", + type=str, + required=True, + help="path of the output CSV file", + ) + parser.add_argument( + "--page_size", + dest="page_size", + type=int, + default=0, + help="the number of records retrieved for each page (this will enable pagination)", + ) def _dump_data(self, page_size, csv_writer): if page_size > 0: - logger.info('Pagination enabled (%d records for page)', page_size) - sa_qs = SliceAnnotation.objects.get_queryset().order_by('creation_date') + logger.info("Pagination enabled (%d records for page)", page_size) + sa_qs = SliceAnnotation.objects.get_queryset().order_by("creation_date") paginator = Paginator(sa_qs, page_size) for x in paginator.page_range: - logger.info('-- page %d --', x) + logger.info("-- page %d --", x) page = paginator.page(x) for sa in page.object_list: self._dump_row(sa, csv_writer) else: - logger.info('Loading full batch') + logger.info("Loading full batch") slice_annotations = SliceAnnotation.objects.all() for sa in slice_annotations: self._dump_row(sa, csv_writer) def _dump_row(self, slice_annotation, csv_writer): try: - action_start_time = slice_annotation.action_start_time.strftime('%Y-%m-%d %H:%M:%S') + action_start_time = slice_annotation.action_start_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) except AttributeError: action_start_time = None try: - action_complete_time = slice_annotation.action_complete_time.strftime('%Y-%m-%d %H:%M:%S') + action_complete_time = slice_annotation.action_complete_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) except AttributeError: action_complete_time = None csv_writer.writerow( { - 'case_id': slice_annotation.slice.slide.case.id, - 'slide_id': slice_annotation.slice.slide.id, - 'rois_review_step_id': slice_annotation.annotation_step.rois_review_step.label, - 'clinical_review_step_id': slice_annotation.annotation_step.label, - 'reviewer': slice_annotation.author.username, - 'slice_id': slice_annotation.slice.id, - 'slice_label': slice_annotation.slice.label, - 'action_start_time': action_start_time, - 'action_complete_time': action_complete_time, - 'creation_date': slice_annotation.creation_date.strftime('%Y-%m-%d %H:%M:%S'), - 'high_grade_pin': slice_annotation.high_grade_pin, - 'pah': slice_annotation.pah, - 'chronic_inflammation': slice_annotation.chronic_inflammation, - 'acute_inflammation': slice_annotation.acute_inflammation, - 'periglandular_inflammation': slice_annotation.periglandular_inflammation, - 'intraglandular_inflammation': slice_annotation.intraglandular_inflammation, - 'stromal_inflammation': slice_annotation.stromal_inflammation + "case_id": slice_annotation.slice.slide.case.id, + "slide_id": slice_annotation.slice.slide.id, + "rois_review_step_id": slice_annotation.annotation_step.rois_review_step.label, + "clinical_review_step_id": slice_annotation.annotation_step.label, + "reviewer": slice_annotation.author.username, + "slice_id": slice_annotation.slice.id, + "slice_label": slice_annotation.slice.label, + "action_start_time": action_start_time, + "action_complete_time": action_complete_time, + "creation_date": slice_annotation.creation_date.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "high_grade_pin": slice_annotation.high_grade_pin, + "pah": slice_annotation.pah, + "chronic_inflammation": slice_annotation.chronic_inflammation, + "acute_inflammation": slice_annotation.acute_inflammation, + "periglandular_inflammation": slice_annotation.periglandular_inflammation, + "intraglandular_inflammation": slice_annotation.intraglandular_inflammation, + "stromal_inflammation": slice_annotation.stromal_inflammation, } ) def _export_data(self, out_file, page_size): - header = ['case_id', 'slide_id', 'rois_review_step_id', 'clinical_review_step_id', 'reviewer', - 'slice_id', 'slice_label', 'action_start_time', 'action_complete_time', 'creation_date', - 'high_grade_pin', 'pah', 'chronic_inflammation', 'acute_inflammation', 'periglandular_inflammation', - 'intraglandular_inflammation', 'stromal_inflammation'] - with open(out_file, 'w') as ofile: - writer = DictWriter(ofile, delimiter=',', fieldnames=header) + header = [ + "case_id", + "slide_id", + "rois_review_step_id", + "clinical_review_step_id", + "reviewer", + "slice_id", + "slice_label", + "action_start_time", + "action_complete_time", + "creation_date", + "high_grade_pin", + "pah", + "chronic_inflammation", + "acute_inflammation", + "periglandular_inflammation", + "intraglandular_inflammation", + "stromal_inflammation", + ] + with open(out_file, "w") as ofile: + writer = DictWriter(ofile, delimiter=",", fieldnames=header) writer.writeheader() self._dump_data(page_size, writer) def handle(self, *args, **opts): - logger.info('=== Starting export job ===') - self._export_data(opts['output'], opts['page_size']) - logger.info('=== Data saved to %s ===', opts['output']) + logger.info("=== Starting export job ===") + self._export_data(opts["output"], opts["page_size"]) + logger.info("=== Data saved to %s ===", opts["output"]) diff --git a/promort/clinical_annotations_manager/migrations/0021_auto_20221010_0718_squashed_0022_auto_20221010_0858.py b/promort/clinical_annotations_manager/migrations/0021_auto_20221010_0718_squashed_0022_auto_20221010_0858.py new file mode 100644 index 00000000..cd23633c --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0021_auto_20221010_0718_squashed_0022_auto_20221010_0858.py @@ -0,0 +1,71 @@ +# Generated by Django 3.1.13 on 2022-10-10 08:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + replaces = [('clinical_annotations_manager', '0021_auto_20221010_0718'), ('clinical_annotations_manager', '0022_auto_20221010_0858')] + + dependencies = [ + ('rois_manager', '0024_auto_20220303_1519'), + ('clinical_annotations_manager', '0020_auto_20211206_1018'), + ('reviews_manager', '0020_predictionreview'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RenameModel( + old_name='GleasonElement', + new_name='GleasonPattern', + ), + migrations.RenameField( + model_name='gleasonpattern', + old_name='json_path', + new_name='roi_json', + ), + migrations.RemoveField( + model_name='gleasonpattern', + name='cells_count', + ), + migrations.RemoveField( + model_name='gleasonpattern', + name='cellular_density', + ), + migrations.RemoveField( + model_name='gleasonpattern', + name='cellular_density_helper_json', + ), + migrations.AddField( + model_name='gleasonpattern', + name='annotation_step', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gleason_annotations', to='reviews_manager.clinicalannotationstep'), + ), + migrations.AddField( + model_name='gleasonpattern', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='gleasonpattern', + name='details_json', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='gleasonpattern', + name='focus_region', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gleason_patterns', to='rois_manager.focusregion'), + ), + migrations.AddField( + model_name='gleasonpattern', + name='label', + field=models.CharField(blank=True, max_length=25, null=True), + ), + migrations.AlterField( + model_name='gleasonpattern', + name='gleason_type', + field=models.CharField(choices=[('G1', 'GLEASON 1'), ('G2', 'GLEASON 2'), ('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), ('G5', 'GLEASON 5'), ('ST', 'STROMA'), ('OT', 'OTHER')], max_length=2), + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0022_auto_20221010_0905.py b/promort/clinical_annotations_manager/migrations/0022_auto_20221010_0905.py new file mode 100644 index 00000000..bc2e3839 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0022_auto_20221010_0905.py @@ -0,0 +1,64 @@ +# Copyright (c) 2022, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generated by Django 3.1.13 on 2022-10-10 09:05 + +from django.db import migrations +from collections import Counter +import json + + +def update_gleason_patterns(apps, schema_editor): + FocusRegionAnnotation = apps.get_model( + "clinical_annotations_manager", "FocusRegionAnnotation" + ) + gleason_patterns_counter = Counter() + for fr in FocusRegionAnnotation.objects.all(): + for gleason_element in fr.gleason_elements.all(): + gleason_patterns_counter[fr.annotation_step.label] += 1 + gleason_element.label = "gleason_{0}".format(gleason_patterns_counter[fr.annotation_step.label]) + gleason_element.focus_region = ( + gleason_element.focus_region_annotation.focus_region + ) + gleason_element.annotation_step = ( + gleason_element.focus_region_annotation.annotation_step + ) + gleason_element.author = gleason_element.focus_region_annotation.author + if gleason_element.gleason_type in ("G1", "G2"): + gleason_element.details_json = json.dumps( + { + "notes": "previously classified as {0}".format( + gleason_element.gleason_type + ) + } + ) + gleason_element.gleason_type = "OT" + gleason_element.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "clinical_annotations_manager", + "0021_auto_20221010_0718_squashed_0022_auto_20221010_0858", + ), + ] + + operations = [migrations.RunPython(update_gleason_patterns)] diff --git a/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py b/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py new file mode 100644 index 00000000..ed2fc06f --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py @@ -0,0 +1,70 @@ +# Copyright (c) 2022, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generated by Django 3.1.13 on 2022-10-10 14:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('reviews_manager', '0020_predictionreview'), + ('rois_manager', '0024_auto_20220303_1519'), + ('clinical_annotations_manager', '0022_auto_20221010_0905'), + ] + + operations = [ + migrations.AlterField( + model_name='gleasonpattern', + name='annotation_step', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='gleason_annotations', to='reviews_manager.clinicalannotationstep'), + ), + migrations.AlterField( + model_name='gleasonpattern', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='auth.user'), + ), + migrations.AlterField( + model_name='gleasonpattern', + name='focus_region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='gleason_patterns', to='rois_manager.focusregion'), + ), + migrations.AlterField( + model_name='gleasonpattern', + name='gleason_type', + field=models.CharField(choices=[('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), ('G5', 'GLEASON 5'), ('ST', 'STROMA'), ('OT', 'OTHER')], max_length=2), + ), + migrations.AlterField( + model_name='gleasonpattern', + name='label', + field=models.CharField(max_length=25), + ), + migrations.AlterUniqueTogether( + name='gleasonpattern', + unique_together={('label', 'annotation_step')}, + ), + migrations.RemoveField( + model_name='gleasonpattern', + name='focus_region_annotation', + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py b/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py new file mode 100644 index 00000000..d517b66e --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py @@ -0,0 +1,45 @@ +# Copyright (c) 2022, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generated by Django 3.1.13 on 2022-10-10 14:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0023_auto_20221010_1405'), + ] + + operations = [ + migrations.CreateModel( + name='GleasonPatternSubregion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=25)), + ('roi_json', models.TextField()), + ('area', models.FloatField()), + ('details_json', models.TextField(blank=True, default=None, null=True)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('gleason_pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subregions', to='clinical_annotations_manager.gleasonpattern')), + ], + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0025_auto_20221031_0938.py b/promort/clinical_annotations_manager/migrations/0025_auto_20221031_0938.py new file mode 100644 index 00000000..5553a992 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0025_auto_20221031_0938.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generated by Django 3.1.13 on 2022-10-31 09:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0024_gleasonpatternsubregion'), + ] + + operations = [ + migrations.AlterField( + model_name='gleasonpattern', + name='gleason_type', + field=models.CharField(choices=[('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), ('G5', 'GLEASON 5'), ('ST', 'STROMA'), ('OT', 'OTHER'), ('LG', 'LEGACY')], max_length=2), + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0026_auto_20221031_0938.py b/promort/clinical_annotations_manager/migrations/0026_auto_20221031_0938.py new file mode 100644 index 00000000..cbc8d826 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0026_auto_20221031_0938.py @@ -0,0 +1,43 @@ +# Copyright (c) 2022, CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generated by Django 3.1.13 on 2022-10-31 09:38 + +from django.db import migrations + + +def update_legacy_gleason_patterns(apps, schema_editor): + GleasonPattern = apps.get_model( + "clinical_annotations_manager", "GleasonPattern" + ) + legacy_gleason_patterns = GleasonPattern.objects.filter(gleason_type="OT") + for lgp in legacy_gleason_patterns: + lgp.gleason_type="LG" + lgp.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0025_auto_20221031_0938'), + ] + + operations = [ + migrations.RunPython(update_legacy_gleason_patterns) + ] diff --git a/promort/clinical_annotations_manager/migrations/0027_auto_20221031_1033.py b/promort/clinical_annotations_manager/migrations/0027_auto_20221031_1033.py new file mode 100644 index 00000000..fdaea0f1 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0027_auto_20221031_1033.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-10-31 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0026_auto_20221031_0938'), + ] + + operations = [ + migrations.AlterField( + model_name='gleasonpattern', + name='gleason_type', + field=models.CharField(choices=[('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), ('G5', 'GLEASON 5'), ('LG', 'LEGACY')], max_length=2), + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0028_auto_20221107_1442.py b/promort/clinical_annotations_manager/migrations/0028_auto_20221107_1442.py new file mode 100644 index 00000000..c0fbbf4a --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0028_auto_20221107_1442.py @@ -0,0 +1,73 @@ +# Generated by Django 3.1.13 on 2022-11-07 14:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0027_auto_20221031_1033'), + ] + + operations = [ + migrations.AddField( + model_name='coreannotation', + name='central_maturation', + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='extra_cribriform_gleason_score', + field=models.CharField(default=None, max_length=11, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='extraprostatic_extension', + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='highest_rsg', + field=models.CharField(default=None, max_length=1, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='intraluminal_acinar_differentiation_grade', + field=models.CharField(default=None, max_length=1, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='intraluminal_secretions', + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='nuclear_grade_size', + field=models.CharField(default=None, max_length=1, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='perineural_growth_with_cribriform_patterns', + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='perineural_invasion', + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='predominant_rsg', + field=models.CharField(default=None, max_length=1, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='rsg_in_area_of_cribriform_morphology', + field=models.CharField(default=None, max_length=1, null=True), + ), + migrations.AddField( + model_name='coreannotation', + name='rsg_within_highest_grade_area', + field=models.CharField(default=None, max_length=1, null=True), + ), + ] diff --git a/promort/clinical_annotations_manager/migrations/0029_auto_20230531_1444.py b/promort/clinical_annotations_manager/migrations/0029_auto_20230531_1444.py new file mode 100644 index 00000000..1616bb55 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0029_auto_20230531_1444.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.13 on 2023-05-31 14:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clinical_annotations_manager', '0028_auto_20221107_1442'), + ] + + operations = [ + migrations.AlterField( + model_name='coreannotation', + name='gleason_group', + field=models.CharField(choices=[('GG1', 'GRADE_GROUP_1'), ('GG2', 'GRADE_GROUP_2'), ('GG3', 'GRADE_GROUP_3'), ('GG4', 'GRADE_GROUP_4'), ('GG5', 'GRADE_GROUP_5')], default=None, max_length=3, null=True), + ), + migrations.AlterField( + model_name='coreannotation', + name='primary_gleason', + field=models.IntegerField(default=None, null=True), + ), + migrations.AlterField( + model_name='coreannotation', + name='secondary_gleason', + field=models.IntegerField(default=None, null=True), + ), + ] diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index 052da71c..c9cdad11 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -23,6 +23,8 @@ from reviews_manager.models import ClinicalAnnotationStep from rois_manager.models import Slice, Core, FocusRegion +from collections import Counter + class SliceAnnotation(models.Model): author = models.ForeignKey(User, on_delete=models.PROTECT, blank=False) @@ -95,15 +97,97 @@ class CoreAnnotation(models.Model): action_start_time = models.DateTimeField(null=True, default=None) action_complete_time = models.DateTimeField(null=True, default=None) creation_date = models.DateTimeField(default=timezone.now) - primary_gleason = models.IntegerField(blank=False) - secondary_gleason = models.IntegerField(blank=False) + primary_gleason = models.IntegerField(null=True, default=None) + secondary_gleason = models.IntegerField(null=True, default=None) gleason_group = models.CharField( - max_length=3, choices=GLEASON_GROUP_WHO_16, blank=False + max_length=3, choices=GLEASON_GROUP_WHO_16, null=True, default=None ) + # acquire ONLY if at least one Cribriform Pattern (under GleasonPattern type 4) exists + nuclear_grade_size = models.CharField(max_length=1, null=True, default=None) + intraluminal_acinar_differentiation_grade = models.CharField(max_length=1, null=True, default=None) + intraluminal_secretions = models.BooleanField(null=True, default=None) + central_maturation = models.BooleanField(null=True, default=None) + extra_cribriform_gleason_score = models.CharField(max_length=11, null=True, default=None) + # stroma + predominant_rsg = models.CharField(max_length=1, null=True, default=None) + highest_rsg = models.CharField(max_length=1, null=True, default=None) + rsg_within_highest_grade_area = models.CharField(max_length=1, null=True, default=None) + rsg_in_area_of_cribriform_morphology = models.CharField(max_length=1, null=True, default=None) + # other + perineural_invasion = models.BooleanField(null=True, default=None) + perineural_growth_with_cribriform_patterns = models.BooleanField(null=True, default=None) + extraprostatic_extension = models.BooleanField(null=True, default=None) class Meta: unique_together = ('core', 'annotation_step') + def _get_gleason_elements(self): + gleason_elements = list() + for fr in self.core.focus_regions.all(): + gleason_elements.extend( + GleasonPattern.objects.filter( + focus_region = fr, + annotation_step = self.annotation_step + ).all() + ) + return gleason_elements + + def _get_gleason_coverage(self): + g_elems = self._get_gleason_elements() + total_gleason_area = 0 + gleason_patterns_area = Counter() + for g_el in g_elems: + total_gleason_area += g_el.area + gleason_patterns_area[g_el.gleason_type] += g_el.area + gleason_coverage = dict() + for gp, gpa in gleason_patterns_area.items(): + gleason_coverage[gp] = (100 * gpa/total_gleason_area) + return gleason_coverage + + def _get_primary_and_secondary_gleason(self): + gleason_coverage = self._get_gleason_coverage() + if len(gleason_coverage) == 0: + return None, None + primary_gleason = max(gleason_coverage, key=gleason_coverage.get) + gleason_coverage.pop(primary_gleason) + if len(gleason_coverage) == 0: + secondary_gleason = primary_gleason + else: + secondary_gleason = max(gleason_coverage) + return primary_gleason, secondary_gleason + + def get_primary_gleason(self): + if self.primary_gleason is None: + primary_gleason, _ = self._get_primary_and_secondary_gleason() + return primary_gleason + else: + return self.primary_gleason + + def get_secondary_gleason(self): + if self.secondary_gleason is None: + _, secondary_gleason = self._get_primary_and_secondary_gleason() + return secondary_gleason + else: + return self.secondary_gleason + + def get_gleason_group(self): + if self.gleason_group is None: + primary_gleason, secondary_gleason = self._get_primary_and_secondary_gleason() + gleason_score = int(primary_gleason.replace('G', '')) + int(secondary_gleason.replace('G', '')) + if gleason_score <= 6: + return 'GG1' + elif gleason_score == 7: + if primary_gleason == 'G3': + return 'GG2' + else: + return 'GG3' + elif gleason_score == 8: + return 'GG4' + else: + return 'GG5' + else: + return self.gleason_group + def get_gleason_4_total_area(self): gleason_4_total_area = 0.0 for focus_region in self.core.focus_regions.all(): @@ -129,9 +213,27 @@ def get_gleason_4_percentage(self): return -1 def get_grade_group_text(self): + gleason_group = self.get_gleason_group() for choice in self.GLEASON_GROUP_WHO_16: - if choice[0] == self.gleason_group: + if choice[0] == gleason_group: return choice[1] + + def get_gleason_patterns_details(self): + gleason_elements = self._get_gleason_elements() + gleason_total_areas = Counter() + gleason_shapes = dict() + for ge in gleason_elements: + gleason_total_areas[ge.gleason_type] += ge.area + gleason_shapes.setdefault(ge.gleason_type, []).append(ge.label) + gleason_coverage = self._get_gleason_coverage() + gleason_details = {} + for gtype in gleason_shapes.keys(): + gleason_details[gtype] = { + "shapes": gleason_shapes[gtype], + "total_area": gleason_total_areas[gtype], + "total_coverage": round(gleason_coverage[gtype], 2) + } + return gleason_details def get_action_duration(self): if self.action_start_time and self.action_complete_time: @@ -139,6 +241,14 @@ def get_action_duration(self): else: return None + def get_largest_confluent_sheet(self): + # TODO: get largest cribriform object among all Gleason 4 elements of a core + return None + + def get_total_cribriform_area(self): + # TODO: sum of all cribriform objects defined on a core + return None + class FocusRegionAnnotation(models.Model): author = models.ForeignKey(User, on_delete=models.PROTECT, blank=False) @@ -172,14 +282,33 @@ class FocusRegionAnnotation(models.Model): class Meta: unique_together = ('focus_region', 'annotation_step') + def get_gleason_elements(self): + gleason_elements_map = dict() + for gp in self.annotation_step.gleason_annotations.filter(focus_region=self.focus_region).all(): + gleason_elements_map.setdefault(gp.gleason_type, []).append(gp) + return gleason_elements_map + + def get_gleason_4_elements(self): + return self.get_gleason_elements().get("G4", []) + + def get_total_gleason_area(self, gleason_pattern): + gleason_area = 0 + for g in self.get_gleason_elements().get(gleason_pattern, []): + gleason_area += g.area + return gleason_area + def get_total_gleason_4_area(self): g4_area = 0 for g4 in self.get_gleason_4_elements(): g4_area += g4.area return g4_area - def get_gleason_4_elements(self): - return self.gleason_elements.filter(gleason_type='G4') + def get_gleason_percentage(self, gleason_pattern): + gleason_area = self.get_total_gleason_area(gleason_pattern) + try: + return (gleason_area / self.focus_region.area) * 100.0 + except ZeroDivisionError: + return -1 def get_gleason_4_percentage(self): g4_area = self.get_total_gleason_4_area() @@ -195,25 +324,29 @@ def get_action_duration(self): return None -class GleasonElement(models.Model): +class GleasonPattern(models.Model): GLEASON_TYPES = ( - ('G1', 'GLEASON 1'), - ('G2', 'GLEASON 2'), ('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), - ('G5', 'GLEASON 5') + ('G5', 'GLEASON 5'), + ('LG', 'LEGACY') ) - focus_region_annotation = models.ForeignKey(FocusRegionAnnotation, related_name='gleason_elements', - blank=False, on_delete=models.CASCADE) + label = models.CharField(max_length=25, blank=False) + focus_region = models.ForeignKey(FocusRegion, related_name="gleason_patterns", blank=False, + on_delete=models.PROTECT) + annotation_step = models.ForeignKey(ClinicalAnnotationStep, on_delete=models.PROTECT, + blank=False, related_name="gleason_annotations") + author = models.ForeignKey(User, blank=False, on_delete=models.PROTECT) gleason_type = models.CharField(max_length=2, choices=GLEASON_TYPES, blank=False, null=False) - json_path = models.TextField(blank=False, null=False) + roi_json = models.TextField(blank=False, null=False) + details_json = models.TextField(blank=True, null=True) area = models.FloatField(blank=False, null=False) - cellular_density_helper_json = models.TextField(blank=True, null=True) - cellular_density = models.IntegerField(blank=True, null=True) - cells_count = models.IntegerField(blank=True, null=True) action_start_time = models.DateTimeField(null=True, default=None) action_complete_time = models.DateTimeField(null=True, default=None) creation_date = models.DateTimeField(default=timezone.now) + + class Meta: + unique_together = ('label', 'annotation_step') def get_gleason_type_label(self): for choice in self.GLEASON_TYPES: @@ -225,3 +358,13 @@ def get_action_duration(self): return (self.action_complete_time-self.action_start_time).total_seconds() else: return None + + +class GleasonPatternSubregion(models.Model): + gleason_pattern = models.ForeignKey(GleasonPattern, related_name="subregions", blank=False, + on_delete=models.CASCADE) + label = models.CharField(max_length=25, blank=False) + roi_json = models.TextField(blank=False, null=False) + area = models.FloatField(blank=False, null=False) + details_json = models.TextField(blank=True, null=True, default=None) + creation_date = models.DateTimeField(auto_now_add=True) diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 97365b7c..ea09f5c5 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -28,7 +28,7 @@ from rois_manager.models import Slice, Core, FocusRegion from clinical_annotations_manager.models import SliceAnnotation, CoreAnnotation, \ - FocusRegionAnnotation, GleasonElement + FocusRegionAnnotation, GleasonPattern, GleasonPatternSubregion from rois_manager.serializers import SliceSerializer, CoreSerializer, FocusRegionSerializer @@ -71,26 +71,71 @@ class CoreAnnotationSerializer(serializers.ModelSerializer): ) gleason_score = serializers.SerializerMethodField() gleason_4_percentage = serializers.SerializerMethodField() + largest_confluent_sheet = serializers.SerializerMethodField() + total_cribriform_area = serializers.SerializerMethodField() class Meta: model = CoreAnnotation fields = ('id', 'author', 'core', 'annotation_step', 'action_start_time', 'action_complete_time', - 'creation_date', 'primary_gleason', 'secondary_gleason', 'gleason_score', - 'gleason_4_percentage', 'gleason_group') - read_only_fields = ('id', 'creation_date', 'gleason_score', 'gleason_4_percentage') + 'creation_date', 'gleason_score', 'gleason_4_percentage', 'nuclear_grade_size', + 'intraluminal_acinar_differentiation_grade', 'intraluminal_secretions', + 'central_maturation', 'extra_cribriform_gleason_score', + 'largest_confluent_sheet', 'total_cribriform_area', 'predominant_rsg', + 'highest_rsg', 'rsg_within_highest_grade_area', 'rsg_in_area_of_cribriform_morphology', + 'perineural_invasion', 'perineural_growth_with_cribriform_patterns', + 'extraprostatic_extension') + read_only_fields = ('id', 'creation_date', 'gleason_score', 'gleason_4_percentage', 'largest_confluent_sheet', + 'total_cribriform_area') write_only_fields = ('annotation_step',) @staticmethod def get_gleason_score(obj): - return '%d + %d' % (obj.primary_gleason, obj.secondary_gleason) + return '{0} + {1}'.format(*obj._get_primary_and_secondary_gleason()) @staticmethod def get_gleason_4_percentage(obj): return obj.get_gleason_4_percentage() + + @staticmethod + def get_largest_confluent_sheet(obj): + return obj.get_largest_confluent_sheet() + + @staticmethod + def get_total_cribriform_area(obj): + return obj.get_total_cribriform_area() class CoreAnnotationDetailsSerializer(CoreAnnotationSerializer): core = CoreSerializer(read_only=True) + + primary_gleason = serializers.SerializerMethodField() + secondary_gleason = serializers.SerializerMethodField() + gleason_group = serializers.SerializerMethodField() + details = serializers.SerializerMethodField() + + class Meta: + model = CoreAnnotation + fields = CoreAnnotationSerializer.Meta.fields + ('primary_gleason', 'secondary_gleason', + 'gleason_group', 'details') + + read_only_fields = CoreAnnotationSerializer.Meta.read_only_fields + ('primary_gleason', 'secondary_gleason', + 'gleason_group', 'details') + + @staticmethod + def get_primary_gleason(obj): + return obj.get_primary_gleason() + + @staticmethod + def get_secondary_gleason(obj): + return obj.get_secondary_gleason() + + @staticmethod + def get_gleason_group(obj): + return obj.get_gleason_group() + + @staticmethod + def get_details(obj): + return obj.get_gleason_patterns_details() class CoreAnnotationInfosSerializer(serializers.ModelSerializer): @@ -100,62 +145,92 @@ class Meta: read_only_fields = ('id', 'annotation_step') -class GleasonElementSerializer(serializers.ModelSerializer): - gleason_label = serializers.SerializerMethodField() +class FocusRegionAnnotationSerializer(serializers.ModelSerializer): + author = serializers.SlugRelatedField( + slug_field='username', + queryset=User.objects.all() + ) class Meta: - model = GleasonElement - fields = ('id', 'gleason_type', 'gleason_label', 'json_path', 'area', - 'cellular_density_helper_json', 'cellular_density', 'cells_count', - 'creation_date', 'action_start_time', 'action_complete_time') - read_only_fields = ('gleason_label',) + model = FocusRegionAnnotation + fields = ('id', 'author', 'focus_region', 'annotation_step', 'action_start_time', 'action_complete_time', + 'creation_date', 'perineural_involvement', 'intraductal_carcinoma', 'ductal_carcinoma', + 'poorly_formed_glands', 'cribriform_pattern', 'small_cell_signet_ring', 'hypernephroid_pattern', + 'mucinous', 'comedo_necrosis', 'inflammation', 'pah', 'atrophic_lesions', 'adenosis', + 'cellular_density_helper_json', 'cellular_density', 'cells_count') + read_only_fields = ('id', 'creation_date') + write_only_fields = ('annotation_step', 'author') - @staticmethod - def get_gleason_label(obj): - return obj.get_gleason_type_label() +class GleasonPatternSubregionSerializer(serializers.ModelSerializer): + class Meta: + model = GleasonPatternSubregion + fields = ('id', 'gleason_pattern', 'label', 'roi_json', 'area', 'details_json', 'creation_date') + read_only_fields = ('id', 'creation_date', 'gleason_pattern') + @staticmethod - def validate_json_path(value): + def validate_roi_json(value): try: json.loads(value) return value except ValueError: - raise serializers.ValidationError('Not a valid JSON in \'json_path\' field') - + raise serializers.ValidationError('Not a valid JSON in \'roi_json\' field') + @staticmethod - def validate_cellular_density_helper_json(value): + def validate_details_json(value): if value is None: return value try: json.loads(value) return value except ValueError: - raise serializers.ValidationError('Not a valid JSON in \'cellular_density_helper_json\' field') + raise serializers.ValidationError('Not a valid JSON in \'details_json\' field') -class FocusRegionAnnotationSerializer(serializers.ModelSerializer): +class GleasonPatternSerializer(serializers.ModelSerializer): author = serializers.SlugRelatedField( slug_field='username', queryset=User.objects.all() ) - gleason_elements = GleasonElementSerializer(many=True) - + gleason_label = serializers.SerializerMethodField() + subregions = GleasonPatternSubregionSerializer(many=True) + class Meta: - model = FocusRegionAnnotation - fields = ('id', 'author', 'focus_region', 'annotation_step', 'action_start_time', 'action_complete_time', - 'creation_date', 'perineural_involvement', 'intraductal_carcinoma', 'ductal_carcinoma', - 'poorly_formed_glands', 'cribriform_pattern', 'small_cell_signet_ring', 'hypernephroid_pattern', - 'mucinous', 'comedo_necrosis', 'inflammation', 'pah', 'atrophic_lesions', 'adenosis', - 'cellular_density_helper_json', 'cellular_density', 'cells_count', 'gleason_elements') - read_only_fields = ('creation_date',) - write_only_fields = ('id', 'annotation_step', 'gleason_elements', 'author') - + model = GleasonPattern + fields = ('id', 'label', 'focus_region', 'annotation_step', 'author', 'gleason_type', 'gleason_label', + 'roi_json', 'details_json', 'area', 'subregions', + 'action_start_time', 'action_complete_time', 'creation_date') + read_only_fields = ('id', 'creation_date', 'gleason_label') + write_only_fields = ('annotation_step', 'author') + def create(self, validated_data): - gleason_elements_data = validated_data.pop('gleason_elements') - annotation = FocusRegionAnnotation.objects.create(**validated_data) - for element_data in gleason_elements_data: - GleasonElement.objects.create(focus_region_annotation=annotation, **element_data) - return annotation + gleason_subregions_data = validated_data.pop('subregions') + gleason_pattern_obj = GleasonPattern.objects.create(**validated_data) + for subregion_data in gleason_subregions_data: + GleasonPatternSubregion.objects.create(gleason_pattern=gleason_pattern_obj, **subregion_data) + return gleason_pattern_obj + + @staticmethod + def get_gleason_label(obj): + return obj.get_gleason_type_label() + + @staticmethod + def validate_roi_json(value): + try: + json.loads(value) + return value + except ValueError: + raise serializers.ValidationError('Not a valid JSON in \'roi_json\' field') + + @staticmethod + def validate_details_json(value): + if value is None: + return value + try: + json.loads(value) + return value + except ValueError: + raise serializers.ValidationError('Not a valid JSON in \'details_json\' field') class FocusRegionAnnotationDetailsSerializer(FocusRegionAnnotationSerializer): @@ -169,13 +244,21 @@ class Meta: read_only_fields = ('id', 'annotation_step') +class AnnotatedGleasonPatternSerializer(serializers.ModelSerializer): + class Meta: + model = GleasonPattern + fields = ('id', 'label', 'focus_region', 'roi_json', 'area', + 'annotation_step') + + class AnnotatedFocusRegionSerializer(serializers.ModelSerializer): clinical_annotations = FocusRegionAnnotationInfosSerializer(many=True) + gleason_patterns = AnnotatedGleasonPatternSerializer(many=True) class Meta: model = FocusRegion fields = ('id', 'label', 'core', 'roi_json', 'length', 'area', - 'tissue_status', 'clinical_annotations') + 'tissue_status', 'gleason_patterns', 'clinical_annotations') read_only_fields = fields diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index baf4f7c3..59163610 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -21,20 +21,24 @@ import simplejson as json except ImportError: import json +from collections import Counter from rest_framework.views import APIView -from rest_framework import permissions, status +from rest_framework import permissions, status, exceptions from rest_framework.response import Response from rest_framework.exceptions import NotFound from django.db import IntegrityError +from view_templates.views import GenericDetailView +from rois_manager.models import Core +from rois_manager.serializers import CoreSerializer from reviews_manager.models import ROIsAnnotationStep, ClinicalAnnotationStep from reviews_manager.serializers import ClinicalAnnotationStepROIsTreeSerializer -from clinical_annotations_manager.models import SliceAnnotation, CoreAnnotation, FocusRegionAnnotation +from clinical_annotations_manager.models import SliceAnnotation, CoreAnnotation, FocusRegionAnnotation, GleasonPattern from clinical_annotations_manager.serializers import SliceAnnotationSerializer, SliceAnnotationDetailsSerializer,\ CoreAnnotationSerializer, CoreAnnotationDetailsSerializer, FocusRegionAnnotationSerializer, \ - FocusRegionAnnotationDetailsSerializer + FocusRegionAnnotationDetailsSerializer, GleasonPatternSerializer import logging logger = logging.getLogger('promort') @@ -50,14 +54,21 @@ def _get_clinical_annotation_step_id(self, clinical_annotation_step_label): except ClinicalAnnotationStep.DoesNotExist: raise NotFound('There is no Clinical Annotation step with label \'%s\'' % clinical_annotation_step_label) - def _update_annotation(self, roi_data, clinical_annotation_step_label): - clinical_annotation_id = self._get_clinical_annotation_step_id(clinical_annotation_step_label) + def _update_annotation(self, roi_data, clinical_annotation_step_id): annotation_status = {'annotated': False} annotations = roi_data.pop('clinical_annotations') for annotation in annotations: - if annotation['annotation_step'] == int(clinical_annotation_id): + if annotation['annotation_step'] == int(clinical_annotation_step_id): annotation_status['annotated'] = True roi_data.update(annotation_status) + + def _prepare_gleason_patterns(self, gleason_patterns, clinical_annotation_step_id): + filtered_gp = list() + for gp in gleason_patterns: + if gp['annotation_step'] == int(clinical_annotation_step_id): + gp['annotated'] = True + filtered_gp.append(gp) + return filtered_gp def get(self, request, rois_annotation_step, clinical_annotation_step, format=None): try: @@ -65,13 +76,18 @@ def get(self, request, rois_annotation_step, clinical_annotation_step, format=No except ROIsAnnotationStep.DoesNotExist: raise NotFound('There is no ROIsAnnotationStep with ID %s' % rois_annotation_step) serializer = ClinicalAnnotationStepROIsTreeSerializer(obj) + clinical_annotation_step_id = self._get_clinical_annotation_step_id(clinical_annotation_step) rois_tree = serializer.data for slice in rois_tree['slices']: - self._update_annotation(slice, clinical_annotation_step) + self._update_annotation(slice, clinical_annotation_step_id) for core in slice['cores']: - self._update_annotation(core, clinical_annotation_step) + self._update_annotation(core, clinical_annotation_step_id) for focus_region in core['focus_regions']: - self._update_annotation(focus_region, clinical_annotation_step) + self._update_annotation(focus_region, clinical_annotation_step_id) + if len(focus_region['gleason_patterns']) > 0: + focus_region['gleason_patterns'] = self._prepare_gleason_patterns( + focus_region['gleason_patterns'], clinical_annotation_step_id + ) return Response(rois_tree, status=status.HTTP_200_OK) @@ -214,6 +230,77 @@ def delete(self, request, core_id, label, format=None): return Response(status=status.HTTP_204_NO_CONTENT) +class CoreGleasonDetail(APIView): + permissions = (permissions.IsAuthenticated,) + + def _get_gleason_elements(self, core_obj, annotation_step_label): + gleason_elements = list() + for fr in core_obj.focus_regions.all(): + gleason_elements.extend( + GleasonPattern.objects.filter( + focus_region=fr, + annotation_step__label=annotation_step_label + ).all() + ) + return gleason_elements + + def _get_gleason_coverage(self, gleason_patterns_area): + total_area = sum(gleason_patterns_area.values()) + gleason_coverage = dict() + for gp, gpa in gleason_patterns_area.items(): + gleason_coverage[gp] = (100 * gpa/total_area) + return gleason_coverage + + def _get_primary_and_secondary_gleason(self, gleason_coverage): + if len(gleason_coverage) == 0: + return None, None + primary_gleason = max(gleason_coverage, key=gleason_coverage.get) + gleason_coverage.pop(primary_gleason) + if len(gleason_coverage) == 0: + secondary_gleason = primary_gleason + else: + secondary_gleason = max(gleason_coverage) + return primary_gleason, secondary_gleason + + def _get_gleason_details(self, core_obj, annotation_step_label): + gleason_elements = self._get_gleason_elements(core_obj, annotation_step_label) + gleason_total_area = Counter() + gleason_shapes = dict() + gleason_subregions = dict() + for ge in gleason_elements: + gleason_total_area[ge.gleason_type] += ge.area + gleason_shapes.setdefault(ge.gleason_type, []).append(ge.label) + gleason_subregions.setdefault(ge.gleason_type, set()) + for subr in ge.subregions.all(): + gleason_subregions[ge.gleason_type].add(json.loads(subr.details_json)["type"]) + gleason_coverage = self._get_gleason_coverage(gleason_total_area) + gleason_details = {"details": {}} + for gtype in gleason_shapes.keys(): + gleason_details["details"][gtype] = { + "shapes": gleason_shapes[gtype], + "total_area": gleason_total_area[gtype], + "total_coverage": round(gleason_coverage[gtype], 2), + "subregions": gleason_subregions[gtype] + } + primary_gleason, secondary_gleason = self._get_primary_and_secondary_gleason(gleason_coverage) + gleason_details.update({ + "primary_gleason": primary_gleason, + "secondary_gleason": secondary_gleason + }) + return gleason_details + + def get(self, request, core_id, label, format=None): + try: + core = Core.objects.get(pk__iexact=core_id) + except Core.DoesNotExist: + raise NotFound('There is no Core with ID {0}'.format(core_id)) + gleason_details = self._get_gleason_details(core, label) + core_data = CoreSerializer(core).data + core_data.update(gleason_details) + core_data.pop("roi_json") + return Response(core_data, status=status.HTTP_200_OK) + + class FocusRegionAnnotationList(APIView): permissions = (permissions.IsAuthenticated,) @@ -240,23 +327,11 @@ def get(self, request, focus_region_id, label, format=None): serializer = FocusRegionAnnotationDetailsSerializer(focus_region_annotation) return Response(serializer.data, status=status.HTTP_200_OK) - def _prepare_gleason_elements(self, gleason_elements): - for element in gleason_elements: - element['json_path'] = json.dumps(element['json_path']) - try: - element['cellular_density_helper_json'] = json.dumps(element['cellular_density_helper_json']) - except KeyError: - element['cellular_density_helper_json'] = None - return gleason_elements - def post(self, request, focus_region_id, label, format=None): focus_region_annotation_data = request.data focus_region_annotation_data['focus_region'] = focus_region_id focus_region_annotation_data['annotation_step'] = self._get_clinical_annotation_step_id(label) focus_region_annotation_data['author'] = request.user.username - if focus_region_annotation_data.get('gleason_elements'): - focus_region_annotation_data['gleason_elements'] = \ - self._prepare_gleason_elements(focus_region_annotation_data['gleason_elements']) if focus_region_annotation_data.get('cellular_density_helper_json'): focus_region_annotation_data['cellular_density_helper_json'] = \ json.dumps(focus_region_annotation_data['cellular_density_helper_json']) @@ -283,3 +358,42 @@ def delete(self, request, focus_region_id, label, format=None): 'message': 'unable to complete delete operation, there are still references to this object' }, status=status.HTTP_409_CONFLICT) return Response(status=status.HTTP_204_NO_CONTENT) + + +class GleasonPatternList(ClinicalAnnotationStepObject): + permissions = (permissions.IsAuthenticated,) + + def get(self, request, focus_region_id, label, format=None): + gleason_patterns = GleasonPattern.objects.filter( + focus_region=focus_region_id, annotation_step__label=label + ) + serializer = GleasonPatternSerializer(gleason_patterns, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, focus_region_id, label, format=None): + gleason_pattern_data = request.data + gleason_pattern_data['focus_region'] = focus_region_id + gleason_pattern_data['annotation_step'] = self._get_clinical_annotation_step_id(label) + gleason_pattern_data['author'] = request.user.username + serializer = GleasonPatternSerializer(data=gleason_pattern_data) + if serializer.is_valid(): + try: + serializer.save() + except IntegrityError: + return Response({ + 'status': 'ERROR', + 'message': 'duplicated gleason pattern label {0} for annotation step {1}'.format( + gleason_pattern_data['label'], label + ) + }, status=status.HTTP_409_CONFLICT) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class GleasonPatternDetail(GenericDetailView): + model = GleasonPattern + model_serializer = GleasonPatternSerializer + permission_classes = (permissions.IsAuthenticated,) + + def put(self, request, pk, format=None): + raise exceptions.MethodNotAllowed(method='put') diff --git a/promort/predictions_manager/management/commands/tissue_to_rois.py b/promort/predictions_manager/management/commands/tissue_to_rois.py index d96d2714..076267a3 100644 --- a/promort/predictions_manager/management/commands/tissue_to_rois.py +++ b/promort/predictions_manager/management/commands/tissue_to_rois.py @@ -155,6 +155,8 @@ def _load_annotation_steps(self, reviewer=None): filter_["rois_annotation__reviewer__username"] = reviewer annotations_steps = ROIsAnnotationStep.objects.filter(**filter_) + logger.info("Filtering annotation steps previously processed but not started yet") + annotations_steps = [ann for ann in annotations_steps if ann.slices.count() == 0] logger.info("Loaded %d ROIs annotation steps" % len(annotations_steps)) return annotations_steps diff --git a/promort/promort/urls.py b/promort/promort/urls.py index 95f03800..49c8f669 100644 --- a/promort/promort/urls.py +++ b/promort/promort/urls.py @@ -34,8 +34,8 @@ from rois_manager.views import SlideROIsList, SliceList, SliceDetail, CoreList, \ CoreDetail, FocusRegionList, FocusRegionDetail, ROIsTreeList from clinical_annotations_manager.views import AnnotatedROIsTreeList, ClinicalAnnotationStepAnnotationsList, \ - SliceAnnotationList, SliceAnnotationDetail, CoreAnnotationList, CoreAnnotationDetail, \ - FocusRegionAnnotationList, FocusRegionAnnotationDetail + SliceAnnotationList, SliceAnnotationDetail, CoreAnnotationList, CoreAnnotationDetail, CoreGleasonDetail,\ + FocusRegionAnnotationList, FocusRegionAnnotationDetail, GleasonPatternList, GleasonPatternDetail import predictions_manager.views as pmv import shared_datasets_manager.views as shdv import odin.views as od @@ -155,6 +155,8 @@ def to_url(self, value): SliceAnnotationDetail.as_view()), path('api/cores//clinical_annotations/', CoreAnnotationList.as_view()), + path('api/cores//clinical_annotations//gleason_details/', + CoreGleasonDetail.as_view()), path('api/cores//clinical_annotations//', CoreAnnotationDetail.as_view()), path('api/focus_regions//clinical_annotations/', @@ -162,6 +164,10 @@ def to_url(self, value): path( 'api/focus_regions//clinical_annotations//', FocusRegionAnnotationDetail.as_view()), + path( + 'api/focus_regions//clinical_annotations//gleason_patterns/', + GleasonPatternList.as_view()), + path('api/gleason_patterns//', GleasonPatternDetail.as_view()), # ROIs annotations path('api/rois_annotations/', rmv.ROIsAnnotationsList.as_view()), diff --git a/promort/rois_manager/management/commands/dump_rois.py b/promort/rois_manager/management/commands/dump_rois.py index 91d99b1e..383aa4a2 100644 --- a/promort/rois_manager/management/commands/dump_rois.py +++ b/promort/rois_manager/management/commands/dump_rois.py @@ -18,10 +18,14 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from django.core.management.base import BaseCommand, CommandError +from slides_manager.models import Slide from rois_manager.models import Slice, Core, FocusRegion from rois_manager.serializers import SliceSerializer, CoreSerializer, FocusRegionSerializer +from promort.settings import OME_SEADRAGON_BASE_URL -import csv, os, copy + +import csv, os, copy, requests +from urllib.parse import urljoin try: import simplejson as json except ImportError: @@ -39,6 +43,8 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('--rois-list', dest='rois_list', type=str, required=True, help='A file containing the list of ROIs that will be extracted') + parser.add_argument('--remove-limit-bounds', dest='remove_limit_bounds', action='store_true', + help='remove X and Y offsets applied by the limit-bounds option of the DZI slide_obj') parser.add_argument('--out-folder', dest='out_folder', type=str, required=True, help='The output folder for the extracted data') @@ -51,6 +57,22 @@ def _load_rois_map(self, rois_file): rois_map.setdefault(row['slide_id'], dict()).setdefault(row['roi_type'], set()).add(int(row['roi_id'])) return rois_map + def _get_slide_obj_bounds(self, slide_obj_id): + slide_obj = Slide.objects.get(pk=slide_obj_id) + if slide_obj.image_type == 'OMERO_IMG': + url = urljoin(OME_SEADRAGON_BASE_URL, 'deepzoom/slide_bounds/%d.dzi' % slide_obj.omero_id) + elif slide_obj.image_type == 'MIRAX': + url = urljoin(OME_SEADRAGON_BASE_URL, 'mirax/deepzoom/slide_bounds/%s.dzi' % slide_obj.id) + else: + logger.error('Unknown image type %s for slide_obj %s', slide_obj.image_type, slide_obj.id) + return None + response = requests.get(url) + if response.status_code == requests.codes.OK: + return response.json() + else: + logger.error('Error while loading slide_obj bounds %s', slide_obj.id) + return None + def _get_related(self, rois): related_rois = copy.copy(rois) related_rois.setdefault('slice', set()) @@ -82,9 +104,31 @@ def _get_related(self, rois): len(related_rois['slice']), len(related_rois['core']), len(related_rois['focus_region'])) return related_rois + + def _adjust_shape_points(self, roi_json, slide_bounds): + if not slide_bounds is None: + new_segments = list() + shape = json.loads(roi_json) + old_segments = shape.get('segments') + for p in old_segments: + new_p = { + 'x': p['point']['x'] + int(slide_bounds['bounds_x']), + 'y': p['point']['y'] + int(slide_bounds['bounds_y']) + } + np = copy.copy(p) + np['point'] = new_p + new_segments.append(np) + shape['segments'] = new_segments + return json.dumps(shape) + else: + return roi_json - def _dump_slide_rois(self, slide_id, rois, output_folder): - logger.info('Dumping ROIs for slide %s', slide_id) + def _dump_slide_obj_rois(self, slide_obj_id, rois, remove_limit_bounds, output_folder): + logger.info('Dumping ROIs for slide_obj %s', slide_obj_id) + if remove_limit_bounds: + slide_obj_bounds = self._get_slide_obj_bounds(slide_obj_id) + else: + slide_obj_bounds = None rois = self._get_related(rois) labels_map = { 'slice': dict(), @@ -100,7 +144,7 @@ def _dump_slide_rois(self, slide_id, rois, output_folder): labels_map['slice'][ser_obj.get('id')] = ser_obj['label'] slice_obj = { 'label': ser_obj['label'], - 'roi_json': ser_obj['roi_json'], + 'roi_json': self._adjust_shape_points(ser_obj['roi_json'], slide_obj_bounds), 'total_cores': ser_obj['total_cores'] } to_be_saved['slice'].append(slice_obj) @@ -110,7 +154,7 @@ def _dump_slide_rois(self, slide_id, rois, output_folder): core_obj = { 'label': ser_obj['label'], 'slice': labels_map['slice'].get(ser_obj['slice']), - 'roi_json': ser_obj['roi_json'], + 'roi_json': self._adjust_shape_points(ser_obj['roi_json'], slide_obj_bounds), 'length': ser_obj['length'], 'area': ser_obj['area'], 'tumor_length': ser_obj['tumor_length'] @@ -121,24 +165,24 @@ def _dump_slide_rois(self, slide_id, rois, output_folder): focus_region_obj = { 'label': ser_obj['label'], 'core': labels_map['core'].get(ser_obj['core']), - 'roi_json': ser_obj['roi_json'], + 'roi_json': self._adjust_shape_points(ser_obj['roi_json'], slide_obj_bounds), 'length': ser_obj['length'], 'area': ser_obj['area'], 'tissue_status': ser_obj['tissue_status'] } to_be_saved['focus_region'].append(focus_region_obj) - with open(os.path.join(output_folder, '%s.json' % slide_id), 'w') as out_file: + with open(os.path.join(output_folder, '%s.json' % slide_obj_id), 'w') as out_file: json.dump(to_be_saved, out_file) - def _dump_rois(self, rois_map, output_folder): + def _dump_rois(self, rois_map, remove_limit_bounds, output_folder): logger.debug('Checking if folder %s exists' % output_folder) if not os.path.isdir(output_folder): raise CommandError('Output folder %s does not exist, exit' % output_folder) - for slide, rois in rois_map.items(): - self._dump_slide_rois(slide, rois, output_folder) + for slide_obj, rois in rois_map.items(): + self._dump_slide_obj_rois(slide_obj, rois, remove_limit_bounds, output_folder) def handle(self, *args, **opts): logger.info('== Starting job ==') rois = self._load_rois_map(opts['rois_list']) - self._dump_rois(rois, opts['out_folder']) + self._dump_rois(rois, opts['remove_limit_bounds'], opts['out_folder']) logger.info('== Job completed ==') diff --git a/promort/rois_manager/management/commands/extract_focus_regions.py b/promort/rois_manager/management/commands/extract_focus_regions.py index 42985705..eabc11de 100644 --- a/promort/rois_manager/management/commands/extract_focus_regions.py +++ b/promort/rois_manager/management/commands/extract_focus_regions.py @@ -44,18 +44,22 @@ def add_arguments(self, parser): help='path of the output folder for the extracted JSON objects') parser.add_argument('--limit-bounds', dest='limit_bounds', action='store_true', help='extract ROIs considering only the non-empty slide region') + parser.add_argument('--exclude_rejected', dest='exclude_rejected', action='store_true', + help='exclude cores from review steps rejected by the user') parser.add_argument('--reviewer', dest='reviewer', type=str, help='filter review steps by reviewer username') parser.add_argument('--tissue-type', dest='tissue_type', type=str, choices=['TUMOR', 'NORMAL'], help='filter focus regions by tissue type') - def _load_rois_annotation_steps(self, reviewer=None): + def _load_rois_annotation_steps(self, exclude_rejected, reviewer=None): if reviewer is not None: logger.info(f'Filtering by reviewer: {reviewer}') steps = ROIsAnnotationStep.objects.filter(completion_date__isnull=False, rois_annotation__reviewer__username=reviewer) else: steps = ROIsAnnotationStep.objects.filter(completion_date__isnull=False) + if exclude_rejected: + steps = [s for s in steps if s.slide_evaluation.adequate_slide] return steps def _get_slide_bounds(self, slide): @@ -90,7 +94,11 @@ def _extract_points(self, roi_json, slide_bounds): return None def _extract_bounding_box(self, roi_points): - polygon = Polygon(roi_points) + try: + polygon = Polygon(roi_points) + except ValueError as e: + logger.error('{0}, skipping focus region'.format(e)) + return None bounds = polygon.bounds return [(bounds[0], bounds[1]), (bounds[2], bounds[3])] @@ -99,6 +107,8 @@ def _dump_focus_region(self, focus_region, slide_id, slide_bounds, out_folder): points = self._extract_points(focus_region.roi_json, slide_bounds) if points: bbox = self._extract_bounding_box(points) + if bbox is None: + return None with open(file_path, 'w') as ofile: json.dump(points, ofile) return { @@ -147,13 +157,13 @@ def _dump_focus_regions(self, step, out_folder, limit_bounds, tissue_type=None): focus_regions_details.append(frd) self._dump_details(focus_regions_details, out_path) - def _export_data(self, out_folder, limit_bounds=False, reviewer=None, tissue_type=None): - steps = self._load_rois_annotation_steps(reviewer) + def _export_data(self, out_folder, limit_bounds=False, exclude_rejected=False, reviewer=None, tissue_type=None): + steps = self._load_rois_annotation_steps(exclude_rejected, reviewer) logger.info('Loaded %d ROIs Annotation Steps', len(steps)) for s in steps: self._dump_focus_regions(s, out_folder, limit_bounds, tissue_type) def handle(self, *args, **opts): logger.info('=== Starting export job ===') - self._export_data(opts['out_folder'], opts['limit_bounds'], opts['reviewer'], opts['tissue_type']) + self._export_data(opts['out_folder'], opts['limit_bounds'], opts['exclude_rejected'], opts['reviewer'], opts['tissue_type']) logger.info('=== Export completed ===') diff --git a/promort/slides_manager/management/commands/import_ome_slides.py b/promort/slides_manager/management/commands/import_ome_slides.py index d40a828f..cf5b0a6b 100644 --- a/promort/slides_manager/management/commands/import_ome_slides.py +++ b/promort/slides_manager/management/commands/import_ome_slides.py @@ -34,6 +34,10 @@ class Command(BaseCommand): Import slides from a running OMERO server (with ome_seadragon plugin) to ProMort and create related Case and Slide objects """ + + def add_arguments(self, parser): + parser.add_argument('--filter_by_set', action="store_true", + help='Filter bigger slide in set') def _split_slide_name(self, slide_name): regex = re.compile(r'[a-zA-Z0-9]+-[0-9]+(_[a-zA-Z][0-9]?)?(\.[a-zA-Z0-9]{2,4})?$') @@ -64,11 +68,14 @@ def _filter_slides(self, slides): filtered_slides.append(self._get_bigger_in_fileset(fs_slides)) return filtered_slides - def _load_ome_images(self): + def _load_ome_images(self, filter_by_set): url = urljoin(OME_SEADRAGON_BASE_URL, 'get/images/index') response = requests.get(url, params={'full_series': True}) if response.status_code == requests.codes.OK: - slides = self._filter_slides(response.json()) + if filter_by_set: + slides = self._filter_slides(response.json()) + else: + slides = response.json() slides_map = dict() for s in slides: case_id, _ = self._split_slide_name(s['name']) @@ -120,7 +127,7 @@ def _update_ome_info(self, slide_obj, omero_id, image_type, image_mpp): def handle(self, *args, **opts): logger.info('=== Starting import job ===') - slides_map = self._load_ome_images() + slides_map = self._load_ome_images(opts['filter_by_set']) for case_id, slides in slides_map.items(): case = self._get_or_create_case(case_id) for slide_json in slides: diff --git a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.controllers.js b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.controllers.js index 25489154..ae87a9d6 100644 --- a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.controllers.js +++ b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.controllers.js @@ -31,15 +31,17 @@ .controller('NewCoreAnnotationController', NewCoreAnnotationController) .controller('ShowCoreAnnotationController', ShowCoreAnnotationController) .controller('NewFocusRegionAnnotationController', NewFocusRegionAnnotationController) - .controller('ShowFocusRegionAnnotationController', ShowFocusRegionAnnotationController); + .controller('ShowFocusRegionAnnotationController', ShowFocusRegionAnnotationController) + .controller('NewGleasonPatternAnnotationController', NewGleasonPatternAnnotationController) + .controller('ShowGleasonPatternAnnotationController', ShowGleasonPatternAnnotationController); ClinicalAnnotationsManagerController.$inject = ['$scope', '$rootScope', '$routeParams', '$compile', '$location', '$log', 'ngDialog', 'AnnotationsViewerService', 'ClinicalAnnotationStepService', 'ClinicalAnnotationStepManagerService', 'CurrentSlideDetailsService']; function ClinicalAnnotationsManagerController($scope, $rootScope, $routeParams, $compile, $location, $log, ngDialog, - AnnotationsViewerService, ClinicalAnnotationStepService, - ClinicalAnnotationStepManagerService, CurrentSlideDetailsService) { + AnnotationsViewerService, ClinicalAnnotationStepService, + ClinicalAnnotationStepManagerService, CurrentSlideDetailsService) { var vm = this; vm.slide_id = undefined; vm.slide_index = undefined; @@ -51,26 +53,34 @@ vm.cores_map = undefined; vm.focus_regions_map = undefined; + vm.positive_fr_count = undefined; + vm.ui_active_modes = { 'annotate_slice': false, 'annotate_core': false, 'annotate_focus_region': false, + 'annotate_gleason_pattern': false, 'show_slice': false, 'show_core': false, - 'show_focus_region': false + 'show_focus_region': false, + 'show_gleason_pattern': false }; vm.roisTreeLocked = false; vm._registerSlice = _registerSlice; vm._registerCore = _registerCore; vm._registerFocusRegion = _registerFocusRegion; + vm._registerGleasonPattern = _registerGleasonPattern; + vm._unregisterGleasonPattern = _unregisterGleasonPattern; vm._getSliceLabel = _getSliceLabel; vm._getCoreLabel = _getCoreLabel; vm._getFocusRegionLabel = _getFocusRegionLabel; + vm._getGleasonPatternLabel = _getGleasonPatternLabel; vm._createListItem = _createListItem; vm._createNewSubtree = _createNewSubtree; vm._focusOnShape = _focusOnShape; + vm._clearGleasonSubregions = _clearGleasonSubregions; vm.showROIPanel = showROIPanel; vm.selectROI = selectROI; vm.deselectROI = deselectROI; @@ -87,7 +97,7 @@ vm.newSliceAnnotationModeActive = newSliceAnnotationModeActive; vm.activateShowSliceAnnotationMode = activateShowSliceAnnotationMode; vm.showSliceAnnotationModeActive = showSliceAnnotationModeActive; - vm.activateNewCoreAnnoationMode = activateNewCoreAnnotationMode; + vm.activateNewCoreAnnotationMode = activateNewCoreAnnotationMode; vm.newCoreAnnotationModeActive = newCoreAnnotationModeActive; vm.activateShowCoreAnnotationMode = activateShowCoreAnnotationMode; vm.showCoreAnnotationModeActive = showCoreAnnotationModeActive; @@ -95,6 +105,12 @@ vm.newFocusRegionAnnotationModeActive = newFocusRegionAnnotationModeActive; vm.activateShowFocusRegionAnnotationMode = activateShowFocusRegionAnnotationMode; vm.showFocusRegionAnnotationModeActive = showFocusRegionAnnotationModeActive; + vm.getPositiveFocusRegionsCount = getPositiveFocusRegionsCount; + vm.activateNewGleasonPatternAnnotationMode = activateNewGleasonPatternAnnotationMode; + vm.newGleasonPatternAnnotationModeActive = newGleasonPatternAnnotationModeActive; + vm.activateShowGleasonPatternAnnotationMode = activateShowGleasonPatternAnnotationMode; + vm.showGleasonPatternAnnotationModeActive = showGleasonPatternAnnotationModeActive; + vm.annotationModeActive = annotationModeActive; activate(); @@ -103,12 +119,14 @@ vm.case_id = CurrentSlideDetailsService.getCaseId(); vm.clinical_annotation_step_label = $routeParams.label; vm.clinical_annotation_label = vm.clinical_annotation_step_label.split('-')[0]; - $log.debug('clinical annotation label is ' + vm.clinical_annotation_label); vm.slide_index = vm.clinical_annotation_step_label.split('-')[1]; - vm.slices_map = []; - vm.cores_map = []; - vm.focus_regions_map = []; + vm.slices_map = {}; + vm.cores_map = {}; + vm.focus_regions_map = {}; + vm.gleason_patterns_map = {}; + + vm.positive_fr_count = 0; vm.slices_edit_mode = []; vm.cores_edit_mode = []; @@ -117,6 +135,8 @@ $rootScope.slices = []; $rootScope.cores = []; $rootScope.focus_regions = []; + $rootScope.gleason_patterns = []; + $rootScope.gleason_patterns_subregions = []; ClinicalAnnotationStepService.getDetails(vm.clinical_annotation_step_label) .then(getClinicalAnnotationStepSuccessFn, getClinicalAnnotationStepErrorFn); @@ -127,13 +147,20 @@ } $scope.$on('annotation_panel.closed', - function() { + function () { + vm._clearGleasonSubregions(); + vm.allModesOff(); + } + ); + + $scope.$on('tool.destroyed', + function () { vm.allModesOff(); } ); $scope.$on('slice.new', - function(event, slice_info) { + function (event, slice_info) { vm._registerSlice(slice_info); vm.allModesOff(); var $tree = $("#rois_tree"); @@ -151,7 +178,7 @@ ); $scope.$on('core.new', - function(event, core_info) { + function (event, core_info) { vm._registerCore(core_info); vm.allModesOff(); var $tree = $("#" + vm._getSliceLabel(core_info.slice) + "_tree"); @@ -169,23 +196,53 @@ ); $scope.$on('focus_region.new', - function(event, focus_region_info) { + function (event, focus_region_info) { + if (focus_region_info.tumor == true) { + vm.positive_fr_count += 1; + } vm._registerFocusRegion(focus_region_info); vm.allModesOff(); var $tree = $("#" + vm._getCoreLabel(focus_region_info.core) + "_tree"); var $new_focus_region_item = $(vm._createListItem(focus_region_info.label, - vm.focus_regions_edit_mode[focus_region_info.id], false)); + vm.focus_regions_edit_mode[focus_region_info.id], true)); var $anchor = $new_focus_region_item.find('a'); $anchor.attr('ng-click', 'cmc.showROIPanel("focus_region", ' + focus_region_info.id + ')') .attr('ng-mouseenter', 'cmc.selectROI("focus_region", ' + focus_region_info.id + ')') .attr('ng-mouseleave', 'cmc.deselectROI("focus_region", ' + focus_region_info.id + ')'); $compile($anchor)($scope); + var new_focus_region_subtree = vm._createNewSubtree(focus_region_info.label); + $new_focus_region_item.append(new_focus_region_subtree); $tree.append($new_focus_region_item); } ); + $scope.$on('gleason_pattern.new', + function (event, gleason_pattern_info) { + vm._registerGleasonPattern(gleason_pattern_info); + vm.allModesOff(); + var $tree = $("#" + vm._getFocusRegionLabel(gleason_pattern_info.focus_region) + "_tree"); + var $new_gleason_pattern_item = $(vm._createListItem(gleason_pattern_info.label, + false, false)); + var $anchor = $new_gleason_pattern_item.find('a'); + $anchor.attr('ng-click', 'cmc.showROIPanel("gleason_pattern", ' + gleason_pattern_info.id + ')') + .attr('ng-mouseenter', 'cmc.selectROI("gleason_pattern", ' + gleason_pattern_info.id + ')') + .attr('ng-mouseleave', 'cmc.deselectROI("gleason_pattern", ' + gleason_pattern_info.id + ')'); + $compile($anchor)($scope); + $tree.append($new_gleason_pattern_item); + } + ); + + $scope.$on('gleason_pattern.deleted', + function (event, opt) { + AnnotationsViewerService.deleteShape(opt.gleason_pattern_label); + $("#" + opt.gleason_pattern_label + "_list").remove(); + vm._unregisterGleasonPattern(opt.gleason_pattern_id); + vm.allModesOff(); + } + ); + $scope.$on('slice_annotation.saved', - function(event, slice_label, slice_id) { + function (event, slice_label, slice_id) { var $icon = $("#" + slice_label).find('i'); $icon.removeClass("icon-black_question"); $icon.addClass("icon-check_circle"); @@ -195,7 +252,7 @@ ); $scope.$on('slice_annotation.deleted', - function(event, slice_label, slice_id) { + function (event, slice_label, slice_id) { if (slice_id in vm.slices_edit_mode) { var $icon = $("#" + slice_label).find('i'); $icon.removeClass("icon-check_circle"); @@ -207,7 +264,7 @@ ); $scope.$on('core_annotation.saved', - function(event, core_label, core_id) { + function (event, core_label, core_id) { var $icon = $("#" + core_label).find('i'); $icon.removeClass("icon-black_question"); $icon.addClass("icon-check_circle"); @@ -217,7 +274,7 @@ ); $scope.$on('core_annotation.deleted', - function(event, core_label, core_id) { + function (event, core_label, core_id) { if (core_id in vm.cores_edit_mode) { var $icon = $("#" + core_label).find('i'); $icon.removeClass("icon-check_circle"); @@ -229,7 +286,7 @@ ); $scope.$on('focus_region_annotation.saved', - function(event, focus_region_label, focus_region_id) { + function (event, focus_region_label, focus_region_id) { var $icon = $("#" + focus_region_label).find('i'); $icon.removeClass("icon-black_question"); $icon.addClass("icon-check_circle"); @@ -239,7 +296,7 @@ ); $scope.$on('focus_region_annotation.deleted', - function(event, focus_region_label, focus_region_id) { + function (event, focus_region_label, focus_region_id) { if (focus_region_id in vm.focus_regions_edit_mode) { var $icon = $("#" + focus_region_label).find('i'); $icon.removeClass('icon-check_circle'); @@ -291,6 +348,24 @@ return vm.focus_regions_map[focus_region_id]; } + function _registerGleasonPattern(gleason_pattern_info) { + $rootScope.gleason_patterns.push(gleason_pattern_info); + vm.gleason_patterns_map[gleason_pattern_info.id] = gleason_pattern_info.label; + } + + function _unregisterGleasonPattern(gleason_pattern_id) { + delete vm.gleason_patterns_map[gleason_pattern_id]; + $rootScope.gleason_patterns = $.grep($rootScope.gleason_patterns, + function(value) { + return value.id !== gleason_pattern_id; + } + ); + } + + function _getGleasonPatternLabel(gleason_pattern_id) { + return vm.gleason_patterns_map[gleason_pattern_id]; + } + function _createListItem(label, edit_mode, set_neg_margin_cls) { var html = '
  • '; + var html = '
      '; return html; } @@ -327,10 +402,19 @@ case 'focus_region': shape_id = vm.focus_regions_map[roi_id]; break; + case 'gleason_pattern': + shape_id = vm.gleason_patterns_map[roi_id]; + break; } AnnotationsViewerService.focusOnShape(shape_id); } + function _clearGleasonSubregions() { + while($rootScope.gleason_patterns_subregions.length > 0) { + AnnotationsViewerService.deleteShape($rootScope.gleason_patterns_subregions.pop()); + } + } + function canCloseAnnotation() { // only cores annotation is mandatory for (var x in vm.cores_edit_mode) { @@ -395,7 +479,7 @@ if (confirm_obj.value === true) { ClinicalAnnotationStepService.closeAnnotationStep(vm.clinical_annotation_step_label, confirm_obj.notes).then(closeClinicalAnnotationStepSuccessFn, - closeClinicalAnnotationStepErrorFn); + closeClinicalAnnotationStepErrorFn); } function closeClinicalAnnotationStepSuccessFn(response) { @@ -490,6 +574,7 @@ } function showROIPanel(roi_type, roi_id) { + vm._clearGleasonSubregions(); if (!vm.roisTreeLocked) { var edit_mode = undefined; switch (roi_type) { @@ -503,6 +588,9 @@ edit_mode = roi_id in vm.focus_regions_edit_mode ? vm.focus_regions_edit_mode[roi_id] : false; break; + case 'gleason_pattern': + edit_mode = false; + break; } vm.deselectROI(roi_type, roi_id); vm._focusOnShape(roi_type, roi_id); @@ -513,10 +601,11 @@ vm.activateNewSliceAnnotationMode(roi_id); break; case 'core': - vm.activateNewCoreAnnoationMode(roi_id); + vm.activateNewCoreAnnotationMode(roi_id); break; case 'focus_region': vm.activateNewFocusRegionAnnotationMode(roi_id); + break; } } } else { @@ -529,6 +618,10 @@ break; case 'focus_region': vm.activateShowFocusRegionAnnotationMode(roi_id); + break; + case 'gleason_pattern': + vm.activateShowGleasonPatternAnnotationMode(roi_id); + break; } } } @@ -546,6 +639,9 @@ case 'focus_region': AnnotationsViewerService.selectShape(vm._getFocusRegionLabel(roi_id)); break; + case 'gleason_pattern': + AnnotationsViewerService.selectShape(vm._getGleasonPatternLabel(roi_id)); + break; } } } @@ -562,6 +658,9 @@ case 'focus_region': AnnotationsViewerService.deselectShape(vm._getFocusRegionLabel(roi_id)); break; + case 'gleason_pattern': + AnnotationsViewerService.deselectShape(vm._getGleasonPatternLabel(roi_id)); + break; } } } @@ -580,6 +679,7 @@ for (var mode in vm.ui_active_modes) { vm.ui_active_modes[mode] = false; } + vm._clearGleasonSubregions(); vm._unlockRoisTree(); } @@ -653,6 +753,40 @@ function showFocusRegionAnnotationModeActive() { return vm.ui_active_modes['show_focus_region']; } + + function activateNewGleasonPatternAnnotationMode() { + vm.allModesOff(); + vm._lockRoisTree(); + vm.ui_active_modes['annotate_gleason_pattern'] = true; + $rootScope.$broadcast('gleason_pattern.creation_mode'); + } + + function newGleasonPatternAnnotationModeActive() { + return vm.ui_active_modes['annotate_gleason_pattern']; + } + + function activateShowGleasonPatternAnnotationMode(gleason_pattern_id) { + vm.allModesOff(); + $rootScope.$broadcast('gleason_pattern.show', gleason_pattern_id); + vm.ui_active_modes['show_gleason_pattern'] = true; + } + + function showGleasonPatternAnnotationModeActive() { + return vm.ui_active_modes['show_gleason_pattern']; + } + + function getPositiveFocusRegionsCount() { + return vm.positive_fr_count; + } + + function annotationModeActive() { + return ( + vm.ui_active_modes.annotate_slice + || vm.ui_active_modes.annotate_core + || vm.ui_active_modes.annotate_focus_region + || vm.ui_active_modes.annotate_gleason_pattern + ); + } } RejectClinicalAnnotationStepController.$inject = ['$scope', '$log', 'ClinicalAnnotationStepManagerService']; @@ -684,7 +818,7 @@ 'SlicesManagerService', 'SliceAnnotationsManagerService']; function NewSliceAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - SlicesManagerService, SliceAnnotationsManagerService) { + SlicesManagerService, SliceAnnotationsManagerService) { var vm = this; vm.slice_id = undefined; vm.slice_label = undefined; @@ -714,7 +848,7 @@ function activate() { vm.clinical_annotation_step_label = $routeParams.label; $scope.$on('slice_annotation.new', - function(event, slice_id) { + function (event, slice_id) { vm.slice_id = slice_id; SlicesManagerService.get(vm.slice_id) .then(getSliceSuccessFn, getSliceErrorFn); @@ -807,7 +941,7 @@ 'SliceAnnotationsManagerService']; function ShowSliceAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - SliceAnnotationsManagerService) { + SliceAnnotationsManagerService) { var vm = this; vm.slice_id = undefined; vm.slice_label = undefined; @@ -834,7 +968,7 @@ function activate() { vm.clinical_annotation_step_label = $routeParams.label; $scope.$on('slice_annotation.show', - function(event, slice_id) { + function (event, slice_id) { vm.slice_id = slice_id; SliceAnnotationsManagerService.getAnnotation(vm.slice_id, vm.clinical_annotation_step_label) .then(getSliceAnnotationSuccessFn, getSliceAnnotationErrorFn); @@ -922,20 +1056,36 @@ } NewCoreAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', - 'CoresManagerService', 'CoreAnnotationsManagerService']; + 'CoreGleasonDetailsManagerService', 'CoreAnnotationsManagerService', 'AnnotationsViewerService']; function NewCoreAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - CoresManagerService, CoreAnnotationsManagerService) { + CoreGleasonDetailsManagerService, CoreAnnotationsManagerService, AnnotationsViewerService) { var vm = this; vm.core_id = undefined; vm.core_label = undefined; vm.coreArea = undefined; vm.coreLength = undefined; vm.tumorLength = undefined; - vm.primaryGleason = undefined; - vm.secondaryGleason = undefined; + // vm.primaryGleason = undefined; + // vm.secondaryGleason = undefined; + vm.gleasonScore = undefined; vm.gradeGroupWho = undefined; - vm.gradeGroupWhoLabel = ''; + vm.gradeGroupWhoLabel = '' + vm.gleasonDetails = undefined; + vm.gleasonHighlighted = undefined; + vm.predominant_rsg = undefined; + vm.highest_rsg = undefined; + vm.rsg_within_highest_grade_area = undefined; + vm.rsg_in_area_of_cribriform_morphology = undefined; + vm.perineural_invasion = undefined; + vm.perineural_growth_with_cribriform_patterns = undefined; + vm.extraprostatic_extension = undefined; + // only if at least one cribriform pattern exists + vm.nuclear_grade_size = undefined; + vm.intraluminal_acinar_differentiation_grade = undefined; + vm.intraluminal_secretions = undefined; + vm.central_maturation = undefined; + vm.extra_cribriform_gleason_score = undefined; vm.actionStartTime = undefined; @@ -954,16 +1104,30 @@ ]; vm.areaUOM = [ - { id: 1, unit_of_measure: 'μm²'}, - { id: Math.pow(10, -6), unit_of_measure: 'mm²'} + { id: 1, unit_of_measure: 'μm²' }, + { id: Math.pow(10, -6), unit_of_measure: 'mm²' } ]; + vm.gleasonPatternsColors = { + "G3": "#ffcc99", + "G4": "#ff9966", + "G5": "#cc5200" + } + vm._clean = _clean; + vm._parseGleasonScore = _parseGleasonScore; + vm.getCoverage = getCoverage; + vm.gleasonDetailsAvailable = gleasonDetailsAvailable; + vm._getGleasonShapesLabels = _getGleasonShapesLabels; + vm.gleasonHighlightSwitch = gleasonHighlightSwitch; + vm.isHighlighted = isHighlighted; + vm.selectGleasonPatterns = selectGleasonPatterns; vm.isReadOnly = isReadOnly; vm.isLocked = isLocked; vm.formValid = formValid; vm.destroy = destroy; - vm.upgradeGradeGroupWho = updateGradeGroupWho; + vm.updateGradeGroupWho = updateGradeGroupWho; + vm.containsCribriformPattern = containsCribriformPattern; vm.save = save; vm.updateTumorLength = updateTumorLength; vm.updateCoreLength = updateCoreLength; @@ -978,9 +1142,9 @@ vm.clinical_annotation_step_label = $routeParams.label; $scope.$on('core_annotation.new', - function(event, core_id) { + function (event, core_id) { vm.core_id = core_id; - CoresManagerService.get(vm.core_id) + CoreGleasonDetailsManagerService.get(vm.core_id, vm.clinical_annotation_step_label) .then(getCoreSuccessFn, getCoreErrorFn); } ); @@ -993,6 +1157,20 @@ vm.updateCoreLength(); vm.tumorLength = response.data.tumor_length; vm.updateTumorLength(); + vm.gleasonScore = vm._parseGleasonScore( + response.data.primary_gleason, + response.data.secondary_gleason + ) + vm.updateGradeGroupWho( + response.data.primary_gleason, + response.data.secondary_gleason + ); + vm.gleasonDetails = response.data.details; + vm.gleasonHighlighted = { + "G3": false, + "G4": false, + "G5": false + } vm.actionStartTime = new Date(); } @@ -1008,14 +1186,109 @@ vm.coreArea = undefined; vm.coreLength = undefined; vm.tumorLength = undefined; - vm.primaryGleason = undefined; - vm.secondaryGleason = undefined; + vm.gleasonScore = undefined; vm.gradeGroupWho = undefined; vm.gradeGroupWhoLabel = ''; + vm.gleasonDetails = undefined; + vm.gleasonHighlighted = { + "G3": false, + "G4": false, + "G5": false + } + vm.predominant_rsg = undefined; + vm.highest_rsg = undefined; + vm.rsg_within_highest_grade_area = undefined; + vm.rsg_in_area_of_cribriform_morphology = undefined; + vm.perineural_invasion = undefined; + vm.perineural_growth_with_cribriform_patterns = undefined; + vm.extraprostatic_extension = undefined; + vm.nuclear_grade_size = undefined; + vm.intraluminal_acinar_differentiation_grade = undefined; + vm.intraluminal_secretions = undefined; + vm.central_maturation = undefined; + vm.extra_cribriform_gleason_score = undefined; vm.actionStartTime = undefined; } + function _parseGleasonScore(primary_gleason, secondary_gleason) { + if((primary_gleason !== null) && (secondary_gleason !== null)){ + return parseInt(primary_gleason.replace(/\D/g, '')) + "+" + parseInt(secondary_gleason.replace(/\D/g, '')); + } else { + return undefined; + } + } + + function getCoverage(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + var pattern_data = vm.gleasonDetails[gleason_pattern]; + if (pattern_data !== undefined) { + return pattern_data.total_coverage + " %"; + } else { + return "0 %"; + } + } + } + + function gleasonDetailsAvailable(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + return vm.gleasonDetails.hasOwnProperty(gleason_pattern); + } else { + return false; + } + } + + function _getGleasonShapesLabels(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + return vm.gleasonDetails[gleason_pattern].shapes; + } + } + return undefined; + } + + function gleasonHighlightSwitch(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + var gleason_shapes = vm._getGleasonShapesLabels(gleason_pattern); + var pattern_highlighted = vm.gleasonHighlighted[gleason_pattern]; + if (pattern_highlighted) { + var shape_color = "#ffffff"; + var shape_alpha = "0"; + } else { + var shape_color = vm.gleasonPatternsColors[gleason_pattern]; + var shape_alpha = "0.35"; + } + for (const shape of gleason_shapes) { + AnnotationsViewerService.setShapeFillColor(shape, shape_color, shape_alpha); + } + vm.gleasonHighlighted[gleason_pattern] = !vm.gleasonHighlighted[gleason_pattern]; + } + } + } + + function isHighlighted(gleason_pattern) { + if (vm.gleasonDetails !== undefined && vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + return vm.gleasonHighlighted[gleason_pattern]; + } else { + return false; + } + + } + + function selectGleasonPatterns(gleason_pattern, activate) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + var gleason_shapes = vm._getGleasonShapesLabels(gleason_pattern); + if (activate) { + AnnotationsViewerService.selectShapes(gleason_shapes); + } else { + AnnotationsViewerService.deselectShapes(gleason_shapes); + } + } + } + } + function isReadOnly() { return false; } @@ -1025,8 +1298,7 @@ } function formValid() { - return ((typeof vm.primaryGleason !== 'undefined') && - (typeof vm.secondaryGleason !== 'undefined')); + return true; } function destroy() { @@ -1034,13 +1306,13 @@ $rootScope.$broadcast('annotation_panel.closed'); } - function updateGradeGroupWho() { - if ((typeof vm.primaryGleason !== 'undefined') && (typeof vm.secondaryGleason !== 'undefined')) { - var gleason_score = Number(vm.primaryGleason) + Number(vm.secondaryGleason); + function updateGradeGroupWho(primary_gleason, secondary_gleason) { + if((primary_gleason !== null) && (secondary_gleason !== null)) { + var gleason_score = parseInt(primary_gleason.replace(/\D/g, '')) + parseInt(secondary_gleason.replace(/\D/g, '')); if (gleason_score <= 6) { vm.gradeGroupWho = 'GG1'; vm.gradeGroupWhoLabel = 'Group 1' - } else if (gleason_score == 7) { + } else if (gleason_score == 7) { if (vm.primaryGleason == 3) { vm.gradeGroupWho = 'GG2'; vm.gradeGroupWhoLabel = 'Group 2'; @@ -1061,6 +1333,14 @@ } } + function containsCribriformPattern() { + if (typeof(vm.gleasonDetails) === "undefined") { + return false; + } else { + return vm.gleasonDetails.hasOwnProperty("G4") && (vm.gleasonDetails["G4"].subregions.indexOf("cribriform_pattern") > -1); + } + } + function save() { var dialog = undefined; dialog = ngDialog.open({ @@ -1071,11 +1351,22 @@ closeByDocument: false }); var obj_config = { - primary_gleason: Number(vm.primaryGleason), - secondary_gleason: Number(vm.secondaryGleason), - gleason_group: vm.gradeGroupWho, action_start_time: vm.actionStartTime, - action_complete_time: new Date() + action_complete_time: new Date(), + predominant_rsg: vm.predominant_rsg, + highest_rsg: vm.highest_rsg, + rsg_within_highest_grade_area: vm.rsg_within_highest_grade_area, + rsg_in_area_of_cribriform_morphology: vm.rsg_in_area_of_cribriform_morphology, + perineural_invasion: typeof (vm.perineural_invasion) == "undefined" ? false : vm.perineural_invasion, + perineural_growth_with_cribriform_patterns: typeof (vm.perineural_growth_with_cribriform_patterns) == "undefined" ? false : vm.perineural_growth_with_cribriform_patterns, + extraprostatic_extension: typeof (vm.extraprostatic_extension) == "undefined" ? false : vm.extraprostatic_extension + } + if (vm.containsCribriformPattern()) { + obj_config.nuclear_grade_size = vm.nuclear_grade_size; + obj_config.intraluminal_acinar_differentiation_grade = vm.intraluminal_acinar_differentiation_grade; + obj_config.intraluminal_secretions = typeof (vm.intraluminal_secretions) == "undefined" ? false : vm.intraluminal_secretions; + obj_config.central_maturation = typeof (vm.central_maturation) == "undefined" ? false : vm.central_maturation; + obj_config.extra_cribriform_gleason_score = vm.extra_cribriform_gleason_score; } CoreAnnotationsManagerService.createAnnotation(vm.core_id, vm.clinical_annotation_step_label, obj_config) .then(createAnnotationSuccessFn, createAnnotationErrorFn); @@ -1113,10 +1404,10 @@ } ShowCoreAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', - 'CoreAnnotationsManagerService', 'CoresManagerService']; + 'CoreAnnotationsManagerService', 'CoresManagerService', 'AnnotationsViewerService']; function ShowCoreAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - CoreAnnotationsManagerService, CoresManagerService) { + CoreAnnotationsManagerService, CoresManagerService, AnnotationsViewerService) { var vm = this; vm.core_id = undefined; vm.core_label = undefined; @@ -1126,7 +1417,21 @@ vm.normalTissuePercentage = undefined; vm.gleasonScore = undefined; vm.gradeGroupWhoLabel = undefined; - vm.gleason4Percentage = undefined; + vm.gleasonDetails = undefined; + vm.gleasonHighlighted = undefined; + vm.predominant_rsg = undefined; + vm.highest_rsg = undefined; + vm.rsg_within_highest_grade_area = undefined; + vm.rsg_in_area_of_cribriform_morphology = undefined; + vm.perineural_invasion = undefined; + vm.perineural_growth_with_cribriform_patterns = undefined; + vm.extraprostatic_extension = undefined; + // only if at least one cribriform pattern exists + vm.nuclear_grade_size = undefined; + vm.intraluminal_acinar_differentiation_grade = undefined; + vm.intraluminal_secretions = undefined; + vm.central_maturation = undefined; + vm.extra_cribriform_gleason_score = undefined; vm.clinical_annotation_step_label = undefined; @@ -1143,10 +1448,16 @@ ]; vm.areaUOM = [ - { id: 1, unit_of_measure: 'μm²'}, - { id: Math.pow(10, -6), unit_of_measure: 'mm²'} + { id: 1, unit_of_measure: 'μm²' }, + { id: Math.pow(10, -6), unit_of_measure: 'mm²' } ]; + vm.gleasonPatternsColors = { + "G3": "#ffcc99", + "G4": "#ff9966", + "G5": "#cc5200" + } + vm.locked = undefined; vm.isReadOnly = isReadOnly; @@ -1156,6 +1467,12 @@ vm.updateTumorLength = updateTumorLength; vm.updateCoreLength = updateCoreLength; vm.updateCoreArea = updateCoreArea; + vm.getCoverage = getCoverage; + vm.gleasonDetailsAvailable = gleasonDetailsAvailable; + vm._getGleasonShapesLabels = _getGleasonShapesLabels; + vm.gleasonHighlightSwitch = gleasonHighlightSwitch; + vm.isHighlighted = isHighlighted; + vm.selectGleasonPatterns = selectGleasonPatterns; activate(); @@ -1184,7 +1501,12 @@ vm.updateTumorLength(); vm.normalTissuePercentage = Number(parseFloat(response.data.core.normal_tissue_percentage).toFixed(3)); vm.gleasonScore = response.data.gleason_score; - vm.gleason4Percentage = Number(parseFloat(response.data.gleason_4_percentage).toFixed(3)); + vm.gleasonDetails = response.data.details; + vm.gleasonHighlighted = { + "G3": false, + "G4": false, + "G5": false + } switch (response.data.gleason_group) { case 'GG1': vm.gradeGroupWhoLabel = 'Group 1'; @@ -1202,6 +1524,19 @@ vm.gradeGroupWhoLabel = 'Group 5'; break } + vm.predominant_rsg = response.data.predominant_rsg; + vm.highest_rsg = response.data.highest_rsg; + vm.rsg_within_highest_grade_area = response.data.rsg_within_highest_grade_area; + vm.rsg_in_area_of_cribriform_morphology = response.data.rsg_in_area_of_cribriform_morphology; + vm.perineural_invasion = response.data.perineural_invasion; + vm.perineural_growth_with_cribriform_patterns = response.data.perineural_growth_with_cribriform_patterns; + vm.extraprostatic_extension = response.data.extraprostatic_extension; + // only if at least one cribriform pattern exists + vm.nuclear_grade_size = response.data.nuclear_grade_size; + vm.intraluminal_acinar_differentiation_grade = response.data.intraluminal_acinar_differentiation_grade; + vm.intraluminal_secretions = response.data.intraluminal_secretions; + vm.central_maturation = response.data.central_maturation; + vm.extra_cribriform_gleason_score = response.data.extra_cribriform_gleason_score; } function getCoreAnnotationErrorFn(response) { @@ -1279,7 +1614,20 @@ vm.normalTissuePercentage = undefined; vm.gleasonScore = undefined; vm.gradeGroupWhoLabel = undefined; - vm.gleason4Percentage = undefined; + vm.gleasonDetails = undefined;; + vm.gleasonHighlighted = undefined; + vm.predominant_rsg = undefined; + vm.highest_rsg = undefined; + vm.rsg_within_highest_grade_area = undefined; + vm.rsg_in_area_of_cribriform_morphology = undefined; + vm.perineural_invasion = undefined; + vm.perineural_growth_with_cribriform_patterns = undefined; + vm.extraprostatic_extension = undefined; + vm.nuclear_grade_size = undefined; + vm.intraluminal_acinar_differentiation_grade = undefined; + vm.intraluminal_secretions = undefined; + vm.central_maturation = undefined; + vm.extra_cribriform_gleason_score = undefined; dialog.close(); } @@ -1307,6 +1655,77 @@ (vm.coreArea * vm.coreAreaScaleFactor.id), 3 ); } + + function getCoverage(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + var pattern_data = vm.gleasonDetails[gleason_pattern]; + if (pattern_data !== undefined) { + return pattern_data.total_coverage + " %"; + } else { + return "0 %"; + } + } + } + + function gleasonDetailsAvailable(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + return vm.gleasonDetails.hasOwnProperty(gleason_pattern); + } else { + return false; + } + } + + function _getGleasonShapesLabels(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + return vm.gleasonDetails[gleason_pattern].shapes; + } + } + return undefined; + } + + function gleasonHighlightSwitch(gleason_pattern) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + var gleason_shapes = vm._getGleasonShapesLabels(gleason_pattern); + var pattern_highlighted = vm.gleasonHighlighted[gleason_pattern]; + if (pattern_highlighted) { + var shape_color = "#ffffff"; + var shape_alpha = "0"; + } else { + var shape_color = vm.gleasonPatternsColors[gleason_pattern]; + var shape_alpha = "0.35"; + } + for (const shape of gleason_shapes) { + AnnotationsViewerService.setShapeFillColor(shape, shape_color, shape_alpha); + } + vm.gleasonHighlighted[gleason_pattern] = !vm.gleasonHighlighted[gleason_pattern]; + } + } + } + + function isHighlighted(gleason_pattern) { + if (vm.gleasonDetails !== undefined && vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + return vm.gleasonHighlighted[gleason_pattern]; + } else { + return false; + } + + } + + function selectGleasonPatterns(gleason_pattern, activate) { + if (vm.gleasonDetails !== undefined) { + if (vm.gleasonDetails.hasOwnProperty(gleason_pattern)) { + var gleason_shapes = vm._getGleasonShapesLabels(gleason_pattern); + if (activate) { + AnnotationsViewerService.selectShapes(gleason_shapes); + } else { + AnnotationsViewerService.deselectShapes(gleason_shapes); + } + } + } + } + } NewFocusRegionAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', @@ -1314,8 +1733,8 @@ 'ClinicalAnnotationStepManagerService']; function NewFocusRegionAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - FocusRegionsManagerService, FocusRegionAnnotationsManagerService, - AnnotationsViewerService, ClinicalAnnotationStepManagerService) { + FocusRegionsManagerService, FocusRegionAnnotationsManagerService, + AnnotationsViewerService, ClinicalAnnotationStepManagerService) { var vm = this; vm.focus_region_id = undefined; vm.focus_region_label = undefined; @@ -1353,8 +1772,8 @@ ]; vm.areaUOM = [ - { id: 1, unit_of_measure: 'μm²'}, - { id: Math.pow(10, -6), unit_of_measure: 'mm²'} + { id: 1, unit_of_measure: 'μm²' }, + { id: Math.pow(10, -6), unit_of_measure: 'mm²' } ]; vm.gleason_element_types = undefined; @@ -1440,7 +1859,7 @@ vm.displayedGleasonElementsLabels = []; $scope.$on('focus_region_annotation.new', - function(event, focus_region_id) { + function (event, focus_region_id) { vm.focus_region_id = focus_region_id; FocusRegionsManagerService.get(vm.focus_region_id) .then(getFocusRegionSuccessFn, getFocusRegionErrorFn); @@ -1463,7 +1882,7 @@ function fetchGleasonElementTypesSuccessFn(response) { vm.gleason_element_types = response.data; vm.gleason_types_map = {}; - for (var i=0; i 0); + } + + function pausePolygonTool() { + AnnotationsViewerService.disableActiveTool(); + vm.polygon_tool_paused = true; + } + + function unpausePolygonTool() { + AnnotationsViewerService.startPolygonsTool(); + vm.polygon_tool_paused = false; + } + + function pauseFreehandTool() { + AnnotationsViewerService.disableActiveTool(); + if (vm.temporaryShapeExists()) { + AnnotationsViewerService.deactivatePreviewMode(); + } + vm.freehand_tool_paused = true; + } + + function unpauseFreehandTool() { + AnnotationsViewerService.startFreehandDrawingTool(); + if (vm.temporaryShapeExists()) { + AnnotationsViewerService.activatePreviewMode(); + } + vm.freehand_tool_paused = false; + } + + function pauseSubregionDrawingTool() { + AnnotationsViewerService.disableActiveTool(); + if (vm.temporaryShapeExists()) { + AnnotationsViewerService.deactivatePreviewMode(); + } + vm.subregion_tool_paused = true; + } + + function unpauseSubregionDrawingTool() { + AnnotationsViewerService.startFreehandDrawingTool(); + if (vm.temporaryShapeExists()) { + AnnotationsViewerService.activatePreviewMode(); + } + vm.subregion_tool_paused = false; + } + + function confirmPolygon() { + ngDialog.open({ + template: '/static/templates/dialogs/rois_check.html', + showClose: false, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false, + name: 'checkGleasonPattern', + onOpenCallback: function () { + var canvas_label = AnnotationsViewerService.getCanvasLabel(); + var $canvas = $("#" + canvas_label); + $canvas.on('polygon_saved', + function (event, polygon_label) { + var focus_regions = $rootScope.focus_regions; + for (var fr in focus_regions) { + if (AnnotationsViewerService.checkContainment(focus_regions[fr].label, polygon_label) || + AnnotationsViewerService.checkContainment(polygon_label, focus_regions[fr].label)) { + AnnotationsViewerService.adaptToContainer(focus_regions[fr].label, polygon_label); + if (vm.shape_label !== polygon_label) { + AnnotationsViewerService.changeShapeId(polygon_label, vm.shape_label); + vm.shape = AnnotationsViewerService.getShapeJSON(vm.shape_label); + } else { + vm.shape = AnnotationsViewerService.getShapeJSON(polygon_label); + } + vm._updateGleasonPatternData(vm.shape.shape_id, focus_regions[fr]); + break; + } + } + ngDialog.close('checkGleasonPattern'); + if (typeof vm.shape === 'undefined') { + AnnotationsViewerService.deleteShape(polygon_label); + ngDialog.open({ + 'template': '/static/templates/dialogs/invalid_gleason_pattern.html' + }); + } + vm.abortTool(); + $scope.$apply(); + } + ); + setTimeout(function () { + AnnotationsViewerService.saveTemporaryPolygon('gleason_pattern'); + }, 10); + } + }); + } + + function confirmFreehandShape() { + ngDialog.open({ + template: '/static/templates/dialogs/rois_check.html', + showClose: false, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false, + name: 'checkGleasonPattern', + onOpenCallback: function () { + var canvas_label = AnnotationsViewerService.getCanvasLabel(); + var $canvas = $("#" + canvas_label); + $canvas.on('freehand_polygon_saved', + function (event, polygon_label) { + if (vm.active_tool = vm.FREEHAND_TOOL) { + var focus_regions = $rootScope.focus_regions; + for (var fr in focus_regions) { + if (AnnotationsViewerService.checkContainment(focus_regions[fr].label, polygon_label) || + AnnotationsViewerService.checkContainment(polygon_label, focus_regions[fr].label)) { + AnnotationsViewerService.adaptToContainer(focus_regions[fr].label, polygon_label); + if (vm.shape_label !== polygon_label) { + AnnotationsViewerService.changeShapeId(polygon_label, vm.shape_label); + vm.shape = AnnotationsViewerService.getShapeJSON(vm.shape_label); + } else { + vm.shape = AnnotationsViewerService.getShapeJSON(polygon_label); + } + vm._updateGleasonPatternData(vm.shape.shape_id, focus_regions[fr]); + break; + } + } + ngDialog.close('checkGleasonPattern'); + if (typeof vm.shape === 'undefined') { + AnnotationsViewerService.delete_shape(polygon_label); + ngDialog.open({ + 'template': '/static/templates/dialogs/invalid_gleason_pattern.html' + }); + } + vm.abortTool(); + $scope.$apply(); + } + } + ); + setTimeout(function () { + AnnotationsViewerService.saveTemporaryFreehandShape(); + }, 10) + } + }); + } + + function confirmTemporarySubregionShape() { + ngDialog.open({ + template: '/static/templates/dialogs/rois_check.html', + showClose: false, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false, + name: 'checkTemporarySubregion', + onOpenCallback: function () { + var canvas_label = AnnotationsViewerService.getCanvasLabel(); + var $canvas = $("#" + canvas_label); + $canvas.on("freehand_polygon_saved", + function (event, polygon_label) { + if (vm.active_tool == vm.SUBREGION_TOOL) { + if (AnnotationsViewerService.checkContainment(vm.shape_label, polygon_label) || + AnnotationsViewerService.checkContainment(polygon_label, vm.shape_label)) { + AnnotationsViewerService.adaptToContainer(vm.shape_label, polygon_label); + vm.tmp_subregion_label = polygon_label; + vm.tmp_subregion = AnnotationsViewerService.getShapeJSON(polygon_label); + } + ngDialog.close('checkTemporarySubregion'); + vm.abortTool(true); + $scope.$apply(); + } + } + ); + setTimeout(function () { + AnnotationsViewerService.saveTemporaryFreehandShape(); + }, 10); + } + }); + } + + function polygonRollbackPossible() { + return AnnotationsViewerService.temporaryPolygonExists(); + } + + function polygonRestorePossible() { + return AnnotationsViewerService.polygonRestoreHistoryExists(); + } + + function rollbackPolygon() { + AnnotationsViewerService.rollbackPolygon(); + } + + function restorePolygon() { + AnnotationsViewerService.restorePolygon(); + } + + function shapeRollbackPossible() { + return (AnnotationsViewerService.tmpFreehandPathExists() || + AnnotationsViewerService.shapeUndoHistoryExists()); + } + + function shapeRestorePossible() { + return AnnotationsViewerService.shapeRestoreHistoryExists(); + } + + function rollbackFreehandShape() { + AnnotationsViewerService.rollbackTemporaryFreehandShape(); + } + + function restoreFreehandShape() { + AnnotationsViewerService.restoreTemporaryFreehandShape(); + } + + function clear(destroy_shape) { + vm.deleteShape(destroy_shape); + if (vm.subregionCreationInProgress()) { + vm.deleteTemporarySubregion(); + } + vm.deleteSubregions(); + vm.shape_label = undefined; + vm.default_shape_label = undefined; + vm.actionStartTime = undefined; + } + + function abortTool(keep_subregion_tool_active = false) { + if (vm.active_tool === vm.POLYGON_TOOL) { + AnnotationsViewerService.clearTemporaryPolygon(); + $("#" + AnnotationsViewerService.getCanvasLabel()).unbind('polygon_saved'); + } + if (vm.active_tool === vm.FREEHAND_TOOL || vm.active_tool === vm.SUBREGION_TOOL) { + AnnotationsViewerService.clearTemporaryFreehandShape(); + $("#" + AnnotationsViewerService.getCanvasLabel()) + .unbind('freehand_polygon_saved') + .unbind('freehand_polygon_paused'); + if (vm.active_tool === vm.SUBREGION_TOOL && !keep_subregion_tool_active) { + vm.deactivateSubregionCreationMode(); + } + } + AnnotationsViewerService.disableActiveTool(); + vm.active_tool = undefined; + vm.polygon_tool_paused = false; + vm.freehand_tool_paused = false; + vm.subregion_tool_paused = false; + } + + function deleteTemporaryGleasonPattern(destroy_shape) { + if (Object.keys(vm.subregions_list).length > 0) { + vm.deleteSubregions(); + } + vm.deleteShape(destroy_shape); + } + + function deleteShape(destroy_shape) { + if (typeof vm.shape !== 'undefined') { + if (destroy_shape === true) { + AnnotationsViewerService.deleteShape(vm.shape.shape_id); + } + vm.shape = undefined; + vm.gleasonPatternArea = undefined; + vm.parentFocusRegion = undefined; + vm.pattern_type = undefined; + vm.pattern_type_confirmed = false; + } + } + + function deleteTemporarySubregion() { + AnnotationsViewerService.deleteShape(vm.tmp_subregion_label); + vm.resetTemporarySubregion(); + } + + function deleteSubregion(shape_label) { + if (vm.subregions_list.hasOwnProperty(shape_label)) { + AnnotationsViewerService.deleteShape(shape_label); + delete (vm.subregions_list[shape_label]); + } else { + $log.error(shape_label + ' is not a valid subregion label'); + } + } + + function deleteSubregions() { + for (var label in vm.subregions_list) { + vm.deleteSubregion(label); + } + } + + function selectShape(shape_id) { + AnnotationsViewerService.selectShape(shape_id); + } + + function deselectShape(shape_id) { + AnnotationsViewerService.deselectShape(shape_id); + } + + function focusOnShape(shape_id) { + AnnotationsViewerService.focusOnShape(shape_id); + } + + function updateGleasonPatternArea() { + + } + + function updateGleasonShapeColor(pattern_type) { + AnnotationsViewerService.setShapeStrokeColor(vm.shape_label, + vm.pattern_colors[pattern_type], 1.0); + vm.shape.stroke_color = vm.pattern_colors[pattern_type]; + } + + function patternTypeSelected() { + return typeof (vm.pattern_type) != 'undefined'; + } + + function subregionTypeSelected() { + return typeof (vm.tmp_subregion_type) != 'undefined'; + } + + function confirmPatternType() { + vm.pattern_type_confirmed = true; + } + + function acceptTemporarySubregion() { + vm.subregions_list[vm.tmp_subregion_label] = { + "label": vm.tmp_subregion_label, + "roi_json": AnnotationsViewerService.getShapeJSON(vm.tmp_subregion_label), + "area": AnnotationsViewerService.getShapeArea(vm.tmp_subregion_label), + "details_json": { "type": vm.tmp_subregion_type } + }; + vm.abortTool(); + vm.resetTemporarySubregion(); + } + + function checkPatternType(pattern_type) { + return vm.pattern_type === pattern_type; + } + + function resetPatternType() { + vm.pattern_type = undefined; + vm.pattern_type_confirmed = false; + } + + function resetTemporarySubregion() { + vm.tmp_subregion_label = undefined; + vm.tmp_subregion_type = undefined; + vm.tmp_subregion = undefined; + vm.deactivateSubregionCreationMode(); + } + + function patternTypeConfirmed() { + return vm.pattern_type_confirmed; + } + + function formValid() { + return vm.patternTypeConfirmed() && !vm.subregionCreationInProgress(); + } + + function isLocked() { + + } + + function destroy() { + vm.clear(true); + vm.abortTool(); + $rootScope.$broadcast('tool.destroyed'); + } + + function _prepareSubregionsData() { + var subregions_data = [] + for (var x in vm.subregions_list) { + subregions_data.push({ + "label": vm.subregions_list[x]["label"], + "roi_json": JSON.stringify(vm.subregions_list[x]["roi_json"]), + "area": vm.subregions_list[x]["area"], + "details_json": JSON.stringify(vm.subregions_list[x]["details_json"]) + }); + } + return subregions_data; + } + + function save() { + var dialog = ngDialog.open({ + template: '/static/templates/dialogs/saving_data.html', + showClose: false, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false + }); + + var gleason_pattern_config = { + "label": vm.shape_label, + "gleason_type": vm.pattern_type, + "roi_json": JSON.stringify(vm.shape), + "area": vm.gleasonPatternArea, + "subregions": vm._prepareSubregionsData(), + "action_start_time": vm.actionStartTime, + "action_complete_time": new Date() + } + GleasonPatternAnnotationsManagerService.createAnnotation( + vm.parentFocusRegion.id, + vm.clinical_annotation_step_label, + gleason_pattern_config + ).then(createGleasonPatternSuccessFn, createGleasonPatternErrorFn); + + function createGleasonPatternSuccessFn(response) { + var gleason_pattern_info = { + 'id': response.data.id, + 'label': response.data.label, + 'focus_region': response.data.focus_region, + 'annotated': true + }; + vm.clear(false); + $rootScope.$broadcast('gleason_pattern.new', gleason_pattern_info); + dialog.close(); + } + + function createGleasonPatternErrorFn(response) { + $log.error('Unable to save gleason pattern'); + $log.error(response.data); + dialog.close(); + } + } + } + + ShowGleasonPatternAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', + 'AnnotationsViewerService', 'CurrentSlideDetailsService', 'GleasonPatternAnnotationsManagerService']; + + function ShowGleasonPatternAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, + AnnotationsViewerService, CurrentSlideDetailsService, GleasonPatternAnnotationsManagerService) { + + var vm = this; + vm.clinical_annotation_step_label = undefined; + vm.slide_id = undefined; + vm.case_id = undefined; + vm.gleason_pattern_id = undefined; + vm.shape_label = undefined; + vm.pattern_type = undefined; + vm.pattern_label = undefined; + vm.subregions = undefined; + vm.subregions_active = undefined; + + vm.locked = undefined; + + vm.isReadOnly = isReadOnly; + vm.destroy = destroy; + vm.subregionsExist = subregionsExist; + vm.isShapeActive = isShapeActive; + vm.selectShape = selectShape; + vm.deselectShape = deselectShape; + vm.focusOnShape = focusOnShape; + vm.switchShapeActiveState = switchShapeActiveState; + vm.deleteAnnotation = deleteAnnotation; + + activate(); + + function activate() { + vm.slide_id = CurrentSlideDetailsService.getSlideId(); + vm.case_id = CurrentSlideDetailsService.getCaseId(); + + vm.clinical_annotation_step_label = $routeParams.label; + + $scope.$on('gleason_pattern.show', + function(event, gleason_pattern_id) { + vm.subregions = {}; + vm.subregions_active = {}; + vm.locked = false; + vm.gleason_pattern_id = gleason_pattern_id; + GleasonPatternAnnotationsManagerService.getAnnotation(vm.gleason_pattern_id) + .then(getGleasonPatternSuccessFn, getGleasonPatternErrorFn); + } + ); + + function getGleasonPatternSuccessFn(response) { + vm.shape_label = response.data.label; + vm.pattern_type = response.data.gleason_type; + vm.pattern_label = response.data.gleason_label; + for (var s_index in response.data.subregions) { + var subr = response.data.subregions[s_index]; + vm.subregions[subr.label] = { + "label": subr.label, + "area": subr.area, + "id": subr.id, + "type": $.parseJSON(subr.details_json)["type"], + "json_path": $.parseJSON(subr.roi_json) + }; + vm.subregions_active[subr.label] = true; + AnnotationsViewerService.drawShape($.parseJSON(subr.roi_json)); + $rootScope.gleason_patterns_subregions.push(subr.label); + } + } + + function getGleasonPatternErrorFn(response) { + $log.error('Unable to load Gleason Pattern annotation'); + $log.error(response); + } + } + + function isReadOnly() { + return true; + } + + function destroy() { + $rootScope.$broadcast('annotation_panel.closed'); + } + + function subregionsExist() { + if (typeof(vm.subregions) == "undefined") { + return false; + } else { + return (Object.keys(vm.subregions).length > 0); + } + } + + function isShapeActive(shape_id) { + return vm.subregions_active[shape_id]; + } + + function selectShape(shape_id) { + if (vm.isShapeActive(shape_id)) { + AnnotationsViewerService.selectShape(shape_id); + } + } + + function deselectShape(shape_id) { + if (vm.isShapeActive(shape_id)) { + AnnotationsViewerService.deselectShape(shape_id); + } + } + + function focusOnShape(shape_id) { + if (vm.isShapeActive(shape_id)) { + AnnotationsViewerService.focusOnShape(shape_id); + } + } + + function switchShapeActiveState(shape_id) { + vm.subregions_active[shape_id] = !vm.subregions_active[shape_id]; + if(vm.subregions_active[shape_id]) { + AnnotationsViewerService.drawShape(vm.subregions[shape_id].json_path); + vm.selectShape(shape_id); + $("#" + shape_id + "_showhide").removeClass('prm-pale-icon'); + } else { + AnnotationsViewerService.deleteShape(shape_id); + vm.deselectShape(shape_id); + $("#" + shape_id + "_showhide").addClass('prm-pale-icon'); + } + } + + function deleteAnnotation() { + ngDialog.openConfirm({ + template: '/static/templates/dialogs/delete_annotation_confirm.html', + closeByEscape: false, + showClose: false, + closeByNavigation: false, + closeByDocument: false + }).then(confirmFn); + + var dialog = undefined; + + function confirmFn(confirm_value) { + if (confirm_value) { + dialog = ngDialog.open({ + template: '/static/templates/dialogs/deleting_data.html', + showClose: true, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false + }); + GleasonPatternAnnotationsManagerService.deleteAnnotation(vm.gleason_pattern_id) + .then(deleteGleasonPatternSuccessFn, deleteGleasonPatternErrorFn); + } + } + + function deleteGleasonPatternSuccessFn(response) { + $rootScope.$broadcast('gleason_pattern.deleted', + { + 'gleason_pattern_id': vm.gleason_pattern_id, + 'gleason_pattern_label': vm.shape_label + } + ); + dialog.close(); + } + + function deleteGleasonPatternErrorFn(response) { + $log.error('unable to delete core annotation'); + $log.error(response); + dialog.close(); + } + } + } })(); \ No newline at end of file diff --git a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.directives.js b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.directives.js index c2ff8a5d..3605d5fd 100644 --- a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.directives.js +++ b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.directives.js @@ -35,7 +35,13 @@ .directive('newFocusRegionAnnotationForm', newFocusRegionAnnotationForm) .directive('newFocusRegionAnnotationButtons', newFocusRegionAnnotationButtons) .directive('showFocusRegionAnnotationController', showFocusRegionAnnotationForm) - .directive('showFocusRegionAnnotationButtons', showFocusRegionAnnotationButtons); + .directive('showFocusRegionAnnotationButtons', showFocusRegionAnnotationButtons) + .directive('newGleasonPatternAnnotationForm', newGleasonPatternAnnotationForm) + .directive('newGleasonPatternAnnotationButtons', newGleasonPatternAnnotationButtons) + .directive('newGleasonFourAnnotationForm', newGleasonFourAnnotationForm) + .directive('newGleasonFiveAnnotationForm', newGleasonFiveAnnotationForm) + .directive('showGleasonPatternAnnotationForm', showGleasonPatternAnnotationForm) + .directive('showGleasonPatternAnnotationButtons', showGleasonPatternAnnotationButtons); function newSliceAnnotationForm() { var directive = { @@ -200,4 +206,70 @@ }; return directive; } + + function newGleasonPatternAnnotationForm() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/gleason_pattern_annotation.html', + controller: 'NewGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } + + function newGleasonPatternAnnotationButtons() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/buttons_ctrl_group.html', + controller: 'NewGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } + + function newGleasonFourAnnotationForm() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/gleason_four_annotation.html', + controller: 'NewGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } + + function newGleasonFiveAnnotationForm() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/gleason_five_annotation.html', + controller: 'NewGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } + + function showGleasonPatternAnnotationForm() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/gleason_pattern_annotation.html', + controller: 'ShowGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } + + function showGleasonPatternAnnotationButtons() { + var directive = { + replace: true, + restrict: 'E', + templateUrl: '/static/templates/clinical_annotations_manager/buttons_ctrl_group.html', + controller: 'ShowGleasonPatternAnnotationController', + controllerAs: 'cmCtrl' + }; + return directive; + } })(); \ No newline at end of file diff --git a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.services.js b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.services.js index eb4efc76..2321cb68 100644 --- a/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.services.js +++ b/promort/src/js/clinical_annotations_manager/clinical_annotations_manager.services.js @@ -26,8 +26,10 @@ .module('promort.clinical_annotations_manager.services') .factory('ClinicalAnnotationStepManagerService', ClinicalAnnotationStepManagerService) .factory('SliceAnnotationsManagerService', SliceAnnotationsManagerService) + .factory('CoreGleasonDetailsManagerService', CoreGleasonDetailsManagerService) .factory('CoreAnnotationsManagerService', CoreAnnotationsManagerService) - .factory('FocusRegionAnnotationsManagerService', FocusRegionAnnotationsManagerService); + .factory('FocusRegionAnnotationsManagerService', FocusRegionAnnotationsManagerService) + .factory('GleasonPatternAnnotationsManagerService', GleasonPatternAnnotationsManagerService); ClinicalAnnotationStepManagerService.$inject = ['$http', '$log']; @@ -79,6 +81,20 @@ } } + CoreGleasonDetailsManagerService.$inject = ['$http', '$log']; + + function CoreGleasonDetailsManagerService($http, $log) { + var CoreGleasonDetailsManagerService = { + get: get + }; + + return CoreGleasonDetailsManagerService; + + function get(core_id, annotation_step_label) { + return $http.get('/api/cores/' + core_id + '/clinical_annotations/' + annotation_step_label + '/gleason_details/'); + } + } + CoreAnnotationsManagerService.$inject = ['$http', '$log']; function CoreAnnotationsManagerService($http, $log) { @@ -131,4 +147,30 @@ annotation_step_label + '/'); } } + + GleasonPatternAnnotationsManagerService.$inject = ['$http', '$log']; + + function GleasonPatternAnnotationsManagerService($http, $log) { + var GleasonPatternAnnotationsManagerService = { + getAnnotation: getAnnotation, + createAnnotation: createAnnotation, + deleteAnnotation: deleteAnnotation + }; + + return GleasonPatternAnnotationsManagerService; + + function getAnnotation(gleason_pattern_id) { + return $http.get('/api/gleason_patterns/' + gleason_pattern_id + '/'); + } + + function createAnnotation(focus_region_id, annotation_step_label, gleason_pattern_config) { + return $http.post('/api/focus_regions/' + focus_region_id + '/clinical_annotations/' + + annotation_step_label + '/gleason_patterns/', + gleason_pattern_config); + } + + function deleteAnnotation(gleason_pattern_id) { + return $http.delete('api/gleason_patterns/' + gleason_pattern_id + '/'); + } + } })(); \ No newline at end of file diff --git a/promort/src/js/ome_seadragon_viewer/viewer.controllers.js b/promort/src/js/ome_seadragon_viewer/viewer.controllers.js index 96e20e6e..2802bc2a 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.controllers.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.controllers.js @@ -163,8 +163,6 @@ if (vm.loading_tiled_images === 0) { dialog.close(); } - } else { - console.log('Nothing to do...'); } }); @@ -305,8 +303,6 @@ if (vm.loading_tiled_images === 0) { dialog.close(); } - } else { - console.log('Nothing to do...'); } }); @@ -359,7 +355,6 @@ $scope.$on('rois_viewerctrl.components.registered', function(event, rois_read_only, clinical_annotation_step_label) { - console.log(event); var dialog = ngDialog.open({ template: '/static/templates/dialogs/rois_loading.html', showClose: false, @@ -406,7 +401,7 @@ var focus_region = core.focus_regions[fr]; AnnotationsViewerService.drawShape($.parseJSON(focus_region.roi_json)); annotated = false; - if (core.hasOwnProperty('annotated')) { + if (focus_region.hasOwnProperty('annotated')) { annotated = focus_region.annotated; } var focus_region_info = { @@ -418,6 +413,19 @@ 'stressed': focus_region.tissue_status === 'STRESSED' }; $rootScope.$broadcast('focus_region.new', focus_region_info); + if (focus_region.hasOwnProperty('gleason_patterns')) { + for (var gp in focus_region.gleason_patterns) { + var gleason_pattern = focus_region.gleason_patterns[gp]; + AnnotationsViewerService.drawShape($.parseJSON(gleason_pattern.roi_json)); + var gleason_pattern_info = { + 'id': gleason_pattern.id, + 'label': gleason_pattern.label, + 'focus_region': gleason_pattern.focus_region, + 'annotated': true + } + $rootScope.$broadcast('gleason_pattern.new', gleason_pattern_info); + } + } } } } @@ -457,7 +465,6 @@ $log.info('Registering components'); AnnotationsViewerService.registerComponents(viewer_manager, annotations_manager, tools_manager); - $log.debug('--- VERIFY ---'); AnnotationsViewerService.checkComponents(); var clinical_annotation_step_label = undefined; if (rois_read_only) { @@ -562,15 +569,9 @@ ) $scope.$on('slides_sequence.page.change', function(event, args) { - if (args.viewer_id === vm.getViewerID()) { - console.log('Ignore change page trigger, it was me'); - } else { - console.log('Received order to change to page ' + args.page); + if (args.viewer_id !== vm.getViewerID()) { if (vm.checkPage(args.page)) { - console.log('Changing to page ' + args.page); vm.goToPage(args.page, false); - } else { - console.log('SlidesSet has no page ' + args.page); } } }); @@ -636,7 +637,6 @@ SlidesSequenceViewerService.goToPage(vm.getViewerID(), pages_map[page_label]); vm.current_page = page_label; if (trigger_event) { - console.log('Trigger page changed event'); $rootScope.$broadcast('slides_sequence.page.changed', {'page': page_label, 'viewer_id': vm.getViewerID()}); } diff --git a/promort/src/js/ome_seadragon_viewer/viewer.directives.js b/promort/src/js/ome_seadragon_viewer/viewer.directives.js index ff76c288..711eb072 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.directives.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.directives.js @@ -365,27 +365,14 @@ ome_seadragon_viewer.addAnnotationsController(annotations_canvas, true); var tools_manager = new AnnotationsEventsController(annotations_canvas); - //initialize area measuring tools + //initialize tools var shape_config = { - fill_alpha: 0.2, - fill_color: '#ff0000', - stroke_width: 5, - stroke_color: '#ff0000' + 'stroke_color': '#dddddd', + 'stroke_width': 20 }; - tools_manager.initializeAreaMeasuringTool(shape_config); - // initialize cellular count helper tool - var helper_box_config = { - fill_alpha: 0.01, - stroke_width: 5, - stroke_color: '#0000ff' - }; - // box size in microns - var box_size = 50; - tools_manager.initializeCellularCountHelperTool(box_size, helper_box_config); - tools_manager.bindControllers('cell_counter_activate', - 'cell_counter_save'); - tools_manager.bindControllers('g4_cell_counter_activate', - 'g4_cell_counter_save'); + // tools_manager.initializeAreaMeasuringTool(shape_config); + tools_manager.initializePolygonDrawingTool(shape_config); + tools_manager.initializeFreehandDrawingTool(shape_config); scope.avc.registerComponents(ome_seadragon_viewer, annotations_canvas, tools_manager, true); diff --git a/promort/src/js/ome_seadragon_viewer/viewer.services.js b/promort/src/js/ome_seadragon_viewer/viewer.services.js index a8b7bec3..04e29fda 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.services.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.services.js @@ -149,6 +149,8 @@ drawShape: drawShape, selectShape: selectShape, deselectShape: deselectShape, + selectShapes: selectShapes, + deselectShapes: deselectShapes, extendPolygonConfig: extendPolygonConfig, extendPathConfig: extendPathConfig, extendRulerConfig: extendRulerConfig, @@ -204,7 +206,8 @@ restoreAreaRuler: restoreAreaRuler, saveAreaRuler: saveAreaRuler, getAreaCoverage: getAreaCoverage, - setShapeStrokeColor: setShapeStrokeColor + setShapeStrokeColor: setShapeStrokeColor, + setShapeFillColor: setShapeFillColor }; return AnnotationsViewerService; @@ -222,7 +225,6 @@ } function drawShape(shape_json) { - console.log(shape_json); this.roisManager.drawShapeFromJSON(shape_json); } @@ -234,6 +236,14 @@ this.roisManager.deselectShape(shape_id, true); } + function selectShapes(shapes_ids) { + this.roisManager.selectShapes(shapes_ids, true, true); + } + + function deselectShapes(shapes_ids) { + this.roisManager.deselectShapes(shapes_ids, true); + } + function extendPolygonConfig(polygon_config) { this.roisManager.extendPolygonConfig(polygon_config); } @@ -486,6 +496,11 @@ var shape = this.roisManager.getShape(shape_id); shape.setStrokeColor(color, alpha); } + + function setShapeFillColor(shape_id, color, alpha) { + var shape = this.roisManager.getShape(shape_id); + shape.setFillColor(color, alpha); + } } SlidesSequenceViewerService.$inject = ['$log']; diff --git a/promort/src/js/rois_manager/rois_manager.controllers.js b/promort/src/js/rois_manager/rois_manager.controllers.js index 866deb98..b0d69ade 100644 --- a/promort/src/js/rois_manager/rois_manager.controllers.js +++ b/promort/src/js/rois_manager/rois_manager.controllers.js @@ -165,8 +165,6 @@ activate(); function activate() { - console.log("Prediction ID: " + CurrentPredictionDetailsService.getPredictionId()); - vm.slide_id = CurrentSlideDetailsService.getSlideId(); vm.case_id = CurrentSlideDetailsService.getCaseId(); vm.annotation_step_label = $routeParams.label; @@ -265,7 +263,6 @@ $scope.$on('slice.deleted', function (event, slice_id) { - $log.debug('SLICE ' + slice_id + ' DELETED'); var cores = _getSliceCores(slice_id); cores.forEach( function (item, index) { @@ -306,11 +303,9 @@ $scope.$on('core.deleted', function (event, core_id) { - $log.debug('CORE ' + core_id + ' DELETED'); var focus_regions = _getCoreFocusRegions(core_id); focus_regions.forEach( function (item, index) { - $log.debug('Broadcasting delete evento for focus region ' + item.id); $rootScope.$broadcast('focus_region.deleted', item.id); } ); @@ -347,7 +342,6 @@ $scope.$on('focus_region.deleted', function (event, focus_region_id) { - $log.debug('FOCUS REGION ' + focus_region_id + ' DELETED'); AnnotationsViewerService.deleteShape(vm._getFocusRegionLabel(focus_region_id)); $("#" + vm._getFocusRegionLabel(focus_region_id) + "_list").remove(); vm._unregisterFocusRegion(focus_region_id); @@ -456,7 +450,6 @@ } function _getCoreFocusRegions(core_id) { - $log.debug($rootScope.focus_regions); return $.grep($rootScope.focus_regions, function(value) { return value.core === core_id; @@ -569,13 +562,10 @@ } function switchNavmapDisplay() { - console.log('Switching navmap display'); vm.displayNavmap = !vm.displayNavmap; if (vm.navmapDisplayEnabled()) { - console.log('Draw navmap'); vm._drawNavmap(); } else { - console.log('Hide navmap'); vm._hideNavmap(); } } @@ -988,7 +978,6 @@ } function updateOverlayPalette() { - console.log('Current overlay palette is: ' + vm.overlay_palette); HeatmapViewerService.setOverlay(vm.overlay_palette, vm.overlay_threshold); } } @@ -1090,7 +1079,6 @@ } function newPolygon() { - $log.debug('Start polygon drawing tool'); AnnotationsViewerService.extendPolygonConfig(vm.shape_config); AnnotationsViewerService.startPolygonsTool(); vm.active_tool = vm.POLYGON_TOOL; @@ -1111,7 +1099,6 @@ } function newFreehand() { - $log.debug('Start freehand drawing tool'); AnnotationsViewerService.setFreehandToolLabelPrefix('slice'); AnnotationsViewerService.extendPathConfig(vm.shape_config); AnnotationsViewerService.startFreehandDrawingTool(); @@ -1139,14 +1126,11 @@ function setNewLabel() { if (typeof vm.shape !== 'undefined' && vm.shape_label === vm.shape.shape_id) { - $log.debug('Shape label not changed'); vm.deactivateEditLabelMode(); } else { if (AnnotationsViewerService.shapeIdAvailable(vm.shape_label)) { - $log.debug('Label available, assigning to new shape'); vm.deactivateEditLabelMode(); } else { - $log.debug('Label in use, restoring previous label'); vm.abortEditLabelMode(); ngDialog.open({ 'template': '/static/templates/dialogs/invalid_label.html' @@ -1160,10 +1144,8 @@ vm.edit_shape_label = false; // if a shape already exists, change its name if (typeof vm.shape !== 'undefined' && vm.shape.shape_id !== vm.shape_label) { - $log.debug('updating shape id'); AnnotationsViewerService.changeShapeId(vm.shape.shape_id, vm.shape_label); vm.shape = AnnotationsViewerService.getShapeJSON(vm.shape_label); - $log.debug('new shape id is: ' + vm.shape.shape_id); } } @@ -1509,7 +1491,6 @@ function activate() { $scope.$on('slice.load', function(event, slice_id) { - $log.debug('Show slice ' + slice_id); vm.slice_id = slice_id; SlicesManagerService.get(slice_id) .then(getSliceSuccessFn, getSliceErrorFn); @@ -1631,7 +1612,7 @@ vm.tumor_ruler_off_id = 'new_core_tumor_ruler_off'; vm.tumor_ruler_output_id = 'new_core_tumor_ruler_output'; - vm.tmp_ruler_exists =false; + vm.tmp_ruler_exists = false; vm.active_tool = undefined; vm.polygon_tool_paused = false; @@ -1923,14 +1904,11 @@ function setNewLabel() { if (typeof vm.shape !== 'undefined' && vm.shape_label === vm.shape.shape_id){ - $log.debug('Shape label not changed'); vm.deactivateEditLabelMode(); } else { if (AnnotationsViewerService.shapeIdAvailable(vm.shape_label)) { - $log.debug('Label available, assigning to new shape'); vm.deactivateEditLabelMode(); } else { - $log.debug('Label in use, restoring previous label'); vm.abortEditLabelMode(); ngDialog.open({ 'template': '/static/templates/dialogs/invalid_label.html' @@ -1944,10 +1922,8 @@ vm.edit_shape_label = false; // if a shape already exists, change its name if (typeof vm.shape !== 'undefined' && vm.shape.shape_id !== vm.shape_label) { - $log.debug('updating shape id'); AnnotationsViewerService.changeShapeId(vm.shape.shape_id, vm.shape_label); vm.shape = AnnotationsViewerService.getShapeJSON(vm.shape_label); - $log.debug('new shape id is: ' + vm.shape.shape_id); } } @@ -2828,7 +2804,6 @@ $scope.$on('core.load', function(event, core_id) { - $log.debug('Show core ' + core_id); vm.core_id = core_id; CoresManagerService.get(core_id) .then(getCoreSuccessFn, getCoreErrorFn); @@ -3215,14 +3190,11 @@ function setNewLabel() { if (typeof vm.shape !== 'undefined' && vm.shape_label === vm.shape.shape_id) { - $log.debug('Shape label not changed'); vm.deactivateEditLabelMode(); } else { if (AnnotationsViewerService.shapeIdAvailable(vm.shape_label)) { - $log.debug('Label available, assigning to new shape'); vm.deactivateEditLabelMode(); } else { - $log.debug('Label in use, restoring previous label'); vm.abortEditLabelMode(); ngDialog.open({ 'template': '/static/templates/dialogs/invalid_label.html' @@ -3236,10 +3208,8 @@ vm.edit_shape_label = false; // if a shape already exists, change its name if (typeof vm.shape !== 'undefined' && vm.shape.shape_id !== vm.shape_label) { - $log.debug('updating shape id'); AnnotationsViewerService.changeShapeId(vm.shape.shape_id, vm.shape_label); vm.shape = AnnotationsViewerService.getShapeJSON(vm.shape_label); - $log.debug('new shape id is: ' + vm.shape.shape_id); } } @@ -3978,7 +3948,6 @@ $scope.$on('focus_region.load', function (event, focus_region_id, parent_shape_id) { - $log.debug('Show focus region ' + focus_region_id); vm.focus_region_id = focus_region_id; vm.parent_shape_id = parent_shape_id; FocusRegionsManagerService.get(focus_region_id) diff --git a/promort/static_src/css/promort.css b/promort/static_src/css/promort.css index 1e4bc996..6974cd4b 100644 --- a/promort/static_src/css/promort.css +++ b/promort/static_src/css/promort.css @@ -267,6 +267,18 @@ h3.prm-dialog-container { color: #bbbbbb; } +.prm-g3-active-icon { + color: #ffcc99; +} + +.prm-g4-active-icon { + color: #ff9966; +} + +.prm-g5-active-icon { + color: #cc5200; +} + .prm-imq-header { border-top: 1px solid #eee; padding-top: 10px; diff --git a/promort/static_src/templates/clinical_annotations_manager/core_annotation.html b/promort/static_src/templates/clinical_annotations_manager/core_annotation.html index b2420085..da03a92a 100644 --- a/promort/static_src/templates/clinical_annotations_manager/core_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/core_annotation.html @@ -74,68 +74,235 @@

      {{ cmCtrl.core_label }}

      - grading -
      -
      - + gleason grading +
      +
      + +
      + +
      +
      +
      +
      + +
      + +
      +
      +
      +
      + gleason pattern details +
      +
      + +
      +
      +
      +

      Coverage

      +
      + +
      + +
      +
      +
      + +
      +
      +
      +

      Coverage

      +
      + +
      + +
      +
      +
      + +
      +
      +
      +

      Coverage

      +
      + +
      + +
      +
      +
      +
      +
      + cribriform pattern +
      +
      +
      - - -
      -
      -
      - +
      +
      +
      - -
      -
      -
      - +
      + +
      +
      + +
      +
      +
      + +
      +
      + +
      +
      +
      +
      + stroma +
      +
      +
      - - + -->
      +
      -
      -
      - +
      +
      +
      -
      - -
      %
      -
      + -->
      +
      -
      - +
      +
      - + -->
      +
      +
      +
      + +
      + +
      +
      +
      +
      + other +
      + +
      +
      + +
      +
      + +
      \ No newline at end of file diff --git a/promort/static_src/templates/clinical_annotations_manager/focus_region_annotation.html b/promort/static_src/templates/clinical_annotations_manager/focus_region_annotation.html index ce81aa5d..a42b2ac6 100644 --- a/promort/static_src/templates/clinical_annotations_manager/focus_region_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/focus_region_annotation.html @@ -71,183 +71,6 @@

      {{ cmCtrl.focus_region_label }}

      -
      - Gleason Data - - -
      -
      - -
      -
      - - - -
      -
      - -
      - -
      -
      - - - -
      -
      - -
      -
      - - - - - - -
      -
      - - - -
      -
      - -
      -
      - -
      μm²
      - -
      - - -
      -
      -
      - - - -
      -
      -
      - -
      -
      - -
      -
      -
      - - - - -
      -
      - - -
      -
      - - - -
      -
      -
      - - -
      - - -
      -
      -
      - - - -
      -
      -
      - - -
      - -
      -
      -
      - -
      -
      patterns diff --git a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html new file mode 100644 index 00000000..bc1a1b6d --- /dev/null +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -0,0 +1,388 @@ +
      + + +
      +

      NEW GLEASON PATTERN

      +
      +
      +

      {{ cmCtrl.shape_label }}

      +
      +
      +
      + +
      +
      + +
      +
      + + +
      +
      + +
      + + +
      +
      + + +
      +
      + +
      +
      + + + + + + +
      +
      + + +
      +
      + +
      +
      + + + + + + +
      +
      + + +
      +
      + +
      + + +
      +
      + + +
      +
      +
      + +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +

      Pattern {{cmCtrl.pattern_type}}

      +
      +
      +
      + + +
      +
      + +
      +
      +

      {{cmCtrl.pattern_label}}

      +
      +
      + + + +
      +
      +
      +
      + +
      +
      +
      + +
      +
      + + + + + + +
      +
      + +
      +
      + +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + +
      +
      + +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + +
      + + +
      +
      + + +
      + Subregions + +
      +
      +
      + +
      + + +
      +
      +
      +
      + + +
      + Subregions + +
      +
      +
      + +
      + + +
      +
      +
      +
      + +
      \ No newline at end of file diff --git a/promort/static_src/templates/clinical_annotations_manager/manager.html b/promort/static_src/templates/clinical_annotations_manager/manager.html index bafea3f3..bafb0442 100644 --- a/promort/static_src/templates/clinical_annotations_manager/manager.html +++ b/promort/static_src/templates/clinical_annotations_manager/manager.html @@ -28,6 +28,11 @@

      Clinical annotation - Slide {{ cmc.slide_index }}

      class="btn btn-default"> Back to slides + @@ -82,6 +87,14 @@

      ROIs list

      +
      + + +
      +
      + + +
      diff --git a/promort/static_src/templates/dialogs/invalid_gleason_4.html b/promort/static_src/templates/dialogs/invalid_gleason_pattern.html similarity index 95% rename from promort/static_src/templates/dialogs/invalid_gleason_4.html rename to promort/static_src/templates/dialogs/invalid_gleason_pattern.html index 70b73ed6..9c91991b 100644 --- a/promort/static_src/templates/dialogs/invalid_gleason_4.html +++ b/promort/static_src/templates/dialogs/invalid_gleason_pattern.html @@ -25,7 +25,7 @@

      - This Gleason 4 area can't be accepted.
      + This Gleason pattern area can't be accepted.
      It must be contained, even partially, inside the focus region.

      diff --git a/promort/utils/views.py b/promort/utils/views.py index 2d1b8f6e..a6d7cfa1 100644 --- a/promort/utils/views.py +++ b/promort/utils/views.py @@ -25,7 +25,7 @@ import promort.settings as prs from slides_manager.models import SlideEvaluation -from clinical_annotations_manager.models import ClinicalAnnotationStep, GleasonElement +from clinical_annotations_manager.models import ClinicalAnnotationStep, GleasonPattern import logging logger = logging.getLogger('promort') @@ -83,7 +83,7 @@ def get_gleason_element_types(request): { 'value': ch[0], 'text': ch[1] - } for ch in GleasonElement.GLEASON_TYPES + } for ch in GleasonPattern.GLEASON_TYPES ] return Response(gleason_types_map, status=status.HTTP_200_OK) diff --git a/requirements.txt b/requirements.txt index 1b20f5e5..b5bb5ba6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Django==3.1.13 djangorestframework==3.12.4 -pyyaml==5.4.1 +pyyaml==6.0.1 shapely==1.7.1 requests==2.25.1