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.
Files changed (121) hide show
  1. endoreg_db/data/__init__.py +14 -0
  2. endoreg_db/data/active_model/data.yaml +3 -0
  3. endoreg_db/data/center/data.yaml +7 -0
  4. endoreg_db/data/endoscope_type/data.yaml +11 -0
  5. endoreg_db/data/endoscopy_processor/data.yaml +45 -0
  6. endoreg_db/data/examination/examinations/data.yaml +17 -0
  7. endoreg_db/data/examination/time/data.yaml +48 -0
  8. endoreg_db/data/examination/time-type/data.yaml +8 -0
  9. endoreg_db/data/examination/type/data.yaml +5 -0
  10. endoreg_db/data/information_source/data.yaml +30 -0
  11. endoreg_db/data/label/label/data.yaml +62 -0
  12. endoreg_db/data/label/label-set/data.yaml +18 -0
  13. endoreg_db/data/label/label-type/data.yaml +7 -0
  14. endoreg_db/data/model_type/data.yaml +7 -0
  15. endoreg_db/data/profession/data.yaml +70 -0
  16. endoreg_db/data/unit/data.yaml +17 -0
  17. endoreg_db/data/unit/length.yaml +31 -0
  18. endoreg_db/data/unit/volume.yaml +26 -0
  19. endoreg_db/data/unit/weight.yaml +31 -0
  20. endoreg_db/forms/__init__.py +2 -0
  21. endoreg_db/forms/settings/__init__.py +8 -0
  22. endoreg_db/forms/unit.py +6 -0
  23. endoreg_db/management/commands/_load_model_template.py +41 -0
  24. endoreg_db/management/commands/delete_legacy_images.py +19 -0
  25. endoreg_db/management/commands/delete_legacy_videos.py +17 -0
  26. endoreg_db/management/commands/extract_legacy_video_frames.py +18 -0
  27. endoreg_db/management/commands/fetch_legacy_image_dataset.py +32 -0
  28. endoreg_db/management/commands/import_legacy_images.py +94 -0
  29. endoreg_db/management/commands/import_legacy_videos.py +76 -0
  30. endoreg_db/management/commands/load_active_model_data.py +45 -0
  31. endoreg_db/management/commands/load_ai_model_data.py +45 -0
  32. endoreg_db/management/commands/load_base_db_data.py +62 -0
  33. endoreg_db/management/commands/load_center_data.py +43 -0
  34. endoreg_db/management/commands/load_endoscope_type_data.py +45 -0
  35. endoreg_db/management/commands/load_endoscopy_processor_data.py +45 -0
  36. endoreg_db/management/commands/load_examination_data.py +75 -0
  37. endoreg_db/management/commands/load_information_source.py +45 -0
  38. endoreg_db/management/commands/load_label_data.py +67 -0
  39. endoreg_db/management/commands/load_profession_data.py +44 -0
  40. endoreg_db/management/commands/load_unit_data.py +46 -0
  41. endoreg_db/management/commands/load_user_groups.py +67 -0
  42. endoreg_db/management/commands/register_ai_model.py +65 -0
  43. endoreg_db/migrations/0001_initial.py +582 -0
  44. endoreg_db/models/__init__.py +53 -0
  45. endoreg_db/models/ai_model/__init__.py +3 -0
  46. endoreg_db/models/ai_model/active_model.py +9 -0
  47. endoreg_db/models/ai_model/model_meta.py +24 -0
  48. endoreg_db/models/ai_model/model_type.py +26 -0
  49. endoreg_db/models/ai_model/utils.py +8 -0
  50. endoreg_db/models/annotation/__init__.py +2 -0
  51. endoreg_db/models/annotation/binary_classification_annotation_task.py +80 -0
  52. endoreg_db/models/annotation/image_classification.py +27 -0
  53. endoreg_db/models/center.py +19 -0
  54. endoreg_db/models/data_file/__init__.py +4 -0
  55. endoreg_db/models/data_file/base_classes/__init__.py +3 -0
  56. endoreg_db/models/data_file/base_classes/abstract_frame.py +51 -0
  57. endoreg_db/models/data_file/base_classes/abstract_video.py +200 -0
  58. endoreg_db/models/data_file/frame.py +45 -0
  59. endoreg_db/models/data_file/report_file.py +88 -0
  60. endoreg_db/models/data_file/video/__init__.py +7 -0
  61. endoreg_db/models/data_file/video/import_meta.py +25 -0
  62. endoreg_db/models/data_file/video/video.py +25 -0
  63. endoreg_db/models/data_file/video_segment.py +107 -0
  64. endoreg_db/models/examination/__init__.py +4 -0
  65. endoreg_db/models/examination/examination.py +26 -0
  66. endoreg_db/models/examination/examination_time.py +27 -0
  67. endoreg_db/models/examination/examination_time_type.py +24 -0
  68. endoreg_db/models/examination/examination_type.py +18 -0
  69. endoreg_db/models/hardware/__init__.py +2 -0
  70. endoreg_db/models/hardware/endoscope.py +44 -0
  71. endoreg_db/models/hardware/endoscopy_processor.py +143 -0
  72. endoreg_db/models/information_source.py +22 -0
  73. endoreg_db/models/label/__init__.py +1 -0
  74. endoreg_db/models/label/label.py +84 -0
  75. endoreg_db/models/legacy_data/__init__.py +3 -0
  76. endoreg_db/models/legacy_data/image.py +34 -0
  77. endoreg_db/models/patient_examination/__init__.py +35 -0
  78. endoreg_db/models/persons/__init__.py +4 -0
  79. endoreg_db/models/persons/examiner/__init__.py +2 -0
  80. endoreg_db/models/persons/examiner/examiner.py +16 -0
  81. endoreg_db/models/persons/examiner/examiner_type.py +2 -0
  82. endoreg_db/models/persons/patient.py +58 -0
  83. endoreg_db/models/persons/person.py +34 -0
  84. endoreg_db/models/persons/portal_user_information.py +29 -0
  85. endoreg_db/models/prediction/__init__.py +2 -0
  86. endoreg_db/models/prediction/image_classification.py +37 -0
  87. endoreg_db/models/prediction/video_prediction_meta.py +244 -0
  88. endoreg_db/models/unit.py +20 -0
  89. endoreg_db/queries/__init__.py +5 -0
  90. endoreg_db/queries/annotations/__init__.py +3 -0
  91. endoreg_db/queries/annotations/legacy.py +159 -0
  92. endoreg_db/queries/get/__init__.py +6 -0
  93. endoreg_db/queries/get/annotation.py +0 -0
  94. endoreg_db/queries/get/center.py +42 -0
  95. endoreg_db/queries/get/model.py +13 -0
  96. endoreg_db/queries/get/patient.py +14 -0
  97. endoreg_db/queries/get/patient_examination.py +20 -0
  98. endoreg_db/queries/get/prediction.py +0 -0
  99. endoreg_db/queries/get/report_file.py +33 -0
  100. endoreg_db/queries/get/video.py +31 -0
  101. endoreg_db/queries/get/video_import_meta.py +0 -0
  102. endoreg_db/queries/get/video_prediction_meta.py +0 -0
  103. endoreg_db/queries/sanity/__init_.py +0 -0
  104. endoreg_db/serializers/__init__.py +10 -0
  105. endoreg_db/serializers/ai_model.py +19 -0
  106. endoreg_db/serializers/annotation.py +17 -0
  107. endoreg_db/serializers/center.py +11 -0
  108. endoreg_db/serializers/examination.py +33 -0
  109. endoreg_db/serializers/frame.py +13 -0
  110. endoreg_db/serializers/hardware.py +21 -0
  111. endoreg_db/serializers/label.py +22 -0
  112. endoreg_db/serializers/patient.py +10 -0
  113. endoreg_db/serializers/prediction.py +15 -0
  114. endoreg_db/serializers/report_file.py +7 -0
  115. endoreg_db/serializers/video.py +27 -0
  116. endoreg_db-0.2.0.dist-info/LICENSE +674 -0
  117. endoreg_db-0.2.0.dist-info/METADATA +26 -0
  118. endoreg_db-0.2.0.dist-info/RECORD +126 -0
  119. endoreg_db-0.1.0.dist-info/METADATA +0 -19
  120. endoreg_db-0.1.0.dist-info/RECORD +0 -10
  121. {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,2 @@
1
+ from .image_classification import ImageClassificationAnnotation
2
+ from .binary_classification_annotation_task import LegacyBinaryClassificationAnnotationTask, BinaryClassificationAnnotationTask
@@ -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,4 @@
1
+ from .frame import Frame, LegacyFrame
2
+ from .report_file import ReportFile
3
+ from .video import Video, LegacyVideo, VideoImportMeta
4
+ from .video_segment import LegacyLabelVideoSegment, LabelVideoSegment
@@ -0,0 +1,3 @@
1
+ from .abstract_frame import AbstractFrame
2
+ from .abstract_video import AbstractVideo
3
+
@@ -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,7 @@
1
+ from .video import (
2
+ Video,
3
+ LegacyVideo,
4
+ )
5
+ from .import_meta import (
6
+ VideoImportMeta,
7
+ )
@@ -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