Skip to content

Commit

Permalink
Remove retina thumbnail endpoints (#2151)
Browse files Browse the repository at this point in the history
Co-authored-by: James Meakin <12661555+jmsmkn@users.noreply.github.com>
  • Loading branch information
HarmvZ and jmsmkn committed Nov 1, 2021
1 parent 66c9c14 commit f056ffa
Show file tree
Hide file tree
Showing 29 changed files with 71 additions and 770 deletions.
1 change: 0 additions & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@
/app/grandchallenge/evaluation/ @jmsmkn
/app/grandchallenge/reader_studies/ @MikeOverkamp-diag
/app/grandchallenge/retina_api/ @HarmvZ
/app/grandchallenge/retina_core/ @HarmvZ
/app/grandchallenge/products/ @MikeOverkamp-diag
/app/grandchallenge/workstations/ @jmsmkn
4 changes: 0 additions & 4 deletions app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,6 @@ def strtobool(val) -> bool:
"grandchallenge.studies",
"grandchallenge.registrations",
"grandchallenge.annotations",
"grandchallenge.retina_core",
"grandchallenge.retina_api",
"grandchallenge.workstations",
"grandchallenge.workspaces",
Expand Down Expand Up @@ -1152,9 +1151,6 @@ def strtobool(val) -> bool:
os.environ.get("DATA_UPLOAD_MAX_NUMBER_FIELDS", "2048")
)

# Default maximum width or height for thumbnails in retina workstation
RETINA_DEFAULT_THUMBNAIL_SIZE = 128

