major fixes and updates

This commit is contained in:
Alexander Karpov 2022-11-01 12:20:43 +03:00
parent 8c53cb6745
commit dcac066a18
9 changed files with 328 additions and 88 deletions

View File

@ -1,11 +1,19 @@
from dicom.api.views import ( from dicom.api.views import (
AddDicomProjectApi,
CreateCircleApi, CreateCircleApi,
CreateFreeHandApi,
CreateRoiApi, CreateRoiApi,
CreateRulerApi,
DeleteDicomProjectApi,
ListCreateDicomApi, ListCreateDicomApi,
ListCreateProjectApi,
ListUpdateDicomImageNumberApi, ListUpdateDicomImageNumberApi,
RetrieveUpdateDeleteCircleApi, RetrieveUpdateDeleteCircleApi,
RetrieveUpdateDeleteDicomApi, RetrieveUpdateDeleteDicomApi,
RetrieveUpdateDeleteFreeHandApi,
RetrieveUpdateDeleteProjectApi,
RetrieveUpdateDeleteRoiApi, RetrieveUpdateDeleteRoiApi,
RetrieveUpdateDeleteRulerApi,
SmartFileUploadApi, SmartFileUploadApi,
) )
from django.urls import include, path from django.urls import include, path
@ -39,11 +47,21 @@ urlpatterns = [
CreateRoiApi.as_view(), CreateRoiApi.as_view(),
name="create_roi", name="create_roi",
), ),
path(
"<str:slug>/free_hand",
CreateFreeHandApi.as_view(),
name="create_free_hand",
),
path( path(
"<str:slug>/circle", "<str:slug>/circle",
CreateCircleApi.as_view(), CreateCircleApi.as_view(),
name="create_circle", name="create_circle",
), ),
path(
"<str:slug>/ruler",
CreateRulerApi.as_view(),
name="create_ruler",
),
path( path(
"<str:slug>/<int:layer>", "<str:slug>/<int:layer>",
ListUpdateDicomImageNumberApi.as_view(), ListUpdateDicomImageNumberApi.as_view(),
@ -61,11 +79,44 @@ urlpatterns = [
RetrieveUpdateDeleteRoiApi.as_view(), RetrieveUpdateDeleteRoiApi.as_view(),
name="get_update_delete_roi", name="get_update_delete_roi",
), ),
path(
"free_hand/<int:id>",
RetrieveUpdateDeleteFreeHandApi.as_view(),
name="get_update_delete_free_hand",
),
path( path(
"circle/<int:id>", "circle/<int:id>",
RetrieveUpdateDeleteCircleApi.as_view(), RetrieveUpdateDeleteCircleApi.as_view(),
name="get_update_delete_circle", name="get_update_delete_circle",
), ),
path(
"ruler/<int:id>",
RetrieveUpdateDeleteRulerApi.as_view(),
name="get_update_delete_ruler",
),
]
),
),
path(
"project/",
include(
[
path("", ListCreateProjectApi.as_view(), name="list_create_project"),
path(
"<str:slug>",
RetrieveUpdateDeleteProjectApi.as_view(),
name="get_update_delete_project",
),
path(
"<str:slug>/upload",
AddDicomProjectApi.as_view(),
name="add_dicom_api",
),
path(
"<str:slug>/<str:dicom_slug>",
DeleteDicomProjectApi.as_view(),
name="delete_dicom_api",
),
] ]
), ),
), ),

View File

@ -1,6 +1,7 @@
from dicom.models import Circle, Dicom, Roi from dicom.models import Circle, Dicom, FreeHand, Roi
from django.contrib import admin from django.contrib import admin
admin.site.register(Dicom) admin.site.register(Dicom)
admin.site.register(Circle) admin.site.register(Circle)
admin.site.register(Roi) admin.site.register(Roi)
admin.site.register(FreeHand)

View File

