From 44bef90ed8ab1ec4d406d2c07ad70bd4c5c297dd Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 10 Oct 2022 16:17:05 +0200 Subject: [PATCH 01/43] feat: updated Gleason patterns model --- ...0_0718_squashed_0022_auto_20221010_0858.py | 71 +++++++++++++++++++ .../migrations/0022_auto_20221010_0905.py | 64 +++++++++++++++++ .../migrations/0023_auto_20221010_1405.py | 51 +++++++++++++ .../0024_gleasonpatternsubregion.py | 26 +++++++ .../clinical_annotations_manager/models.py | 35 ++++++--- 5 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 promort/clinical_annotations_manager/migrations/0021_auto_20221010_0718_squashed_0022_auto_20221010_0858.py create mode 100644 promort/clinical_annotations_manager/migrations/0022_auto_20221010_0905.py create mode 100644 promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py create mode 100644 promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py 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 0000000..cd23633 --- /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 0000000..bc2e383 --- /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 0000000..1cb58c9 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py @@ -0,0 +1,51 @@ +# 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 0000000..84b0c34 --- /dev/null +++ b/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py @@ -0,0 +1,26 @@ +# 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/models.py b/promort/clinical_annotations_manager/models.py index 052da71..d2e65a5 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -195,25 +195,30 @@ 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'), + ('ST', 'STROMA'), + ('OT', 'OTHER') ) - 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 +230,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) From e81663920d9a4b6a8b3520d1402467652813769c Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 10 Oct 2022 16:35:22 +0200 Subject: [PATCH 02/43] fix: fixed how to retrieve Gleason 4 patterns for FocusRegionAnnotation objs --- promort/clinical_annotations_manager/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index d2e65a5..3b925fe 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -172,15 +172,16 @@ class FocusRegionAnnotation(models.Model): class Meta: unique_together = ('focus_region', 'annotation_step') + + def get_gleason_4_elements(self): + return self.annotation_step.gleason_annotations.filter(focus_region=self.focus_region).all() + 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_4_percentage(self): g4_area = self.get_total_gleason_4_area() try: From 341df4f5e9e49092b1dc290e821c61c856fe48d4 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Tue, 11 Oct 2022 17:40:11 +0200 Subject: [PATCH 03/43] feat: Gleason patterns and subregions serializers --- .../serializers.py | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 97365b7..51d7de3 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 @@ -100,62 +100,93 @@ 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') + write_only_fields = ('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): From 1fffde3df179bc0eeb7efb78e07edfef983ce8c8 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Tue, 11 Oct 2022 17:58:44 +0200 Subject: [PATCH 04/43] fix: get Gleason Pattern types --- promort/utils/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promort/utils/views.py b/promort/utils/views.py index 2d1b8f6..a6d7cfa 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) From d7251a1cb5f7fdb89dbbde7677ddba65f39403d1 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 13 Oct 2022 16:23:41 +0200 Subject: [PATCH 05/43] feat: REST API to get, create, delete GleasonPattern objects --- promort/clinical_annotations_manager/views.py | 69 +++++++++++++++---- promort/promort/urls.py | 6 +- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index baf4f7c..136823b 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -23,18 +23,19 @@ import json 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 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') @@ -240,23 +241,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 +272,53 @@ 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 _prepare_subregions(self, subregions_data): + for subregion in subregions_data: + subregion['roi_json'] = json.dumps(subregion['roi_json']) + try: + subregion['details_json'] = json.dumps(subregion['details_json']) + except KeyError: + subregion['details_json'] = None + return subregions_data + + 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 + if gleason_pattern_data.get('subregions'): + gleason_pattern_data['subregions'] = self._prepare_subregions(gleason_pattern_data['subregion']) + 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/promort/urls.py b/promort/promort/urls.py index 37d6e1a..d010a48 100644 --- a/promort/promort/urls.py +++ b/promort/promort/urls.py @@ -35,7 +35,7 @@ CoreDetail, FocusRegionList, FocusRegionDetail, ROIsTreeList from clinical_annotations_manager.views import AnnotatedROIsTreeList, ClinicalAnnotationStepAnnotationsList, \ SliceAnnotationList, SliceAnnotationDetail, CoreAnnotationList, CoreAnnotationDetail, \ - FocusRegionAnnotationList, FocusRegionAnnotationDetail + FocusRegionAnnotationList, FocusRegionAnnotationDetail, GleasonPatternList, GleasonPatternDetail import predictions_manager.views as pmv import shared_datasets_manager.views as shdv import odin.views as od @@ -161,6 +161,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()), From 1b780e6971d532679b6a2a8233a06c8a24bafa2b Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 14 Oct 2022 10:24:55 +0200 Subject: [PATCH 06/43] feat: include gleason patterns is clinical annotation's ROIs tree list --- .../serializers.py | 10 +++++++- promort/clinical_annotations_manager/views.py | 24 ++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 51d7de3..02b3c6d 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -200,13 +200,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 136823b..631b29b 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -51,14 +51,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: @@ -66,13 +73,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) From af42429c6d8fe90bdff48d2e39d4abb2020b785e Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 31 Oct 2022 11:40:34 +0100 Subject: [PATCH 07/43] feat: updated GleasonPattern model --- .../migrations/0023_auto_20221010_1405.py | 19 ++++++++ .../0024_gleasonpatternsubregion.py | 19 ++++++++ .../migrations/0025_auto_20221031_0938.py | 37 ++++++++++++++++ .../migrations/0026_auto_20221031_0938.py | 43 +++++++++++++++++++ .../migrations/0027_auto_20221031_1033.py | 18 ++++++++ .../clinical_annotations_manager/models.py | 3 +- 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 promort/clinical_annotations_manager/migrations/0025_auto_20221031_0938.py create mode 100644 promort/clinical_annotations_manager/migrations/0026_auto_20221031_0938.py create mode 100644 promort/clinical_annotations_manager/migrations/0027_auto_20221031_1033.py diff --git a/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py b/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py index 1cb58c9..ed2fc06 100644 --- a/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py +++ b/promort/clinical_annotations_manager/migrations/0023_auto_20221010_1405.py @@ -1,3 +1,22 @@ +# 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 diff --git a/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py b/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py index 84b0c34..d517b66 100644 --- a/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py +++ b/promort/clinical_annotations_manager/migrations/0024_gleasonpatternsubregion.py @@ -1,3 +1,22 @@ +# 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 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 0000000..5553a99 --- /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 0000000..cbc8d82 --- /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 0000000..fdaea0f --- /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/models.py b/promort/clinical_annotations_manager/models.py index 3b925fe..e51c1db 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -201,8 +201,7 @@ class GleasonPattern(models.Model): ('G3', 'GLEASON 3'), ('G4', 'GLEASON 4'), ('G5', 'GLEASON 5'), - ('ST', 'STROMA'), - ('OT', 'OTHER') + ('LG', 'LEGACY') ) label = models.CharField(max_length=25, blank=False) focus_region = models.ForeignKey(FocusRegion, related_name="gleason_patterns", blank=False, From fa912aaf08c5651ec5175d614df1525ea7a1c414 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 7 Nov 2022 15:47:16 +0100 Subject: [PATCH 08/43] feat: updated core annotation model --- .../migrations/0028_auto_20221107_1442.py | 73 +++++++++++++++++++ .../clinical_annotations_manager/models.py | 23 ++++++ .../serializers.py | 21 +++++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 promort/clinical_annotations_manager/migrations/0028_auto_20221107_1442.py 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 0000000..c0fbbf4 --- /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/models.py b/promort/clinical_annotations_manager/models.py index e51c1db..576eec6 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -100,6 +100,21 @@ class CoreAnnotation(models.Model): gleason_group = models.CharField( max_length=3, choices=GLEASON_GROUP_WHO_16, blank=False ) + # 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') @@ -138,6 +153,14 @@ def get_action_duration(self): return (self.action_complete_time-self.action_start_time).total_seconds() else: return None + + def get_largest_confluent_sheet(self): + # TODO: get largest cribriform object among all Gleason 4 elements of a core + pass + + def get_total_cribriform_area(self): + # TODO: sum of all cribriform objects defined on a core + pass class FocusRegionAnnotation(models.Model): diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 02b3c6d..aa3eda6 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -71,13 +71,22 @@ 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') + 'gleason_4_percentage', 'gleason_group', '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 @@ -87,6 +96,14 @@ def get_gleason_score(obj): @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): From 256e11c30f4917e554664456fd2d0ac2c0779b14 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 17 Nov 2022 12:03:28 +0100 Subject: [PATCH 09/43] fix: fixed bug in CoreAnnotationSerializer --- promort/clinical_annotations_manager/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index aa3eda6..84a800d 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -72,7 +72,7 @@ class CoreAnnotationSerializer(serializers.ModelSerializer): gleason_score = serializers.SerializerMethodField() gleason_4_percentage = serializers.SerializerMethodField() largest_confluent_sheet = serializers.SerializerMethodField() - total_cribriform_area = serializers.SerializerMethodField + total_cribriform_area = serializers.SerializerMethodField() class Meta: model = CoreAnnotation From 6eb3b8478e9115f64a914e2b9bad1ec28ceab68f Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 17 Nov 2022 16:24:24 +0100 Subject: [PATCH 10/43] feat: handle new core annotations data UI still needs to be improved, this is a partial update to be able to acquire the new annotation fields --- ...linical_annotations_manager.controllers.js | 88 +++++++++- .../core_annotation.html | 153 +++++++++++++++++- 2 files changed, 234 insertions(+), 7 deletions(-) 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 2548915..cae3fe9 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 @@ -935,7 +935,20 @@ vm.primaryGleason = undefined; vm.secondaryGleason = undefined; vm.gradeGroupWho = undefined; - vm.gradeGroupWhoLabel = ''; + vm.gradeGroupWhoLabel = '' + 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; @@ -964,6 +977,7 @@ vm.formValid = formValid; vm.destroy = destroy; vm.upgradeGradeGroupWho = updateGradeGroupWho; + vm.containsCribriformPattern = containsCribriformPattern; vm.save = save; vm.updateTumorLength = updateTumorLength; vm.updateCoreLength = updateCoreLength; @@ -1012,6 +1026,18 @@ vm.secondaryGleason = undefined; vm.gradeGroupWho = undefined; vm.gradeGroupWhoLabel = ''; + 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; } @@ -1061,6 +1087,11 @@ } } + function containsCribriformPattern() { + // TODO: implement this function to check if at least one cribriform pattern exists (using a service?) + return false; + } + function save() { var dialog = undefined; dialog = ngDialog.open({ @@ -1075,8 +1106,23 @@ 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; } + console.log(obj_config); CoreAnnotationsManagerService.createAnnotation(vm.core_id, vm.clinical_annotation_step_label, obj_config) .then(createAnnotationSuccessFn, createAnnotationErrorFn); @@ -1127,6 +1173,19 @@ vm.gleasonScore = undefined; vm.gradeGroupWhoLabel = undefined; vm.gleason4Percentage = 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; @@ -1202,6 +1261,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) { @@ -1280,6 +1352,18 @@ vm.gleasonScore = undefined; vm.gradeGroupWhoLabel = undefined; vm.gleason4Percentage = 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(); } 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 b242008..72b9443 100644 --- a/promort/static_src/templates/clinical_annotations_manager/core_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/core_annotation.html @@ -74,12 +74,12 @@