# Retina specific settings
RETINA_GRADERS_GROUP_NAME = "retina_graders"
RETINA_ADMINS_GROUP_NAME = "retina_admins"
Expand Down
4 changes: 0 additions & 4 deletions app/config/urls/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,6 @@ def handler500(request):
),
),
path("summernote/", include("django_summernote.urls")),
path(
"retina/",
include("grandchallenge.retina_core.urls", namespace="retina"),
),
path(
"aiforradiology/",
include("grandchallenge.products.urls", namespace="products"),
Expand Down
113 changes: 1 addition & 112 deletions app/grandchallenge/cases/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import logging
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import List, Mapping, Union
from typing import List

from actstream.actions import follow
from actstream.models import Follow
Expand All @@ -17,10 +16,6 @@
from django.dispatch import receiver
from django.utils.text import get_valid_filename
from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm
from panimg.image_builders.metaio_utils import (
load_sitk_image,
parse_mh_header,
)
from panimg.models import ColorSpace, ImageType, PatientSex

from grandchallenge.core.models import UUIDModel
Expand Down Expand Up @@ -387,36 +382,6 @@ def shape(self) -> List[int]:
result.append(color_components)
return result

@property
def spacing(self) -> List[float]:
"""
Return the voxel spacing (or size if spacing is nonexistent) of the image.
Returns
-------
The voxel spacing in mm in NumPy ordering [(z), y, x]
Defaults to [(1), 1, 1]
"""
spacing = [
self.voxel_depth_mm,
self.voxel_height_mm,
self.voxel_width_mm,
]
if spacing[0] is None:
spacing = spacing[-2:]
if None in spacing:
mh_header = self.get_mh_header()
spacing_str = mh_header.get(
"ElementSpacing", mh_header.get("ElementSize")
)
if spacing_str is not None:
spacing = list(
reversed([float(x) for x in spacing_str.split(" ")])
)
else:
spacing = [1] * int(mh_header["NDims"])
return spacing

def get_metaimage_files(self):
"""
Return ImageFile object for the related MHA file or MHD and RAW files.
Expand Down Expand Up @@ -455,82 +420,6 @@ def get_metaimage_files(self):

return header_file, image_data_file

def get_mh_header(self) -> Mapping[str, Union[str, None]]:
"""
Return header from mhd/mha file as key value pairs
Returns
-------
MetaIO headers as key value pairs.
Raises
------
FileNotFoundError
Raised when Image has no related mhd/mha ImageFile or actual file
cannot be found on storage
"""

mh_file, _ = self.get_metaimage_files()
return parse_mh_header(mh_file.file)

def get_sitk_image(self):
"""
Return the image that belongs to this model as an SimpleITK image.
Requires that exactly one MHD/RAW file pair is associated with the model.
Otherwise it wil raise a MultipleObjectsReturned or ObjectDoesNotExist
exception.
Returns
-------
A SimpleITK image
"""
files = [i for i in self.get_metaimage_files() if i is not None]

file_size = 0
for file in files:
if not file.file.storage.exists(name=file.file.name):
raise FileNotFoundError(f"No file found for {file.file}")

# Add up file sizes of mhd and raw file to get total file size
file_size += file.file.size

# Check file size to guard for out of memory error
if file_size > settings.MAX_SITK_FILE_SIZE:
raise OSError(
f"File exceeds maximum file size. (Size: {file_size}, Max: {settings.MAX_SITK_FILE_SIZE})"
)

with TemporaryDirectory() as tempdirname:
for file in files:
with file.file.open("rb") as infile, open(
Path(tempdirname) / Path(file.file.name).name, "wb"
) as outfile:
buffer = True
while buffer:
buffer = infile.read(1024)
outfile.write(buffer)

try:
hdr_path = Path(tempdirname) / Path(files[0].file.name).name
sitk_image = load_sitk_image(mhd_file=hdr_path)
except RuntimeError as e:
logging.error(
f"Failed to load SimpleITK image with error: {e}"
)
raise

return sitk_image

def permit_viewing_by_retina_users(self):
"""Set object level view permissions for retina_graders and retina_admins."""
for group_name in (
settings.RETINA_GRADERS_GROUP_NAME,
settings.RETINA_ADMINS_GROUP_NAME,
):
group = Group.objects.get(name=group_name)
assign_perm("view_image", group, self)

def update_viewer_groups_permissions(self, *, exclude_jobs=None):
"""
Update the permissions for the algorithm jobs viewers groups to
Expand Down
1 change: 1 addition & 0 deletions app/grandchallenge/retina_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "grandchallenge.retina_api.apps.RetinaAPIConfig"
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def init_retina_groups(*_, **__):
Group.objects.get_or_create(name=settings.RETINA_GRADERS_GROUP_NAME)


class RetinaCoreConfig(AppConfig):
name = "grandchallenge.retina_core"
class RetinaAPIConfig(AppConfig):
name = "grandchallenge.retina_api"

def ready(self):
post_migrate.connect(init_retina_groups)
54 changes: 0 additions & 54 deletions app/grandchallenge/retina_api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,9 @@
import base64
from io import BytesIO

import SimpleITK
from PIL import Image as PILImage
from django.http import Http404
from rest_framework import serializers

from grandchallenge.cases.models import Image
from grandchallenge.cases.serializers import HyperlinkedImageSerializer


class B64ImageSerializer(serializers.Serializer):
"""
Serializer that returns a b64 encoded image from an Image instance.
If "width" and "height" are passed as extra serializer content, the
PIL image will be resized to those dimensions.
Subclasses PILImageSerializer, so the image may be resized and only the central
slice of a 3d image will be returned
"""

content = serializers.SerializerMethodField(read_only=True)

def get_content(self, obj):
try:
image_itk = obj.get_sitk_image()
except Exception:
raise Http404

pil_image = self.convert_itk_to_pil(image_itk)

if "width" in self.context and "height" in self.context:
new_dims = (self.context["width"], self.context["height"])
try:
pil_image.thumbnail(
new_dims, PILImage.ANTIALIAS,
)
except ValueError:
pil_image = pil_image.resize(new_dims)

return self.create_thumbnail_as_b64(pil_image)

@staticmethod
def convert_itk_to_pil(image_itk):
depth = image_itk.GetDepth()
image_nparray = SimpleITK.GetArrayFromImage(image_itk)
if depth > 0:
# Get center slice of image if 3D
image_nparray = image_nparray[depth // 2]
return PILImage.fromarray(image_nparray)

@staticmethod
def create_thumbnail_as_b64(image_pil):
buffer = BytesIO()
image_pil.save(buffer, format="png")
return base64.b64encode(buffer.getvalue())


class ImageLevelAnnotationsForImageSerializer(serializers.Serializer):
quality = serializers.UUIDField(allow_null=True, read_only=True)
pathology = serializers.UUIDField(allow_null=True, read_only=True)
Expand Down
18 changes: 0 additions & 18 deletions app/grandchallenge/retina_api/urls.py

This file was deleted.

20 changes: 0 additions & 20 deletions app/grandchallenge/retina_api/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as drf_filters
from rest_framework import mixins, viewsets
from rest_framework.exceptions import NotFound
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.response import Response
from rest_framework_guardian import filters

from grandchallenge.annotations.models import (
Expand Down Expand Up @@ -41,7 +37,6 @@
)
from grandchallenge.retina_api.mixins import RetinaAPIPermission
from grandchallenge.retina_api.serializers import (
B64ImageSerializer,
ImageLevelAnnotationsForImageSerializer,
RetinaImageSerializer,
)
Expand All @@ -60,21 +55,6 @@ class ETDRSGridAnnotationViewSet(viewsets.ModelViewSet):
queryset = ETDRSGridAnnotation.objects.all()


class B64ThumbnailAPIView(RetrieveAPIView):
permission_classes = (DjangoObjectPermissions, RetinaAPIPermission)
filters = (filters.ObjectPermissionsFilter,)
queryset = Image.objects.all()
serializer_class = B64ImageSerializer

def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
width = kwargs.get("width", settings.RETINA_DEFAULT_THUMBNAIL_SIZE)
height = kwargs.get("height", settings.RETINA_DEFAULT_THUMBNAIL_SIZE)
serializer_context = {"width": width, "height": height}
serializer = B64ImageSerializer(instance, context=serializer_context)
return Response(serializer.data)


class LandmarkAnnotationSetViewSet(viewsets.ModelViewSet):
permission_classes = (RetinaAPIPermission,)
serializer_class = LandmarkAnnotationSetSerializer
Expand Down
1 change: 0 additions & 1 deletion app/grandchallenge/retina_core/__init__.py

This file was deleted.

11 changes: 0 additions & 11 deletions app/grandchallenge/retina_core/migrations/0001_initial.py

This file was deleted.

Empty file.
1 change: 0 additions & 1 deletion app/grandchallenge/retina_core/static
Submodule static deleted from ee8d10
13 changes: 0 additions & 13 deletions app/grandchallenge/retina_core/urls.py

This file was deleted.

45 changes: 0 additions & 45 deletions app/grandchallenge/retina_core/views.py

This file was deleted.

Loading

0 comments on commit f056ffa

Please sign in to comment.