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.
- endoreg_db/admin.py +90 -1
- endoreg_db/case_generator/case_generator.py +159 -0
- endoreg_db/case_generator/lab_sample_factory.py +33 -0
- endoreg_db/case_generator/utils.py +30 -0
- endoreg_db/data/__init__.py +50 -4
- endoreg_db/data/ai_model/data.yaml +7 -0
- endoreg_db/data/{label → ai_model_label}/label/data.yaml +27 -1
- endoreg_db/data/ai_model_label/label-set/data.yaml +21 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +5 -0
- endoreg_db/data/ai_model_video_segmentation_label/base_segmentation.yaml +176 -0
- endoreg_db/data/ai_model_video_segmentation_labelset/data.yaml +20 -0
- endoreg_db/data/center/data.yaml +35 -5
- endoreg_db/data/contraindication/bleeding.yaml +11 -0
- endoreg_db/data/distribution/numeric/data.yaml +14 -0
- endoreg_db/data/endoscope/data.yaml +93 -0
- endoreg_db/data/examination_indication/endoscopy.yaml +8 -0
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +8 -0
- endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +101 -0
- endoreg_db/data/finding/data.yaml +141 -0
- endoreg_db/data/finding_intervention/endoscopy.yaml +138 -0
- endoreg_db/data/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/finding_location_classification/colonoscopy.yaml +46 -0
- endoreg_db/data/finding_location_classification_choice/colonoscopy.yaml +240 -0
- endoreg_db/data/finding_morphology_classification/colonoscopy.yaml +48 -0
- endoreg_db/data/finding_morphology_classification_choice/colon_lesion_circularity_default.yaml +34 -0
- endoreg_db/data/finding_morphology_classification_choice/colon_lesion_nice.yaml +20 -0
- endoreg_db/data/finding_morphology_classification_choice/colon_lesion_paris.yaml +65 -0
- endoreg_db/data/finding_morphology_classification_choice/colon_lesion_planarity_default.yaml +56 -0
- endoreg_db/data/finding_morphology_classification_choice/colon_lesion_surface_intact_default.yaml +39 -0
- endoreg_db/data/finding_morphology_classification_choice/colonoscopy_size.yaml +57 -0
- endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +79 -0
- endoreg_db/data/finding_type/data.yaml +30 -0
- endoreg_db/data/gender/data.yaml +17 -0
- endoreg_db/data/lab_value/cardiac_enzymes.yaml +7 -1
- endoreg_db/data/lab_value/coagulation.yaml +6 -1
- endoreg_db/data/lab_value/electrolytes.yaml +39 -1
- endoreg_db/data/lab_value/gastrointestinal_function.yaml +12 -0
- endoreg_db/data/lab_value/hematology.yaml +17 -2
- endoreg_db/data/lab_value/hormones.yaml +6 -0
- endoreg_db/data/lab_value/lipids.yaml +12 -3
- endoreg_db/data/lab_value/misc.yaml +5 -2
- endoreg_db/data/lab_value/renal_function.yaml +2 -1
- endoreg_db/data/lx_client_tag/base.yaml +54 -0
- endoreg_db/data/lx_client_type/base.yaml +30 -0
- endoreg_db/data/lx_permission/base.yaml +24 -0
- endoreg_db/data/lx_permission/endoreg.yaml +52 -0
- endoreg_db/data/medication_indication/anticoagulation.yaml +44 -49
- endoreg_db/data/names_first/first_names.yaml +51 -0
- endoreg_db/data/names_last/last_names.yaml +51 -0
- endoreg_db/data/network_device/data.yaml +30 -0
- endoreg_db/data/organ/data.yaml +29 -0
- endoreg_db/data/pdf_type/data.yaml +2 -1
- endoreg_db/data/report_reader_flag/ukw-examination-generic.yaml +4 -0
- endoreg_db/forms/__init__.py +3 -1
- endoreg_db/forms/examination_form.py +11 -0
- endoreg_db/forms/patient_finding_intervention_form.py +19 -0
- endoreg_db/forms/patient_form.py +26 -0
- endoreg_db/management/commands/__init__.py +0 -0
- endoreg_db/management/commands/load_ai_model_data.py +57 -23
- endoreg_db/management/commands/load_ai_model_label_data.py +59 -0
- endoreg_db/management/commands/load_base_db_data.py +160 -118
- endoreg_db/management/commands/{load_endoscope_type_data.py → load_contraindication_data.py} +3 -7
- endoreg_db/management/commands/load_disease_data.py +29 -7
- endoreg_db/management/commands/load_endoscope_data.py +68 -0
- endoreg_db/management/commands/load_examination_indication_data.py +65 -0
- endoreg_db/management/commands/load_finding_data.py +171 -0
- endoreg_db/management/commands/load_lab_value_data.py +3 -3
- endoreg_db/management/commands/load_lx_data.py +64 -0
- endoreg_db/management/commands/load_medication_data.py +83 -21
- endoreg_db/management/commands/load_name_data.py +37 -0
- endoreg_db/management/commands/{load_endoscopy_processor_data.py → load_organ_data.py} +7 -9
- endoreg_db/migrations/0001_initial.py +1206 -728
- endoreg_db/migrations/0002_alter_frame_image_alter_rawframe_image.py +23 -0
- endoreg_db/migrations/0003_alter_frame_image_alter_rawframe_image.py +23 -0
- endoreg_db/migrations/0004_alter_rawvideofile_file_alter_video_file.py +25 -0
- endoreg_db/migrations/0005_rawvideofile_frame_count_and_more.py +33 -0
- endoreg_db/migrations/0006_frame_extracted_rawframe_extracted.py +23 -0
- endoreg_db/migrations/0007_rename_pseudo_patient_video_patient_and_more.py +24 -0
- endoreg_db/migrations/0008_remove_reportfile_patient_examination_and_more.py +48 -0
- endoreg_db/models/__init__.py +331 -28
- endoreg_db/models/ai_model/__init__.py +1 -0
- endoreg_db/models/ai_model/ai_model.py +103 -0
- endoreg_db/models/ai_model/lightning/__init__.py +3 -0
- endoreg_db/models/ai_model/lightning/inference_dataset.py +53 -0
- endoreg_db/models/ai_model/lightning/multilabel_classification_net.py +155 -0
- endoreg_db/models/ai_model/lightning/postprocess.py +53 -0
- endoreg_db/models/ai_model/lightning/predict.py +172 -0
- endoreg_db/models/ai_model/lightning/prediction_visualizer.py +55 -0
- endoreg_db/models/ai_model/lightning/preprocess.py +68 -0
- endoreg_db/models/ai_model/lightning/run_visualizer.py +21 -0
- endoreg_db/models/ai_model/model_meta.py +232 -6
- endoreg_db/models/ai_model/model_type.py +13 -3
- endoreg_db/models/annotation/__init__.py +31 -2
- endoreg_db/models/annotation/anonymized_image_annotation.py +73 -18
- endoreg_db/models/annotation/binary_classification_annotation_task.py +94 -57
- endoreg_db/models/annotation/image_classification.py +73 -14
- endoreg_db/models/annotation/video_segmentation_annotation.py +52 -0
- endoreg_db/models/annotation/video_segmentation_labelset.py +20 -0
- endoreg_db/models/case/__init__.py +1 -0
- endoreg_db/models/{persons/patient/case → case}/case.py +4 -0
- endoreg_db/models/case_template/__init__.py +10 -1
- endoreg_db/models/case_template/case_template.py +57 -13
- endoreg_db/models/case_template/case_template_rule.py +5 -5
- endoreg_db/models/case_template/case_template_rule_value.py +19 -4
- endoreg_db/models/center/__init__.py +7 -0
- endoreg_db/models/center/center.py +31 -5
- endoreg_db/models/center/center_product.py +0 -1
- endoreg_db/models/center/center_resource.py +16 -2
- endoreg_db/models/center/center_waste.py +6 -1
- endoreg_db/models/contraindication/__init__.py +21 -0
- endoreg_db/models/data_file/__init__.py +38 -5
- endoreg_db/models/data_file/base_classes/__init__.py +6 -1
- endoreg_db/models/data_file/base_classes/abstract_frame.py +64 -15
- endoreg_db/models/data_file/base_classes/abstract_pdf.py +136 -0
- endoreg_db/models/data_file/base_classes/abstract_video.py +744 -138
- endoreg_db/models/data_file/base_classes/frame_helpers.py +17 -0
- endoreg_db/models/data_file/base_classes/prepare_bulk_frames.py +19 -0
- endoreg_db/models/data_file/base_classes/utils.py +80 -0
- endoreg_db/models/data_file/frame.py +22 -38
- endoreg_db/models/data_file/import_classes/__init__.py +4 -18
- endoreg_db/models/data_file/import_classes/raw_pdf.py +162 -90
- endoreg_db/models/data_file/import_classes/raw_video.py +239 -294
- endoreg_db/models/data_file/metadata/__init__.py +10 -0
- endoreg_db/models/data_file/metadata/pdf_meta.py +4 -0
- endoreg_db/models/data_file/metadata/sensitive_meta.py +265 -6
- endoreg_db/models/data_file/metadata/video_meta.py +116 -50
- endoreg_db/models/data_file/report_file.py +30 -63
- endoreg_db/models/data_file/video/__init__.py +6 -2
- endoreg_db/models/data_file/video/video.py +187 -16
- endoreg_db/models/data_file/video_segment.py +162 -55
- endoreg_db/models/disease.py +25 -2
- endoreg_db/models/emission/__init__.py +5 -1
- endoreg_db/models/emission/emission_factor.py +71 -6
- endoreg_db/models/event.py +51 -0
- endoreg_db/models/examination/__init__.py +6 -1
- endoreg_db/models/examination/examination.py +53 -12
- endoreg_db/models/examination/examination_indication.py +170 -0
- endoreg_db/models/examination/examination_time.py +31 -5
- endoreg_db/models/examination/examination_time_type.py +28 -4
- endoreg_db/models/examination/examination_type.py +28 -6
- endoreg_db/models/finding/__init__.py +11 -0
- endoreg_db/models/finding/finding.py +75 -0
- endoreg_db/models/finding/finding_intervention.py +60 -0
- endoreg_db/models/finding/finding_location_classification.py +94 -0
- endoreg_db/models/finding/finding_morphology_classification.py +89 -0
- endoreg_db/models/finding/finding_type.py +22 -0
- endoreg_db/models/hardware/endoscope.py +16 -0
- endoreg_db/models/hardware/endoscopy_processor.py +31 -19
- endoreg_db/models/label/label.py +35 -7
- endoreg_db/models/laboratory/lab_value.py +12 -3
- endoreg_db/models/logging/__init__.py +8 -1
- endoreg_db/models/lx/__init__.py +4 -0
- endoreg_db/models/lx/client.py +57 -0
- endoreg_db/models/lx/identity.py +34 -0
- endoreg_db/models/lx/permission.py +18 -0
- endoreg_db/models/lx/user.py +16 -0
- endoreg_db/models/medication/__init__.py +19 -1
- endoreg_db/models/medication/medication.py +7 -122
- endoreg_db/models/medication/medication_indication.py +50 -0
- endoreg_db/models/medication/medication_indication_type.py +34 -0
- endoreg_db/models/medication/medication_intake_time.py +26 -0
- endoreg_db/models/medication/medication_schedule.py +37 -0
- endoreg_db/models/network/__init__.py +7 -1
- endoreg_db/models/network/network_device.py +13 -8
- endoreg_db/models/organ/__init__.py +38 -0
- endoreg_db/models/other/__init__.py +19 -1
- endoreg_db/models/other/distribution/__init__.py +44 -0
- endoreg_db/models/other/distribution/base_value_distribution.py +20 -0
- endoreg_db/models/other/distribution/date_value_distribution.py +91 -0
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +32 -0
- endoreg_db/models/other/distribution/numeric_value_distribution.py +97 -0
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +22 -0
- endoreg_db/models/other/distribution.py +1 -211
- endoreg_db/models/other/material.py +4 -0
- endoreg_db/models/other/transport_route.py +2 -1
- endoreg_db/models/patient/__init__.py +24 -0
- endoreg_db/models/patient/patient_examination.py +182 -0
- endoreg_db/models/patient/patient_finding.py +143 -0
- endoreg_db/models/patient/patient_finding_intervention.py +26 -0
- endoreg_db/models/patient/patient_finding_location.py +120 -0
- endoreg_db/models/patient/patient_finding_morphology.py +166 -0
- endoreg_db/models/persons/__init__.py +29 -2
- endoreg_db/models/persons/examiner/examiner.py +48 -4
- endoreg_db/models/persons/patient/__init__.py +1 -1
- endoreg_db/models/persons/patient/patient.py +227 -54
- endoreg_db/models/persons/patient/patient_disease.py +6 -0
- endoreg_db/models/persons/patient/patient_event.py +31 -1
- endoreg_db/models/persons/patient/patient_examination_indication.py +32 -0
- endoreg_db/models/persons/patient/patient_lab_sample.py +4 -2
- endoreg_db/models/persons/patient/patient_lab_value.py +37 -16
- endoreg_db/models/persons/patient/patient_medication.py +27 -12
- endoreg_db/models/persons/patient/patient_medication_schedule.py +62 -2
- endoreg_db/models/prediction/__init__.py +7 -1
- endoreg_db/models/prediction/image_classification.py +20 -6
- endoreg_db/models/prediction/video_prediction_meta.py +151 -89
- endoreg_db/models/product/__init__.py +10 -1
- endoreg_db/models/product/product.py +15 -2
- endoreg_db/models/product/product_group.py +8 -0
- endoreg_db/models/product/product_material.py +4 -0
- endoreg_db/models/product/product_weight.py +12 -0
- endoreg_db/models/product/reference_product.py +19 -3
- endoreg_db/models/quiz/__init__.py +8 -1
- endoreg_db/models/report_reader/__init__.py +6 -1
- endoreg_db/serializers/__init__.py +1 -1
- endoreg_db/serializers/annotation.py +2 -5
- endoreg_db/serializers/frame.py +1 -5
- endoreg_db/serializers/patient.py +26 -3
- endoreg_db/serializers/prediction.py +2 -7
- endoreg_db/serializers/raw_video_meta_validation.py +13 -0
- endoreg_db/serializers/video.py +6 -13
- endoreg_db/serializers/video_segmentation.py +492 -0
- endoreg_db/templates/admin/patient_finding_intervention.html +253 -0
- endoreg_db/templates/admin/start_examination.html +12 -0
- endoreg_db/templates/timeline.html +176 -0
- endoreg_db/urls.py +173 -0
- endoreg_db/utils/__init__.py +36 -1
- endoreg_db/utils/dataloader.py +45 -19
- endoreg_db/utils/dates.py +39 -0
- endoreg_db/utils/hashs.py +122 -4
- endoreg_db/utils/names.py +74 -0
- endoreg_db/utils/parse_and_generate_yaml.py +46 -0
- endoreg_db/utils/pydantic_models/__init__.py +6 -0
- endoreg_db/utils/pydantic_models/db_config.py +57 -0
- endoreg_db/utils/validate_endo_roi.py +19 -0
- endoreg_db/utils/validate_subcategory_dict.py +91 -0
- endoreg_db/utils/video/__init__.py +13 -0
- endoreg_db/utils/video/extract_frames.py +121 -0
- endoreg_db/utils/video/transcode_videofile.py +111 -0
- endoreg_db/views/__init__.py +2 -0
- endoreg_db/views/csrf.py +7 -0
- endoreg_db/views/patient_views.py +90 -0
- endoreg_db/views/raw_video_meta_validation_views.py +38 -0
- endoreg_db/views/report_views.py +96 -0
- endoreg_db/views/video_segmentation_views.py +149 -0
- endoreg_db/views/views_for_timeline.py +46 -0
- endoreg_db/views.py +0 -3
- endoreg_db-0.6.1.dist-info/METADATA +151 -0
- endoreg_db-0.6.1.dist-info/RECORD +420 -0
- {endoreg_db-0.5.3.dist-info → endoreg_db-0.6.1.dist-info}/WHEEL +1 -1
- endoreg_db/data/active_model/data.yaml +0 -3
- endoreg_db/data/label/label-set/data.yaml +0 -18
- endoreg_db/management/commands/delete_legacy_images.py +0 -19
- endoreg_db/management/commands/delete_legacy_videos.py +0 -17
- endoreg_db/management/commands/extract_legacy_video_frames.py +0 -18
- endoreg_db/management/commands/import_legacy_images.py +0 -94
- endoreg_db/management/commands/import_legacy_videos.py +0 -76
- endoreg_db/management/commands/load_label_data.py +0 -67
- endoreg_db/migrations/0002_anonymizedimagelabel_anonymousimageannotation_and_more.py +0 -55
- endoreg_db/migrations/0003_anonymousimageannotation_original_image_url_and_more.py +0 -39
- endoreg_db/migrations/0004_alter_rawpdffile_file.py +0 -20
- endoreg_db/migrations/0005_uploadedfile_alter_rawpdffile_file_anonymizedfile.py +0 -40
- endoreg_db/migrations/0006_alter_rawpdffile_file.py +0 -20
- endoreg_db/migrations/0007_networkdevicelogentry_datetime_and_more.py +0 -43
- endoreg_db/migrations/0008_networkdevicelogentry_aglnet_ip_and_more.py +0 -28
- endoreg_db/migrations/0009_alter_networkdevicelogentry_vpn_service_status.py +0 -18
- endoreg_db/migrations/0010_remove_networkdevicelogentry_hostname.py +0 -17
- endoreg_db/models/legacy_data/__init__.py +0 -3
- endoreg_db/models/legacy_data/image.py +0 -34
- endoreg_db/models/patient_examination/__init__.py +0 -35
- endoreg_db/utils/video_metadata.py +0 -87
- endoreg_db-0.5.3.dist-info/METADATA +0 -28
- endoreg_db-0.5.3.dist-info/RECORD +0 -319
- /endoreg_db/{models/persons/patient/case → case_generator}/__init__.py +0 -0
- /endoreg_db/data/{label → ai_model_label}/label-type/data.yaml +0 -0
- /endoreg_db/data/{model_type → ai_model_type}/data.yaml +0 -0
- /endoreg_db/{data/distribution/numeric/.init → management/__init__.py} +0 -0
- /endoreg_db/management/commands/{load_report_reader_flag.py → load_report_reader_flag_data.py} +0 -0
- {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
|
endoreg_db/views/csrf.py
ADDED
|
@@ -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