From 9019d31d48084b1d9007cf7d3f81f66f7242a43f Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Sun, 6 Nov 2022 03:35:27 +0300 Subject: [PATCH] added layers --- image_markuper/config/api_router.py | 15 +++ image_markuper/dicom/api/serializers.py | 152 +++++++++++++++++------- image_markuper/dicom/api/views.py | 35 +++++- image_markuper/dicom/models/base.py | 15 +++ image_markuper/dicom/models/shapes.py | 26 ++-- image_markuper/dicom/services.py | 1 - image_markuper/dicom/signals.py | 13 +- 7 files changed, 195 insertions(+), 62 deletions(-) diff --git a/image_markuper/config/api_router.py b/image_markuper/config/api_router.py index 6ad7268..fb46324 100644 --- a/image_markuper/config/api_router.py +++ b/image_markuper/config/api_router.py @@ -2,6 +2,7 @@ from dicom.api.views import ( AddDicomProjectApi, CreateCircleApi, CreateFreeHandApi, + CreateLayerApi, CreateRoiApi, CreateRulerApi, DeleteDicomProjectApi, @@ -12,6 +13,7 @@ from dicom.api.views import ( RetrieveUpdateDeleteCircleApi, RetrieveUpdateDeleteDicomApi, RetrieveUpdateDeleteFreeHandApi, + RetrieveUpdateDeleteLayerApi, RetrieveUpdateDeleteProjectApi, RetrieveUpdateDeleteRoiApi, RetrieveUpdateDeleteRulerApi, @@ -129,4 +131,17 @@ urlpatterns = [ ] ), ), + path( + "layer//", + include( + [ + path("", CreateLayerApi.as_view(), name="create_layer"), + path( + "", + RetrieveUpdateDeleteLayerApi.as_view(), + name="get_update_delete_project", + ), + ] + ), + ), ] diff --git a/image_markuper/dicom/api/serializers.py b/image_markuper/dicom/api/serializers.py index cc116e3..b3cf881 100644 --- a/image_markuper/dicom/api/serializers.py +++ b/image_markuper/dicom/api/serializers.py @@ -4,6 +4,7 @@ from dicom.models import ( Coordinate, Dicom, FreeHand, + Layer, Project, Roi, Ruler, @@ -20,6 +21,27 @@ class CoordinateSerializer(serializers.ModelSerializer): fields = ["x", "y"] +class ListProjectSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name="get_update_delete_project", lookup_field="slug" + ) + + class Meta: + model = Project + fields = ["name", "pathology_type", "slug", "url", "created"] + extra_kwargs = { + "slug": {"read_only": True}, + "created": {"read_only": True}, + } + + def create(self, validated_data): + return Project.objects.create( + user=self.context["request"].user, + name=validated_data["name"], + pathology_type=validated_data["pathology_type"], + ) + + class ListDicomSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="get_update_delete_dicom", lookup_field="slug" @@ -28,17 +50,25 @@ class ListDicomSerializer(serializers.ModelSerializer): class Meta: model = Dicom - fields = ["file", "uploaded", "pathology_type", "url"] + fields = ["file", "uploaded", "url"] def create(self, validated_data): return Dicom.objects.create(**validated_data, user=self.context["request"].user) +class ProjectSerializer(serializers.ModelSerializer): + files = ListDicomSerializer(many=True) + + class Meta: + model = Project + fields = ["files", "slug", "created", "stl"] + + class BaseShapeSerializer(serializers.Serializer): model = BaseShape - type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"]) - image_number = serializers.IntegerField() + type = serializers.ChoiceField(choices=["circle", "roi", "free_hand", "ruler"]) + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) coordinates = CoordinateSerializer(many=True) def create(self, validated_data): @@ -51,9 +81,12 @@ class BaseShapeSerializer(serializers.Serializer): 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"] - ) + if validated_data["layer"]: + layer = get_object_or_404(Layer, slug=validated_data["layer"]) + else: + layer = dicom.layers.filter(parent__isnull=True).first() + + obj = self.model.objects.create(layer_fk=layer) create_coordinate(validated_data["coordinates"], obj) return obj @@ -66,65 +99,121 @@ class BaseShapeSerializer(serializers.Serializer): if self.model.min_coordinates: if len(validated_data["coordinates"]) < self.model.min_coordinates: raise serializers.ValidationError + if validated_data["layer"]: + layer = get_object_or_404(Layer, slug=validated_data["layer"]) + else: + layer = obj.dicom.layers.filter(parent__isnull=True).first() + if obj.layer_fk != layer: + obj.layer_fk = layer + obj.save() create_coordinate(validated_data["coordinates"], obj) return obj class BaseShapeLayerSerializer(serializers.Serializer): - type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"]) + type = serializers.ChoiceField(choices=["circle", "roi", "free_hand", "ruler"]) + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) radius = serializers.FloatField(required=False) coordinates = CoordinateSerializer(many=True) +class LayerChildSerializer(serializers.ModelSerializer): + class Meta: + model = Layer + fields = ["name", "slug"] + + +class LayerSerializer(serializers.ModelSerializer): + children = LayerChildSerializer(many=True, read_only=True) + parent = serializers.SlugField(max_length=8, allow_blank=True, write_only=True) + + def validate_parent(self, val): + if val: + return get_object_or_404(Layer, slug=val) + return ( + get_object_or_404( + Dicom, + slug=self.context["request"].parser_context["kwargs"]["dicom_slug"], + ) + .layers.filter(parent__isnull=True) + .first() + ) + + class Meta: + model = Layer + fields = ["name", "slug", "children", "parent"] + extra_kwargs = { + "children": {"read_only": True}, + "slug": {"read_only": True}, + "parent": {"write_only": True}, + } + + def create(self, validated_data): + return Layer.objects.create( + name=validated_data["name"], + dicom=validated_data["parent"].dicom, + parent=validated_data["parent"], + ) + + class DicomSerializer(serializers.ModelSerializer): file = serializers.FileField() shapes = serializers.SerializerMethodField("get_dicom_shapes") + layers = serializers.SerializerMethodField("get_dicom_layers") - @extend_schema_field(field=BaseShapeSerializer) + @extend_schema_field(field=BaseShapeSerializer(many=True)) def get_dicom_shapes(self, obj): return [x.serialize_self() for x in obj.shapes.all()] + @extend_schema_field(field=LayerSerializer(many=True)) + def get_dicom_layers(self, obj): + return obj.get_layers() + class Meta: model = Dicom - fields = ["file", "uploaded", "pathology_type", "shapes"] + fields = ["file", "uploaded", "shapes", "layers"] class RoiSerializer(BaseShapeSerializer, serializers.ModelSerializer): coordinates = CoordinateSerializer(many=True) + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) model = Roi class Meta: model = Roi - fields = ["id", "image_number", "coordinates"] + fields = ["id", "layer", "coordinates"] extra_kwargs = {"id": {"read_only": True}} class FreeHandSerializer(BaseShapeSerializer, serializers.ModelSerializer): + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) coordinates = CoordinateSerializer(many=True) model = FreeHand class Meta: model = FreeHand - fields = ["id", "image_number", "coordinates"] + fields = ["id", "layer", "coordinates"] extra_kwargs = {"id": {"read_only": True}} class RulerSerializer(BaseShapeSerializer, serializers.ModelSerializer): coordinates = CoordinateSerializer(many=True) + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) model = Ruler class Meta: model = FreeHand - fields = ["id", "image_number", "coordinates"] + fields = ["id", "layer", "coordinates"] extra_kwargs = {"id": {"read_only": True}} class CircleSerializer(serializers.ModelSerializer): coordinates = CoordinateSerializer(many=True) + layer = serializers.SlugField(max_length=8, required=False, allow_blank=True) class Meta: model = Circle - fields = ["id", "image_number", "radius", "coordinates"] + fields = ["id", "layer", "radius", "coordinates"] extra_kwargs = {"id": {"read_only": True}} def create(self, validated_data): @@ -136,9 +225,12 @@ class CircleSerializer(serializers.ModelSerializer): dicom = get_object_or_404( Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"] ) + if validated_data["layer"]: + layer = get_object_or_404(Layer, slug=validated_data["layer"]) + else: + layer = dicom.layers.filter(parent__isnull=True).first() circle = Circle.objects.create( - dicom=dicom, - image_number=validated_data["image_number"], + layer_fk=layer, radius=validated_data["radius"], ) @@ -148,8 +240,13 @@ class CircleSerializer(serializers.ModelSerializer): def update(self, obj: Circle, validated_data): Coordinate.objects.filter(shape=obj).delete() create_coordinate(validated_data["coordinates"], obj) + if validated_data["layer"]: + layer = get_object_or_404(Layer, slug=validated_data["layer"]) + else: + layer = obj.dicom.layers.filter(parent__isnull=True).first() if "radius" in validated_data: obj.radius = validated_data["radius"] + obj.layer_fk = layer obj.save() return obj @@ -158,31 +255,6 @@ class SmartFileUploadSerializer(serializers.Serializer): file = serializers.FileField() -class ListProjectSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name="get_update_delete_project", lookup_field="slug" - ) - - class Meta: - model = Project - fields = ["slug", "url", "created"] - extra_kwargs = { - "slug": {"read_only": True}, - "created": {"read_only": True}, - } - - def create(self, validated_data): - return Project.objects.create(user=self.context["request"].user) - - -class ProjectSerializer(serializers.ModelSerializer): - files = ListDicomSerializer(many=True) - - class Meta: - model = Project - fields = ["files", "slug", "created", "stl"] - - class PatologyGenerateSerializer(serializers.Serializer): project_slug = serializers.CharField() points = serializers.ListField(child=CoordinateSerializer()) diff --git a/image_markuper/dicom/api/views.py b/image_markuper/dicom/api/views.py index 233b072..ebdff47 100644 --- a/image_markuper/dicom/api/views.py +++ b/image_markuper/dicom/api/views.py @@ -5,7 +5,7 @@ from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response -from ..models import Circle, Dicom, Project, Roi +from ..models import Circle, Dicom, Layer, Project, Roi from ..services import process_files from .serializers import ( BaseShapeLayerSerializer, @@ -13,6 +13,7 @@ from .serializers import ( CircleSerializer, DicomSerializer, FreeHandSerializer, + LayerSerializer, ListDicomSerializer, ListProjectSerializer, PatologyGenerateSerializer, @@ -29,12 +30,12 @@ class ListCreateDicomApi(generics.ListCreateAPIView): parser_classes = [MultiPartParser, FormParser] def get_queryset(self): - return Dicom.objects.filter(user=self.request.user) + return Dicom.objects.all() class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self): - return Dicom.objects.filter(user=self.request.user) + return Dicom.objects.all() serializer_class = DicomSerializer parser_classes = [MultiPartParser, FormParser] @@ -211,6 +212,23 @@ class ListCreateProjectApi(generics.ListCreateAPIView): def get_queryset(self): return Project.objects.filter(user=self.request.user) + @extend_schema( + description="""(0, 'Без патологий'), + (1, 'COVID-19; все доли; многочисленные; размер любой'), + (2, 'COVID-19; Нижняя доля правого лёгкого, Нижняя доля левого лёгкого'), + (3, 'Немногочисленные; 10-20 мм'), + (4, 'Рак лёгкого; Нижняя доля правого лёгкого, Единичное; 10-20 мм'), + (5, 'Рак лёгкого; Средняя доля правого лёгкого, Единичное; >20 мм'), + (6, 'Рак лёгкого; Нижняя доля левого лёгкого, Единичное; 10-20 мм'), + (7, 'Рак лёгкого; Верхняя доля правого лёгкого, Единичное; 5-10 мм'), + (8, 'Рак лёгкого; Верхняя доля левого лёгкого, Единичное; 5-10 мм'), + (9, 'Метастатическое поражение лёгких; Все доли; Многочисленные; 5-10 мм'), + (10, 'Метастатическое поражение лёгких; Все доли; Многочисленные; 10-20 мм'), + (11, 'Метастатическое поражение лёгких; Все доли; Немногочисленные; 5-10 мм')""" + ) + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + class RetrieveUpdateDeleteProjectApi(generics.RetrieveUpdateDestroyAPIView): serializer_class = ProjectSerializer @@ -226,3 +244,14 @@ class GeneratePatology(generics.CreateAPIView): # data = self.get_serializer(request.data).data # bbox = get_bbox(data["project_slug"], data["points"], data["depth"]) return Response(data={}, status=200) + + +class CreateLayerApi(generics.CreateAPIView): + serializer_class = LayerSerializer + + +class RetrieveUpdateDeleteLayerApi(generics.RetrieveUpdateDestroyAPIView): + serializer_class = LayerSerializer + queryset = Layer.objects.all() + + lookup_field = "slug" diff --git a/image_markuper/dicom/models/base.py b/image_markuper/dicom/models/base.py index 4a7b483..1528854 100644 --- a/image_markuper/dicom/models/base.py +++ b/image_markuper/dicom/models/base.py @@ -1,3 +1,4 @@ +from dicom.models.shapes import BaseShape from django.contrib.auth import get_user_model from django.db import models from django.urls import reverse @@ -75,6 +76,13 @@ class Dicom(models.Model): def __str__(self): return self.file.name + @property + def shapes(self): + return BaseShape.objects.filter(layer_fk__dicom=self) + + def get_layers(self): + return self.layers.filter(parent__isnull=True).first().serialize_self() + def get_absolute_url(self): return reverse("get_update_delete_dicom", kwargs={"slug": self.slug}) @@ -87,5 +95,12 @@ class Layer(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(max_length=8) + def serialize_self(self): + return { + "slug": self.slug, + "name": self.name, + "children": [x.serialize_self() for x in self.children.all()], + } + def __str__(self): return f"layer on {self.dicom}" diff --git a/image_markuper/dicom/models/shapes.py b/image_markuper/dicom/models/shapes.py index 778930d..55e0395 100644 --- a/image_markuper/dicom/models/shapes.py +++ b/image_markuper/dicom/models/shapes.py @@ -1,4 +1,3 @@ -from dicom.models import Layer from django.db import models from polymorphic.models import PolymorphicModel @@ -7,14 +6,16 @@ class BaseShape(PolymorphicModel): TYPE = "no_type" min_coordinates = None max_coordinates = None - layer = models.ForeignKey(Layer, related_name="shapes", on_delete=models.CASCADE) + layer_fk = models.ForeignKey( + "dicom.Layer", related_name="shapes", on_delete=models.CASCADE + ) def serialize_self(self): return { "id": self.id, "type": self.TYPE, - "image_number": self.image_number, "coordinates": self.coordinates, + "layer": self.layer, } def serialize_self_without_layer(self): @@ -24,12 +25,16 @@ class BaseShape(PolymorphicModel): "coordinates": self.coordinates, } + @property + def layer(self): + return self.layer_fk.slug + @property def coordinates(self) -> [(int, int)]: return self.shape_coordinates.all().values("x", "y") def __str__(self): - return self.dicom.file.name + return f"{self.TYPE} on {self.layer}" class Coordinate(models.Model): @@ -53,7 +58,6 @@ class Circle(BaseShape): return { "id": self.id, "type": "circle", - "image_number": self.image_number, "radius": self.radius, "coordinates": self.coordinates, } @@ -66,28 +70,16 @@ class Circle(BaseShape): "coordinates": self.coordinates, } - def __str__(self): - return f"circle on {self.dicom.file.name}" - class Roi(BaseShape): TYPE = "roi" - def __str__(self): - 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}" diff --git a/image_markuper/dicom/services.py b/image_markuper/dicom/services.py index 4155172..d031b54 100644 --- a/image_markuper/dicom/services.py +++ b/image_markuper/dicom/services.py @@ -43,7 +43,6 @@ def process_files( Dicom.objects.create( file=File(f, name=file_in_d.split("/")[-1]), project=project, - user=user, ) shutil.rmtree(dit_path) tasks.process_project.apply_async(kwargs={"pk": project.pk}, countdown=3) diff --git a/image_markuper/dicom/signals.py b/image_markuper/dicom/signals.py index 47305e8..747ca9b 100644 --- a/image_markuper/dicom/signals.py +++ b/image_markuper/dicom/signals.py @@ -1,4 +1,4 @@ -from dicom.models import Dicom, Project +from dicom.models import Dicom, Layer, Project from django.db.models.signals import post_save from django.dispatch import receiver from utils.generators import generate_charset @@ -22,3 +22,14 @@ def create_dicom(sender, instance: Dicom, created, **kwargs): slug = generate_charset(5) instance.slug = slug instance.save() + Layer.objects.create(parent=None, dicom=instance, name="root") + + +@receiver(post_save, sender=Layer) +def create_layer(sender, instance: Layer, created, **kwargs): + if created: + slug = generate_charset(8) + while Layer.objects.filter(slug=slug): + slug = generate_charset(8) + instance.slug = slug + instance.save()