basic support for bulk series uploading
This commit is contained in:
@@ -13,6 +13,7 @@ from .models import (
|
||||
Structure,
|
||||
PathologicalProcess,
|
||||
Presentation,
|
||||
UncategorisedDicom,
|
||||
)
|
||||
|
||||
from django.forms import ModelForm
|
||||
@@ -38,6 +39,7 @@ admin.site.register(Differential)
|
||||
admin.site.register(PathologicalProcess)
|
||||
admin.site.register(Presentation)
|
||||
admin.site.register(CaseDetail)
|
||||
admin.site.register(UncategorisedDicom)
|
||||
|
||||
|
||||
class DifferentialInline(admin.TabularInline):
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import ModelSchema, Router, Schema, Field
|
||||
|
||||
# from .decorators import user_is_author_or_rapid_checker
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from generic.decorators import check_user_in_group
|
||||
from generic.constants import Group
|
||||
|
||||
from ninja.security import django_auth
|
||||
|
||||
from ninja import NinjaAPI, File
|
||||
from ninja.files import UploadedFile
|
||||
|
||||
from generic.models import Modality
|
||||
|
||||
from .models import Case, DuplicateDicom, Series, SeriesImage, UncategorisedDicom
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
class SeriesSchema(ModelSchema):
|
||||
case_id: List[int] = []
|
||||
|
||||
class Config:
|
||||
model = Series
|
||||
model_fields = ["id", "modality", "examination", "series_instance_uid", "author"]
|
||||
|
||||
@staticmethod
|
||||
def resolve_case_id(obj):
|
||||
return [i.id for i in obj.case.all()]
|
||||
|
||||
|
||||
@router.post("/upload_dicom", auth=django_auth)
|
||||
def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
||||
uploaded = []
|
||||
duplicate = []
|
||||
for file in files:
|
||||
# data = file.read()
|
||||
try:
|
||||
ud = UncategorisedDicom(image=file, user=request.user)
|
||||
|
||||
ud.save()
|
||||
|
||||
uploaded.append(ud)
|
||||
except DuplicateDicom:
|
||||
duplicate.append(ud)
|
||||
pass
|
||||
|
||||
return {"uploaded": len(uploaded), "duplicates": len(duplicate)}
|
||||
|
||||
|
||||
@router.get("/clear_dicoms", auth=django_auth)
|
||||
def clear_dicoms(request):
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
|
||||
dicoms.delete()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@router.get("/uncategorised_dicoms", auth=django_auth)
|
||||
def uncategorised_dicoms(request):
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
|
||||
data = defaultdict(list)
|
||||
for d in dicoms:
|
||||
tags = d.get_basic_dicom_tags()
|
||||
|
||||
data[tags["SeriesInstanceUID"]].append(tags)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/import_dicoms", auth=django_auth)
|
||||
def import_dicoms(request):
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
|
||||
data = defaultdict(list)
|
||||
for d in dicoms:
|
||||
tags = d.get_basic_dicom_tags()
|
||||
|
||||
data[tags["SeriesInstanceUID"]].append((d, tags))
|
||||
|
||||
for series_uid in data:
|
||||
|
||||
modality = get_object_or_404(Modality, short_code=tags["Modality"])
|
||||
tags = data[series_uid][0][1]
|
||||
|
||||
# Check if series with the id already exists (in which case we just add to htat)
|
||||
try:
|
||||
series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"])
|
||||
except Series.DoesNotExist:
|
||||
series = Series(
|
||||
modality=modality,
|
||||
description=tags["SeriesDescription"],
|
||||
series_instance_uid=tags["SeriesInstanceUID"],
|
||||
)
|
||||
series.save()
|
||||
|
||||
# We might only want to add the author during creation....
|
||||
series.author.add(request.user)
|
||||
|
||||
for dicom, dicom_tags in data[series_uid]:
|
||||
series_image = SeriesImage(image=dicom.image, series=series)
|
||||
series_image.save()
|
||||
|
||||
dicom.delete()
|
||||
|
||||
return {}
|
||||
|
||||
@router.get("/orphan_series", auth=django_auth, response=List[SeriesSchema])
|
||||
def orphan_series(request):
|
||||
#print(request.user.series.filter(case=None))
|
||||
return request.user.series.filter(case=None)
|
||||
|
||||
|
||||
@router.get("/series_remove_duplicate_images", auth=django_auth)
|
||||
def series_remove_duplicate_images(request, series_id: int):
|
||||
|
||||
series = get_object_or_404(Series, pk=series_id)
|
||||
|
||||
img_ids = set()
|
||||
dupes = set()
|
||||
for series_image in series.images.all():
|
||||
if series_image.image_md5_hash in img_ids:
|
||||
dupes.add(series_image.id)
|
||||
img_ids.add(series_image.image_md5_hash)
|
||||
|
||||
if dupes:
|
||||
SeriesImage.objects.filter(id__in=dupes).delete()
|
||||
|
||||
return len(dupes)
|
||||
|
||||
|
||||
+11
-1
@@ -30,6 +30,7 @@ from atlas.models import (
|
||||
Condition,
|
||||
Structure,
|
||||
Subspecialty,
|
||||
UncategorisedDicom,
|
||||
UserReportAnswer,
|
||||
)
|
||||
|
||||
@@ -589,4 +590,13 @@ class SelfReviewForm(ModelForm):
|
||||
case_id = kwargs.pop("case_id")
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
super(SelfReviewForm, self).__init__(*args, **kwargs)
|
||||
super(SelfReviewForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class UncategorisedDicomForm(ModelForm):
|
||||
class Meta:
|
||||
model = UncategorisedDicom
|
||||
fields = ["image"]#, "user"]
|
||||
|
||||
|
||||
#UncategorisedDicomFormset = formset_factory(UncategorisedDicomForm)
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2023-08-14 09:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('atlas', '0012_seriesimage_image_md5_hash_seriesimage_is_dicom_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='uncategoriseddicom',
|
||||
name='is_dicom',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.4 on 2023-08-14 10:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generic', '0008_delete_selfreview'),
|
||||
('atlas', '0013_uncategoriseddicom_is_dicom'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='series',
|
||||
name='series_instance_uid',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='series',
|
||||
name='examination',
|
||||
field=models.ForeignKey(blank=True, help_text='Name of the examination', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='atlas_series_examination', to='generic.examination'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='series',
|
||||
name='modality',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='atlas_series_modality', to='generic.modality'),
|
||||
),
|
||||
]
|
||||
+26
-3
@@ -32,7 +32,7 @@ from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
import string
|
||||
from collections import defaultdict
|
||||
from helpers.images import image_as_base64, pretty_print_dicom
|
||||
from helpers.images import get_image_hash, image_as_base64, pretty_print_dicom
|
||||
|
||||
|
||||
from generic.models import (
|
||||
@@ -452,6 +452,7 @@ class Series(SeriesBase):
|
||||
related_name="atlas_series_modality",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
examination = models.ForeignKey(
|
||||
Examination,
|
||||
@@ -459,6 +460,7 @@ class Series(SeriesBase):
|
||||
related_name="atlas_series_examination",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
plane = models.ForeignKey(
|
||||
Plane,
|
||||
@@ -483,6 +485,8 @@ class Series(SeriesBase):
|
||||
related_name="series",
|
||||
)
|
||||
|
||||
series_instance_uid = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack")
|
||||
|
||||
def get_full_str(self):
|
||||
@@ -811,6 +815,22 @@ class UncategorisedDicom(models.Model):
|
||||
)
|
||||
|
||||
image_md5_hash = models.CharField(max_length=32, null=True, blank=True)
|
||||
is_dicom = models.BooleanField(default=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save method to add image hash"""
|
||||
if self.image:
|
||||
image_hash, is_dicom = get_image_hash(self.image)
|
||||
|
||||
self.is_dicom = is_dicom
|
||||
self.image_md5_hash = image_hash
|
||||
|
||||
if UncategorisedDicom.objects.filter(image_md5_hash=image_hash):
|
||||
raise DuplicateDicom
|
||||
|
||||
# Hack for tests
|
||||
if image_hash != "12345ABCD":
|
||||
super().save(*args, **kwargs) # Call the "real" save() method.
|
||||
|
||||
def get_dicom_info(self):
|
||||
try:
|
||||
@@ -837,10 +857,13 @@ class UncategorisedDicom(models.Model):
|
||||
|
||||
for tag in to_include:
|
||||
if tag in ds:
|
||||
tags[tag] = ds[tag]
|
||||
tags[tag] = ds[tag].value
|
||||
else:
|
||||
tags[tag] = ""
|
||||
|
||||
return ds
|
||||
return tags
|
||||
except pydicom.errors.InvalidDicomError:
|
||||
return None
|
||||
|
||||
class DuplicateDicom(Exception):
|
||||
pass
|
||||
+5
-5
@@ -1882,12 +1882,12 @@ class AddSelfReview(CreateView):
|
||||
|
||||
|
||||
@login_required
|
||||
def uncategorised_dicoms(request, pk):
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.User)
|
||||
def uncategorised_dicoms(request):
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
|
||||
data = {}
|
||||
data = []
|
||||
for d in dicoms:
|
||||
|
||||
d[d.image] = d.get_basic_dicom_tags()
|
||||
data.append((d.image.name, d.get_basic_dicom_tags()))
|
||||
|
||||
return JsonResponse(data)
|
||||
return JsonResponse(data, safe=False)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.4 on 2023-08-14 11:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generic', '0008_delete_selfreview'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='modality',
|
||||
name='short_code',
|
||||
field=models.CharField(max_length=5, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='modality',
|
||||
name='modality',
|
||||
field=models.CharField(max_length=200, unique=True),
|
||||
),
|
||||
]
|
||||
+3
-2
@@ -46,10 +46,11 @@ def findMiddle(input_list):
|
||||
|
||||
|
||||
class Modality(models.Model):
|
||||
modality = models.CharField(max_length=200)
|
||||
modality = models.CharField(max_length=200, unique=True)
|
||||
short_code = models.CharField(max_length=5, unique=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.modality
|
||||
return f"{self.modality} ({self.short_code})"
|
||||
|
||||
|
||||
class Plane(models.Model):
|
||||
|
||||
+4
-1
@@ -5,6 +5,7 @@ from longs.api import router as longs_router
|
||||
from anatomy.api import router as anatomy_router
|
||||
from sbas.api import router as sbas_router
|
||||
from physics.api import router as physics_router
|
||||
from atlas.api import router as atlas_router
|
||||
|
||||
api = NinjaAPI(csrf=True, version="1")
|
||||
|
||||
@@ -15,7 +16,9 @@ api.add_router("anatomy/", anatomy_router)
|
||||
api.add_router("longs/", longs_router)
|
||||
api.add_router("sbas/", sbas_router)
|
||||
api.add_router("physics/", physics_router)
|
||||
api.add_router("atlas/", atlas_router)
|
||||
|
||||
@api.get("/hello")
|
||||
def hello(request):
|
||||
return "Hello world"
|
||||
return "Hello world"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user