@ -1,18 +1,19 @@
from dicom.models import Circle, Coordinate, Dicom, Roi from dicom.models import (
BaseShape,
Circle,
Coordinate,
Dicom,
FreeHand,
Project,
Roi,
Ruler,
)
from dicom.services import create_coordinate
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
def create_coordinate(coordinates, obj):
for coordinate in coordinates:
Coordinate.objects.create(
x=coordinate["x"],
y=coordinate["y"],
shape=obj,
)
class CoordinateSerializer(serializers.ModelSerializer): class CoordinateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Coordinate model = Coordinate
@ -34,13 +35,43 @@ class ListDicomSerializer(serializers.ModelSerializer):
class BaseShapeSerializer(serializers.Serializer): class BaseShapeSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["circle", "roi"]) model = BaseShape
type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"])
image_number = serializers.IntegerField() image_number = serializers.IntegerField()
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
def create(self, validated_data):
if self.model.max_coordinates:
if len(validated_data["coordinates"]) > self.model.max_coordinates:
raise serializers.ValidationError
if self.model.min_coordinates:
if len(validated_data["coordinates"]) < self.model.min_coordinates:
raise serializers.ValidationError
dicom = get_object_or_404(
Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"]
)
obj = self.model.objects.create(
dicom=dicom, image_number=validated_data["image_number"]
)
create_coordinate(validated_data["coordinates"], obj)
return obj
def update(self, obj, validated_data):
Coordinate.objects.filter(shape=obj).delete()
if self.model.max_coordinates:
if len(validated_data["coordinates"]) > self.model.max_coordinates:
raise serializers.ValidationError
if self.model.min_coordinates:
if len(validated_data["coordinates"]) < self.model.min_coordinates:
raise serializers.ValidationError
create_coordinate(validated_data["coordinates"], obj)
return obj
class BaseShapeLayerSerializer(serializers.Serializer): class BaseShapeLayerSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["circle", "roi"]) type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"])
radius = serializers.FloatField(required=False) radius = serializers.FloatField(required=False)
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
@ -58,31 +89,34 @@ class DicomSerializer(serializers.ModelSerializer):
fields = ["file", "uploaded", "pathology_type", "shapes"] fields = ["file", "uploaded", "pathology_type", "shapes"]
class RoiSerializer(serializers.ModelSerializer): class RoiSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
model = Roi
class Meta: class Meta:
model = Roi model = Roi
fields = ["id", "image_number", "coordinates"] fields = ["id", "image_number", "coordinates"]
extra_kwargs = {"id": {"read_only": True}} extra_kwargs = {"id": {"read_only": True}}
def create(self, validated_data):
if "coordinates" not in validated_data:
raise serializers.ValidationError
dicom = get_object_or_404(
Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"]
)
roi = Roi.objects.create(
dicom=dicom, image_number=validated_data["image_number"]
)
create_coordinate(validated_data["coordinates"], roi) class FreeHandSerializer(BaseShapeSerializer, serializers.ModelSerializer):
return roi coordinates = CoordinateSerializer(many=True)
model = FreeHand
def update(self, obj: Circle, validated_data): class Meta:
Coordinate.objects.filter(shape=obj).delete() model = FreeHand
create_coordinate(validated_data["coordinates"], obj) fields = ["id", "image_number", "coordinates"]
return obj extra_kwargs = {"id": {"read_only": True}}
class RulerSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True)
model = Ruler
class Meta:
model = FreeHand
fields = ["id", "image_number", "coordinates"]
extra_kwargs = {"id": {"read_only": True}}
class CircleSerializer(serializers.ModelSerializer): class CircleSerializer(serializers.ModelSerializer):
@ -122,3 +156,24 @@ class CircleSerializer(serializers.ModelSerializer):
class SmartFileUploadSerializer(serializers.Serializer): class SmartFileUploadSerializer(serializers.Serializer):
file = serializers.FileField() file = serializers.FileField()
class ListProjectSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="get_update_delete_project", lookup_field="slug"
)
class Meta:
model = Project
fields = ["url", "created"]
extra_kwargs = {
"created": {"read_only": True},
}
class ProjectSerializer(serializers.ModelSerializer):
files = ListDicomSerializer(many=True)
class Meta:
model = Project
fields = ["files", "created"]

View File

