added layers

This commit is contained in:
Alexander-D-Karpov 2022-11-06 03:35:27 +03:00
parent 23db12cf8a
commit 9019d31d48
7 changed files with 195 additions and 62 deletions

View File

@ -2,6 +2,7 @@ from dicom.api.views import (
AddDicomProjectApi, AddDicomProjectApi,
CreateCircleApi, CreateCircleApi,
CreateFreeHandApi, CreateFreeHandApi,
CreateLayerApi,
CreateRoiApi, CreateRoiApi,
CreateRulerApi, CreateRulerApi,
DeleteDicomProjectApi, DeleteDicomProjectApi,
@ -12,6 +13,7 @@ from dicom.api.views import (
RetrieveUpdateDeleteCircleApi, RetrieveUpdateDeleteCircleApi,
RetrieveUpdateDeleteDicomApi, RetrieveUpdateDeleteDicomApi,
RetrieveUpdateDeleteFreeHandApi, RetrieveUpdateDeleteFreeHandApi,
RetrieveUpdateDeleteLayerApi,
RetrieveUpdateDeleteProjectApi, RetrieveUpdateDeleteProjectApi,
RetrieveUpdateDeleteRoiApi, RetrieveUpdateDeleteRoiApi,
RetrieveUpdateDeleteRulerApi, RetrieveUpdateDeleteRulerApi,
@ -129,4 +131,17 @@ urlpatterns = [
] ]
), ),
), ),
path(
"layer/<str:dicom_slug>/",
include(
[
path("", CreateLayerApi.as_view(), name="create_layer"),
path(
"<str:slug>",
RetrieveUpdateDeleteLayerApi.as_view(),
name="get_update_delete_project",
),
]
),
),
] ]

View File

