Skip to content

Commit

Permalink
Release v0.10.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lucalianas committed May 9, 2022
2 parents 051e1e3 + d8c0c6f commit 8cf494d
Show file tree
Hide file tree
Showing 16 changed files with 840 additions and 68 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
## 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
2 changes: 1 addition & 1 deletion promort/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.4-2
0.10.0
46 changes: 31 additions & 15 deletions promort/predictions_manager/management/commands/tissue_to_rois.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==")
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -174,7 +190,7 @@ def _create_slice(
annotation_step,
user,
slide_bounds,
collection
collection,
):
slice_coordinates = self._adjust_roi_coordinates(
slice_coordinates, slide_bounds
Expand All @@ -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_
Expand All @@ -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(
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions promort/predictions_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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, )

Expand Down
1 change: 1 addition & 0 deletions promort/promort/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def to_url(self, value):
path('api/cases/<slug:pk>/', CaseDetail.as_view()),
path('api/slides/', SlideList.as_view()),
path('api/slides/<slug:pk>/', SlideDetail.as_view()),
path('api/slides/<slug:pk>/predictions/', pmv.PredictionDetailBySlide.as_view()),
path('api/slides_set/', SlidesSetList.as_view()),
path('api/slides_set/<slug:pk>/', SlidesSetDetail.as_view()),

Expand Down
102 changes: 94 additions & 8 deletions promort/src/js/ome_seadragon_viewer/viewer.controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -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));
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 ---');
Expand All @@ -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',
Expand Down
Loading

0 comments on commit 8cf494d

Please sign in to comment.