diff --git a/README.md b/README.md index 698dfc43..cab48bd9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# ProMort Image Management System -The goal of the **ProMort** project is to conduct a large scale epidemiological study of patients that have beendiagnosed -with a benign form of prostatic cancer but nevertheless died before expected. +# CRS4 Digital Pathology Platform +The Digital Pathology platform developed by CRS4 is a web-based application tailored for interactive annotation of Whole Slide Images in the context of clinical research. It is a multi-component system that integrates [OME Remote Objects (OMERO)](https://www.openmicroscopy.org/omero/), thought the integration of the [ome_seadragon plugin](https://github.com/crs4/ome_seadragon), with a system for annotating tumour tissues, developed entirely at CRS4. +The platform was born out of a collaboration with the Karolinska Institutet in Stockholm in the context of the [ProMort](https://academic.oup.com/aje/article/188/6/1165/5320054?login=true) project, where it allowed pathologists to annotate thousands of prostate tissue images. Its development has continued, incorporating several new functionalities. Most recently, the ability to apply deep learning models to images for detecting tumour regions in prostate biopsies and for classifying their severity was added (in the [DeepHealth](https://deephealth-project.eu/) project). The use of the platform for the study of prostate cancer has also been [validated in a collaborative study by CRS4 and KI](https://www.nature.com/articles/s41598-021-82911-z). -The specific goal of the **ProMort Image Management System** is to create a collection of fully annotated images related -to biopsies slides. \ No newline at end of file +## Docker images + +* Django based web server: https://hub.docker.com/repository/docker/crs4/promort-web +* Nginx server with static files: https://hub.docker.com/repository/docker/crs4/promort-nginx diff --git a/promort/VERSION b/promort/VERSION index 45dfa9b3..78bc1abd 100644 --- a/promort/VERSION +++ b/promort/VERSION @@ -1 +1 @@ -0.9.4-2 \ No newline at end of file +0.10.0 diff --git a/promort/predictions_manager/management/commands/tissue_to_rois.py b/promort/predictions_manager/management/commands/tissue_to_rois.py index df498131..d96d2714 100644 --- a/promort/predictions_manager/management/commands/tissue_to_rois.py +++ b/promort/predictions_manager/management/commands/tissue_to_rois.py @@ -54,6 +54,12 @@ def add_arguments(self, parser): default=None, help="apply only to ROIs annotation steps assigned to this reviewer", ) + parser.add_argument( + "--limit-bounds", + dest="limit_bounds", + action="store_true", + help="apply limit bounds when converting to ROIs", + ) def handle(self, *args, **opts): logger.info("== Starting import job ==") @@ -64,19 +70,28 @@ def handle(self, *args, **opts): for step in annotation_steps: logger.info("Processing ROIs annotation step %s", step.label) - latest_prediction = Prediction.objects.filter( - slide=step.slide, type='TISSUE' - ).order_by('-creation_date').first() - - fragments_collection = latest_prediction.fragments_collection.order_by('-creation_date').first() - + latest_prediction = ( + Prediction.objects.filter(slide=step.slide, type="TISSUE") + .order_by("-creation_date") + .first() + ) + + fragments_collection = latest_prediction.fragments_collection.order_by( + "-creation_date" + ).first() + if fragments_collection and fragments_collection.fragments.count() > 0: fragments = fragments_collection.fragments.all() - slide_bounds = self._get_slide_bounds(step.slide) + if opts["limit_bounds"]: + slide_bounds = self._get_slide_bounds(step.slide) + else: + slide_bounds = {"bounds_x": 0, "bounds_y": 0} slide_mpp = step.slide.image_microns_per_pixel - all_shapes = [json.loads(fragment.shape_json) for fragment in fragments] + all_shapes = [ + json.loads(fragment.shape_json) for fragment in fragments + ] grouped_shapes = self._group_nearest_cores(all_shapes) for idx, shapes in enumerate(grouped_shapes): slice_label = idx + 1 @@ -89,7 +104,7 @@ def handle(self, *args, **opts): step, user, slide_bounds, - fragments_collection + fragments_collection, ) logger.info("Slice saved with ID %d", slice_obj.id) for core_index, core in enumerate(shapes): @@ -107,13 +122,14 @@ def handle(self, *args, **opts): core_index + 1, user, slide_bounds, - fragments_collection + fragments_collection, ) logger.info("Core saved with ID %d", core_obj.id) else: logger.info( "Skipping prediction %s for step %s, no tissue fragment found", - latest_prediction.label, step.label, + latest_prediction.label, + step.label, ) continue else: @@ -174,7 +190,7 @@ def _create_slice( annotation_step, user, slide_bounds, - collection + collection, ): slice_coordinates = self._adjust_roi_coordinates( slice_coordinates, slide_bounds @@ -189,7 +205,7 @@ def _create_slice( author=user, roi_json=json.dumps(roi_json), total_cores=cores_count, - source_collection=collection + source_collection=collection, ) slice_.save() return slice_ @@ -204,7 +220,7 @@ def _create_core( core_id, user, slide_bounds, - collection + collection, ): core_coordinates = self._adjust_roi_coordinates(core_coordinates, slide_bounds) roi_json = self._create_roi_json( @@ -217,7 +233,7 @@ def _create_core( roi_json=json.dumps(roi_json), length=core_length, area=core_area, - source_collection=collection + source_collection=collection, ) core.save() return core diff --git a/promort/predictions_manager/views.py b/promort/predictions_manager/views.py index b8b2eee5..20a1f0d5 100644 --- a/promort/predictions_manager/views.py +++ b/promort/predictions_manager/views.py @@ -20,6 +20,8 @@ import json import logging +from distutils.util import strtobool + from predictions_manager.models import (Prediction, TissueFragment, TissueFragmentsCollection) from predictions_manager.serializers import ( @@ -46,6 +48,31 @@ class PredictionDetail(GenericDetailView): permission_classes = (permissions.IsAuthenticated, ) +class PredictionDetailBySlide(APIView): + model_serializer = PredictionSerializer + permission_classes = (permissions.IsAuthenticated, ) + + def _find_predictions_by_slide_id(self, slide_id, type=None, fetch_latest=False): + if type is None: + predictions = Prediction.objects.filter(slide__id=slide_id) + else: + predictions = Prediction.objects.filter(slide__id=slide_id, type=type) + if fetch_latest: + return predictions.order_by('-creation_date').first() + return predictions.all() + + + def get(self, request, pk, format=None): + fetch_latest = strtobool(request.GET.get('latest', 'false')) + prediction_type = request.GET.get('type') + predictions = self._find_predictions_by_slide_id(pk, prediction_type, fetch_latest) + if (fetch_latest and predictions is None) or (not fetch_latest and len(predictions) == 0): + raise NotFound(f'No predictions found for the required query') + else: + serializer = self.model_serializer(predictions, many = not fetch_latest) + return Response(serializer.data, status=status.HTTP_200_OK) + + class PredictionRequireReview(APIView): permission_classes = (permissions.IsAuthenticated, ) diff --git a/promort/promort/urls.py b/promort/promort/urls.py index bac53793..37d6e1a8 100644 --- a/promort/promort/urls.py +++ b/promort/promort/urls.py @@ -100,6 +100,7 @@ def to_url(self, value): path('api/cases//', CaseDetail.as_view()), path('api/slides/', SlideList.as_view()), path('api/slides//', SlideDetail.as_view()), + path('api/slides//predictions/', pmv.PredictionDetailBySlide.as_view()), path('api/slides_set/', SlidesSetList.as_view()), path('api/slides_set//', SlidesSetDetail.as_view()), diff --git a/promort/src/js/ome_seadragon_viewer/viewer.controllers.js b/promort/src/js/ome_seadragon_viewer/viewer.controllers.js index 9737ec72..8d6e72f1 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.controllers.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.controllers.js @@ -247,32 +247,74 @@ AnnotationsViewerController.$inject = ['$scope', '$rootScope', '$location', '$log', 'ngDialog', 'ViewerService', 'AnnotationsViewerService', 'ROIsAnnotationStepManagerService', - 'CurrentSlideDetailsService', 'CurrentAnnotationStepsDetailsService']; + 'CurrentSlideDetailsService', 'CurrentAnnotationStepsDetailsService', + 'HeatmapViewerService', 'CurrentPredictionDetailsService']; function AnnotationsViewerController($scope, $rootScope, $location, $log, ngDialog, ViewerService, AnnotationsViewerService, ROIsAnnotationStepManagerService, - CurrentSlideDetailsService, CurrentAnnotationStepsDetailsService) { + CurrentSlideDetailsService, CurrentAnnotationStepsDetailsService, + HeatmapViewerService, CurrentPredictionDetailsService) { var vm = this; + vm.ome_base_url = undefined; vm.slide_id = undefined; + vm.prediction_id = undefined; vm.annotation_step_label = undefined; vm.slide_details = undefined; + vm.prediction_details = undefined; vm.dzi_url = undefined; vm.static_files_url = undefined; + vm.loading_tiled_images = undefined; + vm.current_opacity = undefined; vm.getDZIURL = getDZIURL; + vm.enableHeatmapLayer = enableHeatmapLayer; + vm.getDatasetDZIURL = getDatasetDZIURL; vm.getStaticFilesURL = getStaticFilesURL; vm.getSlideMicronsPerPixel = getSlideMicronsPerPixel; vm.registerComponents = registerComponents; + vm.registerHeatmapComponents = registerHeatmapComponents; + vm.setOverlayOpacity = setOverlayOpacity; + vm.updateOverlayOpacity = updateOverlayOpacity; activate(); function activate() { + var dialog = undefined; + vm.slide_id = CurrentSlideDetailsService.getSlideId(); + vm.prediction_id = CurrentPredictionDetailsService.getPredictionId(); vm.annotation_step_label = CurrentAnnotationStepsDetailsService.getROIsAnnotationStepLabel(); + + vm.loading_tiled_images = 0; + + $scope.$on('viewer.tiledimage.added', function() { + if (vm.loading_tiled_images == 0) { + dialog = ngDialog.open({ + template: '/static/templates/dialogs/heatmap_loading.html', + showClose: false, + closeByEscape: false, + closeByNavigation: false, + closeByDocument: false + }); + } + vm.loading_tiled_images += 1; + }); + + $scope.$on('viewer.tiledimage.loaded', function() { + if (vm.loading_tiled_images > 0) { + vm.loading_tiled_images -= 1; + if (vm.loading_tiled_images === 0) { + dialog.close(); + } + } else { + console.log('Nothing to do...'); + } + }); + ViewerService.getOMEBaseURLs() .then(OMEBaseURLSuccessFn, OMEBaseURLErrorFn); function OMEBaseURLSuccessFn(response) { - var base_url = response.data.base_url; + vm.ome_base_url = response.data.base_url; vm.static_files_url = response.data.static_files_url + '/ome_seadragon/img/openseadragon/'; ViewerService.getSlideInfo(vm.slide_id) @@ -281,11 +323,28 @@ function SlideInfoSuccessFn(response) { vm.slide_details = response.data; if (vm.slide_details.image_type === 'MIRAX') { - vm.dzi_url = base_url + 'mirax/deepzoom/get/' + vm.slide_details.id + '.dzi'; + vm.dzi_url = vm.ome_base_url + 'mirax/deepzoom/get/' + vm.slide_details.id + '.dzi'; } else { - vm.dzi_url = base_url + 'deepzoom/get/' + vm.slide_details.omero_id + '.dzi'; + vm.dzi_url = vm.ome_base_url + 'deepzoom/get/' + vm.slide_details.omero_id + '.dzi'; + } + + if (typeof(vm.prediction_id) !== 'undefined') { + HeatmapViewerService.getPredictionInfo(vm.prediction_id) + .then(PredictionInfoSuccessFn, PredictionInfoErrorFn); + + function PredictionInfoSuccessFn(response) { + vm.prediction_details = response.data; + + $rootScope.$broadcast('viewer.controller_initialized'); + } + + function PredictionInfoErrorFn(response) { + $log.error(response.error); + $location.url('404'); + } + } else { + $rootScope.$broadcast('viewer.controller_initialized'); } - $rootScope.$broadcast('viewer.controller_initialized'); } function SlideInfoErrorFn(response) { @@ -298,8 +357,9 @@ $log.error(response.error); } - $scope.$on('viewerctrl.components.registered', + $scope.$on('rois_viewerctrl.components.registered', function(event, rois_read_only, clinical_annotation_step_label) { + console.log(event); var dialog = ngDialog.open({ template: '/static/templates/dialogs/rois_loading.html', showClose: false, @@ -312,6 +372,8 @@ .then(getROIsSuccessFn, getROIsErrorFn); function getROIsSuccessFn(response) { + $log.info('Loaded ROIS'); + for (var sl in response.data.slices) { var slice = response.data.slices[sl]; AnnotationsViewerService.drawShape($.parseJSON(slice.roi_json)); @@ -375,6 +437,14 @@ return vm.dzi_url; } + function enableHeatmapLayer() { + return (typeof(vm.prediction_id) !== 'undefined'); + } + + function getDatasetDZIURL(color_palette) { + return HeatmapViewerService.getDatasetBaseUrl() + '?palette=' + color_palette; + } + function getStaticFilesURL() { return vm.static_files_url; } @@ -384,6 +454,7 @@ } function registerComponents(viewer_manager, annotations_manager, tools_manager, rois_read_only) { + $log.info('Registering components'); AnnotationsViewerService.registerComponents(viewer_manager, annotations_manager, tools_manager); $log.debug('--- VERIFY ---'); @@ -393,9 +464,24 @@ clinical_annotation_step_label = CurrentAnnotationStepsDetailsService.getClinicalAnnotationStepLabel(); } - $rootScope.$broadcast('viewerctrl.components.registered', rois_read_only, + $rootScope.$broadcast('rois_viewerctrl.components.registered', rois_read_only, clinical_annotation_step_label); } + + function registerHeatmapComponents(viewer_manager) { + HeatmapViewerService.registerComponents(viewer_manager, vm.ome_base_url, vm.prediction_details); + } + + function setOverlayOpacity(opacity, update) { + this.current_opacity = opacity; + if (typeof(update) !== 'undefined' && update === true) { + this.updateOverlayOpacity(); + } + } + + function updateOverlayOpacity() { + HeatmapViewerService.setOverlayOpacity(this.current_opacity); + } } SlidesSequenceViewerController.$inject = ['$scope', '$routeParams', '$rootScope', '$location', '$log', diff --git a/promort/src/js/ome_seadragon_viewer/viewer.directives.js b/promort/src/js/ome_seadragon_viewer/viewer.directives.js index c9b1868e..ff76c288 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.directives.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.directives.js @@ -211,7 +211,7 @@ link: function(scope, element, attrs) { function setViewerHeight() { var used_v_space = $("#pg_header").height() + $("#pg_footer").height() - + $("#index_navbar").height() + 100; + + $("#index_navbar").height() + $("#heatmap_controls").height() + 115; var available_v_space = $(window).height() - used_v_space; @@ -244,6 +244,16 @@ ); ome_seadragon_viewer.buildViewer(); + ome_seadragon_viewer.viewer.world.addHandler('add-item', function(data) { + scope.$broadcast('viewer.tiledimage.added'); + + data.item.addHandler('fully-loaded-change', function(data) { + if (data.fullyLoaded === true) { + scope.$broadcast('viewer.tiledimage.loaded'); + } + }); + }); + var scalebar_config = { 'xOffset': 10, 'yOffset': 10, @@ -259,6 +269,18 @@ ome_seadragon_viewer.viewer.addHandler('open', function() { ome_seadragon_viewer.setMinDZILevel(8); + if(scope.avc.enableHeatmapLayer()) { + scope.avc.registerHeatmapComponents(ome_seadragon_viewer); + ome_seadragon_viewer.initOverlaysLayer( + { + 'red': scope.avc.getDatasetDZIURL('Reds_9') + }, 0.5 + ); + scope.avc.setOverlayOpacity(0.5); + + ome_seadragon_viewer.activateOverlay('red', '0.5'); + } + var annotations_canvas = new AnnotationsController('rois_canvas'); annotations_canvas.buildAnnotationsCanvas(ome_seadragon_viewer); ome_seadragon_viewer.addAnnotationsController(annotations_canvas, true); diff --git a/promort/src/js/ome_seadragon_viewer/viewer.services.js b/promort/src/js/ome_seadragon_viewer/viewer.services.js index 9f19c14d..a8b7bec3 100644 --- a/promort/src/js/ome_seadragon_viewer/viewer.services.js +++ b/promort/src/js/ome_seadragon_viewer/viewer.services.js @@ -92,22 +92,45 @@ var HeatmapViewerService = { registerComponents: registerComponents, getPredictionInfo: getPredictionInfo, + getDatasetBaseUrl: getDatasetBaseUrl, + getShapedFromDatasetBaseUrl: getShapedFromDatasetBaseUrl, + getShapesFromPrediction: getShapesFromPrediction, setOverlay: setOverlay, setOverlayOpacity: setOverlayOpacity }; return HeatmapViewerService; - function registerComponents(viewer_manager, dataset_base_url) { + function registerComponents(viewer_manager, ome_base_url, prediction_details) { this.viewerManager = viewer_manager; - this.dataset_base_url = dataset_base_url; - $rootScope.$broadcast('viewerctrl.components.registered'); + this.dataset_base_url = ome_base_url + 'arrays/deepzoom/get/' + prediction_details.omero_id + '.dzi'; + this.dataset_shapes_base_url = ome_base_url + 'arrays/shapes/get/' + prediction_details.omero_id; + $rootScope.$broadcast('hm_viewerctrl.components.registered'); } function getPredictionInfo(prediction_id) { return $http.get('api/predictions/' + prediction_id + '/'); } + function getDatasetBaseUrl() { + return this.dataset_base_url; + } + + function getShapedFromDatasetBaseUrl() { + return this.dataset_shapes_base_url; + } + + function getShapesFromPrediction(threshold, cluster_size) { + return $http.get(this.dataset_shapes_base_url, + {'params' :{ + 'threshold': threshold, + 'cluster_min_distance': cluster_size, + 'cluster_min_area': 2, + 'shape_mode': 'patch' + }} + ); + } + function setOverlay(palette, threshold) { this.viewerManager.setOverlay(this.dataset_base_url, palette, threshold); } @@ -199,6 +222,7 @@ } function drawShape(shape_json) { + console.log(shape_json); this.roisManager.drawShapeFromJSON(shape_json); } diff --git a/promort/src/js/predictions_manager/predictions_manager.services.js b/promort/src/js/predictions_manager/predictions_manager.services.js index e755b6d1..43f6e7d0 100644 --- a/promort/src/js/predictions_manager/predictions_manager.services.js +++ b/promort/src/js/predictions_manager/predictions_manager.services.js @@ -36,6 +36,7 @@ var CurrentPredictionDetailsService = { getPredictionByReviewStep: getPredictionByReviewStep, + getLatestPredictionBySlide: getLatestPredictionBySlide, registerCurrentPrediction: registerCurrentPrediction, getPredictionId: getPredictionId, getSlideId: getSlideId, @@ -52,6 +53,15 @@ return $http.get('api/prediction_review/' + review_step_label + '/prediction/'); } + function getLatestPredictionBySlide(slide_id, type) { + predictionID = undefined; + slideID = undefined; + caseID = undefined; + + return $http.get('api/slides/' + slide_id + '/predictions/', + {'params': {'latest': true, 'type': type}}); + } + function registerCurrentPrediction(prediction_id, slide_id, case_id) { predictionID = prediction_id; slideID = slide_id; diff --git a/promort/src/js/rois_manager/rois_manager.controllers.js b/promort/src/js/rois_manager/rois_manager.controllers.js index 075c0a71..84557ada 100644 --- a/promort/src/js/rois_manager/rois_manager.controllers.js +++ b/promort/src/js/rois_manager/rois_manager.controllers.js @@ -38,11 +38,13 @@ ROIsManagerController.$inject = ['$scope', '$routeParams', '$rootScope', '$compile', '$location', '$log', 'ngDialog', 'ROIsAnnotationStepService', 'ROIsAnnotationStepManagerService', - 'AnnotationsViewerService', 'CurrentSlideDetailsService']; + 'AnnotationsViewerService', 'CurrentSlideDetailsService', 'CurrentPredictionDetailsService', + 'HeatmapViewerService']; function ROIsManagerController($scope, $routeParams, $rootScope, $compile, $location, $log, ngDialog, ROIsAnnotationStepService, ROIsAnnotationStepManagerService, - AnnotationsViewerService, CurrentSlideDetailsService) { + AnnotationsViewerService, CurrentSlideDetailsService, + CurrentPredictionDetailsService, HeatmapViewerService) { var vm = this; vm.slide_id = undefined; vm.slide_index = undefined; @@ -50,10 +52,27 @@ vm.annotation_label = undefined; vm.annotation_step_label = undefined; + vm.prediction_id = undefined; + vm.overlay_palette = undefined; + vm.overlay_opacity = undefined; + vm.overlay_threshold = undefined; + vm.navmap_cluster_size = undefined; + + vm.oo_percentage = undefined; + vm.slices_map = undefined; vm.cores_map = undefined; vm.focus_regions_map = undefined; + vm.displayNavmap = undefined; + + vm.full_navmap_items = undefined; + vm.navmap_items = undefined; + vm.navmap_items_label = undefined; + + vm.navmap_selected_item = undefined; + vm.navmap_selected_filter = undefined; + vm.ui_active_modes = { 'new_slice': false, 'new_core': false, @@ -71,6 +90,29 @@ vm._createNewSubtree = _createNewSubtree; vm._lockRoisTree = _lockRoisTree; vm._unlockRoisTree = _unlockRoisTree; + vm._drawNavmapCluster = _drawNavmapCluster; + vm._drawNavmapItem = _drawNavmapItem; + vm._drawNavmap = _drawNavmap; + vm._deleteNavmapItem = _deleteNavmapItem; + vm._hideNavmap = _hideNavmap; + vm._clearNavmap = _clearNavmap; + vm._filterNavmapByShape = _filterNavmapByShape; + vm._updateNavmap = _updateNavmap; + vm.navmapDisplayEnabled = navmapDisplayEnabled; + vm.switchNavmapDisplay = switchNavmapDisplay; + vm.removeSliceNavmapFilter = removeSliceNavmapFilter; + vm.filterNavmapBySlice = filterNavmapBySlice; + vm.jumpToNextNavmapItem = jumpToNextNavmapItem; + vm.jumpToPreviousNavmapItem = jumpToPreviousNavmapItem; + vm.jumpToNavmapItem = jumpToNavmapItem; + vm.jumpToSelectedNavmapItem = jumpToSelectedNavmapItem; + vm.noNavmapItemSelected = noNavmapItemSelected; + vm.showSelectedNavmapItem = showSelectedNavmapItem; + vm.hideSelectedNavmapItem = hideSelectedNavmapItem; + vm.selectNavmapItem = selectNavmapItem; + vm.deselectNavmapItem = deselectNavmapItem; + vm.isFirstItemSelected = isFirstItemSelected; + vm.isLastItemSelected = isLastItemSelected; vm.allModesOff = allModesOff; vm.showROI = showROI; vm.editROI = editROI; @@ -98,6 +140,7 @@ vm.editFocusRegionModeActive = editFocusRegionModeActive; vm.newItemCreationModeActive = newItemCreationModeActive; vm.editItemModeActive = editItemModeActive; + vm._registerNavmapItem = _registerNavmapItem; vm._registerSlice = _registerSlice; vm._unregisterSlice = _unregisterSlice; vm._registerCore = _registerCore; @@ -113,19 +156,43 @@ vm.getCoresCount = getCoresCount; vm.getFocusRegionsCount = getFocusRegionsCount; + vm.updateOverlayOpacity = updateOverlayOpacity; + vm.updateOverlayThreshold = updateOverlayThreshold; + vm.updateNavmapClusterSize = updateNavmapClusterSize; + vm.updateOverlayPalette = updateOverlayPalette; + activate(); function activate() { + console.log("Prediction ID: " + CurrentPredictionDetailsService.getPredictionId()); + vm.slide_id = CurrentSlideDetailsService.getSlideId(); vm.case_id = CurrentSlideDetailsService.getCaseId(); vm.annotation_step_label = $routeParams.label; vm.annotation_label = vm.annotation_step_label.split('-')[0]; vm.slide_index = vm.annotation_step_label.split('-')[1]; + vm.prediction_id = CurrentPredictionDetailsService.getPredictionId(); + + vm.overlay_palette = 'Reds_9'; + vm.overlay_opacity = 0.5; + vm.overlay_threshold = "0.8"; + vm.navmap_cluster_size = "2"; + + vm.oo_percentage = Math.floor(vm.overlay_opacity * 100); + vm.slices_map = {}; vm.cores_map = {}; vm.focus_regions_map = {}; + vm.displayNavmap = false; + vm.full_navmap_items = {} + vm.navmap_items = {}; + vm.navmap_items_label = []; + + vm.navmap_selected_item = undefined; + vm.navmap_selected_filter = undefined; + $rootScope.slices = []; $rootScope.cores = []; $rootScope.focus_regions = []; @@ -141,6 +208,28 @@ if (response.data.slide_evaluation !== null && response.data.slide_evaluation.adequate_slide) { + $scope.$on('hm_viewerctrl.components.registered', + function() { + // load navigation map + $log.info('Building navigation map'); + HeatmapViewerService.getShapesFromPrediction(vm.overlay_threshold, vm.navmap_cluster_size) + .then(getShapesSuccessFn, getShapesErrorFn); + + function getShapesSuccessFn(response) { + for (var sh in response.data.shapes) { + vm._registerNavmapItem(sh, response.data.shapes[sh]); + vm.navmap_items = vm.full_navmap_items; + vm.navmap_items_label = Object.keys(vm.navmap_items); + } + } + + function getShapesErrorFn(response) { + $log.error('Error when loading shapes from prediction'); + $log.error(response); + } + } + ); + // shut down creation forms when specific events occur $scope.$on('tool.destroyed', function () { @@ -281,18 +370,27 @@ } } + function _registerNavmapItem(item_index, item_shape) { + var item_label = 'cluster_' + (parseInt(item_index)+1); + vm.full_navmap_items[item_label] = item_shape; + } + function _registerSlice(slice_info) { $rootScope.slices.push(slice_info); vm.slices_map[slice_info.id] = slice_info.label; } function _unregisterSlice(slice_id) { + var slice_label = vm.slices_map[slice_id]; delete vm.slices_map[slice_id]; $rootScope.slices = $.grep($rootScope.slices, function(value) { return value.id !== slice_id; } ); + if (vm.navmap_selected_filter === slice_label) { + vm.removeSliceNavmapFilter(); + } } function _getSliceLabel(slice_id) { @@ -380,6 +478,194 @@ $(".prm-tree-el").removeClass("prm-tree-el-disabled"); } + function _drawNavmapCluster(item_label, item_shape, hidden) { + if (hidden==true) { + var stroke_alpha = 0; + } else { + var stroke_alpha = 1; + } + var shape_json = { + 'shape_id': item_label, + 'fill_color': '#fff', + 'fill_alpha': 0.0, + 'stroke_color': '#0000ff', + 'stroke_alpha': stroke_alpha, + 'stroke_width': 50, + 'hidden': false, + 'segments': item_shape, + 'type': 'polygon' + }; + AnnotationsViewerService.drawShape(shape_json); + } + + function _drawNavmapItem(item_label, hidden) { + var item_shape = vm.navmap_items[item_label]; + vm._drawNavmapCluster(item_label, item_shape, hidden); + } + + function _deleteNavmapItem(item_label) { + AnnotationsViewerService.deleteShape(item_label); + } + + function _drawNavmap() { + for (var ilabel in vm.navmap_items) { + vm._drawNavmapItem(ilabel, false); + } + } + + function _hideNavmap() { + for (var sh in vm.navmap_items) { + vm._deleteNavmapItem(sh); + } + } + + function _clearNavmap(keep_source) { + for (var sh in vm.navmap_items) { + vm._deleteNavmapItem(sh); + } + if (!keep_source) { + vm.full_navmap_items = {}; + } + vm.navmap_items = {}; + vm.navmap_items_label = []; + vm.navmap_selected_item = undefined; + } + + function _filterNavmapByShape(shape_label) { + for (var ilabel in vm.full_navmap_items) { + vm._drawNavmapCluster(ilabel, vm.full_navmap_items[ilabel], true); + if (AnnotationsViewerService.checkContainment(shape_label, ilabel)) { + vm.navmap_items[ilabel] = vm.full_navmap_items[ilabel]; + vm.navmap_items_label.push(ilabel); + } + vm._deleteNavmapItem(ilabel); + } + } + + function _updateNavmap(new_shapes) { + vm._clearNavmap(false); + $("#selected_navmap_item").text("-- Select an item --"); + for (var sh in new_shapes) { + vm._registerNavmapItem(sh, new_shapes[sh]); + } + if (typeof(vm.navmap_selected_filter) !== 'undefined') { + vm._filterNavmapByShape(vm.navmap_selected_filter); + } else { + vm.navmap_items = vm.full_navmap_items; + vm.navmap_items_label = Object.keys(vm.navmap_items); + } + if (vm.navmapDisplayEnabled()) { + vm._drawNavmap(); + } + } + + function navmapDisplayEnabled() { + return vm.displayNavmap; + } + + function switchNavmapDisplay() { + console.log('Switching navmap display'); + vm.displayNavmap = !vm.displayNavmap; + if (vm.navmapDisplayEnabled()) { + console.log('Draw navmap'); + vm._drawNavmap(); + } else { + console.log('Hide navmap'); + vm._hideNavmap(); + } + } + + function removeSliceNavmapFilter() { + vm._clearNavmap(true); + vm.navmap_items = vm.full_navmap_items; + vm.navmap_items_label = Object.keys(vm.navmap_items); + if (vm.navmapDisplayEnabled()) { + vm._drawNavmap(); + } + vm.navmap_selected_filter = undefined; + $("#selected_slice_filter").text("-- No filter --"); + } + + function filterNavmapBySlice(slice) { + vm._clearNavmap(true); + vm._filterNavmapByShape(slice); + vm.navmap_selected_filter = slice; + $("#selected_slice_filter").text(vm.navmap_selected_filter); + if (vm.navmapDisplayEnabled()) { + vm._drawNavmap(); + } + } + + function jumpToNextNavmapItem() { + var next_item_label = vm.navmap_items_label[vm.navmap_items_label.indexOf(vm.navmap_selected_item)+1]; + if(!vm.navmapDisplayEnabled()) { + vm._drawNavmapItem(next_item_label, true); + } + vm.jumpToNavmapItem(next_item_label); + if(!vm.navmapDisplayEnabled()) { + vm._deleteNavmapItem(next_item_label); + } + } + + function jumpToPreviousNavmapItem() { + var prev_item_label = vm.navmap_items_label[vm.navmap_items_label.indexOf(vm.navmap_selected_item)-1]; + if(!vm.navmapDisplayEnabled()) { + vm._drawNavmapItem(prev_item_label, true); + } + vm.jumpToNavmapItem(prev_item_label); + if(!vm.navmapDisplayEnabled()) { + vm._deleteNavmapItem(prev_item_label); + } + } + + function jumpToNavmapItem(label) { + vm.navmap_selected_item = label; + $("#selected_navmap_item").text(label); + vm.jumpToSelectedNavmapItem(); + } + + function jumpToSelectedNavmapItem() { + AnnotationsViewerService.focusOnShape(vm.navmap_selected_item); + } + + function noNavmapItemSelected() { + return vm.navmap_selected_item == undefined; + } + + function showSelectedNavmapItem() { + if(!vm.noNavmapItemSelected()) { + vm.selectNavmapItem(vm.navmap_selected_item); + } + } + + function hideSelectedNavmapItem() { + if(!vm.noNavmapItemSelected()) { + vm.deselectNavmapItem(vm.navmap_selected_item); + } + } + + function selectNavmapItem(label) { + if(!vm.navmapDisplayEnabled()) { + vm._drawNavmapItem(label, true); + } + AnnotationsViewerService.selectShape(label); + } + + function deselectNavmapItem(label) { + AnnotationsViewerService.deselectShape(label); + if(!vm.navmapDisplayEnabled()) { + vm._deleteNavmapItem(label); + } + } + + function isFirstItemSelected() { + return (vm.navmap_items_label.indexOf(vm.navmap_selected_item) == 0); + } + + function isLastItemSelected() { + return (vm.navmap_items_label.indexOf(vm.navmap_selected_item) == (vm.navmap_items_label.length - 1)); + } + function allModesOff() { for (var mode in vm.ui_active_modes) { vm.ui_active_modes[mode] = false; @@ -494,6 +780,7 @@ $("#rois_tree").children().remove(); vm.allModesOff(); + vm.removeSliceNavmapFilter(); dialog.close(); } @@ -659,6 +946,46 @@ function getFocusRegionsCount() { return $rootScope.focus_regions.length; } + + function updateOverlayOpacity() { + HeatmapViewerService.setOverlayOpacity(vm.overlay_opacity); + vm.oo_percentage = Math.floor(vm.overlay_opacity * 100); + } + + function updateOverlayThreshold() { + HeatmapViewerService.setOverlay(vm.overlay_palette, vm.overlay_threshold); + + HeatmapViewerService.getShapesFromPrediction(vm.overlay_threshold, vm.navmap_cluster_size) + .then(getShapesSuccessFn, getShapesErrorFn); + + function getShapesSuccessFn(response) { + vm._updateNavmap(response.data.shapes); + } + + function getShapesErrorFn(response) { + $log.error('Error when loading shapes from prediction'); + $log.error(response); + } + } + + function updateNavmapClusterSize() { + HeatmapViewerService.getShapesFromPrediction(vm.overlay_threshold, vm.navmap_cluster_size) + .then(getShapesSuccessFn, getShapesErrorFn); + + function getShapesSuccessFn(response) { + vm._updateNavmap(response.data.shapes); + } + + function getShapesErrorFn(response) { + $log.error('Error when loading shapes from prediction'); + $log.error(response); + } + } + + function updateOverlayPalette() { + console.log('Current overlay palette is: ' + vm.overlay_palette); + HeatmapViewerService.setOverlay(vm.overlay_palette, vm.overlay_threshold); + } } NewScopeController.$inject = ['$scope', '$log',]; @@ -1400,7 +1727,7 @@ vm.tumorLengthScaleFactor = vm.lengthUOM[0]; vm.coreAreaScaleFactor = vm.areaUOM[0]; - $scope.$on('viewerctrl.components.registered', + $scope.$on('rois_viewerctrl.components.registered', function() { vm.initializeRuler(); vm.initializeTumorRuler(); @@ -2124,7 +2451,7 @@ activate(); function activate() { - $scope.$on('viewerctrl.components.registered', + $scope.$on('rois_viewerctrl.components.registered', function() { vm.initializeRuler(); vm.initializeTumorRuler(); @@ -2724,13 +3051,13 @@ vm.slide_id = CurrentSlideDetailsService.getSlideId(); vm.case_id = CurrentSlideDetailsService.getCaseId(); - // by default, mark region as NORMAL - vm.tissueStatus = 'NORMAL'; + // by default, mark region as TUMOR + vm.tissueStatus = 'TUMOR'; vm.regionLengthScaleFactor = vm.lengthUOM[0]; vm.regionAreaScaleFactor = vm.areaUOM[0]; - $scope.$on('viewerctrl.components.registered', + $scope.$on('rois_viewerctrl.components.registered', function() { vm.initializeRuler(); } @@ -3344,7 +3671,7 @@ activate(); function activate() { - $scope.$on('viewerctrl.components.registered', + $scope.$on('rois_viewerctrl.components.registered', function() { vm.initializeRuler(); } diff --git a/promort/src/js/rois_manager/rois_manager.services.js b/promort/src/js/rois_manager/rois_manager.services.js index ace75043..64b79356 100644 --- a/promort/src/js/rois_manager/rois_manager.services.js +++ b/promort/src/js/rois_manager/rois_manager.services.js @@ -54,10 +54,15 @@ } function getROIs(step_label, read_only, clinical_step_label) { - if (!read_only) { - return $http.get('/api/rois_annotation_steps/' + step_label + '/rois_list/'); + $log.info('Called getROIs service with params: ' + step_label + ' ' + read_only + ' ' + clinical_step_label); + if(typeof(read_only) == 'undefined') { + $log.error('missing required parameter'); } else { - return $http.get('/api/rois_annotation_steps/' + step_label + '/rois_list/' + clinical_step_label + '/'); + if (!read_only) { + return $http.get('/api/rois_annotation_steps/' + step_label + '/rois_list/'); + } else { + return $http.get('/api/rois_annotation_steps/' + step_label + '/rois_list/' + clinical_step_label + '/'); + } } } diff --git a/promort/src/js/slides_manager/slides_manager.controllers.js b/promort/src/js/slides_manager/slides_manager.controllers.js index a208ba69..a808a020 100644 --- a/promort/src/js/slides_manager/slides_manager.controllers.js +++ b/promort/src/js/slides_manager/slides_manager.controllers.js @@ -27,10 +27,12 @@ .controller('QualityControlController', QualityControlController); QualityControlController.$inject = ['$scope', '$routeParams', '$location', '$log', 'Authentication', - 'SlideEvaluationService', 'ROIsAnnotationStepService', 'SlideService', 'CurrentSlideDetailsService']; + 'SlideEvaluationService', 'ROIsAnnotationStepService', 'SlideService', 'CurrentSlideDetailsService', + 'CurrentPredictionDetailsService']; function QualityControlController($scope, $routeParams, $location, $log, Authentication, SlideEvaluationService, - ROIsAnnotationStepService, SlideService, CurrentSlideDetailsService) { + ROIsAnnotationStepService, SlideService, CurrentSlideDetailsService, + CurrentPredictionDetailsService) { var vm = this; vm.annotation_label = undefined; vm.annotation_step_label = undefined; @@ -75,7 +77,26 @@ } } else { if (response.data.slide_evaluation.adequate_slide) { - $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + console.log('Getting prediction details'); + CurrentPredictionDetailsService.getLatestPredictionBySlide(CurrentSlideDetailsService.getSlideId(), 'TUMOR') + .then(getPredictionSuccessFn, getPredictionErrorFn); + + function getPredictionSuccessFn(response) { + CurrentPredictionDetailsService.registerCurrentPrediction( + response.data.id, response.data.slide, + CurrentSlideDetailsService.getCaseId() + ); + $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + } + + function getPredictionErrorFn(response) { + if(response.status == 404) { + $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + } else { + $log.error('Error when loading predictions'); + $log.error(response); + } + } } else { $location.url('worklist/' + vm.annotation_label); } @@ -126,7 +147,27 @@ //noinspection JSAnnotator function startAnnotationSuccessFn(response) { - $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + console.log('Getting prediction details'); + CurrentPredictionDetailsService.getLatestPredictionBySlide(CurrentSlideDetailsService.getSlideId(), 'TUMOR') + .then(getPredictionSuccessFn, getPredictionErrorFn); + + function getPredictionSuccessFn(response) { + console.log('Prediction details: ' + response.data); + CurrentPredictionDetailsService.registerCurrentPrediction( + response.data.id, response.data.slide, + CurrentSlideDetailsService.getCaseId() + ); + $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + } + + function getPredictionErrorFn(response) { + if(response.status == 404) { + $location.url('worklist/' + vm.annotation_step_label + '/rois_manager'); + } else { + $log.error('Error when loading predictions'); + $log.error(response); + } + } } //noinspection JSAnnotator diff --git a/promort/src/js/worklist/worklist.controllers.js b/promort/src/js/worklist/worklist.controllers.js index 8c39b542..259da459 100644 --- a/promort/src/js/worklist/worklist.controllers.js +++ b/promort/src/js/worklist/worklist.controllers.js @@ -160,10 +160,12 @@ } ROIsAnnotationController.$inject = ['$scope', '$routeParams', '$location', '$log', 'ngDialog', - 'ROIsAnnotationStepService', 'CurrentSlideDetailsService', 'CurrentAnnotationStepsDetailsService']; + 'ROIsAnnotationStepService', 'CurrentSlideDetailsService', 'CurrentAnnotationStepsDetailsService', + 'CurrentPredictionDetailsService']; function ROIsAnnotationController($scope, $routeParams, $location, $log, ngDialog, ROIsAnnotationStepService, - CurrentSlideDetailsService, CurrentAnnotationStepsDetailsService) { + CurrentSlideDetailsService, CurrentAnnotationStepsDetailsService, + CurrentPredictionDetailsService) { var vm = this; vm.annotationSteps = []; vm.label = undefined; @@ -261,7 +263,26 @@ if (skip_qc === false) { $location.url('worklist/' + annotationStep.label + '/quality_control'); } else { - $location.url('worklist/' + annotationStep.label + '/rois_manager'); + console.log('Getting prediction details'); + CurrentPredictionDetailsService.getLatestPredictionBySlide(CurrentSlideDetailsService.getSlideId(), 'TUMOR') + .then(getPredictionSuccessFn, getPredictionErrorFn); + + function getPredictionSuccessFn(response) { + CurrentPredictionDetailsService.registerCurrentPrediction( + response.data.id, response.data.slide, + CurrentSlideDetailsService.getCaseId() + ); + $location.url('worklist/' + annotationStep.label + '/rois_manager'); + } + + function getPredictionErrorFn(response) { + if(response.status == 404) { + $location.url('worklist/' + annotationStep.label + '/rois_manager'); + } else { + $log.error('Error when loading predictions'); + $log.error(response); + } + } } } diff --git a/promort/static_src/css/promort.css b/promort/static_src/css/promort.css index 450c21d4..1e4bc996 100644 --- a/promort/static_src/css/promort.css +++ b/promort/static_src/css/promort.css @@ -27,6 +27,7 @@ div.prm-valign { div.prm-viewer_frame { background-color: white; + padding: 5px; } div.prm-ome_viewer_simple { @@ -172,6 +173,12 @@ h3.prm-dialog-container { height: 30px; } +.prm-full-col-btn { + display: block; + width: 100%; + text-align: center; +} + .prm-icon-btn-ruler { margin-left: 4px !important; border-radius: 4px !important; @@ -299,6 +306,19 @@ h3.prm-dialog-container { transform: scale(-1, 1); } +.prm-hm-controls { + background-color: white; + margin-top: 0; + margin-bottom: 5px; + padding-top: 10px; + padding-bottom: 0; +} + +#navigation_map_controls { + padding-top: 0; + padding-bottom: 0; +} + /* Hidden placeholder */ select option[disabled]:first-child { display: none; diff --git a/promort/static_src/templates/rois_manager/focus_region.html b/promort/static_src/templates/rois_manager/focus_region.html index 79fd46c7..ff9cca13 100644 --- a/promort/static_src/templates/rois_manager/focus_region.html +++ b/promort/static_src/templates/rois_manager/focus_region.html @@ -283,9 +283,9 @@

{{ rmCtrl.label }}

diff --git a/promort/static_src/templates/rois_manager/manager.html b/promort/static_src/templates/rois_manager/manager.html index 1af6231f..2f2109f4 100644 --- a/promort/static_src/templates/rois_manager/manager.html +++ b/promort/static_src/templates/rois_manager/manager.html @@ -27,19 +27,21 @@

ROIs editing - Slide {{ rmc.slide_index }}

Back to slides - + + + + + + + + +
+
+ +
+
+ +
+
+ -
- +
+
+
+
+

PALETTE

+
+
+ +
+
+
+
+

OPACITY

+
+
+
+
0
+ +
100
+
+
+
+
+
+

THRESHOLD

+
+
+ +
+
+
+
+ +