endoreg-db 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- endoreg_db/data/__init__.py +14 -0
- endoreg_db/data/active_model/data.yaml +3 -0
- endoreg_db/data/center/data.yaml +7 -0
- endoreg_db/data/endoscope_type/data.yaml +11 -0
- endoreg_db/data/endoscopy_processor/data.yaml +45 -0
- endoreg_db/data/examination/examinations/data.yaml +17 -0
- endoreg_db/data/examination/time/data.yaml +48 -0
- endoreg_db/data/examination/time-type/data.yaml +8 -0
- endoreg_db/data/examination/type/data.yaml +5 -0
- endoreg_db/data/information_source/data.yaml +30 -0
- endoreg_db/data/label/label/data.yaml +62 -0
- endoreg_db/data/label/label-set/data.yaml +18 -0
- endoreg_db/data/label/label-type/data.yaml +7 -0
- endoreg_db/data/model_type/data.yaml +7 -0
- endoreg_db/data/profession/data.yaml +70 -0
- endoreg_db/data/unit/data.yaml +17 -0
- endoreg_db/data/unit/length.yaml +31 -0
- endoreg_db/data/unit/volume.yaml +26 -0
- endoreg_db/data/unit/weight.yaml +31 -0
- endoreg_db/forms/__init__.py +2 -0
- endoreg_db/forms/settings/__init__.py +8 -0
- endoreg_db/forms/unit.py +6 -0
- endoreg_db/management/commands/_load_model_template.py +41 -0
- endoreg_db/management/commands/delete_legacy_images.py +19 -0
- endoreg_db/management/commands/delete_legacy_videos.py +17 -0
- endoreg_db/management/commands/extract_legacy_video_frames.py +18 -0
- endoreg_db/management/commands/fetch_legacy_image_dataset.py +32 -0
- endoreg_db/management/commands/import_legacy_images.py +94 -0
- endoreg_db/management/commands/import_legacy_videos.py +76 -0
- endoreg_db/management/commands/load_active_model_data.py +45 -0
- endoreg_db/management/commands/load_ai_model_data.py +45 -0
- endoreg_db/management/commands/load_base_db_data.py +62 -0
- endoreg_db/management/commands/load_center_data.py +43 -0
- endoreg_db/management/commands/load_endoscope_type_data.py +45 -0
- endoreg_db/management/commands/load_endoscopy_processor_data.py +45 -0
- endoreg_db/management/commands/load_examination_data.py +75 -0
- endoreg_db/management/commands/load_information_source.py +45 -0
- endoreg_db/management/commands/load_label_data.py +67 -0
- endoreg_db/management/commands/load_profession_data.py +44 -0
- endoreg_db/management/commands/load_unit_data.py +46 -0
- endoreg_db/management/commands/load_user_groups.py +67 -0
- endoreg_db/management/commands/register_ai_model.py +65 -0
- endoreg_db/migrations/0001_initial.py +582 -0
- endoreg_db/models/__init__.py +53 -0
- endoreg_db/models/ai_model/__init__.py +3 -0
- endoreg_db/models/ai_model/active_model.py +9 -0
- endoreg_db/models/ai_model/model_meta.py +24 -0
- endoreg_db/models/ai_model/model_type.py +26 -0
- endoreg_db/models/ai_model/utils.py +8 -0
- endoreg_db/models/annotation/__init__.py +2 -0
- endoreg_db/models/annotation/binary_classification_annotation_task.py +80 -0
- endoreg_db/models/annotation/image_classification.py +27 -0
- endoreg_db/models/center.py +19 -0
- endoreg_db/models/data_file/__init__.py +4 -0
- endoreg_db/models/data_file/base_classes/__init__.py +3 -0
- endoreg_db/models/data_file/base_classes/abstract_frame.py +51 -0
- endoreg_db/models/data_file/base_classes/abstract_video.py +200 -0
- endoreg_db/models/data_file/frame.py +45 -0
- endoreg_db/models/data_file/report_file.py +88 -0
- endoreg_db/models/data_file/video/__init__.py +7 -0
- endoreg_db/models/data_file/video/import_meta.py +25 -0
- endoreg_db/models/data_file/video/video.py +25 -0
- endoreg_db/models/data_file/video_segment.py +107 -0
- endoreg_db/models/examination/__init__.py +4 -0
- endoreg_db/models/examination/examination.py +26 -0
- endoreg_db/models/examination/examination_time.py +27 -0
- endoreg_db/models/examination/examination_time_type.py +24 -0
- endoreg_db/models/examination/examination_type.py +18 -0
- endoreg_db/models/hardware/__init__.py +2 -0
- endoreg_db/models/hardware/endoscope.py +44 -0
- endoreg_db/models/hardware/endoscopy_processor.py +143 -0
- endoreg_db/models/information_source.py +22 -0
- endoreg_db/models/label/__init__.py +1 -0
- endoreg_db/models/label/label.py +84 -0
- endoreg_db/models/legacy_data/__init__.py +3 -0
- endoreg_db/models/legacy_data/image.py +34 -0
- endoreg_db/models/patient_examination/__init__.py +35 -0
- endoreg_db/models/persons/__init__.py +4 -0
- endoreg_db/models/persons/examiner/__init__.py +2 -0
- endoreg_db/models/persons/examiner/examiner.py +16 -0
- endoreg_db/models/persons/examiner/examiner_type.py +2 -0
- endoreg_db/models/persons/patient.py +58 -0
- endoreg_db/models/persons/person.py +34 -0
- endoreg_db/models/persons/portal_user_information.py +29 -0
- endoreg_db/models/prediction/__init__.py +2 -0
- endoreg_db/models/prediction/image_classification.py +37 -0
- endoreg_db/models/prediction/video_prediction_meta.py +244 -0
- endoreg_db/models/unit.py +20 -0
- endoreg_db/queries/__init__.py +5 -0
- endoreg_db/queries/annotations/__init__.py +3 -0
- endoreg_db/queries/annotations/legacy.py +159 -0
- endoreg_db/queries/get/__init__.py +6 -0
- endoreg_db/queries/get/annotation.py +0 -0
- endoreg_db/queries/get/center.py +42 -0
- endoreg_db/queries/get/model.py +13 -0
- endoreg_db/queries/get/patient.py +14 -0
- endoreg_db/queries/get/patient_examination.py +20 -0
- endoreg_db/queries/get/prediction.py +0 -0
- endoreg_db/queries/get/report_file.py +33 -0
- endoreg_db/queries/get/video.py +31 -0
- endoreg_db/queries/get/video_import_meta.py +0 -0
- endoreg_db/queries/get/video_prediction_meta.py +0 -0
- endoreg_db/queries/sanity/__init_.py +0 -0
- endoreg_db/serializers/__init__.py +10 -0
- endoreg_db/serializers/ai_model.py +19 -0
- endoreg_db/serializers/annotation.py +17 -0
- endoreg_db/serializers/center.py +11 -0
- endoreg_db/serializers/examination.py +33 -0
- endoreg_db/serializers/frame.py +13 -0
- endoreg_db/serializers/hardware.py +21 -0
- endoreg_db/serializers/label.py +22 -0
- endoreg_db/serializers/patient.py +10 -0
- endoreg_db/serializers/prediction.py +15 -0
- endoreg_db/serializers/report_file.py +7 -0
- endoreg_db/serializers/video.py +27 -0
- endoreg_db-0.2.0.dist-info/LICENSE +674 -0
- endoreg_db-0.2.0.dist-info/METADATA +26 -0
- endoreg_db-0.2.0.dist-info/RECORD +126 -0
- endoreg_db-0.1.0.dist-info/METADATA +0 -19
- endoreg_db-0.1.0.dist-info/RECORD +0 -10
- {endoreg_db-0.1.0.dist-info → endoreg_db-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
class ModelMetaManager(models.Manager):
|
|
4
|
+
# natural key is name and version
|
|
5
|
+
def get_by_natural_key(self, name, version):
|
|
6
|
+
return self.get(name=name, version=version)
|
|
7
|
+
|
|
8
|
+
class ModelMeta(models.Model):
|
|
9
|
+
name = models.CharField(max_length=255)
|
|
10
|
+
version = models.CharField(max_length=255)
|
|
11
|
+
type = models.ForeignKey("ModelType", on_delete=models.CASCADE, related_name="models")
|
|
12
|
+
labelset = models.ForeignKey("LabelSet", on_delete=models.CASCADE, related_name="models")
|
|
13
|
+
weights = models.FileField(upload_to='weights/')
|
|
14
|
+
|
|
15
|
+
description = models.TextField(blank=True, null=True)
|
|
16
|
+
date_created = models.DateTimeField(auto_now_add=True)
|
|
17
|
+
objects = ModelMetaManager()
|
|
18
|
+
|
|
19
|
+
def natural_key(self):
|
|
20
|
+
return (self.name, self.version)
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"{self.name} (v: {self.version})"
|
|
24
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.core import serializers
|
|
3
|
+
|
|
4
|
+
class ModelTypeManager(models.Manager):
|
|
5
|
+
def get_by_natural_key(self, name):
|
|
6
|
+
return self.get(name=name)
|
|
7
|
+
|
|
8
|
+
class ModelType(models.Model):
|
|
9
|
+
"""
|
|
10
|
+
A class representing a model type.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name (str): The name of the model type.
|
|
14
|
+
description (str): A description of the model type.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
name = models.CharField(max_length=255)
|
|
18
|
+
description = models.TextField(blank=True, null=True)
|
|
19
|
+
|
|
20
|
+
objects = ModelTypeManager()
|
|
21
|
+
|
|
22
|
+
def natural_key(self):
|
|
23
|
+
return (self.name,)
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.name
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .model_meta import ModelMeta
|
|
2
|
+
|
|
3
|
+
# get latest model meta by model name
|
|
4
|
+
|
|
5
|
+
# TODO MOVE THIS TO A QUERY FILE
|
|
6
|
+
def get_latest_model_meta_by_model_name(model_name):
|
|
7
|
+
model_meta = ModelMeta.objects.filter(name=model_name).order_by('-version').first()
|
|
8
|
+
return model_meta
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
from ..label import Label
|
|
4
|
+
from ..data_file.video_segment import LegacyLabelVideoSegment
|
|
5
|
+
from .image_classification import ImageClassificationAnnotation
|
|
6
|
+
|
|
7
|
+
ANNOTATION_PER_S_THRESHOLD = 2
|
|
8
|
+
|
|
9
|
+
def clear_finished_legacy_tasks():
|
|
10
|
+
# fetch all BinaryClassificationAnnotationTasks that are finished and delete them
|
|
11
|
+
tasks = LegacyBinaryClassificationAnnotationTask.objects.filter(finished=True)
|
|
12
|
+
|
|
13
|
+
# delete tasks with a bulk operation
|
|
14
|
+
tasks.delete()
|
|
15
|
+
|
|
16
|
+
def get_legacy_binary_classification_annotation_tasks_by_label(label:Label, n:int=100, legacy=False):
|
|
17
|
+
clear_finished_legacy_tasks()
|
|
18
|
+
|
|
19
|
+
if legacy:
|
|
20
|
+
# fetch all LegacyLabelVideoSegments with the given label
|
|
21
|
+
_segments = LegacyLabelVideoSegment.objects.filter(label=label)
|
|
22
|
+
frames_for_tasks = []
|
|
23
|
+
|
|
24
|
+
for segment in _segments:
|
|
25
|
+
# check if the segment has already been annotated
|
|
26
|
+
annotations = list(ImageClassificationAnnotation.objects.filter(legacy_frame__in=segment.get_frames(), label=label))
|
|
27
|
+
segment_len_in_s = segment.get_segment_len_in_s()
|
|
28
|
+
|
|
29
|
+
target_annotation_number = segment_len_in_s * ANNOTATION_PER_S_THRESHOLD
|
|
30
|
+
|
|
31
|
+
if len(annotations) < target_annotation_number:
|
|
32
|
+
get_frame_number = int(target_annotation_number - len(annotations))
|
|
33
|
+
frames = segment.get_frames_without_annotation(get_frame_number)
|
|
34
|
+
frames_for_tasks.extend(frames)
|
|
35
|
+
|
|
36
|
+
if len(frames_for_tasks) >= n:
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
# create tasks
|
|
40
|
+
tasks = []
|
|
41
|
+
for frame in frames_for_tasks:
|
|
42
|
+
|
|
43
|
+
# get_or_create task
|
|
44
|
+
task, created = LegacyBinaryClassificationAnnotationTask.objects.get_or_create(
|
|
45
|
+
label=label,
|
|
46
|
+
image_path=frame.image.path,
|
|
47
|
+
frame_id=frame.pk,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AbstractBinaryClassificationAnnotationTask(models.Model):
|
|
52
|
+
label = models.ForeignKey("Label", on_delete=models.CASCADE)
|
|
53
|
+
is_finished = models.BooleanField(default=False)
|
|
54
|
+
date_created = models.DateTimeField(auto_now_add=True)
|
|
55
|
+
date_finished = models.DateTimeField(blank=True, null=True)
|
|
56
|
+
image_path = models.CharField(max_length=255, blank=True, null=True)
|
|
57
|
+
image_type = models.CharField(max_length=255, blank=True, null=True)
|
|
58
|
+
frame_id = models.IntegerField(blank=True, null=True)
|
|
59
|
+
labelstudio_project_id = models.IntegerField(blank=True, null=True)
|
|
60
|
+
labelstudio_task_id = models.IntegerField(blank=True, null=True)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
abstract = True
|
|
64
|
+
|
|
65
|
+
class BinaryClassificationAnnotationTask(AbstractBinaryClassificationAnnotationTask):
|
|
66
|
+
frame = models.ForeignKey("Frame", on_delete=models.CASCADE, related_name="binary_classification_annotation_tasks")
|
|
67
|
+
image_type = models.CharField(max_length=255, default="frame")
|
|
68
|
+
|
|
69
|
+
def get_frame(self):
|
|
70
|
+
return self.video_segment.get_frame_by_id(self.frame_id)
|
|
71
|
+
|
|
72
|
+
class LegacyBinaryClassificationAnnotationTask(AbstractBinaryClassificationAnnotationTask):
|
|
73
|
+
frame = models.ForeignKey("LegacyFrame", on_delete=models.CASCADE, related_name="binary_classification_annotation_tasks")
|
|
74
|
+
image_type = models.CharField(max_length=255, default="legacy")
|
|
75
|
+
|
|
76
|
+
def get_frame(self):
|
|
77
|
+
return self.frame
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
class ImageClassificationAnnotation(models.Model):
|
|
4
|
+
"""
|
|
5
|
+
A class representing an image classification annotation.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
label (str): The label that was assigned to the image.
|
|
9
|
+
confidence (float): The confidence that the label is correct.
|
|
10
|
+
annotator (User): The user that created this annotation.
|
|
11
|
+
date (datetime.datetime): The date and time when this annotation was created.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
# Foreign keys to Frame, LegacyFrame, and LegacyImage (only one of these should be set)
|
|
15
|
+
frame = models.ForeignKey("Frame", on_delete=models.CASCADE, blank=True, null=True, related_name="image_classification_annotations")
|
|
16
|
+
legacy_frame = models.ForeignKey("LegacyFrame", on_delete=models.CASCADE, blank=True, null=True, related_name="image_classification_annotations")
|
|
17
|
+
legacy_image = models.ForeignKey("LegacyImage", on_delete=models.CASCADE, blank=True, null=True, related_name="image_classification_annotations")
|
|
18
|
+
|
|
19
|
+
label = models.ForeignKey("Label", on_delete=models.CASCADE, related_name="image_classification_annotations")
|
|
20
|
+
value = models.BooleanField()
|
|
21
|
+
annotator = models.CharField(max_length=255)
|
|
22
|
+
date_created = models.DateTimeField(auto_now_add=True)
|
|
23
|
+
date_modified = models.DateTimeField(auto_now=True)
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.label.name + " - " + str(self.value)
|
|
27
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
class CenterManager(models.Manager):
|
|
4
|
+
def get_by_natural_key(self, name):
|
|
5
|
+
return self.get(name=name)
|
|
6
|
+
|
|
7
|
+
class Center(models.Model):
|
|
8
|
+
objects = CenterManager()
|
|
9
|
+
|
|
10
|
+
# import_id = models.IntegerField(primary_key=True)
|
|
11
|
+
name = models.CharField(max_length=255)
|
|
12
|
+
name_de = models.CharField(max_length=255, blank=True, null=True)
|
|
13
|
+
name_en = models.CharField(max_length=255, blank=True, null=True)
|
|
14
|
+
|
|
15
|
+
def natural_key(self):
|
|
16
|
+
return (self.name,)
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return self.name
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from endoreg_db.models.annotation.image_classification import ImageClassificationAnnotation
|
|
2
|
+
from endoreg_db.models.label.label import Label
|
|
3
|
+
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractFrame(models.Model):
|
|
8
|
+
video = None # Placeholder for the video field, to be defined in derived classes
|
|
9
|
+
frame_number = models.IntegerField()
|
|
10
|
+
# Add any other fields you need to store frame-related information
|
|
11
|
+
image = models.ImageField(upload_to="frames") # Or some other field type, depending on how you're storing the frame
|
|
12
|
+
suffix = models.CharField(max_length=255)
|
|
13
|
+
# ImageClassificationAnnotation has a foreign key to this model (related name: image_classification_annotations)
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
# Ensure that for each video, the frame_number is unique
|
|
17
|
+
abstract = True
|
|
18
|
+
unique_together = ('video', 'frame_number')
|
|
19
|
+
# Optimize for retrieval in frame_number order
|
|
20
|
+
indexes = [models.Index(fields=['video', 'frame_number'])]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return self.video.file.path + " - " + str(self.frame_number)
|
|
25
|
+
|
|
26
|
+
def get_frame_model(self):
|
|
27
|
+
assert 1 == 2, "This method should be overridden in derived classes"
|
|
28
|
+
|
|
29
|
+
def get_classification_annotations(self):
|
|
30
|
+
"""
|
|
31
|
+
Get all image classification annotations for this frame.
|
|
32
|
+
"""
|
|
33
|
+
return ImageClassificationAnnotation.objects.filter(frame=self)
|
|
34
|
+
|
|
35
|
+
def get_classification_annotations_by_label(self, label:Label):
|
|
36
|
+
"""
|
|
37
|
+
Get all image classification annotations for this frame with the given label.
|
|
38
|
+
"""
|
|
39
|
+
return ImageClassificationAnnotation.objects.filter(frame=self, label=label)
|
|
40
|
+
|
|
41
|
+
def get_classification_annotations_by_value(self, value:bool):
|
|
42
|
+
"""
|
|
43
|
+
Get all image classification annotations for this frame with the given value.
|
|
44
|
+
"""
|
|
45
|
+
return ImageClassificationAnnotation.objects.filter(frame=self, value=value)
|
|
46
|
+
|
|
47
|
+
def get_classification_annotations_by_label_and_value(self, label:Label, value:bool):
|
|
48
|
+
"""
|
|
49
|
+
Get all image classification annotations for this frame with the given label and value.
|
|
50
|
+
"""
|
|
51
|
+
return ImageClassificationAnnotation.objects.filter(frame=self, label=label, value=value)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# import cv2
|
|
2
|
+
from PIL import Image
|
|
3
|
+
from django.core.files.base import ContentFile
|
|
4
|
+
from django.db import models, transaction
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
import io
|
|
7
|
+
from datetime import date
|
|
8
|
+
|
|
9
|
+
BATCH_SIZE = 1000
|
|
10
|
+
|
|
11
|
+
class AbstractVideo(models.Model):
|
|
12
|
+
file = models.FileField(upload_to="raw_videos", blank=True, null=True)
|
|
13
|
+
video_hash = models.CharField(max_length=255, unique=True)
|
|
14
|
+
patient = models.ForeignKey("Patient", on_delete=models.CASCADE, blank=True, null=True)
|
|
15
|
+
date = models.DateField(blank=True, null=True)
|
|
16
|
+
suffix = models.CharField(max_length=255)
|
|
17
|
+
fps = models.FloatField()
|
|
18
|
+
duration = models.FloatField()
|
|
19
|
+
width = models.IntegerField()
|
|
20
|
+
height = models.IntegerField()
|
|
21
|
+
endoscope_image_x = models.IntegerField(blank=True, null=True)
|
|
22
|
+
endoscope_image_y = models.IntegerField(blank=True, null=True)
|
|
23
|
+
endoscope_image_width = models.IntegerField(blank=True, null=True)
|
|
24
|
+
endoscope_image_height = models.IntegerField(blank=True, null=True)
|
|
25
|
+
center = models.ForeignKey("Center", on_delete=models.CASCADE, blank=True, null=True)
|
|
26
|
+
endoscopy_processor = models.ForeignKey("EndoscopyProcessor", on_delete=models.CASCADE, blank=True, null=True)
|
|
27
|
+
frames_extracted = models.BooleanField(default=False)
|
|
28
|
+
|
|
29
|
+
meta = models.JSONField(blank=True, null=True)
|
|
30
|
+
|
|
31
|
+
class Meta:
|
|
32
|
+
abstract = True
|
|
33
|
+
|
|
34
|
+
def get_roi_endoscope_image(self):
|
|
35
|
+
return {
|
|
36
|
+
'x': self.endoscope_image_content_x,
|
|
37
|
+
'y': self.endoscope_image_content_y,
|
|
38
|
+
'width': self.endoscope_image_content_width,
|
|
39
|
+
'height': self.endoscope_image_content_height,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def initialize_metadata_in_db(self, video_meta=None):
|
|
43
|
+
if not video_meta:
|
|
44
|
+
video_meta = self.meta
|
|
45
|
+
self.set_examination_date_from_video_meta(video_meta)
|
|
46
|
+
self.patient, created = self.get_or_create_patient(video_meta)
|
|
47
|
+
self.save()
|
|
48
|
+
|
|
49
|
+
def get_or_create_patient(self, video_meta=None):
|
|
50
|
+
from ...persons import Patient
|
|
51
|
+
if not video_meta:
|
|
52
|
+
video_meta = self.meta
|
|
53
|
+
|
|
54
|
+
patient_first_name = video_meta['patient_first_name']
|
|
55
|
+
patient_last_name = video_meta['patient_last_name']
|
|
56
|
+
patient_dob = video_meta['patient_dob']
|
|
57
|
+
|
|
58
|
+
# assert that we got all the necessary information
|
|
59
|
+
assert patient_first_name and patient_last_name and patient_dob, "Missing patient information"
|
|
60
|
+
|
|
61
|
+
patient, created = Patient.objects.get_or_create(
|
|
62
|
+
first_name=patient_first_name,
|
|
63
|
+
last_name=patient_last_name,
|
|
64
|
+
dob=patient_dob
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return patient, created
|
|
68
|
+
|
|
69
|
+
def get_frame_model(self):
|
|
70
|
+
assert 1 == 2, "This method should be overridden in derived classes"
|
|
71
|
+
|
|
72
|
+
def get_video_model(self):
|
|
73
|
+
assert 1 == 2, "This method should be overridden in derived classes"
|
|
74
|
+
|
|
75
|
+
def get_frame_number(self):
|
|
76
|
+
"""
|
|
77
|
+
Get the number of frames in the video.
|
|
78
|
+
"""
|
|
79
|
+
frame_model = self.get_frame_model()
|
|
80
|
+
framecount = frame_model.objects.filter(video=self).count()
|
|
81
|
+
return framecount
|
|
82
|
+
|
|
83
|
+
def set_frames_extracted(self, value:bool=True):
|
|
84
|
+
self.frames_extracted = value
|
|
85
|
+
self.save()
|
|
86
|
+
|
|
87
|
+
def get_frames(self):
|
|
88
|
+
"""
|
|
89
|
+
Retrieve all frames for this video in the correct order.
|
|
90
|
+
"""
|
|
91
|
+
frame_model = self.get_frame_model()
|
|
92
|
+
return frame_model.objects.filter(video=self).order_by('frame_number')
|
|
93
|
+
|
|
94
|
+
def get_frame(self, frame_number):
|
|
95
|
+
"""
|
|
96
|
+
Retrieve a specific frame for this video.
|
|
97
|
+
"""
|
|
98
|
+
frame_model = self.get_frame_model()
|
|
99
|
+
return frame_model.objects.get(video=self, frame_number=frame_number)
|
|
100
|
+
|
|
101
|
+
def get_frame_range(self, start_frame_number:int, end_frame_number:int):
|
|
102
|
+
"""
|
|
103
|
+
Expects numbers of start and stop frame.
|
|
104
|
+
Returns all frames of this video within the given range in ascending order.
|
|
105
|
+
"""
|
|
106
|
+
frame_model = self.get_frame_model()
|
|
107
|
+
return frame_model.objects.filter(video=self, frame_number__gte=start_frame_number, frame_number__lte=end_frame_number).order_by('frame_number')
|
|
108
|
+
|
|
109
|
+
def _create_frame_object(self, frame_number, image_file):
|
|
110
|
+
frame_model = self.get_frame_model()
|
|
111
|
+
frame = frame_model(
|
|
112
|
+
video=self,
|
|
113
|
+
frame_number=frame_number,
|
|
114
|
+
suffix='jpg',
|
|
115
|
+
)
|
|
116
|
+
frame.image_file = image_file # Temporary store the file-like object
|
|
117
|
+
|
|
118
|
+
return frame
|
|
119
|
+
|
|
120
|
+
def _bulk_create_frames(self, frames_to_create):
|
|
121
|
+
frame_model = self.get_frame_model()
|
|
122
|
+
with transaction.atomic():
|
|
123
|
+
frame_model.objects.bulk_create(frames_to_create)
|
|
124
|
+
|
|
125
|
+
# After the DB operation, save the ImageField for each object
|
|
126
|
+
for frame in frames_to_create:
|
|
127
|
+
frame_name = f"video_{self.id}_frame_{str(frame.frame_number).zfill(7)}.jpg"
|
|
128
|
+
frame.image.save(frame_name, frame.image_file)
|
|
129
|
+
|
|
130
|
+
# Clear the list for the next batch
|
|
131
|
+
frames_to_create = []
|
|
132
|
+
|
|
133
|
+
def set_examination_date_from_video_meta(self, video_meta=None):
|
|
134
|
+
if not video_meta:
|
|
135
|
+
video_meta = self.meta
|
|
136
|
+
date_str = video_meta['examination_date'] # e.g. 2020-01-01
|
|
137
|
+
if date_str:
|
|
138
|
+
self.date = date.fromisoformat(date_str)
|
|
139
|
+
self.save()
|
|
140
|
+
|
|
141
|
+
def extract_all_frames(self):
|
|
142
|
+
"""
|
|
143
|
+
Extract all frames from the video and store them in the database.
|
|
144
|
+
Uses Django's bulk_create for more efficient database operations.
|
|
145
|
+
"""
|
|
146
|
+
# Open the video file
|
|
147
|
+
video = cv2.VideoCapture(self.file.path)
|
|
148
|
+
|
|
149
|
+
# Initialize video properties
|
|
150
|
+
self.initialize_video_specs(video)
|
|
151
|
+
|
|
152
|
+
# Prepare for batch operation
|
|
153
|
+
frames_to_create = []
|
|
154
|
+
|
|
155
|
+
# Extract frames
|
|
156
|
+
for frame_number in tqdm(range(int(self.duration * self.fps))):
|
|
157
|
+
# Read the frame
|
|
158
|
+
success, image = video.read()
|
|
159
|
+
if not success:
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
# Convert the numpy array to a PIL Image object
|
|
163
|
+
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
|
164
|
+
|
|
165
|
+
# Save the PIL Image to a buffer
|
|
166
|
+
buffer = io.BytesIO()
|
|
167
|
+
pil_image.save(buffer, format='JPEG')
|
|
168
|
+
|
|
169
|
+
# Create a file-like object from the byte data in the buffer
|
|
170
|
+
image_file = ContentFile(buffer.getvalue())
|
|
171
|
+
|
|
172
|
+
# Prepare Frame instance (don't save yet)
|
|
173
|
+
frame = self._create_frame_object(frame_number, image_file)
|
|
174
|
+
frames_to_create.append(frame)
|
|
175
|
+
|
|
176
|
+
# Perform bulk create when reaching BATCH_SIZE
|
|
177
|
+
if len(frames_to_create) >= BATCH_SIZE:
|
|
178
|
+
self._bulk_create_frames(frames_to_create)
|
|
179
|
+
frames_to_create = []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Handle remaining frames
|
|
183
|
+
if frames_to_create:
|
|
184
|
+
self._bulk_create_frames(frames_to_create)
|
|
185
|
+
frames_to_create = []
|
|
186
|
+
|
|
187
|
+
# Close the video file
|
|
188
|
+
video.release()
|
|
189
|
+
self.set_frames_extracted(True)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def initialize_video_specs(self, video):
|
|
193
|
+
"""
|
|
194
|
+
Initialize and save video metadata like framerate, dimensions, and duration.
|
|
195
|
+
"""
|
|
196
|
+
self.fps = video.get(cv2.CAP_PROP_FPS)
|
|
197
|
+
self.width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
198
|
+
self.height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
199
|
+
self.duration = video.get(cv2.CAP_PROP_FRAME_COUNT) / self.fps
|
|
200
|
+
self.save()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from endoreg_db.models.annotation.image_classification import ImageClassificationAnnotation
|
|
2
|
+
from endoreg_db.models.label.label import Label
|
|
3
|
+
from .base_classes import AbstractFrame
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
class Frame(AbstractFrame):
|
|
7
|
+
video = models.ForeignKey("Video", on_delete=models.CASCADE, related_name="frames")
|
|
8
|
+
|
|
9
|
+
class LegacyFrame(AbstractFrame):
|
|
10
|
+
video = models.ForeignKey("LegacyVideo", on_delete=models.CASCADE, related_name='frames')
|
|
11
|
+
image = models.ImageField(upload_to="legacy_frames", blank=True, null=True)
|
|
12
|
+
suffix = models.CharField(max_length=255)
|
|
13
|
+
# ImageClassificationAnnotation has a foreign key to this model (related name: image_classification_annotations)
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
unique_together = ('video', 'frame_number')
|
|
17
|
+
indexes = [
|
|
18
|
+
models.Index(fields=['video', 'frame_number']),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def get_classification_annotations(self):
|
|
22
|
+
"""
|
|
23
|
+
Get all image classification annotations for this frame.
|
|
24
|
+
"""
|
|
25
|
+
return ImageClassificationAnnotation.objects.filter(legacy_frame=self)
|
|
26
|
+
|
|
27
|
+
def get_classification_annotations_by_label(self, label:Label):
|
|
28
|
+
"""
|
|
29
|
+
Get all image classification annotations for this frame with the given label.
|
|
30
|
+
"""
|
|
31
|
+
return ImageClassificationAnnotation.objects.filter(legacy_frame=self, label=label)
|
|
32
|
+
|
|
33
|
+
def get_classification_annotations_by_value(self, value:bool):
|
|
34
|
+
"""
|
|
35
|
+
Get all image classification annotations for this frame with the given value.
|
|
36
|
+
"""
|
|
37
|
+
return ImageClassificationAnnotation.objects.filter(legacy_frame=self, value=value)
|
|
38
|
+
|
|
39
|
+
def get_classification_annotations_by_label_and_value(self, label:Label, value:bool):
|
|
40
|
+
"""
|
|
41
|
+
Get all image classification annotations for this frame with the given label and value.
|
|
42
|
+
"""
|
|
43
|
+
return ImageClassificationAnnotation.objects.filter(legacy_frame=self, label=label, value=value)
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from ..center import Center
|
|
3
|
+
import hashlib
|
|
4
|
+
from datetime import date, time
|
|
5
|
+
|
|
6
|
+
class ReportFile(models.Model):
|
|
7
|
+
pdf = models.FileField(upload_to="raw_report_pdfs", blank=True, null=True)
|
|
8
|
+
pdf_hash = models.CharField(max_length=255, unique=True)
|
|
9
|
+
center = models.ForeignKey(Center, on_delete=models.CASCADE)
|
|
10
|
+
meta = models.JSONField(blank=True, null=True)
|
|
11
|
+
text = models.TextField(blank=True, null=True)
|
|
12
|
+
text_anonymized = models.TextField(blank=True, null=True)
|
|
13
|
+
patient = models.ForeignKey("Patient", on_delete=models.CASCADE, blank=True, null=True)
|
|
14
|
+
examiner = models.ForeignKey("Examiner", on_delete=models.CASCADE, blank=True, null=True)
|
|
15
|
+
date = models.DateField(blank=True, null=True)
|
|
16
|
+
time = models.TimeField(blank=True, null=True)
|
|
17
|
+
|
|
18
|
+
def get_pdf_hash(self):
|
|
19
|
+
pdf = self.pdf
|
|
20
|
+
pdf_hash = None
|
|
21
|
+
|
|
22
|
+
if pdf:
|
|
23
|
+
# Open the file in binary mode and read its contents
|
|
24
|
+
with pdf.open(mode='rb') as f:
|
|
25
|
+
pdf_contents = f.read()
|
|
26
|
+
# Create a hash object using SHA-256 algorithm
|
|
27
|
+
hash_object = hashlib.sha256(pdf_contents, usedforsecurity=False)
|
|
28
|
+
# Get the hexadecimal representation of the hash
|
|
29
|
+
pdf_hash = hash_object.hexdigest()
|
|
30
|
+
assert len(pdf_hash) <= 255, "Hash length exceeds 255 characters"
|
|
31
|
+
|
|
32
|
+
return pdf_hash
|
|
33
|
+
|
|
34
|
+
def initialize_metadata_in_db(self, report_meta=None):
|
|
35
|
+
if not report_meta:
|
|
36
|
+
report_meta = self.meta
|
|
37
|
+
self.set_examination_date_and_time(report_meta)
|
|
38
|
+
self.patient, created = self.get_or_create_patient(report_meta)
|
|
39
|
+
self.examiner, created = self.get_or_create_examiner(report_meta)
|
|
40
|
+
self.save()
|
|
41
|
+
|
|
42
|
+
def get_or_create_patient(self, report_meta=None):
|
|
43
|
+
from ..persons import Patient
|
|
44
|
+
if not report_meta:
|
|
45
|
+
report_meta = self.meta
|
|
46
|
+
patient_first_name = report_meta['patient_first_name']
|
|
47
|
+
patient_last_name = report_meta['patient_last_name']
|
|
48
|
+
patient_dob = report_meta['patient_dob']
|
|
49
|
+
|
|
50
|
+
patient, created = Patient.objects.get_or_create(
|
|
51
|
+
first_name=patient_first_name,
|
|
52
|
+
last_name=patient_last_name,
|
|
53
|
+
dob=patient_dob
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return patient, created
|
|
57
|
+
|
|
58
|
+
def get_or_create_examiner(self, report_meta= None):
|
|
59
|
+
from ..persons import Examiner
|
|
60
|
+
if not report_meta:
|
|
61
|
+
report_meta = self.meta
|
|
62
|
+
examiner_first_name = report_meta['examiner_first_name']
|
|
63
|
+
examiner_last_name = report_meta['examiner_last_name']
|
|
64
|
+
examiner_center = self.center
|
|
65
|
+
|
|
66
|
+
examiner, created = Examiner.objects.get_or_create(
|
|
67
|
+
first_name=examiner_first_name,
|
|
68
|
+
last_name=examiner_last_name,
|
|
69
|
+
center=examiner_center
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return examiner, created
|
|
73
|
+
|
|
74
|
+
def set_examination_date_and_time(self, report_meta=None):
|
|
75
|
+
if not report_meta:
|
|
76
|
+
report_meta = self.meta
|
|
77
|
+
examination_date_str = report_meta['examination_date']
|
|
78
|
+
examination_time_str = report_meta['examination_time']
|
|
79
|
+
|
|
80
|
+
if examination_date_str:
|
|
81
|
+
# TODO: get django DateField compatible date from string (e.g. "2021-01-01")
|
|
82
|
+
self.date = date.fromisoformat(examination_date_str)
|
|
83
|
+
if examination_time_str:
|
|
84
|
+
# TODO: get django TimeField compatible time from string (e.g. "12:00")
|
|
85
|
+
self.time = time.fromisoformat(examination_time_str)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
class VideoImportMeta(models.Model):
|
|
4
|
+
processor = models.ForeignKey('EndoscopyProcessor', on_delete=models.CASCADE)
|
|
5
|
+
endoscope = models.ForeignKey('Endoscope', on_delete=models.CASCADE, blank=True, null=True)
|
|
6
|
+
center = models.ForeignKey('Center', on_delete=models.CASCADE)
|
|
7
|
+
video_anonymized = models.BooleanField(default=False)
|
|
8
|
+
video_patient_data_detected = models.BooleanField(default=False)
|
|
9
|
+
outside_detected = models.BooleanField(default=False)
|
|
10
|
+
patient_data_removed = models.BooleanField(default=False)
|
|
11
|
+
outside_removed = models.BooleanField(default=False)
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
result_html = ""
|
|
15
|
+
|
|
16
|
+
result_html += f"Processor: {self.processor.name}<br>"
|
|
17
|
+
result_html += f"Endoscope: {self.endoscope.name}<br>"
|
|
18
|
+
result_html += f"Center: {self.center.name}<br>"
|
|
19
|
+
result_html += f"Video anonymized: {self.video_anonymized}<br>"
|
|
20
|
+
result_html += f"Video patient data detected: {self.video_patient_data_detected}<br>"
|
|
21
|
+
result_html += f"Outside detected: {self.outside_detected}<br>"
|
|
22
|
+
result_html += f"Patient data removed: {self.patient_data_removed}<br>"
|
|
23
|
+
result_html += f"Outside removed: {self.outside_removed}<br>"
|
|
24
|
+
return result_html
|
|
25
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from ..base_classes import AbstractVideo
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
from endoreg_db.models.data_file.frame import Frame
|
|
5
|
+
from endoreg_db.models.data_file.frame import LegacyFrame
|
|
6
|
+
|
|
7
|
+
BATCH_SIZE = 1000
|
|
8
|
+
|
|
9
|
+
class Video(AbstractVideo):
|
|
10
|
+
import_meta = models.OneToOneField('VideoImportMeta', on_delete=models.CASCADE, blank=True, null=True)
|
|
11
|
+
def get_video_model(self):
|
|
12
|
+
return Video
|
|
13
|
+
|
|
14
|
+
def get_frame_model(self):
|
|
15
|
+
return Frame
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LegacyVideo(AbstractVideo):
|
|
19
|
+
file = models.FileField(upload_to="legacy_videos", blank=True, null=True)
|
|
20
|
+
|
|
21
|
+
def get_video_model(self):
|
|
22
|
+
return LegacyVideo
|
|
23
|
+
|
|
24
|
+
def get_frame_model(self):
|
|
25
|
+
return LegacyFrame
|