@ -5,15 +5,19 @@ from rest_framework.generics import GenericAPIView, get_object_or_404
from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response from rest_framework.response import Response
from ..models import Circle, Dicom, Roi from ..models import Circle, Dicom, Project, Roi
from ..services import process_files from ..services import process_files
from .serializers import ( from .serializers import (
BaseShapeLayerSerializer, BaseShapeLayerSerializer,
BaseShapeSerializer, BaseShapeSerializer,
CircleSerializer, CircleSerializer,
DicomSerializer, DicomSerializer,
FreeHandSerializer,
ListDicomSerializer, ListDicomSerializer,
ListProjectSerializer,
ProjectSerializer,
RoiSerializer, RoiSerializer,
RulerSerializer,
SmartFileUploadSerializer, SmartFileUploadSerializer,
create_coordinate, create_coordinate,
) )
@ -41,55 +45,110 @@ class CreateRoiApi(generics.CreateAPIView):
serializer_class = RoiSerializer serializer_class = RoiSerializer
class CreateFreeHandApi(generics.CreateAPIView):
serializer_class = FreeHandSerializer
class CreateCircleApi(generics.CreateAPIView): class CreateCircleApi(generics.CreateAPIView):
serializer_class = CircleSerializer serializer_class = CircleSerializer
class RetrieveUpdateDeleteRoiApi(generics.RetrieveUpdateDestroyAPIView): class CreateRulerApi(generics.CreateAPIView):
serializer_class = RulerSerializer
class RetrieveUpdateDeleteBaseShape(generics.RetrieveUpdateDestroyAPIView):
def get_object(self):
return get_object_or_404(
self.serializer_class.Meta.model,
id=self.request.parser_context["kwargs"]["id"],
)
@extend_schema(description="Note: coordinated are dropped on update")
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
@extend_schema(description="Note: coordinated are dropped on update")
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
class RetrieveUpdateDeleteRoiApi(RetrieveUpdateDeleteBaseShape):
serializer_class = RoiSerializer serializer_class = RoiSerializer
def get_object(self):
return get_object_or_404(Roi, id=self.request.parser_context["kwargs"]["id"])
@extend_schema(description="Note: coordinated are dropped on update") class RetrieveUpdateDeleteFreeHandApi(RetrieveUpdateDeleteBaseShape):
def put(self, request, *args, **kwargs): serializer_class = FreeHandSerializer
return self.update(request, *args, **kwargs)
@extend_schema(description="Note: coordinated are dropped on update")
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
class RetrieveUpdateDeleteCircleApi(generics.RetrieveUpdateDestroyAPIView): class RetrieveUpdateDeleteCircleApi(RetrieveUpdateDeleteBaseShape):
serializer_class = CircleSerializer serializer_class = CircleSerializer
def get_object(self):
return get_object_or_404(Circle, id=self.request.parser_context["kwargs"]["id"])
@extend_schema(description="Note: coordinated are dropped on update") class RetrieveUpdateDeleteRulerApi(RetrieveUpdateDeleteBaseShape):
def patch(self, request, *args, **kwargs): serializer_class = CircleSerializer
return self.partial_update(request, *args, **kwargs)
@extend_schema(description="Note: coordinated are dropped on update")
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
class SmartFileUploadApi(GenericAPIView): class SmartFileUploadApi(GenericAPIView):
parser_classes = [MultiPartParser, FormParser] parser_classes = [MultiPartParser, FormParser]
serializer_class = SmartFileUploadSerializer serializer_class = SmartFileUploadSerializer
@extend_schema(responses={201: DicomSerializer(many=True)}) @extend_schema(responses={201: ListDicomSerializer(many=True)})
def post(self, request): def post(self, request):
if "file" not in request.data: if "file" not in request.data:
raise ValidationError("no files") raise ValidationError("no files")
d_list = process_files(request.FILES.getlist("file"), request.user) project = process_files(
request.FILES.getlist("file"),
request.user,
)
return Response( return Response(
DicomSerializer(d_list.files.all(), many=True).data, ListDicomSerializer(project.files.all(), many=True).data,
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
class AddDicomProjectApi(GenericAPIView):
parser_classes = [MultiPartParser, FormParser]
serializer_class = SmartFileUploadSerializer
@extend_schema(
operation_id="add_dicom_to_project",
responses={201: ListDicomSerializer(many=True)},
)
def post(self, request, slug):
if "file" not in request.data:
raise ValidationError("no files")
get_object_or_404(Project, slug=slug)
project = process_files(
request.FILES.getlist("file"),
request.user,
slug,
)
return Response(
ListDicomSerializer(project.files.all(), many=True).data,
status=status.HTTP_201_CREATED,
)
class DeleteDicomProjectApi(GenericAPIView):
serializer_class = SmartFileUploadSerializer
@extend_schema(
operation_id="add_dicom_to_project",
request=None,
responses={200: ListDicomSerializer(many=True)},
)
def delete(self, request, slug, dicom_slug):
project = get_object_or_404(Project, slug=slug)
project.files.filter(slug=dicom_slug).delete()
return Response(
ListDicomSerializer(
project.files.all(), many=True, context={"request": request}
).data,
status=status.HTTP_200_OK,
)
class ListUpdateDicomImageNumberApi(GenericAPIView): class ListUpdateDicomImageNumberApi(GenericAPIView):
serializer_class = BaseShapeSerializer(many=True) serializer_class = BaseShapeSerializer(many=True)
@ -134,3 +193,27 @@ class ListUpdateDicomImageNumberApi(GenericAPIView):
) )
] ]
return Response(shapes, status=status.HTTP_200_OK) return Response(shapes, status=status.HTTP_200_OK)
@extend_schema(
request=None,
responses={204: None},
operation_id="delete_dicom_layer",
)
def delete(self, request, slug, layer):
dicom = get_object_or_404(Dicom, slug=slug)
dicom.shapes.filter(image_number=layer).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ListCreateProjectApi(generics.ListCreateAPIView):
serializer_class = ListProjectSerializer
def get_queryset(self):
return Project.objects.filter(user=self.request.user)
class RetrieveUpdateDeleteProjectApi(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ProjectSerializer
queryset = Project.objects.all()
lookup_field = "slug"

View File

@ -1,3 +1,3 @@
# flake8: noqa # flake8: noqa
from .base import Dicom, ListOfDicom from .base import Dicom, Project
from .blocks import BaseShape, Circle, Coordinate, Roi from .shapes import BaseShape, Circle, Coordinate, FreeHand, Roi, Ruler

View File

@ -6,8 +6,14 @@ from utils.files import media_upload_path
User = get_user_model() User = get_user_model()
class ListOfDicom(models.Model): class Project(models.Model):
pass user = models.ForeignKey(User, related_name="projects", on_delete=models.CASCADE)
slug = models.SlugField(max_length=10)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user.username}'s project"
class Dicom(models.Model): class Dicom(models.Model):
@ -22,8 +28,8 @@ class Dicom(models.Model):
uploaded = models.DateTimeField(auto_now_add=True) uploaded = models.DateTimeField(auto_now_add=True)
pathology_type = models.IntegerField(choices=PathologyType.choices, default=0) pathology_type = models.IntegerField(choices=PathologyType.choices, default=0)
list = models.ForeignKey( project = models.ForeignKey(
ListOfDicom, related_name="files", null=True, on_delete=models.SET_NULL Project, related_name="files", null=True, on_delete=models.SET_NULL
) )
def __str__(self): def __str__(self):

