import json from rad.settings import REMOTE_URL from django.db.models.fields.files import ImageField from django.db.models.fields.related import ForeignKey from django.db import models from django.shortcuts import get_object_or_404 from django.utils import timezone import tagulous import tagulous.models from django.core.files.storage import FileSystemStorage from django.conf import settings from django.utils.html import format_html from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.utils.html import mark_safe from django.core.exceptions import ValidationError from sortedm2m.fields import SortedManyToManyField import string from collections import defaultdict from helpers.images import image_as_base64, pretty_print_dicom from anatomy.models import Modality from generic.models import ( Examination, # Condition, ExamBase, Plane, Contrast, QuestionNote, ) # from generic.models import Examination, Site, Condition, Sign from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.exceptions import InvalidImageFormatError import pydicom import pydicom.errors import datetime from django.utils import timezone import reversion from django.contrib.contenttypes.fields import GenericRelation image_storage = FileSystemStorage( # Physical file location ROOT location="{0}atlas/".format(settings.MEDIA_ROOT), # Url for file base_url="{0}atlas/".format(settings.MEDIA_URL), ) def image_directory_path(instance, filename): return "atlas/picture/{0}".format(filename) def findMiddle(input_list): middle = float(len(input_list)) / 2 if middle % 2 != 0: return input_list[int(middle - 0.5)] else: return input_list[int(middle)] return (input_list[int(middle)], input_list[int(middle - 1)]) class SynMixin(object): # class Meta: # abstract = True def __str__(self) -> str: if self.primary: if self.synonym.count() == 0: return self.name else: synonyms = ",".join([i.name for i in self.synonym.all()]) return f"{self.name} ({synonyms})" else: return f"{self.name} [syn]" def get_synonym(self): if self.primary: return "[Primary]" else: s = self.synonym.filter(primary=True).values_list("name", flat=True) return ", ".join(s) def get_synonym_link(self): if self.primary: return "[Primary]" else: syns = self.synonym.filter(primary=True) return ", ".join( [f"{s.name}" for s in syns] ) def get_link(self): return format_html("{}", self.get_absolute_url(), self.name) class Finding(SynMixin, models.Model): name = models.CharField(max_length=255, unique=True) synonym = models.ManyToManyField("self", blank=True) primary = models.BooleanField(default="True") def get_absolute_url(self): return reverse("atlas:finding_detail", kwargs={"pk": self.pk}) class Condition(SynMixin, models.Model): name = models.CharField(max_length=255, unique=True) synonym = models.ManyToManyField("self", blank=True) parent = models.ManyToManyField("self", blank=True) primary = models.BooleanField(default="True") subspecialty = models.ManyToManyField("subspecialty", blank=True) def get_absolute_url(self): return reverse("atlas:condition_detail", kwargs={"pk": self.pk}) class Presentation(models.Model): name = models.CharField(max_length=255) subspecialty = models.ManyToManyField("subspecialty", blank=True) def __str__(self) -> str: if self.subspecialty: return ( f"{self.name} ({', '.join([str(i) for i in self.subspecialty.all()])})" ) return self.name def get_absolute_url(self): return reverse("atlas:presentation_detail", kwargs={"pk": self.pk}) def get_link(self): return format_html("{}", self.get_absolute_url(), self.name) class Subspecialty(models.Model): name = models.CharField(max_length=255) def __str__(self) -> str: return self.name def get_absolute_url(self): return reverse("atlas:subspecialty_detail", kwargs={"pk": self.pk}) def get_link(self): return format_html("{}", self.get_absolute_url(), self.name) class PathologicalProcess(models.Model): name = models.CharField(max_length=255) def __str__(self) -> str: return self.name def get_absolute_url(self): return reverse("atlas:pathological_process_detail", kwargs={"pk": self.pk}) def get_link(self): return format_html("{}", self.get_absolute_url(), self.name) class Differential(models.Model): condition = models.ForeignKey(Condition, on_delete=models.CASCADE) case = models.ForeignKey( "Case", on_delete=models.CASCADE, related_name="differentialcase" ) text = models.TextField(null=True, blank=True) class Structure(SynMixin, models.Model): name = models.CharField(max_length=255, unique=True) synonym = models.ManyToManyField("self", blank=True) primary = models.BooleanField(default="True") def get_absolute_url(self): return reverse("atlas:structure_detail", kwargs={"pk": self.pk}) @reversion.register class Case(models.Model): # class SubspecialtyChoices(models.TextChoices): # BREAST = "BR", _("Breast") # CARDIAC = "CA", _("Cardiac") # GASTRO = "GI", _("Gastrointestinal and hepatobiliary") # HEADNECK = "HN", _("Head and Neck") # MSK = "MS", _("Musculoskeletal") # NEURO = "NE", _("Neuroradiology") # OBSGYN = "OG", _("Obstectric and Gynaecological") # PAED = "PA", _("Paediatric") # URO = "UR", _("Uroradiology") # VASC = "VA", _("Vascular") # HAEMONC = "HA", _("Haemotology and Oncology") title = models.CharField(max_length=255, help_text="Title of the case", default="") # author = models.ForeignKey('auth.User', on_delete=models.CASCADE) # image = models.ImageField() description = models.TextField( blank=True, help_text="Description of the case", ) history = models.TextField(null=True, blank=True) discussion = models.TextField(null=True, blank=True) # findings = models.TextField(null=True, blank=True) # subspecialty = models.CharField(max_length=2, choices=SubspecialtyChoices.choices) subspecialty = models.ManyToManyField(Subspecialty, blank=True) condition = models.ManyToManyField(Condition, blank=True) presentation = models.ManyToManyField(Presentation, blank=True) pathological_process = models.ManyToManyField(PathologicalProcess, blank=True) differential = models.ManyToManyField( Condition, through=Differential, related_name="casedifferential" ) verified = models.BooleanField(default=False) created_date = models.DateTimeField(default=timezone.now) published_date = models.DateTimeField(blank=True, null=True) author = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, help_text="Author of question", related_name="atlas_authored_questions", ) scrapped = models.BooleanField( default=False, help_text="Question has been scrapped and will not be shown" ) open_access = models.BooleanField( help_text="If a question should be freely available to browse", default=True ) series = SortedManyToManyField("Series", related_name="case") notes = GenericRelation(QuestionNote) # question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) def get_absolute_url(self): return reverse("atlas:case_detail", kwargs={"pk": self.pk}) def get_authors(self): """Returns a comma seperated text list of authors""" authors = ", ".join([i.username for i in self.author.all()]) return authors def get_author_objects(self): """Returns a comma seperated text list of authors""" authors = [i for i in self.author.all()] return authors def get_link(self): return format_html("{}", self.get_absolute_url(), str(self)) def __str__(self): examinations = [series.get_examination() for series in self.series.all()] return f"{self.pk}:{self.title} {'/'.join([str(c) for c in self.condition.all()])} [{', '.join(examinations)}]" class SeriesImage(models.Model): image = models.FileField(upload_to=image_directory_path) position = models.IntegerField(default=0) upload_filename = models.CharField(max_length=255, blank=True) series = models.ForeignKey( "Series", related_name="images", on_delete=models.SET_NULL, null=True ) class Meta: ordering = ["position"] def get_dicom_info(self): try: info = pretty_print_dicom(pydicom.read_file(self.image)) except pydicom.errors.InvalidDicomError: info = "File is not a dicom." return info class SeriesFinding(models.Model): series = models.ForeignKey( "Series", related_name="findings", on_delete=models.SET_NULL, null=True ) description = models.TextField( null=True, blank=True, help_text="Findings on the series / stack" ) findings = models.ManyToManyField(Finding, blank=True) structures = models.ManyToManyField(Structure, blank=True) annotation_json = models.TextField(null=True, blank=True) viewport_json = models.TextField(null=True, blank=True) def __str__(self) -> str: findings = self.findings.all().values_list("name") return f"{self.series.id}/{findings}/{self.description}" def annotation_as_string(self) -> str: return json.dumps(self.annotation_json) @reversion.register class Series(models.Model): modality = models.ForeignKey( Modality, related_name="atlas_series_modality", on_delete=models.SET_NULL, null=True, ) examination = models.ForeignKey( Examination, help_text="Name of the examination", related_name="atlas_series_examination", on_delete=models.SET_NULL, null=True, ) plane = models.ForeignKey( Plane, help_text="Plane of the examination", related_name="atlas_series_plane", on_delete=models.SET_NULL, null=True, blank=True, ) contrast = models.ForeignKey( Contrast, help_text="MRI / CT contrast", related_name="atlas_series_contrast", on_delete=models.SET_NULL, null=True, blank=True, ) description = models.TextField( blank=True, help_text="Description of stack", ) author = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="series", ) # findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack") open_access = models.BooleanField( help_text="If a question should be freely available to browse", default=True ) def __str__(self): if self.case: case_id = ", ".format([case.pk for case in self.case.all()]) # case_id = self.case.pk else: case_id = "None" return "{}/{} : {} [{}]".format( self.pk, self.get_examination_full(), self.description, case_id ) def get_absolute_url(self): return reverse("series:question_detail", kwargs={"pk": self.pk}) # def get_string(self): # examination = self.get_examination_full() # return def get_author_objects(self): """Returns a comma seperated text list of authors""" if self.author: return self.author.all() else: return ["None"] def get_author_display(self): return ", ".join([i.username for i in self.get_author_objects()]) def get_examination(self): """Returns a comma seperated text list of regions""" return str(self.examination) def get_examination_full(self): examination = "" plane = "" contrast = "" if self.examination: examination = self.examination if self.plane: plane = " {}".format(self.plane) if self.contrast: contrast = " {}".format(self.contrast) return "{}{}{}".format(examination, plane, contrast) def get_absolute_url(self): return reverse("atlas:series_detail", kwargs={"pk": self.pk}) def get_image_urls(self, findings=False): images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()] return ",".join(images) def get_image_url_array(self, findings=False): images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()] return json.dumps(images) def get_thumbnail(self): images = self.images.all() if len(images) < 1: return "No images", 0 img = findMiddle(images).image try: thumbnailer = get_thumbnailer(img) thumbnail = thumbnailer["exam-list"] except InvalidImageFormatError: return format_html('Invalid image url', img), len( images ) return format_html('', thumbnail), len(images) def get_thumbnail_link(self): return format_html( "{}", self.get_absolute_url(), self.get_thumbnail()[0] ) def get_block(self): examination = self.get_examination_full() thumb, image_number = self.get_thumbnail() return format_html( "
{}
{}
Images: {}
", examination, thumb, image_number ) def order_by_upload_filename(self): images = self.images.all() filenames = [] map = {} for i in images: filenames.append(i.upload_filename) map[i.upload_filename] = i filenames = sorted(filenames) n = 1 for f in filenames: i = map[f] i.position = n i.save() n = n + 1 def order_by_dicom(self, field="SliceLocation"): images = self.images.all() files = [] for i in images: files.append((i, pydicom.dcmread(i.image.path))) # print("file count: {}".format(len(files))) # skip files with no SliceLocation (eg scout views) slices = [] map = {} skipcount = 0 for i, f in files: if hasattr(f, field): slices.append(f) # map[f.SliceLocation] = i map[f[field].value] = i else: skipcount = skipcount + 1 print("skipped, no {}: {}".format(field, skipcount)) # ensure they are in the correct order slices = sorted(slices, key=lambda s: s[field].value) # print(slices) n = 1 for f in slices: i = map[f[field].value] i.position = n i.save() n = n + 1 def get_total_image_size(self): images = self.images.all() size = 0 for i in images: size += i.image.size return size