{{ cmCtrl.core_label }}

- grading + gleason grading
-
+
+ -->
@@ -138,4 +138,147 @@

{{ cmCtrl.core_label }}

+
+ cribriform pattern +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+ stroma +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ other +
+ +
+
+ +
+
+ +
+
\ No newline at end of file From cbe8eb99a4dd4b1c4c1ac5773085b3670496f0aa Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Tue, 29 Nov 2022 15:55:29 +0100 Subject: [PATCH 11/43] feat: create new Gleason patterns (still not saving into DB) -- safety commit --- ...linical_annotations_manager.controllers.js | 460 +++++++++++++++++- ...clinical_annotations_manager.directives.js | 38 +- .../viewer.controllers.js | 1 + .../ome_seadragon_viewer/viewer.directives.js | 21 +- .../rois_manager/rois_manager.controllers.js | 4 +- .../gleason_pattern_annotation.html | 148 ++++++ .../clinical_annotations_manager/manager.html | 9 + ...on_4.html => invalid_gleason_pattern.html} | 2 +- 8 files changed, 654 insertions(+), 29 deletions(-) create mode 100644 promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html rename promort/static_src/templates/dialogs/{invalid_gleason_4.html => invalid_gleason_pattern.html} (95%) 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 cae3fe9..8073c6f 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,7 +31,9 @@ .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', @@ -51,23 +53,29 @@ 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._getSliceLabel = _getSliceLabel; vm._getCoreLabel = _getCoreLabel; vm._getFocusRegionLabel = _getFocusRegionLabel; + vm._getGleasonPatternLabel = _getGleasonPatternLabel; vm._createListItem = _createListItem; vm._createNewSubtree = _createNewSubtree; vm._focusOnShape = _focusOnShape; @@ -87,7 +95,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 +103,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 +117,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 +133,7 @@ $rootScope.slices = []; $rootScope.cores = []; $rootScope.focus_regions = []; + $rootScope.gleason_patterns = []; ClinicalAnnotationStepService.getDetails(vm.clinical_annotation_step_label) .then(getClinicalAnnotationStepSuccessFn, getClinicalAnnotationStepErrorFn); @@ -132,6 +149,12 @@ } ); + $scope.$on('tool.destroyed', + function() { + vm.allModesOff(); + } + ); + $scope.$on('slice.new', function(event, slice_info) { vm._registerSlice(slice_info); @@ -170,6 +193,9 @@ $scope.$on('focus_region.new', 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"); @@ -184,6 +210,22 @@ } ); + $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, true)); + var $anchor = $new_gleason_pattern_item.find('a'); + $anchor.attr('ng-click', '') + .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('slice_annotation.saved', function(event, slice_label, slice_id) { var $icon = $("#" + slice_label).find('i'); @@ -291,6 +333,15 @@ 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 _getGleasonPatternLabel(gleason_pattern_id) { + return vm.gleason_patterns_map[gleason_pattern_id]; + } + function _createListItem(label, edit_mode, set_neg_margin_cls) { var html = '
  • + + +
    +

    NEW GLEASON PATTERN

    +
    +
    +

    {{ cmCtrl.shape_label }}

    +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    + + +
    +
    + + +
    +
    + +
    +
    + + + + + + +
    +
    + + +
    +
    + +
    + + +
    +
    + + +
    +
    + +
    +
    + +
    + + +
    +
    +
    + + \ 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 bafea3f..7838117 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,10 @@

    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 70b73ed..9c91991 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.

    From a9b5a2aa777bde42c88cb0c96167223e79f3a2ed Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 24 Feb 2023 16:28:36 +0100 Subject: [PATCH 12/43] refactor: switched to radio buttons for Gleason Type selection --- .../gleason_pattern_annotation.html | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) 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 index 437c061..0ff7b7d 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -113,36 +113,40 @@

    {{ cmCtrl.shape_label }}

    -
    - -
    -
    - -
    - -
    +
    +
    + +
    +
    +

    Pattern {{cmCtrl.pattern_type}}

    +
    +
    \ No newline at end of file From 53be8cbb11f2dddf452562b0d5e41a2c10291a48 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 10 Mar 2023 17:55:11 +0100 Subject: [PATCH 13/43] fix: saving Gleason Patterns annotations as nested objects --- promort/clinical_annotations_manager/serializers.py | 3 +-- promort/clinical_annotations_manager/views.py | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 84a800d..6e1a69f 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -138,8 +138,7 @@ 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') - write_only_fields = ('gleason_pattern',) + read_only_fields = ('id', 'creation_date', 'gleason_pattern') @staticmethod def validate_roi_json(value): diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index 631b29b..dec05ca 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -288,15 +288,6 @@ def delete(self, request, focus_region_id, label, format=None): class GleasonPatternList(ClinicalAnnotationStepObject): permissions = (permissions.IsAuthenticated,) - - def _prepare_subregions(self, subregions_data): - for subregion in subregions_data: - subregion['roi_json'] = json.dumps(subregion['roi_json']) - try: - subregion['details_json'] = json.dumps(subregion['details_json']) - except KeyError: - subregion['details_json'] = None - return subregions_data def get(self, request, focus_region_id, label, format=None): gleason_patterns = GleasonPattern.objects.filter( @@ -310,8 +301,6 @@ def post(self, request, focus_region_id, label, format=None): 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 - if gleason_pattern_data.get('subregions'): - gleason_pattern_data['subregions'] = self._prepare_subregions(gleason_pattern_data['subregion']) serializer = GleasonPatternSerializer(data=gleason_pattern_data) if serializer.is_valid(): try: From 9a4cb10e08fca1bfe955002c9cf2ec3a670bdf90 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 10 Mar 2023 17:56:07 +0100 Subject: [PATCH 14/43] feat: creating and saving Gleason Pattern and subregions still WIP, this is mostly a safety commit --- ...linical_annotations_manager.controllers.js | 217 +++++++++++++++++- ...clinical_annotations_manager.directives.js | 24 ++ .../clinical_annotations_manager.services.js | 29 ++- .../gleason_pattern_annotation.html | 99 +++++++- 4 files changed, 354 insertions(+), 15 deletions(-) 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 8073c6f..0e3a25d 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 @@ -2354,12 +2354,13 @@ } } - NewGleasonPatternAnnotationController.$inject = ['$scope', '$rootScope', '$log', 'ngDialog', - 'AnnotationsViewerService', 'CurrentSlideDetailsService']; + NewGleasonPatternAnnotationController.$inject = ['$scope', '$rootScope', '$routeParams', '$log', 'ngDialog', + 'AnnotationsViewerService', 'CurrentSlideDetailsService', 'GleasonPatternAnnotationsManagerService']; - function NewGleasonPatternAnnotationController($scope, $rootScope, $log, ngDialog, AnnotationsViewerService, - CurrentSlideDetailsService) { + function NewGleasonPatternAnnotationController($scope, $rootScope, $routeParams, $log, ngDialog, AnnotationsViewerService, + CurrentSlideDetailsService, GleasonPatternAnnotationsManagerService) { var vm = this; + vm.clinical_annotation_step_label = undefined; vm.slide_id = undefined; vm.case_id = undefined; vm.parentFocusRegion = undefined; @@ -2370,14 +2371,21 @@ vm.pattern_type = undefined; vm.pattern_type_confirmed = undefined; + vm.subregions_list = undefined; + vm.tmp_subregion_label = undefined; + vm.tmp_subregion = undefined; + vm.tmp_subregion_type = undefined; + vm.actionStartTime = undefined; vm.active_tool = undefined; vm.polygon_tool_paused = false; vm.freehand_tool_paused = false; + vm.subregion_tool_paused = false; vm.POLYGON_TOOL = 'polygon_drawing_tool'; vm.FREEHAND_TOOL = 'freehand_drawing_tool'; + vm.SUBREGION_TOOL = 'subregion_drawing_tool'; vm.shape_config = { 'stroke_color': '#FFB533', @@ -2388,39 +2396,56 @@ vm.isEditMode = isEditMode; vm.isEditLabelModeActive = isEditLabelModeActive; vm.newPolygon = newPolygon; + vm._startFreehandDrawingTool = _startFreehandDrawingTool; vm.newFreehand = newFreehand; + vm.newSubregion = newSubregion; vm._updateGleasonPatternData = _updateGleasonPatternData; vm.isPolygonToolActive = isPolygonToolActive; vm.isPolygonToolPaused = isPolygonToolPaused; vm.isFreehandToolActive = isFreehandToolActive; vm.isFreehandToolPaused = isFreehandToolPaused; + vm.isSubregionDrawingToolActive = isSubregionDrawingToolActive; + vm.isSubregionDrawingToolPaused = isSubregionDrawingToolPaused; vm.temporaryPolygonExists = temporaryPolygonExists; vm.temporaryPolygonValid = temporaryPolygonValid; vm.temporaryShapeExists = temporaryShapeExists; vm.temporaryShapeValid = temporaryShapeValid; vm.drawInProgress = drawInProgress; vm.shapeExists = shapeExists; + vm.temporarySubregionExists = temporarySubregionExists; vm.pausePolygonTool = pausePolygonTool; vm.unpausePolygonTool = unpausePolygonTool; vm.pauseFreehandTool = pauseFreehandTool; vm.unpauseFreehandTool = unpauseFreehandTool; + vm.pauseSubregionDrawingTool = pauseSubregionDrawingTool; + vm.unpauseSubregionDrawingTool = unpauseSubregionDrawingTool; vm.confirmPolygon = confirmPolygon; + vm.confirmTemporarySubregionShape = confirmTemporarySubregionShape; vm.polygonRollbackPossible = polygonRollbackPossible; vm.polygonRestorePossible = polygonRestorePossible; vm.rollbackPolygon = rollbackPolygon; vm.restorePolygon = restorePolygon; + vm.shapeRollbackPossible = shapeRollbackPossible; + vm.shapeRestorePossible = shapeRestorePossible; + vm.rollbackFreehandShape = rollbackFreehandShape; + vm.restoreFreehandShape = restoreFreehandShape; vm.clear = clear; vm.abortTool = abortTool; vm.deleteShape = deleteShape; vm.focusOnShape = focusOnShape; vm.updateGleasonPatternArea = updateGleasonPatternArea; vm.patternTypeSelected = patternTypeSelected; + vm.subregionTypeSelected = subregionTypeSelected; vm.confirmPatternType = confirmPatternType; + vm.acceptTemporarySubregion = acceptTemporarySubregion; vm.resetPatternType = resetPatternType; + vm.resetTemporarySubregion = resetTemporarySubregion; vm.patternTypeConfirmed = patternTypeConfirmed; + vm.checkPatternType = checkPatternType; vm.formValid = formValid; vm.isLocked = isLocked; vm.destroy = destroy; + vm._prepareSubregionsData = _prepareSubregionsData; vm.save = save; activate(); @@ -2429,7 +2454,10 @@ vm.slide_id = CurrentSlideDetailsService.getSlideId(); vm.case_id = CurrentSlideDetailsService.getCaseId(); + vm.clinical_annotation_step_label = $routeParams.label; + vm.pattern_type_confirmed = false; + vm.subregions_list = []; $scope.$on('gleason_pattern.creation_mode', function() { @@ -2455,7 +2483,6 @@ function newPolygon() { AnnotationsViewerService.extendPolygonConfig(vm.shape_config); - console.log(AnnotationsViewerService); AnnotationsViewerService.startPolygonsTool(); vm.active_tool = vm.POLYGON_TOOL; var canvas_label = AnnotationsViewerService.getCanvasLabel(); @@ -2474,7 +2501,28 @@ } function newFreehand() { - AnnotationsViewerService.setFreehandToolLabelPrefix('gleason_pattern'); + // AnnotationsViewerService.setFreehandToolLabelPrefix('gleason_pattern'); + // AnnotationsViewerService.extendPathConfig(vm.shape_config); + // AnnotationsViewerService.startFreehandDrawingTool(); + // var canvas_label = AnnotationsViewerService.getCanvasLabel(); + // var $canvas = $("#" + canvas_label); + // $canvas.on('freehand_polygon_paused', + // function(event, polygon_label) { + // AnnotationsViewerService.disableActiveTool(); + // vm.freehand_tool_paused = true; + // $scope.$apply(); + // } + // ); + // vm.active_tool = vm.FREEHAND_TOOL; + vm._startFreehandDrawingTool('gleason_pattern', 'freehand_gleason_tool'); + } + + function newSubregion() { + vm._startFreehandDrawingTool('gp_sub', 'subregion_tool'); + } + + function _startFreehandDrawingTool(label, tool_type) { + AnnotationsViewerService.setFreehandToolLabelPrefix(label); AnnotationsViewerService.extendPathConfig(vm.shape_config); AnnotationsViewerService.startFreehandDrawingTool(); var canvas_label = AnnotationsViewerService.getCanvasLabel(); @@ -2482,11 +2530,26 @@ $canvas.on('freehand_polygon_paused', function(event, polygon_label) { AnnotationsViewerService.disableActiveTool(); - vm.freehand_tool_paused = true; + switch(vm.active_tool) { + case vm.FREEHAND_TOOL: + vm.freehand_tool_paused = true; + break; + case vm.SUBREGION_TOOL: + console.log('Pausing subregion drawing tool'); + vm.subregion_tool_paused = true; + break; + } $scope.$apply(); } ); - vm.active_tool = vm.FREEHAND_TOOL; + switch(tool_type) { + case 'freehand_gleason_tool': + vm.active_tool = vm.FREEHAND_TOOL; + break; + case 'subregion_tool': + vm.active_tool = vm.SUBREGION_TOOL; + break; + } } function _updateGleasonPatternData(polygon_label, parent_focus_region) { @@ -2511,6 +2574,14 @@ return vm.freehand_tool_paused; } + function isSubregionDrawingToolActive() { + return vm.active_tool == vm.SUBREGION_TOOL; + } + + function isSubregionDrawingToolPaused() { + return vm.subregion_tool_paused; + } + function temporaryPolygonExists() { return AnnotationsViewerService.temporaryPolygonExists(); } @@ -2536,6 +2607,10 @@ return vm.shape !== undefined; } + function temporarySubregionExists() { + return vm.tmp_subregion !== undefined; + } + function pausePolygonTool() { AnnotationsViewerService.disableActiveTool(); vm.polygon_tool_paused = true; @@ -2562,6 +2637,22 @@ 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', @@ -2608,6 +2699,40 @@ }); } + 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){ + console.log('freehand shape saved'); + 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(); + $scope.$apply(); + } + ); + setTimeout(function() { + AnnotationsViewerService.saveTemporaryFreehandShape(); + }, 10); + } + }); + } + function polygonRollbackPossible() { return AnnotationsViewerService.temporaryPolygonExists(); } @@ -2624,6 +2749,23 @@ 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); vm.shape_label = undefined; @@ -2635,7 +2777,7 @@ AnnotationsViewerService.clearTemporaryPolygon(); $("#" + AnnotationsViewerService.getCanvasLabel()).unbind('polygon_saved'); } - if (vm.active_tool === vm.FREEHAND_TOOL) { + if (vm.active_tool === vm.FREEHAND_TOOL || vm.active_tool === vm.SUBREGION_TOOL) { AnnotationsViewerService.clearTemporaryFreehandShape(); $("#" + AnnotationsViewerService.getCanvasLabel()) .unbind('freehand_polygon_saved') @@ -2645,6 +2787,7 @@ vm.active_tool = undefined; vm.polygon_tool_paused = false; vm.freehand_tool_paused = false; + vm.subregion_tool_paused = false; } function deleteShape(destroy_shape) { @@ -2670,15 +2813,41 @@ 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.push({ + "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} + }); + console.log(vm.subregions_list); + 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; + } + function patternTypeConfirmed() { return vm.pattern_type_confirmed; } @@ -2697,14 +2866,40 @@ $rootScope.$broadcast('tool.destroyed'); } - function save() { + 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 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 + ) } } ShowGleasonPatternAnnotationController.$inject = []; function ShowGleasonPatternAnnotationController() { - + } })(); \ 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 b3172b1..1c2310f 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 @@ -38,6 +38,8 @@ .directive('showFocusRegionAnnotationButtons', showFocusRegionAnnotationButtons) .directive('newGleasonPatternAnnotationForm', newGleasonPatternAnnotationForm) .directive('newGleasonPatternAnnotationButtons', newGleasonPatternAnnotationButtons) + .directive('newGleasonFourAnnotationForm', newGleasonFourAnnotationForm) + .directive('newGleasonFiveAnnotationForm', newGleasonFiveAnnotationForm) .directive('showGleasonPatternAnnotationForm', showGleasonPatternAnnotationForm); function newSliceAnnotationForm() { @@ -226,6 +228,28 @@ 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, 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 eb4efc7..6424897 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 @@ -27,7 +27,8 @@ .factory('ClinicalAnnotationStepManagerService', ClinicalAnnotationStepManagerService) .factory('SliceAnnotationsManagerService', SliceAnnotationsManagerService) .factory('CoreAnnotationsManagerService', CoreAnnotationsManagerService) - .factory('FocusRegionAnnotationsManagerService', FocusRegionAnnotationsManagerService); + .factory('FocusRegionAnnotationsManagerService', FocusRegionAnnotationsManagerService) + .factory('GleasonPatternAnnotationsManagerService', GleasonPatternAnnotationsManagerService); ClinicalAnnotationStepManagerService.$inject = ['$http', '$log']; @@ -131,4 +132,30 @@ annotation_step_label + '/'); } } + + GleasonPatternAnnotationsManagerService.$inject = ['$http', '$log']; + + function GleasonPatternAnnotationsManagerService($http, $log) { + var GleasonPatternAnnotationsManagerService = { + getAnnotation: getAnnotation, + createAnnotation: createAnnotation, + deleteAnnotation: deleteAnnotation + }; + + return GleasonPatternAnnotationsManagerService; + + function getAnnotation() { + + } + + 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() { + + } + } })(); \ No newline at end of file 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 index 0ff7b7d..bb45553 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -119,15 +119,15 @@

    {{ cmCtrl.shape_label }}

    - +
    - +
    - +
    @@ -149,4 +149,97 @@

    {{ cmCtrl.shape_label }}

    + + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + + + + + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + +
    +
    + \ No newline at end of file From e2a0779752b836b6da3a6b77c013ea3d9a95cc4f Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 16 Mar 2023 10:49:06 +0100 Subject: [PATCH 15/43] feat: added Gleason 5 pattern's subregions types --- .../gleason_pattern_annotation.html | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 index bb45553..5a069ca 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -232,6 +232,30 @@

    {{ cmCtrl.shape_label }}

    +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + +
    +
    + +
    +
    + + + + + + +
    +
    +
    From aa7984c6cb86206f1f6465c626ffaf0c0a89d1ba Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 20 Mar 2023 10:52:46 +0100 Subject: [PATCH 19/43] fix: filter ROI annotation steps previously processed but not started yet by the reviewer --- .../predictions_manager/management/commands/tissue_to_rois.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/promort/predictions_manager/management/commands/tissue_to_rois.py b/promort/predictions_manager/management/commands/tissue_to_rois.py index d96d271..076267a 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 From 2b359cf01dab1070bb7b309f77c4dd29630f7dd0 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 23 Mar 2023 14:37:57 +0100 Subject: [PATCH 20/43] fix: fixed a few UI interactions --- ...linical_annotations_manager.controllers.js | 75 +++++++++++++++++-- .../gleason_pattern_annotation.html | 5 +- 2 files changed, 71 insertions(+), 9 deletions(-) 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 f8209a8..34f484a 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 @@ -2376,6 +2376,7 @@ vm.pattern_type = undefined; vm.pattern_type_confirmed = undefined; + vm.subregionCreationModeActive = undefined; vm.subregions_list = undefined; vm.tmp_subregion_label = undefined; vm.tmp_subregion = undefined; @@ -2416,6 +2417,9 @@ vm.temporaryShapeExists = temporaryShapeExists; vm.temporaryShapeValid = temporaryShapeValid; vm.drawInProgress = drawInProgress; + vm.subregionCreationInProgress = subregionCreationInProgress; + vm.activateSubregionCreationMode = activateSubregionCreationMode; + vm.deactivateSubregionCreationMode = deactivateSubregionCreationMode; vm.shapeExists = shapeExists; vm.temporarySubregionExists = temporarySubregionExists; vm.pausePolygonTool = pausePolygonTool; @@ -2437,7 +2441,11 @@ vm.restoreFreehandShape = restoreFreehandShape; vm.clear = clear; vm.abortTool = abortTool; + vm.deleteTemporaryGleasonPattern = deleteTemporaryGleasonPattern; vm.deleteShape = deleteShape; + vm.deleteTemporarySubregion = deleteTemporarySubregion; + vm.deleteSubregion = deleteSubregion; + vm.deleteSubregions = deleteSubregions; vm.focusOnShape = focusOnShape; vm.updateGleasonPatternArea = updateGleasonPatternArea; vm.patternTypeSelected = patternTypeSelected; @@ -2462,8 +2470,9 @@ vm.clinical_annotation_step_label = $routeParams.label; + vm.subregionCreationModeActive = false; vm.pattern_type_confirmed = false; - vm.subregions_list = []; + vm.subregions_list = {}; $scope.$on('gleason_pattern.creation_mode', function() { @@ -2511,6 +2520,7 @@ } function newSubregion() { + vm.activateSubregionCreationMode(); vm._startFreehandDrawingTool('gp_sub', 'subregion_tool'); } @@ -2596,6 +2606,18 @@ vm.isFreehandToolPaused(); } + function subregionCreationInProgress() { + return vm.subregionCreationModeActive; + } + + function activateSubregionCreationMode() { + vm.subregionCreationModeActive = true; + } + + function deactivateSubregionCreationMode() { + vm.subregionCreationModeActive = false; + } + function shapeExists() { return vm.shape !== undefined; } @@ -2762,7 +2784,7 @@ vm.tmp_subregion = AnnotationsViewerService.getShapeJSON(polygon_label); } ngDialog.close('checkTemporarySubregion'); - vm.abortTool(); + vm.abortTool(true); $scope.$apply(); } } @@ -2809,11 +2831,16 @@ 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() { + function abortTool(keep_subregion_tool_active=false) { if (vm.active_tool === vm.POLYGON_TOOL) { AnnotationsViewerService.clearTemporaryPolygon(); $("#" + AnnotationsViewerService.getCanvasLabel()).unbind('polygon_saved'); @@ -2823,6 +2850,9 @@ $("#" + 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; @@ -2831,6 +2861,13 @@ 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) { @@ -2839,10 +2876,33 @@ 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 focusOnShape() { + console.log(vm.shape); AnnotationsViewerService.focusOnShape(vm.shape.shape_id); } @@ -2863,13 +2923,12 @@ } function acceptTemporarySubregion() { - vm.subregions_list.push({ + 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} - }); - console.log(vm.subregions_list); + }; vm.abortTool(); vm.resetTemporarySubregion(); } @@ -2887,6 +2946,7 @@ vm.tmp_subregion_label = undefined; vm.tmp_subregion_type = undefined; vm.tmp_subregion = undefined; + vm.deactivateSubregionCreationMode(); } function patternTypeConfirmed() { @@ -2894,7 +2954,7 @@ } function formValid() { - return vm.patternTypeConfirmed(); + return vm.patternTypeConfirmed() && !vm.subregionCreationInProgress(); } function isLocked() { @@ -2951,6 +3011,7 @@ 'focus_region': response.data.focus_region, 'annotated': true }; + vm.clear(false); $rootScope.$broadcast('gleason_pattern.new', gleason_pattern_info); dialog.close(); } 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 index 0e8891f..4516dcc 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -138,8 +138,9 @@

    {{ cmCtrl.shape_label }}

    - @@ -196,7 +197,7 @@

    {{ cmCtrl.shape_label }}

    From f4632385566c3501bc43124c24728248f4d377f7 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 24 Mar 2023 12:28:50 +0100 Subject: [PATCH 21/43] feat: show created Gleason subregions --- ...linical_annotations_manager.controllers.js | 20 +++++++-- .../gleason_pattern_annotation.html | 45 ++++++++++++++++--- 2 files changed, 57 insertions(+), 8 deletions(-) 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 34f484a..94fb8b1 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 @@ -2422,6 +2422,7 @@ vm.deactivateSubregionCreationMode = deactivateSubregionCreationMode; vm.shapeExists = shapeExists; vm.temporarySubregionExists = temporarySubregionExists; + vm.subregionsExist = subregionsExist; vm.pausePolygonTool = pausePolygonTool; vm.unpausePolygonTool = unpausePolygonTool; vm.pauseFreehandTool = pauseFreehandTool; @@ -2446,6 +2447,8 @@ vm.deleteTemporarySubregion = deleteTemporarySubregion; vm.deleteSubregion = deleteSubregion; vm.deleteSubregions = deleteSubregions; + vm.selectShape = selectShape; + vm.deselectShape = deselectShape; vm.focusOnShape = focusOnShape; vm.updateGleasonPatternArea = updateGleasonPatternArea; vm.patternTypeSelected = patternTypeSelected; @@ -2626,6 +2629,10 @@ return vm.tmp_subregion !== undefined; } + function subregionsExist() { + return (Object.keys(vm.subregions_list).length > 0); + } + function pausePolygonTool() { AnnotationsViewerService.disableActiveTool(); vm.polygon_tool_paused = true; @@ -2901,9 +2908,16 @@ } } - function focusOnShape() { - console.log(vm.shape); - AnnotationsViewerService.focusOnShape(vm.shape.shape_id); + 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() { 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 index 4516dcc..4909aa3 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -144,7 +144,7 @@

    {{ cmCtrl.shape_label }}

    title="Delete the shape"> - @@ -191,7 +191,7 @@

    {{ cmCtrl.shape_label }}

    -
    +
    @@ -296,14 +296,49 @@

    {{ cmCtrl.shape_label }}

    -
    - +
    + +
    + Subregions + +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    \ No newline at end of file From f9ba5368546faece14d7f59f3a306f7bc1c59d11 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Wed, 19 Apr 2023 18:08:28 +0200 Subject: [PATCH 22/43] feat: automatically calculate primary and secondary Gleason --- .../clinical_annotations_manager/models.py | 9 ++- promort/clinical_annotations_manager/views.py | 69 +++++++++++++++++++ promort/promort/urls.py | 4 +- ...linical_annotations_manager.controllers.js | 6 +- .../clinical_annotations_manager.services.js | 15 ++++ 5 files changed, 97 insertions(+), 6 deletions(-) diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index 576eec6..6421ca4 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -153,7 +153,7 @@ def get_action_duration(self): return (self.action_complete_time-self.action_start_time).total_seconds() else: return None - + def get_largest_confluent_sheet(self): # TODO: get largest cribriform object among all Gleason 4 elements of a core pass @@ -195,9 +195,14 @@ 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[gp.gleason_type] = gp + return gleason_elements_map def get_gleason_4_elements(self): - return self.annotation_step.gleason_annotations.filter(focus_region=self.focus_region).all() + return self.get_gleason_elements()["G4"] def get_total_gleason_4_area(self): g4_area = 0 diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index dec05ca..4d8d6bd 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -21,6 +21,7 @@ import simplejson as json except ImportError: import json +from collections import Counter from rest_framework.views import APIView from rest_framework import permissions, status, exceptions @@ -30,6 +31,8 @@ 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, GleasonPattern @@ -227,6 +230,72 @@ 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() + for ge in gleason_elements: + gleason_total_area[ge.gleason_type] += ge.area + gleason_shapes.setdefault(ge.gleason_type, []).append(ge.label) + 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": gleason_coverage[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,) diff --git a/promort/promort/urls.py b/promort/promort/urls.py index d010a48..cd61ff6 100644 --- a/promort/promort/urls.py +++ b/promort/promort/urls.py @@ -34,7 +34,7 @@ from rois_manager.views import SliceList, SliceDetail, CoreList, \ CoreDetail, FocusRegionList, FocusRegionDetail, ROIsTreeList from clinical_annotations_manager.views import AnnotatedROIsTreeList, ClinicalAnnotationStepAnnotationsList, \ - SliceAnnotationList, SliceAnnotationDetail, CoreAnnotationList, CoreAnnotationDetail, \ + SliceAnnotationList, SliceAnnotationDetail, CoreAnnotationList, CoreAnnotationDetail, CoreGleasonDetail,\ FocusRegionAnnotationList, FocusRegionAnnotationDetail, GleasonPatternList, GleasonPatternDetail import predictions_manager.views as pmv import shared_datasets_manager.views as shdv @@ -154,6 +154,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/', 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 94fb8b1..45ae884 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 @@ -1017,10 +1017,10 @@ } NewCoreAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', - 'CoresManagerService', 'CoreAnnotationsManagerService']; + 'CoreGleasonDetailsManagerService', 'CoreAnnotationsManagerService']; function NewCoreAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - CoresManagerService, CoreAnnotationsManagerService) { + CoreGleasonDetailsManagerService, CoreAnnotationsManagerService) { var vm = this; vm.core_id = undefined; vm.core_label = undefined; @@ -1089,7 +1089,7 @@ $scope.$on('core_annotation.new', 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); } ); 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 6424897..163dcb7 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,6 +26,7 @@ .module('promort.clinical_annotations_manager.services') .factory('ClinicalAnnotationStepManagerService', ClinicalAnnotationStepManagerService) .factory('SliceAnnotationsManagerService', SliceAnnotationsManagerService) + .factory('CoreGleasonDetailsManagerService', CoreGleasonDetailsManagerService) .factory('CoreAnnotationsManagerService', CoreAnnotationsManagerService) .factory('FocusRegionAnnotationsManagerService', FocusRegionAnnotationsManagerService) .factory('GleasonPatternAnnotationsManagerService', GleasonPatternAnnotationsManagerService); @@ -80,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) { From 3dabad4e189b63d9cbfa6c3265bb72e9882fa2f9 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 27 Apr 2023 11:10:18 +0200 Subject: [PATCH 23/43] feat: manage gleason pattern and automatic gleason score in core clinical annotation (write mode) --- promort/clinical_annotations_manager/views.py | 2 +- ...linical_annotations_manager.controllers.js | 295 ++++++++++++------ .../ome_seadragon_viewer/viewer.services.js | 18 +- promort/static_src/css/promort.css | 12 + .../core_annotation.html | 112 ++++--- 5 files changed, 300 insertions(+), 139 deletions(-) diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index 4d8d6bd..22a45ef 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -275,7 +275,7 @@ def _get_gleason_details(self, core_obj, annotation_step_label): gleason_details["details"][gtype] = { "shapes": gleason_shapes[gtype], "total_area": gleason_total_area[gtype], - "total_coverage": gleason_coverage[gtype] + "total_coverage": round(gleason_coverage[gtype], 2) } primary_gleason, secondary_gleason = self._get_primary_and_secondary_gleason(gleason_coverage) gleason_details.update({ 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 45ae884..d7fb3a6 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 @@ -40,8 +40,8 @@ '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; @@ -144,19 +144,19 @@ } $scope.$on('annotation_panel.closed', - function() { + function () { vm.allModesOff(); } ); $scope.$on('tool.destroyed', - function() { + 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"); @@ -174,7 +174,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"); @@ -192,8 +192,8 @@ ); $scope.$on('focus_region.new', - function(event, focus_region_info) { - if(focus_region_info.tumor == true) { + function (event, focus_region_info) { + if (focus_region_info.tumor == true) { vm.positive_fr_count += 1; } vm._registerFocusRegion(focus_region_info); @@ -213,7 +213,7 @@ ); $scope.$on('gleason_pattern.new', - function(event, gleason_pattern_info) { + function (event, gleason_pattern_info) { vm._registerGleasonPattern(gleason_pattern_info); vm.allModesOff(); var $tree = $("#" + vm._getFocusRegionLabel(gleason_pattern_info.focus_region) + "_tree"); @@ -229,7 +229,7 @@ ); $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"); @@ -239,7 +239,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"); @@ -251,7 +251,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"); @@ -261,7 +261,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"); @@ -273,7 +273,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"); @@ -283,7 +283,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'); @@ -364,7 +364,7 @@ } function _createNewSubtree(roi_label) { - var html = '
      '; + var html = '
        '; return html; } @@ -451,7 +451,7 @@ if (confirm_obj.value === true) { ClinicalAnnotationStepService.closeAnnotationStep(vm.clinical_annotation_step_label, confirm_obj.notes).then(closeClinicalAnnotationStepSuccessFn, - closeClinicalAnnotationStepErrorFn); + closeClinicalAnnotationStepErrorFn); } function closeClinicalAnnotationStepSuccessFn(response) { @@ -722,7 +722,7 @@ vm.ui_active_modes['annotate_gleason_pattern'] = true; $rootScope.$broadcast('gleason_pattern.creation_mode'); } - + function newGleasonPatternAnnotationModeActive() { return vm.ui_active_modes['annotate_gleason_pattern']; } @@ -779,7 +779,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; @@ -809,7 +809,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); @@ -902,7 +902,7 @@ 'SliceAnnotationsManagerService']; function ShowSliceAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - SliceAnnotationsManagerService) { + SliceAnnotationsManagerService) { var vm = this; vm.slice_id = undefined; vm.slice_label = undefined; @@ -929,7 +929,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); @@ -1017,20 +1017,23 @@ } NewCoreAnnotationController.$inject = ['$scope', '$routeParams', '$rootScope', '$log', 'ngDialog', - 'CoreGleasonDetailsManagerService', 'CoreAnnotationsManagerService']; + 'CoreGleasonDetailsManagerService', 'CoreAnnotationsManagerService', 'AnnotationsViewerService']; function NewCoreAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - CoreGleasonDetailsManagerService, 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.gleasonDetails = undefined; + vm.gleasonHighlighted = undefined; vm.predominant_rsg = undefined; vm.highest_rsg = undefined; vm.rsg_within_highest_grade_area = undefined; @@ -1062,16 +1065,29 @@ ]; 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; @@ -1087,7 +1103,7 @@ 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; CoreGleasonDetailsManagerService.get(vm.core_id, vm.clinical_annotation_step_label) .then(getCoreSuccessFn, getCoreErrorFn); @@ -1102,6 +1118,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(); } @@ -1117,10 +1147,15 @@ 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; @@ -1137,6 +1172,84 @@ 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; } @@ -1147,7 +1260,7 @@ function formValid() { return ((typeof vm.primaryGleason !== 'undefined') && - (typeof vm.secondaryGleason !== 'undefined')); + (typeof vm.secondaryGleason !== 'undefined')); } function destroy() { @@ -1155,13 +1268,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'; @@ -1206,15 +1319,15 @@ 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 + 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.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; } console.log(obj_config); @@ -1257,7 +1370,7 @@ 'CoreAnnotationsManagerService', 'CoresManagerService']; function ShowCoreAnnotationController($scope, $routeParams, $rootScope, $log, ngDialog, - CoreAnnotationsManagerService, CoresManagerService) { + CoreAnnotationsManagerService, CoresManagerService) { var vm = this; vm.core_id = undefined; vm.core_label = undefined; @@ -1297,8 +1410,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.locked = undefined; @@ -1493,8 +1606,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; @@ -1532,8 +1645,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; @@ -1619,7 +1732,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); @@ -1642,7 +1755,7 @@ function fetchGleasonElementTypesSuccessFn(response) { vm.gleason_element_types = response.data; vm.gleason_types_map = {}; - for (var i=0; i{{ cmCtrl.core_label }}
        gleason grading -
        -
        - -
        - -
        -
        -
        -
        - -
        - -
        -
        -
        +
        -
        -
        -
        - -
        -
        - -
        %
        -
        -
        -
        @@ -138,6 +93,71 @@

        {{ cmCtrl.core_label }}

        +
        + gleason pattern details +
        +
        + +
        +
        +
        +

        Coverage

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

        Coverage

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

        Coverage

        +
        + +
        + +
        +
        +
        +
        cribriform pattern
        From 95570c40aabd429177f9318873b09da4b5171538 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 28 Apr 2023 11:00:28 +0200 Subject: [PATCH 24/43] feat: change gleason pattern and subregions colors accordingly to pattern type --- ...linical_annotations_manager.controllers.js | 34 +++++++++++++++++-- .../ome_seadragon_viewer/viewer.directives.js | 6 ++-- .../gleason_pattern_annotation.html | 12 +++++-- 3 files changed, 42 insertions(+), 10 deletions(-) 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 d7fb3a6..14ad2c7 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 @@ -2507,8 +2507,15 @@ vm.SUBREGION_TOOL = 'subregion_drawing_tool'; vm.shape_config = { - 'stroke_color': '#FFB533', - 'stroke_width': 20 + 'stroke_color': '#dddddd', + 'stroke_width': 20, + 'fill_alpha': 0 + }; + + vm.pattern_colors = { + "G3": "#ffcc99", + "G4": "#ff9966", + "G5": "#cc5200" }; vm.isReadOnly = isReadOnly; @@ -2564,6 +2571,7 @@ vm.deselectShape = deselectShape; vm.focusOnShape = focusOnShape; vm.updateGleasonPatternArea = updateGleasonPatternArea; + vm.updateGleasonShapeColor = updateGleasonShapeColor; vm.patternTypeSelected = patternTypeSelected; vm.subregionTypeSelected = subregionTypeSelected; vm.confirmPatternType = confirmPatternType; @@ -2642,7 +2650,21 @@ function _startFreehandDrawingTool(label, tool_type) { AnnotationsViewerService.setFreehandToolLabelPrefix(label); - AnnotationsViewerService.extendPathConfig(vm.shape_config); + switch (tool_type) { + case 'freehand_gleason_tool': + AnnotationsViewerService.extendPathConfig(vm.shape_config); + break; + case 'subregion_tool': + var shape_config = { + 'stroke_color': vm.pattern_colors[vm.pattern_type], + 'stroke_width': 20, + 'stroke_alpha': 1, + 'fill_color': vm.pattern_colors[vm.pattern_type], + 'fill_alpha': 0.25 + }; + AnnotationsViewerService.extendPathConfig(shape_config); + break; + } AnnotationsViewerService.startFreehandDrawingTool(); var canvas_label = AnnotationsViewerService.getCanvasLabel(); var $canvas = $("#" + canvas_label); @@ -3037,6 +3059,12 @@ } + 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'; } diff --git a/promort/src/js/ome_seadragon_viewer/viewer.directives.js b/promort/src/js/ome_seadragon_viewer/viewer.directives.js index 06d7aa1..711eb07 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.directives.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.directives.js @@ -367,10 +367,8 @@ var tools_manager = new AnnotationsEventsController(annotations_canvas); //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); tools_manager.initializePolygonDrawingTool(shape_config); 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 index 4909aa3..45a9d54 100644 --- a/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/gleason_pattern_annotation.html @@ -159,15 +159,21 @@

        {{ cmCtrl.shape_label }}

        - +
        - +
        - +
        From 852fbacda7c7a7bd4927129971ff88f2ca5937c3 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Wed, 3 May 2023 15:59:26 +0200 Subject: [PATCH 25/43] feat: handling data entry for core with cribriform pattern --- promort/clinical_annotations_manager/views.py | 7 ++++++- ...linical_annotations_manager.controllers.js | 7 +++++-- .../core_annotation.html | 21 ++++++++----------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/promort/clinical_annotations_manager/views.py b/promort/clinical_annotations_manager/views.py index 22a45ef..5916361 100644 --- a/promort/clinical_annotations_manager/views.py +++ b/promort/clinical_annotations_manager/views.py @@ -266,16 +266,21 @@ 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) + "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({ 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 14ad2c7..95d566e 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 @@ -1296,8 +1296,11 @@ } function containsCribriformPattern() { - // TODO: implement this function to check if at least one cribriform pattern exists (using a service?) - return false; + if (typeof(vm.gleasonDetails) === "undefined") { + return false; + } else { + return vm.gleasonDetails.hasOwnProperty("G4") && (vm.gleasonDetails["G4"].subregions.indexOf("cribriform_pattern") > -1); + } } function save() { 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 2643c05..d0759b7 100644 --- a/promort/static_src/templates/clinical_annotations_manager/core_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/core_annotation.html @@ -158,7 +158,7 @@

        {{ cmCtrl.core_label }}

        -
        +
        cribriform pattern
        @@ -166,8 +166,7 @@

        {{ cmCtrl.core_label }}

        + ng-model="cmCtrl.intraluminal_acinar_differentiation_grade"> @@ -194,23 +192,22 @@

        {{ cmCtrl.core_label }}

        -
        +
        -
        +
        +
        -
        Îĵm²
        - -
        - - -
        -
        -
        - - - -
        -
        -
        - -
        -
        - -
        -
        -
        - - - - -
        -
        - - -
        -
        - - - -
        -
        -
        - - -
        - - -
        -
        -
        - - - -
        -
        -
        - - -
        - -
        -
        -
        - -
        -
        patterns From 5d9be71ea2fc09c54c33d1c97802f50d75b71545 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 9 Jun 2023 16:33:25 +0200 Subject: [PATCH 29/43] feat: removed constraints on core clinical annotation creation --- .../migrations/0029_auto_20230531_1444.py | 28 ++++++++++ .../clinical_annotations_manager/models.py | 56 +++++++++++++++++-- .../serializers.py | 5 +- ...linical_annotations_manager.controllers.js | 8 +-- 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 promort/clinical_annotations_manager/migrations/0029_auto_20230531_1444.py 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 0000000..1616bb5 --- /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 6421ca4..a17e2e4 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,10 +97,10 @@ 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) @@ -119,6 +121,52 @@ class CoreAnnotation(models.Model): 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): + primary_gleason, _ = self._get_primary_and_secondary_gleason() + return primary_gleason + + def get_secondary_gleason(self): + _, secondary_gleason = self._get_primary_and_secondary_gleason() + return secondary_gleason + + def get_gleason_group(self): + pass + def get_gleason_4_total_area(self): gleason_4_total_area = 0.0 for focus_region in self.core.focus_regions.all(): @@ -198,7 +246,7 @@ class Meta: 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[gp.gleason_type] = gp + gleason_elements_map.setdefault(gp.gleason_type, []).append(gp) return gleason_elements_map def get_gleason_4_elements(self): diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index 6e1a69f..a323c95 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -77,8 +77,7 @@ class CoreAnnotationSerializer(serializers.ModelSerializer): 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', 'nuclear_grade_size', + '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', @@ -91,7 +90,7 @@ class Meta: @staticmethod def get_gleason_score(obj): - return '%d + %d' % (obj.primary_gleason, obj.secondary_gleason) + return '{0} + {1}'.format(obj.get_primary_gleason(), obj.get_secondary_gleason()) @staticmethod def get_gleason_4_percentage(obj): 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 95d566e..ae9c9e1 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 @@ -573,6 +573,7 @@ break; case 'focus_region': vm.activateNewFocusRegionAnnotationMode(roi_id); + break; } } } else { @@ -585,6 +586,7 @@ break; case 'focus_region': vm.activateShowFocusRegionAnnotationMode(roi_id); + break; } } } @@ -1259,8 +1261,7 @@ } function formValid() { - return ((typeof vm.primaryGleason !== 'undefined') && - (typeof vm.secondaryGleason !== 'undefined')); + return true; } function destroy() { @@ -1313,9 +1314,6 @@ 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(), predominant_rsg: vm.predominant_rsg, From 0a6e4eef30cbd7ad50201a839c8f8195c600b04e Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Tue, 13 Jun 2023 09:53:03 +0200 Subject: [PATCH 30/43] feat: handle Gleason details for CoreAnnotation objects --- .../clinical_annotations_manager/models.py | 53 ++++++++++++++++--- .../serializers.py | 31 ++++++++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index a17e2e4..bcc70d4 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -155,17 +155,38 @@ def _get_primary_and_secondary_gleason(self): else: secondary_gleason = max(gleason_coverage) return primary_gleason, secondary_gleason - + def get_primary_gleason(self): - primary_gleason, _ = self._get_primary_and_secondary_gleason() - return primary_gleason + 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): - _, secondary_gleason = self._get_primary_and_secondary_gleason() - return secondary_gleason + 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): - pass + 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 @@ -192,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: diff --git a/promort/clinical_annotations_manager/serializers.py b/promort/clinical_annotations_manager/serializers.py index a323c95..ea09f5c 100644 --- a/promort/clinical_annotations_manager/serializers.py +++ b/promort/clinical_annotations_manager/serializers.py @@ -90,7 +90,7 @@ class Meta: @staticmethod def get_gleason_score(obj): - return '{0} + {1}'.format(obj.get_primary_gleason(), obj.get_secondary_gleason()) + return '{0} + {1}'.format(*obj._get_primary_and_secondary_gleason()) @staticmethod def get_gleason_4_percentage(obj): @@ -107,6 +107,35 @@ def get_total_cribriform_area(obj): 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): From b629d2653a2a70564b86ae30c86ff2234902d07e Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 31 Aug 2023 10:33:32 +0200 Subject: [PATCH 31/43] feat: updated cores clinical data extraction tool --- .../commands/get_cores_clinical_data.py | 24 +++++++++++++++---- .../clinical_annotations_manager/models.py | 4 ++-- 2 files changed, 22 insertions(+), 6 deletions(-) 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 86bfa65..8c9a557 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 @@ -76,16 +76,32 @@ def _dump_row(self, core_annotation, csv_writer): '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() + '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'] + '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() diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index bcc70d4..8b870a6 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -243,11 +243,11 @@ def get_action_duration(self): def get_largest_confluent_sheet(self): # TODO: get largest cribriform object among all Gleason 4 elements of a core - pass + return None def get_total_cribriform_area(self): # TODO: sum of all cribriform objects defined on a core - pass + return None class FocusRegionAnnotation(models.Model): From d655e203a21e602ec2063792297d30c399ddb5f0 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 31 Aug 2023 10:34:26 +0200 Subject: [PATCH 32/43] style: formatted using black --- .../commands/get_cores_clinical_data.py | 134 +++++++++++------- 1 file changed, 86 insertions(+), 48 deletions(-) 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 8c9a557..076588f 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,80 +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.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() + "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', '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) + 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"]) From b8da7a36888c4d762c438298c624931b102117f4 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 31 Aug 2023 11:34:26 +0200 Subject: [PATCH 33/43] feat: updated focus regions clinical data extraction tool --- .../commands/get_focus_regions_clinical_data.py | 11 ++++++++--- promort/clinical_annotations_manager/models.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) 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 7396243..3aff853 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 @@ -86,8 +86,12 @@ def _dump_row(self, focus_region_annotation, csv_writer): '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() + '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") } ) @@ -96,7 +100,8 @@ def _export_data(self, out_file, page_size): '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'] + '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() diff --git a/promort/clinical_annotations_manager/models.py b/promort/clinical_annotations_manager/models.py index 8b870a6..c9cdad1 100644 --- a/promort/clinical_annotations_manager/models.py +++ b/promort/clinical_annotations_manager/models.py @@ -289,7 +289,13 @@ def get_gleason_elements(self): return gleason_elements_map def get_gleason_4_elements(self): - return self.get_gleason_elements()["G4"] + 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 @@ -297,6 +303,13 @@ def get_total_gleason_4_area(self): g4_area += g4.area return g4_area + 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() try: From e3dd8120304edbcfe15416be6ce52486c7e77237 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 31 Aug 2023 11:35:17 +0200 Subject: [PATCH 34/43] style: formatted with black --- .../get_focus_regions_clinical_data.py | 151 ++++++++++++------ .../commands/get_slices_clinical_data.py | 105 +++++++----- 2 files changed, 171 insertions(+), 85 deletions(-) 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 3aff853..7e77e15 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,58 +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_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") + "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_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) + 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_slices_clinical_data.py b/promort/clinical_annotations_manager/management/commands/get_slices_clinical_data.py index f048dec..c316fcf 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"]) From e6a564712be3a85310a3df599f49468d44c1ad11 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Thu, 31 Aug 2023 17:10:41 +0200 Subject: [PATCH 35/43] feat: extract Gleason pattern data --- .../management/commands/get_gleason_data.py | 95 ------------- .../commands/get_gleason_patterns_data.py | 127 ++++++++++++++++++ 2 files changed, 127 insertions(+), 95 deletions(-) delete mode 100644 promort/clinical_annotations_manager/management/commands/get_gleason_data.py create mode 100644 promort/clinical_annotations_manager/management/commands/get_gleason_patterns_data.py 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 7829875..0000000 --- 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 0000000..ba90853 --- /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"]) From 927cb0b7551130a1bfb9b27d3a8c0f3e6531274d Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Mon, 4 Sep 2023 10:55:03 +0200 Subject: [PATCH 36/43] feat: extract Gleason subregions data --- .../commands/get_gleason_subregions_data.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 promort/clinical_annotations_manager/management/commands/get_gleason_subregions_data.py 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 0000000..0b9152f --- /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']}===") From e27e62992e6f21dc24ad7229f31e4752f6bbc168 Mon Sep 17 00:00:00 2001 From: Luca Lianas Date: Fri, 13 Oct 2023 19:14:03 +0200 Subject: [PATCH 37/43] feat: show Gleason patterns summary in core annotation mode (read and write) --- ...linical_annotations_manager.controllers.js | 157 +++++++++++++++++- .../clinical_annotations_manager.services.js | 4 +- .../core_annotation.html | 21 ++- 3 files changed, 164 insertions(+), 18 deletions(-) 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 ae9c9e1..f265d4e 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 @@ -220,7 +220,7 @@ 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', '') + $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); @@ -559,6 +559,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); @@ -587,6 +590,9 @@ case 'focus_region': vm.activateShowFocusRegionAnnotationMode(roi_id); break; + case 'gleason_pattern': + vm.activateShowGleasonPatternAnnotationMode(roi_id); + break; } } } @@ -729,8 +735,9 @@ return vm.ui_active_modes['annotate_gleason_pattern']; } - function activateShowGleasonPatternAnnotationMode() { + function activateShowGleasonPatternAnnotationMode(gleason_pattern_id) { vm.allModesOff(); + $rootScope.$broadcast('gleason_pattern.show', gleason_pattern_id); vm.ui_active_modes['show_gleason_pattern'] = true; } @@ -1368,10 +1375,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; @@ -1381,7 +1388,8 @@ 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; @@ -1415,6 +1423,12 @@ { id: Math.pow(10, -6), unit_of_measure: 'mm²' } ]; + vm.gleasonPatternsColors = { + "G3": "#ffcc99", + "G4": "#ff9966", + "G5": "#cc5200" + } + vm.locked = undefined; vm.isReadOnly = isReadOnly; @@ -1424,6 +1438,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(); @@ -1452,7 +1472,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'; @@ -1560,7 +1585,8 @@ 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; @@ -1600,6 +1626,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', @@ -3180,9 +3277,51 @@ } } - ShowGleasonPatternAnnotationController.$inject = []; + 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.locked = undefined; + + vm.isReadOnly = isReadOnly; + + activate(); - function ShowGleasonPatternAnnotationController() { + 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.locked = false; + vm.gleason_pattern_id = gleason_pattern_id; + GleasonPatternAnnotationsManagerService.getAnnotation(vm.gleason_pattern_id) + then(getGleasonPatternSuccessFn, getGleasonPatternErrorFn); + } + ); + + function getGleasonPatternSuccessFn(response) { + console.log(response); + } + + function getGleasonPatternErrorFn(response) { + $log.error('Unable to load Gleason Pattern annotation'); + $log.error(response); + } + } + + function isReadOnly() { + return true; + } } })(); \ 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 163dcb7..891eee9 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 @@ -159,8 +159,8 @@ return GleasonPatternAnnotationsManagerService; - function getAnnotation() { - + 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) { 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 d0759b7..da03a92 100644 --- a/promort/static_src/templates/clinical_annotations_manager/core_annotation.html +++ b/promort/static_src/templates/clinical_annotations_manager/core_annotation.html @@ -166,7 +166,8 @@

        {{ cmCtrl.core_label }}

        + ng-model="cmCtrl.intraluminal_acinar_differentiation_grade" + ng-disabled="cmCtrl.isReadOnly()"> @@ -207,7 +209,8 @@

        {{ cmCtrl.core_label }}

        + + + +