endoreg-db 0.5.3__py3-none-any.whl → 0.6.1__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.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

Files changed (268) hide show
  1. endoreg_db/admin.py +90 -1
  2. endoreg_db/case_generator/case_generator.py +159 -0
  3. endoreg_db/case_generator/lab_sample_factory.py +33 -0
  4. endoreg_db/case_generator/utils.py +30 -0
  5. endoreg_db/data/__init__.py +50 -4
  6. endoreg_db/data/ai_model/data.yaml +7 -0
  7. endoreg_db/data/{label → ai_model_label}/label/data.yaml +27 -1
  8. endoreg_db/data/ai_model_label/label-set/data.yaml +21 -0
  9. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +5 -0
  10. endoreg_db/data/ai_model_video_segmentation_label/base_segmentation.yaml +176 -0
  11. endoreg_db/data/ai_model_video_segmentation_labelset/data.yaml +20 -0
  12. endoreg_db/data/center/data.yaml +35 -5
  13. endoreg_db/data/contraindication/bleeding.yaml +11 -0
  14. endoreg_db/data/distribution/numeric/data.yaml +14 -0
  15. endoreg_db/data/endoscope/data.yaml +93 -0
  16. endoreg_db/data/examination_indication/endoscopy.yaml +8 -0
  17. endoreg_db/data/examination_indication_classification/endoscopy.yaml +8 -0
  18. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +101 -0
  19. endoreg_db/data/finding/data.yaml +141 -0
  20. endoreg_db/data/finding_intervention/endoscopy.yaml +138 -0
  21. endoreg_db/data/finding_intervention_type/endoscopy.yaml +15 -0
  22. endoreg_db/data/finding_location_classification/colonoscopy.yaml +46 -0
  23. endoreg_db/data/finding_location_classification_choice/colonoscopy.yaml +240 -0
  24. endoreg_db/data/finding_morphology_classification/colonoscopy.yaml +48 -0
  25. endoreg_db/data/finding_morphology_classification_choice/colon_lesion_circularity_default.yaml +34 -0
  26. endoreg_db/data/finding_morphology_classification_choice/colon_lesion_nice.yaml +20 -0
  27. endoreg_db/data/finding_morphology_classification_choice/colon_lesion_paris.yaml +65 -0
  28. endoreg_db/data/finding_morphology_classification_choice/colon_lesion_planarity_default.yaml +56 -0
  29. endoreg_db/data/finding_morphology_classification_choice/colon_lesion_surface_intact_default.yaml +39 -0
  30. endoreg_db/data/finding_morphology_classification_choice/colonoscopy_size.yaml +57 -0
  31. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +79 -0
  32. endoreg_db/data/finding_type/data.yaml +30 -0
  33. endoreg_db/data/gender/data.yaml +17 -0
  34. endoreg_db/data/lab_value/cardiac_enzymes.yaml +7 -1
  35. endoreg_db/data/lab_value/coagulation.yaml +6 -1
  36. endoreg_db/data/lab_value/electrolytes.yaml +39 -1
  37. endoreg_db/data/lab_value/gastrointestinal_function.yaml +12 -0
  38. endoreg_db/data/lab_value/hematology.yaml +17 -2
  39. endoreg_db/data/lab_value/hormones.yaml +6 -0
  40. endoreg_db/data/lab_value/lipids.yaml +12 -3
  41. endoreg_db/data/lab_value/misc.yaml +5 -2
  42. endoreg_db/data/lab_value/renal_function.yaml +2 -1
  43. endoreg_db/data/lx_client_tag/base.yaml +54 -0
  44. endoreg_db/data/lx_client_type/base.yaml +30 -0
  45. endoreg_db/data/lx_permission/base.yaml +24 -0
  46. endoreg_db/data/lx_permission/endoreg.yaml +52 -0
  47. endoreg_db/data/medication_indication/anticoagulation.yaml +44 -49
  48. endoreg_db/data/names_first/first_names.yaml +51 -0
  49. endoreg_db/data/names_last/last_names.yaml +51 -0
  50. endoreg_db/data/network_device/data.yaml +30 -0
  51. endoreg_db/data/organ/data.yaml +29 -0
  52. endoreg_db/data/pdf_type/data.yaml +2 -1
  53. endoreg_db/data/report_reader_flag/ukw-examination-generic.yaml +4 -0
  54. endoreg_db/forms/__init__.py +3 -1
  55. endoreg_db/forms/examination_form.py +11 -0
  56. endoreg_db/forms/patient_finding_intervention_form.py +19 -0
  57. endoreg_db/forms/patient_form.py +26 -0
  58. endoreg_db/management/commands/__init__.py +0 -0
  59. endoreg_db/management/commands/load_ai_model_data.py +57 -23
  60. endoreg_db/management/commands/load_ai_model_label_data.py +59 -0
  61. endoreg_db/management/commands/load_base_db_data.py +160 -118
  62. endoreg_db/management/commands/{load_endoscope_type_data.py → load_contraindication_data.py} +3 -7
  63. endoreg_db/management/commands/load_disease_data.py +29 -7
  64. endoreg_db/management/commands/load_endoscope_data.py +68 -0
  65. endoreg_db/management/commands/load_examination_indication_data.py +65 -0
  66. endoreg_db/management/commands/load_finding_data.py +171 -0
  67. endoreg_db/management/commands/load_lab_value_data.py +3 -3
  68. endoreg_db/management/commands/load_lx_data.py +64 -0
  69. endoreg_db/management/commands/load_medication_data.py +83 -21
  70. endoreg_db/management/commands/load_name_data.py +37 -0
  71. endoreg_db/management/commands/{load_endoscopy_processor_data.py → load_organ_data.py} +7 -9
  72. endoreg_db/migrations/0001_initial.py +1206 -728
  73. endoreg_db/migrations/0002_alter_frame_image_alter_rawframe_image.py +23 -0
  74. endoreg_db/migrations/0003_alter_frame_image_alter_rawframe_image.py +23 -0
  75. endoreg_db/migrations/0004_alter_rawvideofile_file_alter_video_file.py +25 -0
  76. endoreg_db/migrations/0005_rawvideofile_frame_count_and_more.py +33 -0
  77. endoreg_db/migrations/0006_frame_extracted_rawframe_extracted.py +23 -0
  78. endoreg_db/migrations/0007_rename_pseudo_patient_video_patient_and_more.py +24 -0
  79. endoreg_db/migrations/0008_remove_reportfile_patient_examination_and_more.py +48 -0
  80. endoreg_db/models/__init__.py +331 -28
  81. endoreg_db/models/ai_model/__init__.py +1 -0
  82. endoreg_db/models/ai_model/ai_model.py +103 -0
  83. endoreg_db/models/ai_model/lightning/__init__.py +3 -0
  84. endoreg_db/models/ai_model/lightning/inference_dataset.py +53 -0
  85. endoreg_db/models/ai_model/lightning/multilabel_classification_net.py +155 -0
  86. endoreg_db/models/ai_model/lightning/postprocess.py +53 -0
  87. endoreg_db/models/ai_model/lightning/predict.py +172 -0
  88. endoreg_db/models/ai_model/lightning/prediction_visualizer.py +55 -0
  89. endoreg_db/models/ai_model/lightning/preprocess.py +68 -0
  90. endoreg_db/models/ai_model/lightning/run_visualizer.py +21 -0
  91. endoreg_db/models/ai_model/model_meta.py +232 -6
  92. endoreg_db/models/ai_model/model_type.py +13 -3
  93. endoreg_db/models/annotation/__init__.py +31 -2
  94. endoreg_db/models/annotation/anonymized_image_annotation.py +73 -18
  95. endoreg_db/models/annotation/binary_classification_annotation_task.py +94 -57
  96. endoreg_db/models/annotation/image_classification.py +73 -14
  97. endoreg_db/models/annotation/video_segmentation_annotation.py +52 -0
  98. endoreg_db/models/annotation/video_segmentation_labelset.py +20 -0
  99. endoreg_db/models/case/__init__.py +1 -0
  100. endoreg_db/models/{persons/patient/case → case}/case.py +4 -0
  101. endoreg_db/models/case_template/__init__.py +10 -1
  102. endoreg_db/models/case_template/case_template.py +57 -13
  103. endoreg_db/models/case_template/case_template_rule.py +5 -5
  104. endoreg_db/models/case_template/case_template_rule_value.py +19 -4
  105. endoreg_db/models/center/__init__.py +7 -0
  106. endoreg_db/models/center/center.py +31 -5
  107. endoreg_db/models/center/center_product.py +0 -1
  108. endoreg_db/models/center/center_resource.py +16 -2
  109. endoreg_db/models/center/center_waste.py +6 -1
  110. endoreg_db/models/contraindication/__init__.py +21 -0
  111. endoreg_db/models/data_file/__init__.py +38 -5
  112. endoreg_db/models/data_file/base_classes/__init__.py +6 -1
  113. endoreg_db/models/data_file/base_classes/abstract_frame.py +64 -15
  114. endoreg_db/models/data_file/base_classes/abstract_pdf.py +136 -0
  115. endoreg_db/models/data_file/base_classes/abstract_video.py +744 -138
  116. endoreg_db/models/data_file/base_classes/frame_helpers.py +17 -0
  117. endoreg_db/models/data_file/base_classes/prepare_bulk_frames.py +19 -0
  118. endoreg_db/models/data_file/base_classes/utils.py +80 -0
  119. endoreg_db/models/data_file/frame.py +22 -38
  120. endoreg_db/models/data_file/import_classes/__init__.py +4 -18
  121. endoreg_db/models/data_file/import_classes/raw_pdf.py +162 -90
  122. endoreg_db/models/data_file/import_classes/raw_video.py +239 -294
  123. endoreg_db/models/data_file/metadata/__init__.py +10 -0
  124. endoreg_db/models/data_file/metadata/pdf_meta.py +4 -0
  125. endoreg_db/models/data_file/metadata/sensitive_meta.py +265 -6
  126. endoreg_db/models/data_file/metadata/video_meta.py +116 -50
  127. endoreg_db/models/data_file/report_file.py +30 -63
  128. endoreg_db/models/data_file/video/__init__.py +6 -2
  129. endoreg_db/models/data_file/video/video.py +187 -16
  130. endoreg_db/models/data_file/video_segment.py +162 -55
  131. endoreg_db/models/disease.py +25 -2
  132. endoreg_db/models/emission/__init__.py +5 -1
  133. endoreg_db/models/emission/emission_factor.py +71 -6
  134. endoreg_db/models/event.py +51 -0
  135. endoreg_db/models/examination/__init__.py +6 -1
  136. endoreg_db/models/examination/examination.py +53 -12
  137. endoreg_db/models/examination/examination_indication.py +170 -0
  138. endoreg_db/models/examination/examination_time.py +31 -5
  139. endoreg_db/models/examination/examination_time_type.py +28 -4
  140. endoreg_db/models/examination/examination_type.py +28 -6
  141. endoreg_db/models/finding/__init__.py +11 -0
  142. endoreg_db/models/finding/finding.py +75 -0
  143. endoreg_db/models/finding/finding_intervention.py +60 -0
  144. endoreg_db/models/finding/finding_location_classification.py +94 -0
  145. endoreg_db/models/finding/finding_morphology_classification.py +89 -0
  146. endoreg_db/models/finding/finding_type.py +22 -0
  147. endoreg_db/models/hardware/endoscope.py +16 -0
  148. endoreg_db/models/hardware/endoscopy_processor.py +31 -19
  149. endoreg_db/models/label/label.py +35 -7
  150. endoreg_db/models/laboratory/lab_value.py +12 -3
  151. endoreg_db/models/logging/__init__.py +8 -1
  152. endoreg_db/models/lx/__init__.py +4 -0
  153. endoreg_db/models/lx/client.py +57 -0
  154. endoreg_db/models/lx/identity.py +34 -0
  155. endoreg_db/models/lx/permission.py +18 -0
  156. endoreg_db/models/lx/user.py +16 -0
  157. endoreg_db/models/medication/__init__.py +19 -1
  158. endoreg_db/models/medication/medication.py +7 -122
  159. endoreg_db/models/medication/medication_indication.py +50 -0
  160. endoreg_db/models/medication/medication_indication_type.py +34 -0
  161. endoreg_db/models/medication/medication_intake_time.py +26 -0
  162. endoreg_db/models/medication/medication_schedule.py +37 -0
  163. endoreg_db/models/network/__init__.py +7 -1
  164. endoreg_db/models/network/network_device.py +13 -8
  165. endoreg_db/models/organ/__init__.py +38 -0
  166. endoreg_db/models/other/__init__.py +19 -1
  167. endoreg_db/models/other/distribution/__init__.py +44 -0
  168. endoreg_db/models/other/distribution/base_value_distribution.py +20 -0
  169. endoreg_db/models/other/distribution/date_value_distribution.py +91 -0
  170. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +32 -0
  171. endoreg_db/models/other/distribution/numeric_value_distribution.py +97 -0
  172. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +22 -0
  173. endoreg_db/models/other/distribution.py +1 -211
  174. endoreg_db/models/other/material.py +4 -0
  175. endoreg_db/models/other/transport_route.py +2 -1
  176. endoreg_db/models/patient/__init__.py +24 -0
  177. endoreg_db/models/patient/patient_examination.py +182 -0
  178. endoreg_db/models/patient/patient_finding.py +143 -0
  179. endoreg_db/models/patient/patient_finding_intervention.py +26 -0
  180. endoreg_db/models/patient/patient_finding_location.py +120 -0
  181. endoreg_db/models/patient/patient_finding_morphology.py +166 -0
  182. endoreg_db/models/persons/__init__.py +29 -2
  183. endoreg_db/models/persons/examiner/examiner.py +48 -4
  184. endoreg_db/models/persons/patient/__init__.py +1 -1
  185. endoreg_db/models/persons/patient/patient.py +227 -54
  186. endoreg_db/models/persons/patient/patient_disease.py +6 -0
  187. endoreg_db/models/persons/patient/patient_event.py +31 -1
  188. endoreg_db/models/persons/patient/patient_examination_indication.py +32 -0
  189. endoreg_db/models/persons/patient/patient_lab_sample.py +4 -2
  190. endoreg_db/models/persons/patient/patient_lab_value.py +37 -16
  191. endoreg_db/models/persons/patient/patient_medication.py +27 -12
  192. endoreg_db/models/persons/patient/patient_medication_schedule.py +62 -2
  193. endoreg_db/models/prediction/__init__.py +7 -1
  194. endoreg_db/models/prediction/image_classification.py +20 -6
  195. endoreg_db/models/prediction/video_prediction_meta.py +151 -89
  196. endoreg_db/models/product/__init__.py +10 -1
  197. endoreg_db/models/product/product.py +15 -2
  198. endoreg_db/models/product/product_group.py +8 -0
  199. endoreg_db/models/product/product_material.py +4 -0
  200. endoreg_db/models/product/product_weight.py +12 -0
  201. endoreg_db/models/product/reference_product.py +19 -3
  202. endoreg_db/models/quiz/__init__.py +8 -1
  203. endoreg_db/models/report_reader/__init__.py +6 -1
  204. endoreg_db/serializers/__init__.py +1 -1
  205. endoreg_db/serializers/annotation.py +2 -5
  206. endoreg_db/serializers/frame.py +1 -5
  207. endoreg_db/serializers/patient.py +26 -3
  208. endoreg_db/serializers/prediction.py +2 -7
  209. endoreg_db/serializers/raw_video_meta_validation.py +13 -0
  210. endoreg_db/serializers/video.py +6 -13
  211. endoreg_db/serializers/video_segmentation.py +492 -0
  212. endoreg_db/templates/admin/patient_finding_intervention.html +253 -0
  213. endoreg_db/templates/admin/start_examination.html +12 -0
  214. endoreg_db/templates/timeline.html +176 -0
  215. endoreg_db/urls.py +173 -0
  216. endoreg_db/utils/__init__.py +36 -1
  217. endoreg_db/utils/dataloader.py +45 -19
  218. endoreg_db/utils/dates.py +39 -0
  219. endoreg_db/utils/hashs.py +122 -4
  220. endoreg_db/utils/names.py +74 -0
  221. endoreg_db/utils/parse_and_generate_yaml.py +46 -0
  222. endoreg_db/utils/pydantic_models/__init__.py +6 -0
  223. endoreg_db/utils/pydantic_models/db_config.py +57 -0
  224. endoreg_db/utils/validate_endo_roi.py +19 -0
  225. endoreg_db/utils/validate_subcategory_dict.py +91 -0
  226. endoreg_db/utils/video/__init__.py +13 -0
  227. endoreg_db/utils/video/extract_frames.py +121 -0
  228. endoreg_db/utils/video/transcode_videofile.py +111 -0
  229. endoreg_db/views/__init__.py +2 -0
  230. endoreg_db/views/csrf.py +7 -0
  231. endoreg_db/views/patient_views.py +90 -0
  232. endoreg_db/views/raw_video_meta_validation_views.py +38 -0
  233. endoreg_db/views/report_views.py +96 -0
  234. endoreg_db/views/video_segmentation_views.py +149 -0
  235. endoreg_db/views/views_for_timeline.py +46 -0
  236. endoreg_db/views.py +0 -3
  237. endoreg_db-0.6.1.dist-info/METADATA +151 -0
  238. endoreg_db-0.6.1.dist-info/RECORD +420 -0
  239. {endoreg_db-0.5.3.dist-info → endoreg_db-0.6.1.dist-info}/WHEEL +1 -1
  240. endoreg_db/data/active_model/data.yaml +0 -3
  241. endoreg_db/data/label/label-set/data.yaml +0 -18
  242. endoreg_db/management/commands/delete_legacy_images.py +0 -19
  243. endoreg_db/management/commands/delete_legacy_videos.py +0 -17
  244. endoreg_db/management/commands/extract_legacy_video_frames.py +0 -18
  245. endoreg_db/management/commands/import_legacy_images.py +0 -94
  246. endoreg_db/management/commands/import_legacy_videos.py +0 -76
  247. endoreg_db/management/commands/load_label_data.py +0 -67
  248. endoreg_db/migrations/0002_anonymizedimagelabel_anonymousimageannotation_and_more.py +0 -55
  249. endoreg_db/migrations/0003_anonymousimageannotation_original_image_url_and_more.py +0 -39
  250. endoreg_db/migrations/0004_alter_rawpdffile_file.py +0 -20
  251. endoreg_db/migrations/0005_uploadedfile_alter_rawpdffile_file_anonymizedfile.py +0 -40
  252. endoreg_db/migrations/0006_alter_rawpdffile_file.py +0 -20
  253. endoreg_db/migrations/0007_networkdevicelogentry_datetime_and_more.py +0 -43
  254. endoreg_db/migrations/0008_networkdevicelogentry_aglnet_ip_and_more.py +0 -28
  255. endoreg_db/migrations/0009_alter_networkdevicelogentry_vpn_service_status.py +0 -18
  256. endoreg_db/migrations/0010_remove_networkdevicelogentry_hostname.py +0 -17
  257. endoreg_db/models/legacy_data/__init__.py +0 -3
  258. endoreg_db/models/legacy_data/image.py +0 -34
  259. endoreg_db/models/patient_examination/__init__.py +0 -35
  260. endoreg_db/utils/video_metadata.py +0 -87
  261. endoreg_db-0.5.3.dist-info/METADATA +0 -28
  262. endoreg_db-0.5.3.dist-info/RECORD +0 -319
  263. /endoreg_db/{models/persons/patient/case → case_generator}/__init__.py +0 -0
  264. /endoreg_db/data/{label → ai_model_label}/label-type/data.yaml +0 -0
  265. /endoreg_db/data/{model_type → ai_model_type}/data.yaml +0 -0
  266. /endoreg_db/{data/distribution/numeric/.init → management/__init__.py} +0 -0
  267. /endoreg_db/management/commands/{load_report_reader_flag.py → load_report_reader_flag_data.py} +0 -0
  268. {endoreg_db-0.5.3.dist-info → endoreg_db-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,121 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+ from icecream import ic
5
+ import subprocess
6
+ from django.db import transaction
7
+ from tqdm import tqdm
8
+ from typing import TYPE_CHECKING, Union, List
9
+
10
+ if TYPE_CHECKING:
11
+ from endoreg_db.models import RawVideoFile, Video
12
+
13
+ from django.core.files import File
14
+ import io
15
+
16
+
17
+ def prepare_bulk_frames(frame_paths: List[Path]):
18
+ """
19
+ Reads the frame paths into memory as Django File objects.
20
+ This avoids 'seek of closed file' errors by using BytesIO for each frame.
21
+ """
22
+ for path in frame_paths:
23
+ frame_number = int(path.stem.split("_")[1])
24
+ with open(path, "rb") as f:
25
+ content = f.read()
26
+ file_obj = File(io.BytesIO(content), name=path.name)
27
+ yield frame_number, file_obj
28
+
29
+
30
+ def extract_frames(
31
+ video: Union["RawVideoFile", "Video"],
32
+ quality: int = 2,
33
+ overwrite: bool = False,
34
+ ext="jpg",
35
+ verbose=False,
36
+ ) -> List[Path]:
37
+ """
38
+ Extract frames from the video file and save them to the frame_dir.
39
+ For this, ffmpeg must be available in in the current environment.
40
+ """
41
+ frame_dir = Path(video.frame_dir)
42
+ ic(f"Extracting frames to {frame_dir}")
43
+ if not frame_dir.exists():
44
+ frame_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ if not overwrite and len(list(frame_dir.glob(f"*.{ext}"))) > 0:
47
+ video.state_frames_extracted = True # Mark frames as extracted
48
+ extracted_paths = sorted(frame_dir.glob(f"*.{ext}"))
49
+ return extracted_paths
50
+
51
+ video_path = Path(video.file.path).resolve().as_posix()
52
+
53
+ frame_path_string = frame_dir.resolve().as_posix()
54
+ command = [
55
+ "ffmpeg",
56
+ "-i",
57
+ video_path, #
58
+ "-q:v",
59
+ str(quality),
60
+ os.path.join(frame_path_string, f"frame_%07d.{ext}"),
61
+ ]
62
+
63
+ # Ensure FFmpeg is available
64
+ if not shutil.which("ffmpeg"):
65
+ raise EnvironmentError(
66
+ "FFmpeg could not be found. Ensure it is installed and in your PATH."
67
+ )
68
+
69
+ # Extract frames from the video file
70
+ # Execute the command
71
+ process = subprocess.Popen(
72
+ command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
73
+ )
74
+ stdout_data, stderr_data = process.communicate()
75
+
76
+ if process.returncode != 0:
77
+ raise Exception(f"Error extracting frames: {stderr_data}")
78
+
79
+ if verbose and stdout_data:
80
+ print(stdout_data)
81
+
82
+ # After extracting frames with ffmpeg, parse frame filenames and batch-create
83
+ extracted_paths = sorted(frame_dir.glob(f"*.{ext}"))
84
+
85
+ return extracted_paths
86
+
87
+
88
+ def initialize_frame_objects(
89
+ video: Union["RawVideoFile", "Video"], extracted_paths: List[Path]
90
+ ):
91
+ """
92
+ Initialize frame objects for the extracted frames.
93
+ """
94
+ if video.state_frames_initialized:
95
+ return
96
+ video.frame_count = len(extracted_paths)
97
+ frames_to_create = []
98
+ batch_size = int(os.environ.get("DJANGO_FFMPEG_EXTRACT_FRAME_BATCHSIZE", "500"))
99
+ for i, (frame_number, file_obj) in tqdm(
100
+ enumerate(prepare_bulk_frames(extracted_paths), start=1)
101
+ ):
102
+ frame_obj_instance = video.create_frame_object(
103
+ frame_number, image_file=file_obj, extracted=True
104
+ )
105
+ frames_to_create.append(frame_obj_instance)
106
+
107
+ if i % batch_size == 0:
108
+ with transaction.atomic():
109
+ video.bulk_create_frames(frames_to_create)
110
+ frames_to_create.clear()
111
+
112
+ if frames_to_create:
113
+ with transaction.atomic():
114
+ video.bulk_create_frames(frames_to_create)
115
+
116
+ video.set_frames_extracted(True)
117
+ video.save()
118
+
119
+ frame_dir = extracted_paths[0].parent
120
+ ic(f"Removing frame directory: {frame_dir}")
121
+ shutil.rmtree(frame_dir)
@@ -0,0 +1,111 @@
1
+ import shutil
2
+ import subprocess
3
+ import os
4
+ from pathlib import Path
5
+ from icecream import ic
6
+
7
+
8
+ def get_transcoded_file_path(source_file_path: Path, suffix: str = "mp4"):
9
+ """
10
+ Method to get the transcoded file path.
11
+
12
+ Args:
13
+ source_file_path (Path): Source file path.
14
+ suffix (str): Suffix of the transcoded file.
15
+
16
+ Returns:
17
+ transcoded_file_path (Path): Transcoded file path.
18
+ """
19
+ transcoded_file_name = f"{source_file_path.stem}_transcoded.{suffix}"
20
+ transcoded_file_path = source_file_path.parent / transcoded_file_name
21
+ return transcoded_file_path
22
+
23
+
24
+ def check_require_transcode(
25
+ filepath: Path, transcoded_file_path: Path, target_suffix=".mp4"
26
+ ):
27
+ """
28
+ Checks whether a video file requires transcoding.\
29
+ We check if the current suffix of the file path matches the target suffix\
30
+ and if the transcoded file path exists.\
31
+ If the current suffix does not match the target suffix and the transcoded file path does not exist,\
32
+ transcoding is required.
33
+ """
34
+ current_suffix = filepath.suffix
35
+
36
+ require_transcode = False
37
+ if not current_suffix == target_suffix and not transcoded_file_path.exists():
38
+ if not transcoded_file_path.exists():
39
+ require_transcode = True
40
+ return require_transcode
41
+
42
+
43
+ def transcode_videofile_if_required(filepath: Path):
44
+ """
45
+ Perform transcoding on a video file if required.
46
+ This method checks whether a transcoded version (with an ".mp4" suffix) of the given
47
+ video file exists or needs to be produced. It first computes the expected transcoded file path,
48
+ then uses a class-specific check (check_require_transcode) to decide if transcoding is necessary.
49
+ If so, it transcodes the video file by calling the class method transcode_videofile and returns
50
+ the path of the transcoded file after ensuring that the resulting file path matches the computed one.
51
+ If transcoding is not required, the original file path is returned.
52
+ Args:
53
+ filepath (Path): The path to the original video file that may require transcoding.
54
+ Returns:
55
+ Path: The path to the transcoded video file if transcoding was performed; otherwise, the original file path.
56
+ """
57
+
58
+ transcoded_file_path = get_transcoded_file_path(filepath, suffix=".mp4")
59
+ if check_require_transcode(filepath, transcoded_file_path):
60
+ transcoded_path = transcode_videofile(
61
+ filepath, transcoded_path=transcoded_file_path
62
+ )
63
+ assert transcoded_file_path == transcoded_path
64
+ return transcoded_path
65
+ else:
66
+ return filepath
67
+
68
+
69
+ def transcode_videofile(filepath: Path, transcoded_path: Path):
70
+ """
71
+ Transcodes a video to a compatible MP4 format using ffmpeg.
72
+ If the transcoded file exists, it is returned.
73
+
74
+ Parameters
75
+ ----------
76
+ mov_file : str
77
+ The full path to the video file.
78
+
79
+ Returns
80
+ -------
81
+ transcoded_path : str
82
+ The full path to the transcoded video file.
83
+ """
84
+ ic("Transcoding video")
85
+ ic(f"Input path: {filepath}")
86
+
87
+ # if filepath suffix is .mp4 or .MP4 we dont need to transcode and can copy the file
88
+ if filepath.suffix.lower() in [".mp4"]:
89
+ shutil.copyfile(filepath, transcoded_path)
90
+ return transcoded_path
91
+
92
+ ic(f"Transcoded path: {transcoded_path}")
93
+ if os.path.exists(transcoded_path):
94
+ return transcoded_path
95
+
96
+ # Run ffmpeg to transcode the video using H264 and AAC
97
+ # TODO Document settings, check if we need to change them
98
+ command = [
99
+ "ffmpeg",
100
+ "-i",
101
+ filepath.resolve().as_posix(),
102
+ "-c:v",
103
+ "libx264",
104
+ "-preset",
105
+ "fast",
106
+ "-c:a",
107
+ "aac",
108
+ transcoded_path,
109
+ ]
110
+ subprocess.run(command, check=True)
111
+ return transcoded_path
@@ -0,0 +1,2 @@
1
+ # Import the views so they are available for Django
2
+ from .patient_views import start_examination
@@ -0,0 +1,7 @@
1
+ from django.http import JsonResponse
2
+ from django.middleware.csrf import get_token
3
+
4
+ # New view to return the CSRF token in JSON format
5
+ def csrf_token_view(request):
6
+ token = get_token(request)
7
+ return JsonResponse({'csrf_token': token})
@@ -0,0 +1,90 @@
1
+ from django.shortcuts import render
2
+ from django.contrib.admin.views.decorators import staff_member_required
3
+ from django.http import JsonResponse
4
+ from django.http import JsonResponse
5
+ from django.views.decorators.http import require_GET
6
+ from django.core.exceptions import ObjectDoesNotExist
7
+ from rest_framework import viewsets
8
+ from ..models import Patient
9
+ from ..serializers import PatientSerializer
10
+ from rest_framework.permissions import IsAuthenticatedOrReadOnly
11
+ from endoreg_db.models import (
12
+ FindingLocationClassification,
13
+ FindingLocationClassificationChoice,
14
+ FindingMorphologyClassification,
15
+ FindingMorphologyClassificationType
16
+ )
17
+
18
+ @staff_member_required # Ensures only staff members can access the page
19
+ def start_examination(request):
20
+ return render(request, 'admin/start_examination.html') # Loads the simple HTML page
21
+
22
+ #from ..models.patient.patient_finding_location import PatientFindingLocation
23
+ from ..models import FindingLocationClassification, FindingLocationClassificationChoice # Correct models
24
+
25
+
26
+ #need to implement one with json data after tesing whethe rthis works or not
27
+ """def get_location_choices(request, location_id):
28
+
29
+ try:
30
+ # Ensure the location exists
31
+ location = FindingLocationClassification.objects.get(id=location_id)
32
+ # Get only choices related to the selected location classification
33
+ #location_choices = FindingLocationClassificationChoice.objects.filter(location_classification=location)
34
+ #its many to may relation so
35
+ location_choices = location.choices.all()
36
+
37
+ except FindingLocationClassification.DoesNotExist:
38
+ location_choices = []
39
+
40
+ # Get previously selected values to retain them after reloading
41
+ selected_location = int(location_id) if location_id else None
42
+
43
+ return render(request, 'admin/patient_finding_intervention.html', {
44
+ "location_choices": location_choices, # Pass updated choices to the template
45
+ "selected_location": location_id, # Keep previous selection
46
+ })
47
+ """
48
+
49
+ class PatientViewSet(viewsets.ModelViewSet):
50
+ """API endpoint for managing patients."""
51
+ queryset = Patient.objects.all()
52
+ serializer_class = PatientSerializer
53
+ #permission_classes = [IsAuthenticatedOrReadOnly]
54
+
55
+ def perform_create(self, serializer):
56
+ serializer.save()
57
+
58
+ def update(self, request, *args, **kwargs):
59
+ # custom edit logic here if needed
60
+ return super().update(request, *args, **kwargs)
61
+
62
+ def destroy(self, request, *args, **kwargs):
63
+ # custom delete logic here if needed
64
+ return super().destroy(request, *args, **kwargs)
65
+
66
+ @require_GET
67
+ def get_location_choices(request, location_id):
68
+ """Fetch location choices dynamically based on FindingLocationClassification."""
69
+ try:
70
+ location = FindingLocationClassification.objects.get(id=location_id)
71
+ location_choices = location.choices.all()
72
+ data = [{"id": choice.id, "name": choice.name} for choice in location_choices]
73
+ return JsonResponse({"location_choices": data})
74
+ except FindingLocationClassification.DoesNotExist:
75
+ return JsonResponse({"error": "Location classification not found", "location_choices": []}, status=404)
76
+
77
+ @require_GET
78
+ def get_morphology_choices(request, morphology_id):
79
+ """Fetch morphology choices dynamically based on FindingMorphologyClassification."""
80
+ try:
81
+ morphology_classification = FindingMorphologyClassification.objects.get(id=morphology_id)
82
+ morphology_choices = FindingMorphologyClassificationType.objects.filter(
83
+ id=morphology_classification.classification_type_id
84
+ )
85
+ data = [{"id": choice.id, "name": choice.name} for choice in morphology_choices]
86
+ return JsonResponse({"morphology_choices": data})
87
+ except ObjectDoesNotExist:
88
+ return JsonResponse({"error": "Morphology classification not found", "morphology_choices": []}, status=404)
89
+ except Exception as e:
90
+ return JsonResponse({"error": "Internal server error", "morphology_choices": []}, status=500)
@@ -0,0 +1,38 @@
1
+ from rest_framework.views import APIView
2
+ from rest_framework.response import Response
3
+ from rest_framework import status
4
+ from ..models import RawVideoFile
5
+ from ..serializers.raw_video_meta_validation import VideoFileForMetaSerializer
6
+
7
+ class VideoFileForMetaView(APIView):
8
+ """
9
+ API endpoint to fetch video metadata step-by-step.
10
+ If last_id is not provided Returns the first video.
11
+ If last_id is given Returns the next available video.
12
+ """
13
+
14
+ ##need to change this fucntion , like the previous one
15
+
16
+ def get(self, request):
17
+ """
18
+ Handles:
19
+ First video if last_id is nt in query params.
20
+ Next video where id > last_id` if provided.
21
+ """
22
+ last_id = request.GET.get("last_id") # Get last_id from query params (e.g., ?last_id=2)
23
+
24
+ try:
25
+ # If last_id is provided, fetch the next video where id > last_id
26
+ # id__gt is orm syntax which is equal to SELECT * FROM rawvideofile WHERE id > 2 ORDER BY id ASC LIMIT 1;
27
+
28
+ query_filter = {} if last_id is None else {"id__gt": int(last_id)}
29
+ video_entry = RawVideoFile.objects.select_related("sensitive_meta").filter(**query_filter).order_by('id').first()
30
+
31
+ if not video_entry:
32
+ return Response({"message": "No more videos available."}, status=status.HTTP_404_NOT_FOUND)
33
+
34
+ serializer = VideoFileForMetaSerializer(video_entry)
35
+ return Response(serializer.data, status=status.HTTP_200_OK)
36
+
37
+ except Exception as e:
38
+ return Response({"error": f"Internal server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@@ -0,0 +1,96 @@
1
+ from django.shortcuts import render
2
+ from django.contrib.admin.views.decorators import staff_member_required
3
+
4
+ @staff_member_required # Ensures only staff members can access the page
5
+ def start_examination(request):
6
+ return render(request, 'admin/start_examination.html') # Loads the simple HTML page
7
+
8
+ from django.shortcuts import render
9
+ #from ..models.patient.patient_finding_location import PatientFindingLocation
10
+ from ..models import FindingLocationClassification, FindingLocationClassificationChoice # Correct models
11
+
12
+
13
+ #need to implement one with json data after tesing whethe rthis works or not
14
+ """def get_location_choices(request, location_id):
15
+
16
+ try:
17
+ # Ensure the location exists
18
+ location = FindingLocationClassification.objects.get(id=location_id)
19
+ # Get only choices related to the selected location classification
20
+ #location_choices = FindingLocationClassificationChoice.objects.filter(location_classification=location)
21
+ #its many to may relation so
22
+ location_choices = location.choices.all()
23
+
24
+ except FindingLocationClassification.DoesNotExist:
25
+ location_choices = []
26
+
27
+ # Get previously selected values to retain them after reloading
28
+ selected_location = int(location_id) if location_id else None
29
+
30
+ return render(request, 'admin/patient_finding_intervention.html', {
31
+ "location_choices": location_choices, # Pass updated choices to the template
32
+ "selected_location": location_id, # Keep previous selection
33
+ })
34
+ """
35
+ from django.shortcuts import render
36
+ from rest_framework import viewsets
37
+ from ..models import Patient
38
+ from ..serializers import PatientSerializer
39
+ from rest_framework.permissions import IsAuthenticatedOrReadOnly
40
+
41
+ class PatientViewSet(viewsets.ModelViewSet):
42
+ """API endpoint for managing patients."""
43
+ queryset = Patient.objects.all()
44
+ serializer_class = PatientSerializer
45
+ permission_classes = [IsAuthenticatedOrReadOnly]
46
+
47
+ def perform_create(self, serializer):
48
+ serializer.save()
49
+
50
+ from django.http import JsonResponse
51
+ from ..models import FindingLocationClassification, FindingLocationClassificationChoice
52
+
53
+ def get_location_choices(request, location_id):
54
+ """
55
+ Fetch location choices dynamically based on the selected FindingLocationClassification (Location).
56
+ """
57
+ try:
58
+ location = FindingLocationClassification.objects.get(id=location_id)
59
+ location_choices = location.choices.all() # Get choices via Many-to-Many relationship
60
+ data = [{"id": choice.id, "name": choice.name} for choice in location_choices]
61
+ except FindingLocationClassification.DoesNotExist:
62
+ data = []
63
+
64
+ return JsonResponse({"location_choices": data})
65
+
66
+ from django.http import JsonResponse
67
+ from ..models import FindingMorphologyClassification, FindingMorphologyClassificationChoice, FindingMorphologyClassificationType
68
+ from django.core.exceptions import ObjectDoesNotExist
69
+
70
+
71
+
72
+ def get_morphology_choices(request, morphology_id):
73
+ """
74
+ Fetch morphology choices dynamically based on the selected FindingMorphologyClassification.
75
+ """
76
+ try:
77
+ # Find the selected Morphology Classification
78
+ morphology_classification = FindingMorphologyClassification.objects.get(id=morphology_id)
79
+
80
+ # Fetch choices from FindingMorphologyClassificationType using classification_type_id
81
+ morphology_choices = FindingMorphologyClassificationType.objects.filter(
82
+ id=morphology_classification.classification_type_id
83
+ )
84
+
85
+ # Cpnvert QuerySet to JSON
86
+ data = [{"id": choice.id, "name": choice.name} for choice in morphology_choices]
87
+
88
+ return JsonResponse({"morphology_choices": data}) # Always return JSON
89
+
90
+ except ObjectDoesNotExist:
91
+ return JsonResponse({"error": "Morphology classification not found", "morphology_choices": []}, status=404)
92
+
93
+ except Exception as e:
94
+ print(f"Error fetching morphology choices: {e}") # Debugging Log
95
+ return JsonResponse({"error": "Internal server error", "morphology_choices": []}, status=500)
96
+
@@ -0,0 +1,149 @@
1
+ from rest_framework.views import APIView
2
+ from rest_framework.response import Response
3
+ from rest_framework import status
4
+ from django.http import FileResponse, Http404
5
+ from ..models import RawVideoFile, Label
6
+ from ..serializers.video_segmentation import VideoFileSerializer,VideoListSerializer,LabelSerializer
7
+ import mimetypes
8
+ import os
9
+
10
+
11
+ class VideoView(APIView):
12
+ """
13
+ API endpoint to:
14
+ - Fetch video metadata (JSON)
15
+ - Serve the actual video file dynamically
16
+ """
17
+
18
+ def get(self, request, video_id=None):
19
+ """
20
+ Handles GET requests:
21
+ - If no `video_id` is provided, return a list of all videos for frontend dropdown.
22
+ - If `Accept: application/json` is in the request headers, return metadata for a specific video.
23
+ - Otherwise, return the video file.
24
+ """
25
+ if video_id is None:
26
+ return self.get_all_videos()
27
+ return self.get_video_details(request, video_id)
28
+
29
+ def get_all_videos(self):
30
+ """
31
+ Returns a list of all available videos along with available labels.
32
+ Used to populate the video selection dropdown in Vue.js.
33
+ """
34
+ videos = RawVideoFile.objects.all()
35
+ labels = Label.objects.all() # Fetch all labels
36
+
37
+ video_serializer = VideoListSerializer(videos, many=True)
38
+ label_serializer = LabelSerializer(labels, many=True) # Serialize labels
39
+
40
+ return Response({
41
+ "videos": video_serializer.data, # List of videos
42
+ "labels": label_serializer.data # List of labels
43
+ }, status=status.HTTP_200_OK)
44
+
45
+ def get_video_details(self, request, video_id):
46
+ """
47
+ Returns metadata for a specific video if `Accept: application/json` is set.
48
+ Otherwise, streams the video file.
49
+ """
50
+ try:
51
+ video_entry = RawVideoFile.objects.get(id=video_id)
52
+ serializer = VideoFileSerializer(video_entry, context={'request': request})
53
+
54
+ accept_header = request.headers.get('Accept', '')
55
+
56
+ if "application/json" in accept_header:
57
+ return Response(serializer.data, status=status.HTTP_200_OK)
58
+
59
+ return self.serve_video_file(video_entry)
60
+
61
+ except RawVideoFile.DoesNotExist:
62
+ return Response({"error": "Video not found in the database."}, status=status.HTTP_404_NOT_FOUND)
63
+ except Exception as e:
64
+ return Response({"error": f"Internal error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
65
+
66
+ def serve_video_file(self, video_entry):
67
+ """
68
+ Serves the video file dynamically.
69
+ """
70
+ try:
71
+ full_video_path = video_entry.file.path # Get the correct file path
72
+
73
+ if not os.path.exists(full_video_path):
74
+ raise Http404("Video file not found.")
75
+
76
+ mime_type, _ = mimetypes.guess_type(full_video_path) # Determine the content type (e.g., video/mp4, video/avi)
77
+ response = FileResponse(open(full_video_path, "rb"), content_type=mime_type or "video/mp4")
78
+
79
+ # Enable video streaming and CORS
80
+ response["Access-Control-Allow-Origin"] = "*" # Allow frontend access
81
+ response["Access-Control-Allow-Credentials"] = "true"
82
+ response["Accept-Ranges"] = "bytes" # Enable seeking in video player
83
+ response["Content-Disposition"] = f'inline; filename="{os.path.basename(full_video_path)}"' # Instructs the browser to play the video instead of downloading it.
84
+
85
+ return response
86
+
87
+ except Exception as e:
88
+ return Response({"error": f"Internal error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
89
+
90
+
91
+
92
+ from ..serializers.video_segmentation import VideoFileSerializer
93
+
94
+ class VideoLabelView(APIView):
95
+ """
96
+ API to fetch time segments (start & end times in seconds) for a specific label.
97
+ """
98
+
99
+ def get(self, request, video_id, label_name):
100
+ """
101
+ Handles GET request to return:
102
+ - Time segments for the selected label.
103
+ - Frame-wise predictions within those segments.
104
+ """
105
+ try:
106
+ video_entry = RawVideoFile.objects.get(id=video_id)
107
+
108
+ serializer = VideoFileSerializer(video_entry, context={'request': request})
109
+ label_data = serializer.get_label_time_segments(video_entry)
110
+
111
+ # Ensure the requested label exists
112
+ if label_name not in label_data:
113
+ return Response({"error": f"Label '{label_name}' not found"}, status=status.HTTP_404_NOT_FOUND)
114
+
115
+ return Response({
116
+ "label": label_name,
117
+ "time_segments": label_data[label_name]["time_ranges"],
118
+ "frame_predictions": label_data[label_name]["frame_predictions"]
119
+ }, status=status.HTTP_200_OK)
120
+
121
+ except RawVideoFile.DoesNotExist:
122
+ return Response({"error": "Video not found"}, status=status.HTTP_404_NOT_FOUND)
123
+
124
+ except Exception as e:
125
+ return Response({"error": f"Internal error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
126
+
127
+
128
+ from ..serializers.video_segmentation import LabelSegmentUpdateSerializer, LabelSegmentSerializer
129
+
130
+ class UpdateLabelSegmentsView(APIView):
131
+ """
132
+ API to update or create label segments for a video.
133
+ """
134
+
135
+ def put(self, request, video_id, label_id):
136
+ """
137
+ Handles PUT request to update or create label segments.
138
+ """
139
+ serializer = LabelSegmentUpdateSerializer(data=request.data)
140
+
141
+ if serializer.is_valid():
142
+ result = serializer.save()
143
+ return Response({
144
+ "message": "Segments updated successfully",
145
+ "updated_segments": result["updated_segments"],
146
+ "new_segments": result["new_segments"]
147
+ }, status=status.HTTP_200_OK)
148
+
149
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -0,0 +1,46 @@
1
+ from django.shortcuts import render, get_object_or_404
2
+ from ..models import RawVideoFile
3
+ import json
4
+ from ..serializers.video_segmentation import VideoFileSerializer
5
+
6
+ def video_timeline_view(request, video_id):
7
+ """
8
+ Fetches 'outside' label segments and passes their timestamps to the template.
9
+ """
10
+ video_entry = get_object_or_404(RawVideoFile, id=video_id)
11
+
12
+ # Get metadata (Assumes serializer method exists)
13
+ video_serializer = VideoFileSerializer(video_entry, context={'request': request})
14
+ label_segments = video_serializer.get_label_time_segments(video_entry)
15
+
16
+ segments = []
17
+ frame_markers = [] # Store frame timestamps
18
+ fps = 50 # Fixed FPS
19
+
20
+ # Ensure "outside" label exists
21
+ if "outside" in label_segments:
22
+ for segment in label_segments["outside"]["time_ranges"]:
23
+ segments.append({
24
+ "start_time": segment["start_time"],
25
+ "end_time": segment["end_time"]
26
+ })
27
+
28
+ # Extract frame timestamps for markers
29
+ for frame_num, _ in segment["frames"].items():
30
+ time_in_seconds = frame_num / fps
31
+ frame_markers.append(time_in_seconds)
32
+
33
+ # Set video duration correctly
34
+ video_duration = video_entry.duration if hasattr(video_entry, "duration") and video_entry.duration else 226 # Default to 226 seconds
35
+
36
+ print("Video ID:", video_id)
37
+ print("Segments:", segments)
38
+ print("Video Duration:", video_duration)
39
+
40
+ return render(request, "timeline.html", {
41
+ "video_id": video_id,
42
+ "video_url": video_serializer.get_video_url(video_entry),
43
+ "segments": json.dumps(segments), # Send segment start/end times
44
+ "frame_markers": json.dumps(frame_markers), # Send frame timestamps
45
+ "video_duration": video_duration,
46
+ })
endoreg_db/views.py CHANGED
@@ -1,3 +0,0 @@
1
- from django.shortcuts import render
2
-
3
- # Create your views here.