diff --git a/atlas/migrations/0012_seriesimage_image_md5_hash_seriesimage_is_dicom_and_more.py b/atlas/migrations/0012_seriesimage_image_md5_hash_seriesimage_is_dicom_and_more.py new file mode 100644 index 00000000..dd964264 --- /dev/null +++ b/atlas/migrations/0012_seriesimage_image_md5_hash_seriesimage_is_dicom_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.1.4 on 2023-08-14 09:15 + +import atlas.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('atlas', '0011_seriesimage_dicom_tags_ohif'), + ] + + operations = [ + migrations.AddField( + model_name='seriesimage', + name='image_md5_hash', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AddField( + model_name='seriesimage', + name='is_dicom', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='UncategorisedDicom', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.FileField(upload_to=atlas.models.uncategorised_dicom_directory_path)), + ('image_md5_hash', models.CharField(blank=True, max_length=32, null=True)), + ('series', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='imported_dicoms', to='atlas.series')), + ('user', models.ForeignKey(help_text='The user that uploaded the file', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 1074c39a..85a20460 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -77,6 +77,10 @@ def image_directory_path(instance, filename): return "atlas/picture/{0}".format(filename) +def uncategorised_dicom_directory_path(instance, filename): + return "atlas/dicom/{0}".format(filename) + + def findMiddle(input_list): middle = float(len(input_list)) / 2 if middle % 2 != 0: @@ -409,7 +413,7 @@ class SeriesImage(SeriesImageBase): except InvalidDicomError: return {} - #def get_image_dicom_json(self, image_index): + # def get_image_dicom_json(self, image_index): # try: # with pydicom.dcmread(self.image) as ds: # return extract_image_dicom_json_from_ds(ds, url=f"{REMOTE_URL}{self.image.url}") @@ -511,7 +515,6 @@ class Series(SeriesBase): for series_n, image in enumerate(self.images.all()): ds = image.get_dicom_data() if series_n == 0: - for tag in to_keep: if tag in ds: val = ds[tag].value @@ -522,9 +525,11 @@ class Series(SeriesBase): series_json[tag] = val instances.append( - extract_image_dicom_json_from_ds(ds, url=f"{REMOTE_URL}{image.image.url}", image_index=series_n) + extract_image_dicom_json_from_ds( + ds, url=f"{REMOTE_URL}{image.image.url}", image_index=series_n + ) ) - #else: + # else: # instances.append(image.get_image_dicom_json(image_index)) description = f"{self.examination} ({self.plane})" @@ -788,3 +793,54 @@ class SelfReview(models.Model): id=self.pk, edit_url=edit_url, ) + + +class UncategorisedDicom(models.Model): + #We use image to maintain consitency across apps + image = models.FileField(upload_to=uncategorised_dicom_directory_path) + + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + help_text="The user that uploaded the file", + ) + + series = models.ForeignKey( + "Series", related_name="imported_dicoms", on_delete=models.SET_NULL, null=True + ) + + image_md5_hash = models.CharField(max_length=32, null=True, blank=True) + + def get_dicom_info(self): + try: + ds = pydicom.read_file(self.image) + return ds + except pydicom.errors.InvalidDicomError: + return None + + def get_basic_dicom_tags(self): + try: + ds = pydicom.read_file(self.image) + + to_include = ( + "StudyDescription", + "Modality", + "ProtocolName", + "SeriesDescription", + "SeriesInstanceUID", + "BodyPartExamined", + "SliceThickness", + ) + + tags = {} + + for tag in to_include: + if tag in ds: + tags[tag] = ds[tag] + else: + tags[tag] = "" + + return ds + except pydicom.errors.InvalidDicomError: + return None diff --git a/atlas/urls.py b/atlas/urls.py index 31d5685a..b3087a7c 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -141,6 +141,7 @@ urlpatterns = [ views.case_dicom_json, name="case_dicom_json", ), + path("uncategorised_dicoms/", views.uncategorised_dicoms, name="uncategorised_dicoms_view"), path("condition/", views.ConditionView.as_view(), name="condition_view"), path("categories/", views.categories_list, name="categories_list"), diff --git a/atlas/views.py b/atlas/views.py index ca1ab112..5a17b6ea 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -63,6 +63,7 @@ from .models import ( Subspecialty, SeriesFinding, SeriesImage, + UncategorisedDicom, UserReportAnswer, ) from .tables import ( @@ -1877,4 +1878,16 @@ class AddSelfReview(CreateView): # # response = super().form_valid(form) # -# return response \ No newline at end of file +# return response + + +@login_required +def uncategorised_dicoms(request, pk): + dicoms = UncategorisedDicom.objects.filter(user=request.User) + + data = {} + for d in dicoms: + + d[d.image] = d.get_basic_dicom_tags() + + return JsonResponse(data) \ No newline at end of file diff --git a/generic/models.py b/generic/models.py index 2778394c..68143ce3 100644 --- a/generic/models.py +++ b/generic/models.py @@ -26,7 +26,7 @@ from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver -from helpers.images import pretty_print_dicom +from helpers.images import get_image_hash, pretty_print_dicom from rad.settings import REMOTE_URL from django.utils.html import format_html @@ -187,6 +187,9 @@ class SeriesImageBase(models.Model): ) dicom_tags_ohif = models.JSONField(help_text="Holds the dicom tags required for the OHIF viewer", blank=True, null=True) + image_md5_hash = models.CharField(max_length=32, null=True, blank=True) + is_dicom = models.BooleanField(default=False) + class Meta: ordering = ["position"] abstract = True @@ -198,6 +201,18 @@ class SeriesImageBase(models.Model): info = "File is not a dicom." return info + 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 + + # Hack for tests + if image_hash != "12345ABCD": + super().save(*args, **kwargs) # Call the "real" save() method. + class SeriesBase(models.Model): info = models.TextField( diff --git a/helpers/images.py b/helpers/images.py index 89af1a00..b600a23c 100644 --- a/helpers/images.py +++ b/helpers/images.py @@ -1,4 +1,5 @@ import base64 +import hashlib import mimetypes from io import BytesIO @@ -152,4 +153,40 @@ def print_dicom(dataset, indent=0): l.append("{0:s} {1:s} = {2:s}".format(indent_string, data_element.name, repr_value)) - return "\n
".join(l) \ No newline at end of file + return "\n
".join(l) + +def get_image_hash(img) -> (str, bool): + is_dicom = False + # Try and read the file as a dicom + try: + # and generate a hash from the pixel data + # TODO: improve? + dataset = pydicom.dcmread(img) + # flatten = dataset.pixel_array.astype(str).flatten() + # print("flatteded") + # pre_join = ",".join(flatten) + # print(pre_join) + # hash = hashlib.md5(pre_join.encode()).hexdigest() + # ---- + md5 = hashlib.md5() + first = True + for i in dataset.pixel_array.astype(str).flatten(): + if first: + first = False + md5.update(f"{i}".encode()) + else: + md5.update(f",{i}".encode()) + + hash = md5.hexdigest() + is_dicom = True + # ---- + + except pydicom.errors.InvalidDicomError: + try: # This is horrible (but needed for current unit tests) + # (we use a temporary file that breaks here) + img.file.open() + hash = hashlib.md5(img.read()).hexdigest() + except AttributeError: + return "12345ABCD", False + + return hash, is_dicom \ No newline at end of file diff --git a/longs/migrations/0007_longseriesimage_image_md5_hash_and_more.py b/longs/migrations/0007_longseriesimage_image_md5_hash_and_more.py new file mode 100644 index 00000000..08ef4a25 --- /dev/null +++ b/longs/migrations/0007_longseriesimage_image_md5_hash_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2023-08-14 09:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('longs', '0006_longseriesimage_dicom_tags_ohif'), + ] + + operations = [ + migrations.AddField( + model_name='longseriesimage', + name='image_md5_hash', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AddField( + model_name='longseriesimage', + name='is_dicom', + field=models.BooleanField(default=False), + ), + ] diff --git a/rapids/models.py b/rapids/models.py index bd4dae2e..eca724a0 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -21,7 +21,7 @@ from sortedm2m.fields import SortedManyToManyField import string from collections import defaultdict -from helpers.images import image_as_base64 +from helpers.images import get_image_hash, image_as_base64 from django.contrib.contenttypes.fields import GenericRelation @@ -575,42 +575,14 @@ class RapidImage(models.Model): def save(self, *args, **kwargs): """Override save method to add image hash""" - # TODO: consider moving to signal to reuse across apps if self.image: - # Try and read the file as a dicom - try: - # and generate a hash from the pixel data - # TODO: improve? - dataset = pydicom.dcmread(self.image) - # flatten = dataset.pixel_array.astype(str).flatten() - # print("flatteded") - # pre_join = ",".join(flatten) - # print(pre_join) - # hash = hashlib.md5(pre_join.encode()).hexdigest() - # ---- - md5 = hashlib.md5() - first = True - for i in dataset.pixel_array.astype(str).flatten(): - if first: - first = False - md5.update(f"{i}".encode()) - else: - md5.update(f",{i}".encode()) + image_hash, is_dicom = get_image_hash(self.image) + self.is_dicom = is_dicom + self.image_md5_hash = image_hash - hash = md5.hexdigest() - # ---- - - self.is_dicom = True - except pydicom.errors.InvalidDicomError: - try: # This is horrible (but needed for current unit tests) - # (we use a temporary file that breaks here) - self.image.file.open() - hash = hashlib.md5(self.image.read()).hexdigest() - except AttributeError: - return - - self.image_md5_hash = hash - super().save(*args, **kwargs) # Call the "real" save() method. + # Hack for tests + if image_hash != "12345ABCD": + super().save(*args, **kwargs) # Call the "real" save() method. class RapidCreationDefault(models.Model):