basic support for bulk series uploading
This commit is contained in:
@@ -13,6 +13,7 @@ from .models import (
|
|||||||
Structure,
|
Structure,
|
||||||
PathologicalProcess,
|
PathologicalProcess,
|
||||||
Presentation,
|
Presentation,
|
||||||
|
UncategorisedDicom,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
@@ -38,6 +39,7 @@ admin.site.register(Differential)
|
|||||||
admin.site.register(PathologicalProcess)
|
admin.site.register(PathologicalProcess)
|
||||||
admin.site.register(Presentation)
|
admin.site.register(Presentation)
|
||||||
admin.site.register(CaseDetail)
|
admin.site.register(CaseDetail)
|
||||||
|
admin.site.register(UncategorisedDicom)
|
||||||
|
|
||||||
|
|
||||||
class DifferentialInline(admin.TabularInline):
|
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,
|
Condition,
|
||||||
Structure,
|
Structure,
|
||||||
Subspecialty,
|
Subspecialty,
|
||||||
|
UncategorisedDicom,
|
||||||
UserReportAnswer,
|
UserReportAnswer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -589,4 +590,13 @@ class SelfReviewForm(ModelForm):
|
|||||||
case_id = kwargs.pop("case_id")
|
case_id = kwargs.pop("case_id")
|
||||||
|
|
||||||
ModelForm.__init__(self, *args, **kwargs)
|
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
|
import string
|
||||||
from collections import defaultdict
|
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 (
|
from generic.models import (
|
||||||
@@ -452,6 +452,7 @@ class Series(SeriesBase):
|
|||||||
related_name="atlas_series_modality",
|
related_name="atlas_series_modality",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
examination = models.ForeignKey(
|
examination = models.ForeignKey(
|
||||||
Examination,
|
Examination,
|
||||||
@@ -459,6 +460,7 @@ class Series(SeriesBase):
|
|||||||
related_name="atlas_series_examination",
|
related_name="atlas_series_examination",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
plane = models.ForeignKey(
|
plane = models.ForeignKey(
|
||||||
Plane,
|
Plane,
|
||||||
@@ -483,6 +485,8 @@ class Series(SeriesBase):
|
|||||||
related_name="series",
|
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")
|
# findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack")
|
||||||
|
|
||||||
def get_full_str(self):
|
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)
|
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):
|
def get_dicom_info(self):
|
||||||
try:
|
try:
|
||||||
@@ -837,10 +857,13 @@ class UncategorisedDicom(models.Model):
|
|||||||
|
|
||||||
for tag in to_include:
|
for tag in to_include:
|
||||||
if tag in ds:
|
if tag in ds:
|
||||||
tags[tag] = ds[tag]
|
tags[tag] = ds[tag].value
|
||||||
else:
|
else:
|
||||||
tags[tag] = ""
|
tags[tag] = ""
|
||||||
|
|
||||||
return ds
|
return tags
|
||||||
except pydicom.errors.InvalidDicomError:
|
except pydicom.errors.InvalidDicomError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
class DuplicateDicom(Exception):
|
||||||
|
pass
|
||||||
+5
-5
@@ -1882,12 +1882,12 @@ class AddSelfReview(CreateView):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def uncategorised_dicoms(request, pk):
|
def uncategorised_dicoms(request):
|
||||||
dicoms = UncategorisedDicom.objects.filter(user=request.User)
|
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
|
|
||||||
data = {}
|
data = []
|
||||||
for d in dicoms:
|
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):
|
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):
|
def __str__(self):
|
||||||
return self.modality
|
return f"{self.modality} ({self.short_code})"
|
||||||
|
|
||||||
|
|
||||||
class Plane(models.Model):
|
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 anatomy.api import router as anatomy_router
|
||||||
from sbas.api import router as sbas_router
|
from sbas.api import router as sbas_router
|
||||||
from physics.api import router as physics_router
|
from physics.api import router as physics_router
|
||||||
|
from atlas.api import router as atlas_router
|
||||||
|
|
||||||
api = NinjaAPI(csrf=True, version="1")
|
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("longs/", longs_router)
|
||||||
api.add_router("sbas/", sbas_router)
|
api.add_router("sbas/", sbas_router)
|
||||||
api.add_router("physics/", physics_router)
|
api.add_router("physics/", physics_router)
|
||||||
|
api.add_router("atlas/", atlas_router)
|
||||||
|
|
||||||
@api.get("/hello")
|
@api.get("/hello")
|
||||||
def hello(request):
|
def hello(request):
|
||||||
return "Hello world"
|
return "Hello world"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user