@ -4,6 +4,7 @@ from dicom.models import (
Coordinate, Coordinate,
Dicom, Dicom,
FreeHand, FreeHand,
Layer,
Project, Project,
Roi, Roi,
Ruler, Ruler,
@ -20,6 +21,27 @@ class CoordinateSerializer(serializers.ModelSerializer):
fields = ["x", "y"] 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): class ListDicomSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(
view_name="get_update_delete_dicom", lookup_field="slug" view_name="get_update_delete_dicom", lookup_field="slug"
@ -28,17 +50,25 @@ class ListDicomSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Dicom model = Dicom
fields = ["file", "uploaded", "pathology_type", "url"] fields = ["file", "uploaded", "url"]
def create(self, validated_data): def create(self, validated_data):
return Dicom.objects.create(**validated_data, user=self.context["request"].user) 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): class BaseShapeSerializer(serializers.Serializer):
model = BaseShape model = BaseShape
type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"]) type = serializers.ChoiceField(choices=["circle", "roi", "free_hand", "ruler"])
image_number = serializers.IntegerField() layer = serializers.SlugField(max_length=8, required=False, allow_blank=True)
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
def create(self, validated_data): def create(self, validated_data):
@ -51,9 +81,12 @@ class BaseShapeSerializer(serializers.Serializer):
dicom = get_object_or_404( dicom = get_object_or_404(
Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"] Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"]
) )
obj = self.model.objects.create( if validated_data["layer"]:
dicom=dicom, image_number=validated_data["image_number"] 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) create_coordinate(validated_data["coordinates"], obj)
return obj return obj
@ -66,65 +99,121 @@ class BaseShapeSerializer(serializers.Serializer):
if self.model.min_coordinates: if self.model.min_coordinates:
if len(validated_data["coordinates"]) < self.model.min_coordinates: if len(validated_data["coordinates"]) < self.model.min_coordinates:
raise serializers.ValidationError 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) create_coordinate(validated_data["coordinates"], obj)
return obj return obj
class BaseShapeLayerSerializer(serializers.Serializer): 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) radius = serializers.FloatField(required=False)
coordinates = CoordinateSerializer(many=True) 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): class DicomSerializer(serializers.ModelSerializer):
file = serializers.FileField() file = serializers.FileField()
shapes = serializers.SerializerMethodField("get_dicom_shapes") 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): def get_dicom_shapes(self, obj):
return [x.serialize_self() for x in obj.shapes.all()] 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: class Meta:
model = Dicom model = Dicom
fields = ["file", "uploaded", "pathology_type", "shapes"] fields = ["file", "uploaded", "shapes", "layers"]
class RoiSerializer(BaseShapeSerializer, serializers.ModelSerializer): class RoiSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
layer = serializers.SlugField(max_length=8, required=False, allow_blank=True)
model = Roi model = Roi
class Meta: class Meta:
model = Roi model = Roi
fields = ["id", "image_number", "coordinates"] fields = ["id", "layer", "coordinates"]
extra_kwargs = {"id": {"read_only": True}} extra_kwargs = {"id": {"read_only": True}}
class FreeHandSerializer(BaseShapeSerializer, serializers.ModelSerializer): class FreeHandSerializer(BaseShapeSerializer, serializers.ModelSerializer):
layer = serializers.SlugField(max_length=8, required=False, allow_blank=True)
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
model = FreeHand model = FreeHand
class Meta: class Meta:
model = FreeHand model = FreeHand
fields = ["id", "image_number", "coordinates"] fields = ["id", "layer", "coordinates"]
extra_kwargs = {"id": {"read_only": True}} extra_kwargs = {"id": {"read_only": True}}
class RulerSerializer(BaseShapeSerializer, serializers.ModelSerializer): class RulerSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
layer = serializers.SlugField(max_length=8, required=False, allow_blank=True)
model = Ruler model = Ruler
class Meta: class Meta:
model = FreeHand model = FreeHand
fields = ["id", "image_number", "coordinates"] fields = ["id", "layer", "coordinates"]
extra_kwargs = {"id": {"read_only": True}} extra_kwargs = {"id": {"read_only": True}}
class CircleSerializer(serializers.ModelSerializer): class CircleSerializer(serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True) coordinates = CoordinateSerializer(many=True)
layer = serializers.SlugField(max_length=8, required=False, allow_blank=True)
class Meta: class Meta:
model = Circle model = Circle
fields = ["id", "image_number", "radius", "coordinates"] fields = ["id", "layer", "radius", "coordinates"]
extra_kwargs = {"id": {"read_only": True}} extra_kwargs = {"id": {"read_only": True}}
def create(self, validated_data): def create(self, validated_data):
@ -136,9 +225,12 @@ class CircleSerializer(serializers.ModelSerializer):
dicom = get_object_or_404( dicom = get_object_or_404(
Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"] 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( circle = Circle.objects.create(
dicom=dicom, layer_fk=layer,
image_number=validated_data["image_number"],
radius=validated_data["radius"], radius=validated_data["radius"],
) )
@ -148,8 +240,13 @@ class CircleSerializer(serializers.ModelSerializer):
def update(self, obj: Circle, validated_data): def update(self, obj: Circle, validated_data):
Coordinate.objects.filter(shape=obj).delete() Coordinate.objects.filter(shape=obj).delete()
create_coordinate(validated_data["coordinates"], obj) 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: if "radius" in validated_data:
obj.radius = validated_data["radius"] obj.radius = validated_data["radius"]
obj.layer_fk = layer
obj.save() obj.save()
return obj return obj
@ -158,31 +255,6 @@ 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 = ["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): class PatologyGenerateSerializer(serializers.Serializer):
project_slug = serializers.CharField() project_slug = serializers.CharField()
points = serializers.ListField(child=CoordinateSerializer()) points = serializers.ListField(child=CoordinateSerializer())

View File

@ -5,7 +5,7 @@ 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, Project, Roi from ..models import Circle, Dicom, Layer, Project, Roi
from ..services import process_files from ..services import process_files
from .serializers import ( from .serializers import (
BaseShapeLayerSerializer, BaseShapeLayerSerializer,
@ -13,6 +13,7 @@ from .serializers import (
CircleSerializer, CircleSerializer,
DicomSerializer, DicomSerializer,
FreeHandSerializer, FreeHandSerializer,
LayerSerializer,
ListDicomSerializer, ListDicomSerializer,
ListProjectSerializer, ListProjectSerializer,
PatologyGenerateSerializer, PatologyGenerateSerializer,
@ -29,12 +30,12 @@ class ListCreateDicomApi(generics.ListCreateAPIView):
parser_classes = [MultiPartParser, FormParser] parser_classes = [MultiPartParser, FormParser]
def get_queryset(self): def get_queryset(self):
return Dicom.objects.filter(user=self.request.user) return Dicom.objects.all()
class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView): class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self): def get_queryset(self):
return Dicom.objects.filter(user=self.request.user) return Dicom.objects.all()
serializer_class = DicomSerializer serializer_class = DicomSerializer
parser_classes = [MultiPartParser, FormParser] parser_classes = [MultiPartParser, FormParser]
@ -211,6 +212,23 @@ class ListCreateProjectApi(generics.ListCreateAPIView):
def get_queryset(self): def get_queryset(self):
return Project.objects.filter(user=self.request.user) 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): class RetrieveUpdateDeleteProjectApi(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
@ -226,3 +244,14 @@ class GeneratePatology(generics.CreateAPIView):
# data = self.get_serializer(request.data).data # data = self.get_serializer(request.data).data
# bbox = get_bbox(data["project_slug"], data["points"], data["depth"]) # bbox = get_bbox(data["project_slug"], data["points"], data["depth"])
return Response(data={}, status=200) 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"

View File

@ -1,3 +1,4 @@
from dicom.models.shapes import BaseShape
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -75,6 +76,13 @@ class Dicom(models.Model):
def __str__(self): def __str__(self):
return self.file.name 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): def get_absolute_url(self):
return reverse("get_update_delete_dicom", kwargs={"slug": self.slug}) return reverse("get_update_delete_dicom", kwargs={"slug": self.slug})
@ -87,5 +95,12 @@ class Layer(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(max_length=8) 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): def __str__(self):
return f"layer on {self.dicom}" return f"layer on {self.dicom}"

View File

@ -1,4 +1,3 @@
from dicom.models import Layer
from django.db import models from django.db import models
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
@ -7,14 +6,16 @@ class BaseShape(PolymorphicModel):
TYPE = "no_type" TYPE = "no_type"
min_coordinates = None min_coordinates = None
max_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): def serialize_self(self):
return { return {
"id": self.id, "id": self.id,
"type": self.TYPE, "type": self.TYPE,
"image_number": self.image_number,
"coordinates": self.coordinates, "coordinates": self.coordinates,
"layer": self.layer,
} }
def serialize_self_without_layer(self): def serialize_self_without_layer(self):
@ -24,12 +25,16 @@ class BaseShape(PolymorphicModel):
"coordinates": self.coordinates, "coordinates": self.coordinates,
} }
@property
def layer(self):
return self.layer_fk.slug
@property @property
def coordinates(self) -> [(int, int)]: def coordinates(self) -> [(int, int)]:
return self.shape_coordinates.all().values("x", "y") return self.shape_coordinates.all().values("x", "y")
def __str__(self): def __str__(self):
return self.dicom.file.name return f"{self.TYPE} on {self.layer}"
class Coordinate(models.Model): class Coordinate(models.Model):
@ -53,7 +58,6 @@ class Circle(BaseShape):
return { return {
"id": self.id, "id": self.id,
"type": "circle", "type": "circle",
"image_number": self.image_number,
"radius": self.radius, "radius": self.radius,
"coordinates": self.coordinates, "coordinates": self.coordinates,
} }
@ -66,28 +70,16 @@ class Circle(BaseShape):
"coordinates": self.coordinates, "coordinates": self.coordinates,
} }
def __str__(self):
return f"circle on {self.dicom.file.name}"
class Roi(BaseShape): class Roi(BaseShape):
TYPE = "roi" TYPE = "roi"
def __str__(self):
return f"Roi on {self.dicom.file.name}"
class FreeHand(BaseShape): class FreeHand(BaseShape):
TYPE = "free_hand" TYPE = "free_hand"
def __str__(self):
return f"FreeHand on {self.dicom.file.name}"
class Ruler(BaseShape): class Ruler(BaseShape):
TYPE = "ruler" TYPE = "ruler"
max_coordinates = 2 max_coordinates = 2
min_coordinates = 2 min_coordinates = 2
def __str__(self):
return f"Ruler on {self.dicom.file.name}"

View File

@ -43,7 +43,6 @@ def process_files(
Dicom.objects.create( Dicom.objects.create(
file=File(f, name=file_in_d.split("/")[-1]), file=File(f, name=file_in_d.split("/")[-1]),
project=project, project=project,
user=user,
) )
shutil.rmtree(dit_path) shutil.rmtree(dit_path)
tasks.process_project.apply_async(kwargs={"pk": project.pk}, countdown=3) tasks.process_project.apply_async(kwargs={"pk": project.pk}, countdown=3)

View File

@ -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.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
@ -22,3 +22,14 @@ def create_dicom(sender, instance: Dicom, created, **kwargs):
slug = generate_charset(5) slug = generate_charset(5)
instance.slug = slug instance.slug = slug
instance.save() 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()