View File

@ -4,11 +4,26 @@ from polymorphic.models import PolymorphicModel
class BaseShape(PolymorphicModel): class BaseShape(PolymorphicModel):
TYPE = "no_type"
min_coordinates = None
max_coordinates = None
dicom = models.ForeignKey(Dicom, related_name="shapes", on_delete=models.CASCADE) dicom = models.ForeignKey(Dicom, related_name="shapes", on_delete=models.CASCADE)
image_number = models.IntegerField() image_number = models.IntegerField()
def serialize_self(self): def serialize_self(self):
raise NotImplementedError return {
"id": self.id,
"type": self.TYPE,
"image_number": self.image_number,
"coordinates": self.coordinates,
}
def serialize_self_without_layer(self):
return {
"id": self.id,
"type": self.TYPE,
"coordinates": self.coordinates,
}
@property @property
def coordinates(self) -> [(int, int)]: def coordinates(self) -> [(int, int)]:
@ -33,6 +48,7 @@ class Coordinate(models.Model):
class Circle(BaseShape): class Circle(BaseShape):
radius = models.FloatField() radius = models.FloatField()
max_coordinates = 1
def serialize_self(self): def serialize_self(self):
return { return {
@ -56,21 +72,23 @@ class Circle(BaseShape):
class Roi(BaseShape): class Roi(BaseShape):
def serialize_self(self): TYPE = "roi"
return {
"id": self.id,
"type": "roi",
"image_number": self.image_number,
"coordinates": self.coordinates,
}
def serialize_self_without_layer(self):
return {
"id": self.id,
"type": "roi",
"radius": self.radius,
"coordinates": self.coordinates,
}
def __str__(self): def __str__(self):
return f"Roi on {self.dicom.file.name}" return f"Roi on {self.dicom.file.name}"
class FreeHand(BaseShape):
TYPE = "free_hand"
def __str__(self):
return f"FreeHand on {self.dicom.file.name}"
class Ruler(BaseShape):
TYPE = "ruler"
max_coordinates = 2
min_coordinates = 2
def __str__(self):
return f"Ruler on {self.dicom.file.name}"

View File

@ -5,18 +5,23 @@ import zipfile
from pathlib import Path from pathlib import Path
import magic import magic
from dicom.models import Dicom, ListOfDicom from dicom.models import Coordinate, Dicom, Project
from django.core.files import File from django.core.files import File
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
from utils.generators import generate_charset from utils.generators import generate_charset
def process_files(files: list[TemporaryUploadedFile | InMemoryUploadedFile], user): def process_files(
d_list = ListOfDicom.objects.create() files: list[TemporaryUploadedFile | InMemoryUploadedFile], user, slug=None
):
if slug:
project = Project.objects.get(slug=slug)
else:
project = Project.objects.create(user=user)
for file in files: for file in files:
content_type = magic.from_file(file.temporary_file_path()) content_type = magic.from_file(file.temporary_file_path())
if content_type == "DICOM medical imaging data": if content_type == "DICOM medical imaging data":
Dicom.objects.create(file=file, list=d_list, user=user) Dicom.objects.create(file=file, project=project, user=user)
elif "Zip" in content_type: elif "Zip" in content_type:
dit_path = f"/tmp/{generate_charset(10)}" dit_path = f"/tmp/{generate_charset(10)}"
os.mkdir(dit_path) os.mkdir(dit_path)
@ -32,8 +37,17 @@ def process_files(files: list[TemporaryUploadedFile | InMemoryUploadedFile], use
with path.open(mode="rb") as f: with path.open(mode="rb") as f:
Dicom.objects.create( Dicom.objects.create(
file=File(f, name=file_in_d.split("/")[-1]), file=File(f, name=file_in_d.split("/")[-1]),
list=d_list, project=project,
user=user, user=user,
) )
shutil.rmtree(dit_path) shutil.rmtree(dit_path)
return d_list return project
def create_coordinate(coordinates, obj):
for coordinate in coordinates:
Coordinate.objects.create(
x=coordinate["x"],
y=coordinate["y"],
shape=obj,
)

View File

@ -1,12 +1,24 @@
from dicom.models import Dicom from dicom.models import Dicom, Project
from django.db.models.signals import pre_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from utils.generators import generate_charset from utils.generators import generate_charset
@receiver(pre_save, sender=Dicom) @receiver(post_save, sender=Project)
def create_dicom(sender, instance: Dicom, **kwargs): def create_project(sender, instance: Project, created, **kwargs):
if created:
slug = generate_charset(5)
while Project.objects.filter(slug=slug):
slug = generate_charset(5)
instance.slug = slug
instance.save()
@receiver(post_save, sender=Dicom)
def create_dicom(sender, instance: Dicom, created, **kwargs):
if created:
slug = generate_charset(5) slug = generate_charset(5)
while Dicom.objects.filter(slug=slug): while Dicom.objects.filter(slug=slug):
slug = generate_charset(5) slug = generate_charset(5)
instance.slug = slug instance.slug = slug
instance.save()