endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.8.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.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/authz/auth.py +74 -0
- endoreg_db/authz/backends.py +168 -0
- endoreg_db/authz/management/commands/list_routes.py +18 -0
- endoreg_db/authz/middleware.py +83 -0
- endoreg_db/authz/permissions.py +127 -0
- endoreg_db/authz/policy.py +218 -0
- endoreg_db/authz/views_auth.py +66 -0
- endoreg_db/config/env.py +13 -8
- endoreg_db/data/__init__.py +8 -31
- endoreg_db/data/_examples/disease.yaml +55 -0
- endoreg_db/data/_examples/disease_classification.yaml +13 -0
- endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
- endoreg_db/data/_examples/event.yaml +64 -0
- endoreg_db/data/_examples/examination.yaml +72 -0
- endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/_examples/finding/complication.yaml +16 -0
- endoreg_db/data/_examples/finding/data.yaml +105 -0
- endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
- endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
- endoreg_db/data/_examples/finding/outcome.yaml +12 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/_examples/finding_type/data.yaml +43 -0
- endoreg_db/data/_examples/requirement/age.yaml +26 -0
- endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
- endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
- endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
- endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/_examples/requirement/gender.yaml +25 -0
- endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
- endoreg_db/data/_examples/requirement/medication.yaml +93 -0
- endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
- endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
- endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
- endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
- endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
- endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
- endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
- endoreg_db/data/requirement_set/_old_ +109 -0
- endoreg_db/data/requirement_set_type/data.yaml +21 -0
- endoreg_db/data/setup_config.yaml +4 -4
- endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
- endoreg_db/exceptions.py +5 -2
- endoreg_db/helpers/data_loader.py +1 -1
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
- endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
- endoreg_db/management/commands/import_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +1 -1
- endoreg_db/management/commands/init_default_ai_model.py +1 -1
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_requirement_data.py +60 -31
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/setup_endoreg_db.py +14 -10
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/migrations/0001_initial.py +1799 -1300
- endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
- endoreg_db/migrations/_old/0001_initial.py +1857 -0
- endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
- endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
- endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
- endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
- endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
- endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
- endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
- endoreg_db/models/__init__.py +78 -123
- endoreg_db/models/administration/__init__.py +21 -42
- endoreg_db/models/administration/ai/active_model.py +2 -2
- endoreg_db/models/administration/ai/ai_model.py +7 -6
- endoreg_db/models/administration/case/__init__.py +1 -15
- endoreg_db/models/administration/case/case.py +3 -3
- endoreg_db/models/administration/case/case_template/__init__.py +2 -14
- endoreg_db/models/administration/case/case_template/case_template.py +2 -124
- endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
- endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
- endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
- endoreg_db/models/administration/center/center.py +33 -19
- endoreg_db/models/administration/center/center_product.py +12 -9
- endoreg_db/models/administration/center/center_resource.py +25 -19
- endoreg_db/models/administration/center/center_shift.py +21 -17
- endoreg_db/models/administration/center/center_waste.py +16 -8
- endoreg_db/models/administration/person/__init__.py +2 -0
- endoreg_db/models/administration/person/employee/employee.py +10 -5
- endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
- endoreg_db/models/administration/person/employee/employee_type.py +12 -6
- endoreg_db/models/administration/person/examiner/examiner.py +13 -11
- endoreg_db/models/administration/person/patient/__init__.py +2 -0
- endoreg_db/models/administration/person/patient/patient.py +103 -100
- endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
- endoreg_db/models/administration/person/person.py +4 -0
- endoreg_db/models/administration/person/profession/__init__.py +8 -4
- endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
- endoreg_db/models/administration/product/product.py +20 -15
- endoreg_db/models/administration/product/product_material.py +17 -18
- endoreg_db/models/administration/product/product_weight.py +12 -8
- endoreg_db/models/administration/product/reference_product.py +23 -55
- endoreg_db/models/administration/qualification/qualification.py +7 -3
- endoreg_db/models/administration/qualification/qualification_type.py +7 -3
- endoreg_db/models/administration/shift/scheduled_days.py +8 -5
- endoreg_db/models/administration/shift/shift.py +16 -12
- endoreg_db/models/administration/shift/shift_type.py +23 -31
- endoreg_db/models/label/__init__.py +7 -8
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
- endoreg_db/models/label/label.py +15 -15
- endoreg_db/models/label/label_set.py +19 -6
- endoreg_db/models/label/label_type.py +1 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
- endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
- endoreg_db/models/label/video_segmentation_label.py +4 -0
- endoreg_db/models/label/video_segmentation_labelset.py +4 -3
- endoreg_db/models/media/frame/frame.py +22 -22
- endoreg_db/models/media/pdf/raw_pdf.py +249 -177
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +48 -56
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +359 -204
- endoreg_db/models/media/video/video_file_ai.py +288 -74
- endoreg_db/models/media/video/video_file_anonymize.py +38 -38
- endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
- endoreg_db/models/media/video/video_file_io.py +109 -62
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
- endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
- endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
- endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
- endoreg_db/models/media/video/video_file_segments.py +24 -17
- endoreg_db/models/media/video/video_metadata.py +19 -35
- endoreg_db/models/media/video/video_processing.py +96 -95
- endoreg_db/models/medical/contraindication/__init__.py +13 -3
- endoreg_db/models/medical/disease.py +22 -16
- endoreg_db/models/medical/event.py +31 -18
- endoreg_db/models/medical/examination/__init__.py +13 -6
- endoreg_db/models/medical/examination/examination.py +17 -18
- endoreg_db/models/medical/examination/examination_indication.py +26 -25
- endoreg_db/models/medical/examination/examination_time.py +16 -6
- endoreg_db/models/medical/examination/examination_time_type.py +9 -6
- endoreg_db/models/medical/examination/examination_type.py +3 -4
- endoreg_db/models/medical/finding/finding.py +38 -39
- endoreg_db/models/medical/finding/finding_classification.py +37 -48
- endoreg_db/models/medical/finding/finding_intervention.py +27 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +20 -26
- endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
- endoreg_db/models/medical/laboratory/lab_value.py +62 -91
- endoreg_db/models/medical/medication/medication.py +22 -10
- endoreg_db/models/medical/medication/medication_indication.py +29 -3
- endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
- endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
- endoreg_db/models/medical/medication/medication_schedule.py +27 -16
- endoreg_db/models/medical/organ/__init__.py +15 -12
- endoreg_db/models/medical/patient/medication_examples.py +1 -5
- endoreg_db/models/medical/patient/patient_disease.py +20 -23
- endoreg_db/models/medical/patient/patient_event.py +19 -22
- endoreg_db/models/medical/patient/patient_examination.py +48 -54
- endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
- endoreg_db/models/medical/patient/patient_finding.py +122 -139
- endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
- endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
- endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
- endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
- endoreg_db/models/medical/patient/patient_medication.py +27 -38
- endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
- endoreg_db/models/medical/risk/risk.py +7 -6
- endoreg_db/models/medical/risk/risk_type.py +8 -5
- endoreg_db/models/metadata/model_meta.py +60 -29
- endoreg_db/models/metadata/model_meta_logic.py +139 -18
- endoreg_db/models/metadata/pdf_meta.py +19 -24
- endoreg_db/models/metadata/sensitive_meta.py +102 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +383 -43
- endoreg_db/models/metadata/video_meta.py +51 -31
- endoreg_db/models/metadata/video_prediction_logic.py +16 -23
- endoreg_db/models/metadata/video_prediction_meta.py +29 -33
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
- endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
- endoreg_db/models/other/emission/emission_factor.py +18 -8
- endoreg_db/models/other/gender.py +10 -5
- endoreg_db/models/other/information_source.py +25 -25
- endoreg_db/models/other/material.py +9 -5
- endoreg_db/models/other/resource.py +6 -4
- endoreg_db/models/other/tag.py +10 -5
- endoreg_db/models/other/transport_route.py +13 -8
- endoreg_db/models/other/unit.py +10 -6
- endoreg_db/models/other/waste.py +6 -5
- endoreg_db/models/requirement/requirement.py +580 -272
- endoreg_db/models/requirement/requirement_error.py +85 -0
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
- endoreg_db/models/requirement/requirement_operator.py +36 -33
- endoreg_db/models/requirement/requirement_set.py +74 -57
- endoreg_db/models/state/__init__.py +4 -4
- endoreg_db/models/state/abstract.py +2 -2
- endoreg_db/models/state/anonymization.py +12 -0
- endoreg_db/models/state/audit_ledger.py +46 -47
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +40 -46
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +58 -53
- endoreg_db/models/upload_job.py +32 -55
- endoreg_db/models/utils.py +1 -2
- endoreg_db/root_urls.py +21 -2
- endoreg_db/serializers/__init__.py +26 -57
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/meta/report_meta.py +1 -1
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +33 -91
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/anonymization.py +75 -73
- endoreg_db/services/lookup_service.py +256 -73
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +711 -310
- endoreg_db/services/storage_aware_video_processor.py +140 -114
- endoreg_db/services/video_import.py +266 -117
- endoreg_db/urls/__init__.py +27 -27
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +108 -66
- endoreg_db/urls/root_urls.py +29 -0
- endoreg_db/utils/__init__.py +15 -5
- endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
- endoreg_db/utils/case_generator/__init__.py +3 -0
- endoreg_db/utils/dataloader.py +88 -16
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
- endoreg_db/utils/setup_config.py +8 -5
- endoreg_db/utils/storage.py +115 -0
- endoreg_db/utils/validate_endo_roi.py +8 -2
- endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
- endoreg_db/views/__init__.py +5 -12
- endoreg_db/views/anonymization/media_management.py +198 -163
- endoreg_db/views/anonymization/overview.py +4 -1
- endoreg_db/views/anonymization/validate.py +174 -40
- endoreg_db/views/media/__init__.py +2 -0
- endoreg_db/views/media/pdf_media.py +131 -150
- endoreg_db/views/media/sensitive_metadata.py +46 -6
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/media/video_segments.py +187 -260
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +186 -0
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/pdf/reimport.py +86 -91
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +186 -288
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +0 -4
- endoreg_db/views/video/correction.py +2 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +350 -255
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- endoreg_db/models/rule/__init__.py +0 -13
- endoreg_db/models/rule/rule.py +0 -27
- endoreg_db/models/rule/rule_applicator.py +0 -224
- endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
- endoreg_db/models/rule/rule_type.py +0 -20
- endoreg_db/models/rule/ruleset.py +0 -17
- endoreg_db/serializers/video/video_metadata.py +0 -105
- endoreg_db/urls/report.py +0 -48
- endoreg_db/urls/video.py +0 -61
- endoreg_db/utils/case_generator/case_generator.py +0 -159
- endoreg_db/utils/case_generator/utils.py +0 -30
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/report/__init__.py +0 -9
- endoreg_db/views/report/report_list.py +0 -112
- endoreg_db/views/report/report_with_secure_url.py +0 -28
- endoreg_db/views/report/start_examination.py +0 -7
- endoreg_db/views/video/video_media.py +0 -158
- endoreg_db/views.py +0 -0
- /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
- /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
- /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from django.db import models
|
|
2
1
|
from typing import TYPE_CHECKING
|
|
3
2
|
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
|
|
4
6
|
class LabelSetManager(models.Manager):
|
|
5
7
|
"""
|
|
6
8
|
Manager class for handling LabelSet model operations.
|
|
@@ -10,9 +12,17 @@ class LabelSetManager(models.Manager):
|
|
|
10
12
|
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
|
-
def get_by_natural_key(self, name):
|
|
14
|
-
"""Retrieves a LabelSet instance by its natural key (name)."""
|
|
15
|
-
|
|
15
|
+
def get_by_natural_key(self, name, version=None):
|
|
16
|
+
"""Retrieves a LabelSet instance by its natural key (name[, version])."""
|
|
17
|
+
|
|
18
|
+
queryset = self.filter(name=name)
|
|
19
|
+
if version not in (None, "", -1):
|
|
20
|
+
queryset = queryset.filter(version=version)
|
|
21
|
+
|
|
22
|
+
labelset = queryset.order_by("-version").first()
|
|
23
|
+
if not labelset:
|
|
24
|
+
raise self.model.DoesNotExist(f"LabelSet with name='{name}' and version='{version}' not found")
|
|
25
|
+
return labelset
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
class LabelSet(models.Model):
|
|
@@ -33,12 +43,15 @@ class LabelSet(models.Model):
|
|
|
33
43
|
objects = LabelSetManager()
|
|
34
44
|
|
|
35
45
|
if TYPE_CHECKING:
|
|
46
|
+
from typing import cast
|
|
47
|
+
|
|
36
48
|
from .label import Label
|
|
37
|
-
|
|
49
|
+
|
|
50
|
+
labels = cast(models.manager.RelatedManager["Label"], labels)
|
|
38
51
|
|
|
39
52
|
def natural_key(self):
|
|
40
53
|
"""Return the natural key of this label set"""
|
|
41
|
-
return (self.name,)
|
|
54
|
+
return (self.name, self.version)
|
|
42
55
|
|
|
43
56
|
def __str__(self) -> str:
|
|
44
57
|
return str(self.name)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, Optional
|
|
2
2
|
|
|
3
3
|
if TYPE_CHECKING:
|
|
4
|
-
from endoreg_db.models import Label,
|
|
4
|
+
from endoreg_db.models import Label, VideoFile, VideoPredictionMeta
|
|
5
|
+
|
|
6
|
+
__all__ = ["_create_from_video"]
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def _create_from_video(
|
|
@@ -21,15 +23,10 @@ def _create_from_video(
|
|
|
21
23
|
raise ValueError("Source must be a VideoFile instance.")
|
|
22
24
|
|
|
23
25
|
if start_frame_number < 0 or end_frame_number < 0:
|
|
24
|
-
raise ValueError(
|
|
25
|
-
f"Frame numbers must be non-negative: start={start_frame_number}, end={end_frame_number}"
|
|
26
|
-
)
|
|
26
|
+
raise ValueError(f"Frame numbers must be non-negative: start={start_frame_number}, end={end_frame_number}")
|
|
27
27
|
|
|
28
28
|
if start_frame_number > end_frame_number:
|
|
29
|
-
raise ValueError(
|
|
30
|
-
f"Start frame number ({start_frame_number}) must be less than or equal to "
|
|
31
|
-
f"end frame number ({end_frame_number})"
|
|
32
|
-
)
|
|
29
|
+
raise ValueError(f"Start frame number ({start_frame_number}) must be less than or equal to end frame number ({end_frame_number})")
|
|
33
30
|
|
|
34
31
|
segment = cls(
|
|
35
32
|
start_frame_number=start_frame_number,
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
from django.db import models
|
|
2
|
-
from django.db.models import Q, CheckConstraint, F
|
|
3
|
-
from typing import TYPE_CHECKING, Union, Optional, Tuple
|
|
4
|
-
from tqdm import tqdm
|
|
5
1
|
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union, cast
|
|
3
|
+
|
|
6
4
|
from django.core.exceptions import ObjectDoesNotExist
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models import CheckConstraint, F, Q
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
7
9
|
from ._create_from_video import _create_from_video
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from endoreg_db.models import (
|
|
13
|
-
LabelVideoSegmentState,
|
|
14
|
-
VideoFile,
|
|
15
15
|
Frame,
|
|
16
|
-
|
|
16
|
+
ImageClassificationAnnotation,
|
|
17
17
|
InformationSource,
|
|
18
|
+
Label,
|
|
19
|
+
LabelVideoSegmentState,
|
|
18
20
|
ModelMeta,
|
|
19
|
-
VideoPredictionMeta,
|
|
20
21
|
PatientFinding,
|
|
21
|
-
|
|
22
|
+
VideoFile,
|
|
23
|
+
VideoPredictionMeta,
|
|
22
24
|
)
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
|
|
24
27
|
class LabelVideoSegment(models.Model):
|
|
25
28
|
"""
|
|
26
29
|
Represents a labeled segment within a video, defined by start and end frame numbers.
|
|
@@ -28,11 +31,10 @@ class LabelVideoSegment(models.Model):
|
|
|
28
31
|
A segment must be associated with exactly one `VideoFile`.
|
|
29
32
|
If it originates from a prediction, it links to a single `VideoPredictionMeta`.
|
|
30
33
|
"""
|
|
34
|
+
|
|
31
35
|
start_frame_number = models.IntegerField()
|
|
32
36
|
end_frame_number = models.IntegerField()
|
|
33
|
-
source = models.ForeignKey(
|
|
34
|
-
"InformationSource", on_delete=models.SET_NULL, null=True
|
|
35
|
-
)
|
|
37
|
+
source = models.ForeignKey("InformationSource", on_delete=models.SET_NULL, null=True)
|
|
36
38
|
label = models.ForeignKey("Label", on_delete=models.SET_NULL, null=True, blank=True)
|
|
37
39
|
|
|
38
40
|
# Single ForeignKey to the unified VideoFile model
|
|
@@ -61,31 +63,29 @@ class LabelVideoSegment(models.Model):
|
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
if TYPE_CHECKING:
|
|
64
|
-
video_file: "VideoFile"
|
|
65
|
-
label:
|
|
66
|
-
source:
|
|
67
|
-
prediction_meta:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
video_file: models.ForeignKey["VideoFile"]
|
|
67
|
+
label: models.ForeignKey["Label|None"]
|
|
68
|
+
source: models.ForeignKey["InformationSource|None"]
|
|
69
|
+
prediction_meta: models.ForeignKey["VideoPredictionMeta|None"]
|
|
70
|
+
|
|
71
|
+
patient_findings = cast(models.manager.RelatedManager["PatientFinding"], patient_findings)
|
|
72
|
+
model_meta: models.ForeignKey["ModelMeta|None"]
|
|
73
|
+
state: models.OneToOneField["LabelVideoSegmentState"]
|
|
71
74
|
|
|
72
75
|
class Meta:
|
|
73
76
|
constraints = [
|
|
74
|
-
CheckConstraint(
|
|
75
|
-
condition=Q(start_frame_number__lt=F("end_frame_number")),
|
|
76
|
-
name="segment_start_lt_end"
|
|
77
|
-
),
|
|
77
|
+
CheckConstraint(check=Q(start_frame_number__lt=F("end_frame_number")), name="segment_start_lt_end"),
|
|
78
78
|
]
|
|
79
79
|
indexes = [
|
|
80
|
-
models.Index(fields=[
|
|
81
|
-
models.Index(fields=[
|
|
80
|
+
models.Index(fields=["video_file", "label", "start_frame_number"]),
|
|
81
|
+
models.Index(fields=["prediction_meta", "label"]),
|
|
82
82
|
]
|
|
83
83
|
|
|
84
84
|
@property
|
|
85
85
|
def start_time(self) -> float:
|
|
86
86
|
"""
|
|
87
87
|
Return the segment's start time in seconds, calculated from the start frame number and video FPS.
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
Returns:
|
|
90
90
|
float: Start time in seconds. Returns 0.0 if FPS is unavailable or zero.
|
|
91
91
|
"""
|
|
@@ -93,12 +93,12 @@ class LabelVideoSegment(models.Model):
|
|
|
93
93
|
if fps == 0.0:
|
|
94
94
|
return 0.0
|
|
95
95
|
return self.start_frame_number / fps
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
@property
|
|
98
98
|
def end_time(self) -> float:
|
|
99
99
|
"""
|
|
100
100
|
Return the segment's end time in seconds, calculated from the end frame number and video FPS.
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
Returns:
|
|
103
103
|
float: End time in seconds, or 0.0 if FPS is unavailable.
|
|
104
104
|
"""
|
|
@@ -138,55 +138,43 @@ class LabelVideoSegment(models.Model):
|
|
|
138
138
|
Passes additional keyword arguments to extract_frames.
|
|
139
139
|
"""
|
|
140
140
|
from endoreg_db.models import VideoFile
|
|
141
|
+
|
|
141
142
|
if not isinstance(self.video_file, VideoFile):
|
|
142
143
|
raise ValueError("Cannot extract frame files: No associated VideoFile.")
|
|
143
|
-
return self.video_file.extract_specific_frame_range(
|
|
144
|
-
|
|
145
|
-
end_frame=self.end_frame_number,
|
|
146
|
-
overwrite=overwrite,
|
|
147
|
-
**kwargs
|
|
148
|
-
)
|
|
149
|
-
|
|
144
|
+
return self.video_file.extract_specific_frame_range(start_frame=self.start_frame_number, end_frame=self.end_frame_number, overwrite=overwrite, **kwargs)
|
|
145
|
+
|
|
150
146
|
def delete_frame_files(self) -> None:
|
|
151
147
|
"""
|
|
152
148
|
Delete the frame files corresponding to this segment's frame range from the associated video file.
|
|
153
|
-
|
|
149
|
+
|
|
154
150
|
Raises:
|
|
155
151
|
ValueError: If there is no associated VideoFile.
|
|
156
152
|
"""
|
|
157
153
|
from endoreg_db.models import VideoFile
|
|
154
|
+
|
|
158
155
|
if not isinstance(self.video_file, VideoFile):
|
|
159
156
|
raise ValueError("Cannot delete frame files: No associated VideoFile.")
|
|
160
|
-
self.video_file.delete_specific_frame_range(
|
|
161
|
-
|
|
162
|
-
end_frame=self.end_frame_number
|
|
163
|
-
)
|
|
157
|
+
self.video_file.delete_specific_frame_range(start_frame=self.start_frame_number, end_frame=self.end_frame_number)
|
|
158
|
+
|
|
164
159
|
@classmethod
|
|
165
160
|
def safe_create(cls, video_file, label, start_frame_number, end_frame_number, **kwargs):
|
|
166
161
|
"""
|
|
167
162
|
Create a new LabelVideoSegment instance after validating the frame range.
|
|
168
|
-
|
|
163
|
+
|
|
169
164
|
Validates that the provided start and end frame numbers are appropriate for the given video file before creating the segment. Raises a ValueError if validation fails.
|
|
170
|
-
|
|
165
|
+
|
|
171
166
|
Returns:
|
|
172
|
-
|
|
167
|
+
LabelVideoSegment: The newly created segment instance.
|
|
173
168
|
"""
|
|
174
169
|
cls.validate_frame_range(start_frame_number, end_frame_number, video_file=video_file)
|
|
175
|
-
return cls.objects.create(
|
|
176
|
-
|
|
177
|
-
label=label,
|
|
178
|
-
start_frame_number=start_frame_number,
|
|
179
|
-
end_frame_number=end_frame_number,
|
|
180
|
-
**kwargs
|
|
181
|
-
)
|
|
182
|
-
|
|
170
|
+
return cls.objects.create(video_file=video_file, label=label, start_frame_number=start_frame_number, end_frame_number=end_frame_number, **kwargs)
|
|
171
|
+
|
|
183
172
|
def save(self, *args, **kwargs):
|
|
184
173
|
"""
|
|
185
174
|
Saves the LabelVideoSegment instance and ensures its associated state object exists.
|
|
186
|
-
|
|
175
|
+
|
|
187
176
|
Overrides the default save behavior to guarantee that a related LabelVideoSegmentState is created or retrieved after saving.
|
|
188
177
|
"""
|
|
189
|
-
from endoreg_db.models import LabelVideoSegmentState
|
|
190
178
|
# Call the original save method first
|
|
191
179
|
super().save(*args, **kwargs)
|
|
192
180
|
|
|
@@ -206,6 +194,7 @@ class LabelVideoSegment(models.Model):
|
|
|
206
194
|
if it was created.
|
|
207
195
|
"""
|
|
208
196
|
from endoreg_db.models import LabelVideoSegmentState
|
|
197
|
+
|
|
209
198
|
state, created = LabelVideoSegmentState.objects.get_or_create(origin=self)
|
|
210
199
|
return state, created
|
|
211
200
|
|
|
@@ -221,14 +210,7 @@ class LabelVideoSegment(models.Model):
|
|
|
221
210
|
"""
|
|
222
211
|
Create a LabelVideoSegment instance from a VideoFile.
|
|
223
212
|
"""
|
|
224
|
-
return _create_from_video(
|
|
225
|
-
cls,
|
|
226
|
-
source,
|
|
227
|
-
prediction_meta,
|
|
228
|
-
label,
|
|
229
|
-
start_frame_number,
|
|
230
|
-
end_frame_number
|
|
231
|
-
)
|
|
213
|
+
return _create_from_video(cls, source, prediction_meta, label, start_frame_number, end_frame_number)
|
|
232
214
|
|
|
233
215
|
def get_video(self) -> "VideoFile":
|
|
234
216
|
"""Returns the associated VideoFile instance."""
|
|
@@ -249,10 +231,7 @@ class LabelVideoSegment(models.Model):
|
|
|
249
231
|
active_path = video_obj.active_file_path
|
|
250
232
|
video_identifier = active_path.name if active_path else f"UUID {video_obj.uuid}"
|
|
251
233
|
|
|
252
|
-
str_repr =
|
|
253
|
-
f"{video_identifier} Label - {label_name} - "
|
|
254
|
-
f"{self.start_frame_number} - {self.end_frame_number}"
|
|
255
|
-
)
|
|
234
|
+
str_repr = f"{video_identifier} Label - {label_name} - {self.start_frame_number} - {self.end_frame_number}"
|
|
256
235
|
except ObjectDoesNotExist: # More specific exception
|
|
257
236
|
str_repr = f"Segment {self.pk} (Error: Associated VideoFile missing)"
|
|
258
237
|
except ValueError as e: # Catch specific error from get_video
|
|
@@ -266,7 +245,7 @@ class LabelVideoSegment(models.Model):
|
|
|
266
245
|
def get_model_meta(self) -> Optional["ModelMeta"]:
|
|
267
246
|
"""
|
|
268
247
|
Retrieve the associated ModelMeta object from the segment's prediction metadata, if available.
|
|
269
|
-
|
|
248
|
+
|
|
270
249
|
Returns:
|
|
271
250
|
ModelMeta or None: The related ModelMeta instance, or None if no prediction metadata is set.
|
|
272
251
|
"""
|
|
@@ -278,26 +257,24 @@ class LabelVideoSegment(models.Model):
|
|
|
278
257
|
def frames(self) -> Union[models.QuerySet["Frame"], list]:
|
|
279
258
|
"""
|
|
280
259
|
Return all frames within the segment's frame range.
|
|
281
|
-
|
|
260
|
+
|
|
282
261
|
Returns:
|
|
283
262
|
QuerySet[Frame] or list: Frames from the associated video file that fall within the segment's start and end frame numbers. Returns an empty list if the video file is unavailable.
|
|
284
263
|
"""
|
|
285
264
|
return self.get_frames()
|
|
286
265
|
|
|
287
|
-
def get_frames(self) ->
|
|
266
|
+
def get_frames(self) -> models.QuerySet["Frame"]:
|
|
288
267
|
"""
|
|
289
268
|
Retrieve all frames within the segment's frame range from the associated video.
|
|
290
|
-
|
|
269
|
+
|
|
291
270
|
Returns:
|
|
292
271
|
QuerySet[Frame]: Frames with frame numbers in [start_frame_number, end_frame_number) ordered by frame number, or an empty queryset if unavailable.
|
|
293
272
|
"""
|
|
294
273
|
from endoreg_db.models.media.frame import Frame
|
|
274
|
+
|
|
295
275
|
try:
|
|
296
276
|
video_obj = self.get_video()
|
|
297
|
-
return video_obj.frames.filter(
|
|
298
|
-
frame_number__gte=self.start_frame_number,
|
|
299
|
-
frame_number__lt=self.end_frame_number
|
|
300
|
-
).order_by('frame_number')
|
|
277
|
+
return video_obj.frames.filter(frame_number__gte=self.start_frame_number, frame_number__lt=self.end_frame_number).order_by("frame_number")
|
|
301
278
|
except ValueError:
|
|
302
279
|
logger.error("Cannot get frames for segment %s: No associated VideoFile.", self.pk)
|
|
303
280
|
return Frame.objects.none()
|
|
@@ -309,7 +286,7 @@ class LabelVideoSegment(models.Model):
|
|
|
309
286
|
def all_frame_annotations(self) -> models.QuerySet["ImageClassificationAnnotation"]:
|
|
310
287
|
"""
|
|
311
288
|
Return all image classification annotations for frames within this segment that match the segment's label.
|
|
312
|
-
|
|
289
|
+
|
|
313
290
|
Returns:
|
|
314
291
|
QuerySet: ImageClassificationAnnotation objects for frames in the segment with the segment's label. Returns an empty queryset if the segment is not associated with a video.
|
|
315
292
|
"""
|
|
@@ -321,7 +298,7 @@ class LabelVideoSegment(models.Model):
|
|
|
321
298
|
frame__video=video_obj, # Changed frame__video_file to frame__video
|
|
322
299
|
frame__frame_number__gte=self.start_frame_number,
|
|
323
300
|
frame__frame_number__lt=self.end_frame_number,
|
|
324
|
-
label=self.label
|
|
301
|
+
label=self.label,
|
|
325
302
|
)
|
|
326
303
|
except ValueError:
|
|
327
304
|
logger.error("Cannot get annotations for segment %s: No associated VideoFile.", self.pk)
|
|
@@ -331,7 +308,7 @@ class LabelVideoSegment(models.Model):
|
|
|
331
308
|
def frame_predictions(self) -> models.QuerySet["ImageClassificationAnnotation"]:
|
|
332
309
|
"""
|
|
333
310
|
Return prediction annotations for frames within this segment and matching the segment's label.
|
|
334
|
-
|
|
311
|
+
|
|
335
312
|
Returns:
|
|
336
313
|
QuerySet: ImageClassificationAnnotation objects for frames in the segment, filtered by label and information source type "prediction".
|
|
337
314
|
"""
|
|
@@ -344,17 +321,17 @@ class LabelVideoSegment(models.Model):
|
|
|
344
321
|
frame__frame_number__gte=self.start_frame_number,
|
|
345
322
|
frame__frame_number__lt=self.end_frame_number,
|
|
346
323
|
label=self.label,
|
|
347
|
-
information_source__information_source_types__name="prediction"
|
|
324
|
+
information_source__information_source_types__name="prediction",
|
|
348
325
|
)
|
|
349
326
|
except ValueError:
|
|
350
327
|
logger.error("Cannot get predictions for segment %s: No associated VideoFile.", self.pk)
|
|
351
328
|
return ImageClassificationAnnotation.objects.none()
|
|
352
|
-
|
|
329
|
+
|
|
353
330
|
@property
|
|
354
331
|
def manual_frame_annotations(self) -> models.QuerySet["ImageClassificationAnnotation"]:
|
|
355
332
|
"""
|
|
356
333
|
Return manual image classification annotations for frames within this segment and matching the segment's label.
|
|
357
|
-
|
|
334
|
+
|
|
358
335
|
Returns:
|
|
359
336
|
QuerySet: Manual `ImageClassificationAnnotation` objects for the segment's frames and label. Returns an empty queryset if the segment is not associated with a video.
|
|
360
337
|
"""
|
|
@@ -367,7 +344,7 @@ class LabelVideoSegment(models.Model):
|
|
|
367
344
|
frame__frame_number__gte=self.start_frame_number,
|
|
368
345
|
frame__frame_number__lt=self.end_frame_number,
|
|
369
346
|
label=self.label,
|
|
370
|
-
information_source__information_source_types__name="manual_annotation"
|
|
347
|
+
information_source__information_source_types__name="manual_annotation",
|
|
371
348
|
)
|
|
372
349
|
except ValueError:
|
|
373
350
|
logger.error("Cannot get manual annotations for segment %s: No associated VideoFile.", self.pk)
|
|
@@ -376,7 +353,7 @@ class LabelVideoSegment(models.Model):
|
|
|
376
353
|
def get_segment_len_in_s(self) -> float:
|
|
377
354
|
"""
|
|
378
355
|
Return the duration of the video segment in seconds, based on frame numbers and video FPS.
|
|
379
|
-
|
|
356
|
+
|
|
380
357
|
Returns:
|
|
381
358
|
float: Segment duration in seconds, or 0.0 if FPS is invalid or video is unavailable.
|
|
382
359
|
"""
|
|
@@ -406,10 +383,9 @@ class LabelVideoSegment(models.Model):
|
|
|
406
383
|
logger.warning("Segment %s has no label. Cannot find frames without annotation.", self.pk)
|
|
407
384
|
return []
|
|
408
385
|
|
|
409
|
-
annotated_frame_ids = ImageClassificationAnnotation.objects.filter(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
).values_list('frame_id', flat=True)
|
|
386
|
+
annotated_frame_ids = ImageClassificationAnnotation.objects.filter(frame__in=frames_qs.values_list("id", flat=True), label=self.label).values_list(
|
|
387
|
+
"frame_id", flat=True
|
|
388
|
+
)
|
|
413
389
|
|
|
414
390
|
frames_without_annotation = list(frames_qs.exclude(id__in=annotated_frame_ids)[:n_frames])
|
|
415
391
|
return frames_without_annotation
|
|
@@ -417,11 +393,11 @@ class LabelVideoSegment(models.Model):
|
|
|
417
393
|
def generate_annotations(self):
|
|
418
394
|
"""
|
|
419
395
|
Creates image classification annotations for all frames in the segment if the segment is linked to a prediction, avoiding duplicates.
|
|
420
|
-
|
|
396
|
+
|
|
421
397
|
Annotations are generated only if the segment has associated prediction metadata, model metadata, and label. Existing annotations for the same frame, label, model, and information source are not duplicated. Uses bulk creation for efficiency.
|
|
422
398
|
"""
|
|
423
399
|
if not self.prediction_meta:
|
|
424
|
-
logger.info("Skipping annotation generation for segment %s: Requires linked VideoPredictionMeta.", self.
|
|
400
|
+
logger.info("Skipping annotation generation for segment %s: Requires linked VideoPredictionMeta.", self.pk)
|
|
425
401
|
return
|
|
426
402
|
|
|
427
403
|
from endoreg_db.models import ImageClassificationAnnotation, InformationSource
|
|
@@ -434,27 +410,27 @@ class LabelVideoSegment(models.Model):
|
|
|
434
410
|
label = self.label
|
|
435
411
|
|
|
436
412
|
if not model_meta or not label:
|
|
437
|
-
logger.warning("Missing model_meta or label for segment %s. Skipping annotation generation.", self.
|
|
413
|
+
logger.warning("Missing model_meta or label for segment %s. Skipping annotation generation.", self.pk)
|
|
438
414
|
return
|
|
439
415
|
|
|
440
|
-
frames_queryset = self.get_frames().only(
|
|
416
|
+
frames_queryset = self.get_frames().only("id")
|
|
441
417
|
if not isinstance(frames_queryset, models.QuerySet):
|
|
442
|
-
logger.error("Could not get frame queryset for segment %s. Skipping.", self.
|
|
418
|
+
logger.error("Could not get frame queryset for segment %s. Skipping.", self.pk)
|
|
443
419
|
return
|
|
444
420
|
|
|
445
421
|
existing_annotation_frame_ids = set(
|
|
446
422
|
ImageClassificationAnnotation.objects.filter(
|
|
447
|
-
frame_id__in=frames_queryset.values(
|
|
423
|
+
frame_id__in=frames_queryset.values("id"),
|
|
448
424
|
label=label,
|
|
449
425
|
model_meta=model_meta,
|
|
450
426
|
information_source=information_source,
|
|
451
|
-
).values_list(
|
|
427
|
+
).values_list("frame_id", flat=True)
|
|
452
428
|
)
|
|
453
429
|
|
|
454
430
|
annotations_to_create = []
|
|
455
431
|
frames_to_annotate = frames_queryset.exclude(id__in=existing_annotation_frame_ids)
|
|
456
432
|
|
|
457
|
-
for frame in tqdm(frames_to_annotate.iterator(), total=frames_to_annotate.count(), desc=f"Preparing annotations for segment {self.
|
|
433
|
+
for frame in tqdm(frames_to_annotate.iterator(), total=frames_to_annotate.count(), desc=f"Preparing annotations for segment {self.pk} ({label.name})"):
|
|
458
434
|
annotations_to_create.append(
|
|
459
435
|
ImageClassificationAnnotation(
|
|
460
436
|
frame=frame,
|
|
@@ -466,16 +442,16 @@ class LabelVideoSegment(models.Model):
|
|
|
466
442
|
)
|
|
467
443
|
|
|
468
444
|
if annotations_to_create:
|
|
469
|
-
logger.info("Bulk creating %d annotations for segment %s...", len(annotations_to_create), self.
|
|
445
|
+
logger.info("Bulk creating %d annotations for segment %s...", len(annotations_to_create), self.pk)
|
|
470
446
|
ImageClassificationAnnotation.objects.bulk_create(annotations_to_create, ignore_conflicts=True)
|
|
471
447
|
logger.info("Bulk creation complete.")
|
|
472
448
|
else:
|
|
473
|
-
logger.info("No new annotations needed for segment %s.", self.
|
|
449
|
+
logger.info("No new annotations needed for segment %s.", self.pk)
|
|
474
450
|
|
|
475
451
|
def _get_fps_safe(self):
|
|
476
452
|
"""
|
|
477
453
|
Safely retrieves the frames per second (FPS) value from the associated video.
|
|
478
|
-
|
|
454
|
+
|
|
479
455
|
Returns:
|
|
480
456
|
float: The FPS of the associated video, or 0.0 if unavailable or invalid.
|
|
481
457
|
"""
|
|
@@ -488,12 +464,12 @@ class LabelVideoSegment(models.Model):
|
|
|
488
464
|
def validate_frame_range(start_frame_number: int, end_frame_number: int, video_file=None):
|
|
489
465
|
"""
|
|
490
466
|
Validate that the provided frame numbers define a valid segment range, optionally checking against a video's frame count.
|
|
491
|
-
|
|
467
|
+
|
|
492
468
|
Parameters:
|
|
493
469
|
start_frame_number (int): The starting frame number of the segment.
|
|
494
470
|
end_frame_number (int): The ending frame number of the segment.
|
|
495
471
|
video_file: Optional video file object to validate frame numbers against its frame count.
|
|
496
|
-
|
|
472
|
+
|
|
497
473
|
Raises:
|
|
498
474
|
ValueError: If frame numbers are not integers, are negative, are out of order, or exceed the video's frame count.
|
|
499
475
|
"""
|
|
@@ -504,8 +480,6 @@ class LabelVideoSegment(models.Model):
|
|
|
504
480
|
if end_frame_number < start_frame_number:
|
|
505
481
|
raise ValueError("end_frame_number must be equal or greater than start_frame_number.")
|
|
506
482
|
if video_file is not None:
|
|
507
|
-
frame_count = getattr(video_file,
|
|
483
|
+
frame_count = getattr(video_file, "frame_count", None)
|
|
508
484
|
if frame_count is not None and end_frame_number > frame_count:
|
|
509
485
|
raise ValueError(f"end_frame_number ({end_frame_number}) exceeds video frame count ({frame_count}).")
|
|
510
|
-
|
|
511
|
-
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class VideoSegmentationLabelManager(models.Manager):
|
|
4
5
|
"""
|
|
5
6
|
Manager for VideoSegmentationLabel with custom query methods.
|
|
6
7
|
"""
|
|
8
|
+
|
|
7
9
|
def get_by_natural_key(self, name: str) -> "VideoSegmentationLabel":
|
|
8
10
|
return self.get(name=name)
|
|
9
11
|
|
|
12
|
+
|
|
10
13
|
class VideoSegmentationLabel(models.Model):
|
|
11
14
|
"""
|
|
12
15
|
Represents a label for video segmentation annotations.
|
|
@@ -17,6 +20,7 @@ class VideoSegmentationLabel(models.Model):
|
|
|
17
20
|
color (str): The color associated with the label.
|
|
18
21
|
order_priority (int): The priority for ordering labels.
|
|
19
22
|
"""
|
|
23
|
+
|
|
20
24
|
objects = VideoSegmentationLabelManager()
|
|
21
25
|
|
|
22
26
|
name = models.CharField(max_length=255)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import TYPE_CHECKING, cast
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from django.db import models
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from endoreg_db.models import VideoSegmentationLabel
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class VideoSegmentationLabelSetManager(models.Manager):
|
|
9
10
|
def get_by_natural_key(self, name):
|
|
10
11
|
return self.get(name=name)
|
|
@@ -18,7 +19,7 @@ class VideoSegmentationLabelSet(models.Model):
|
|
|
18
19
|
objects = VideoSegmentationLabelSetManager()
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
|
-
labels
|
|
22
|
+
labels = cast(models.manager.RelatedManager["VideoSegmentationLabel"], labels)
|
|
22
23
|
|
|
23
24
|
def natural_key(self):
|
|
24
25
|
return (self.name,)
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import TYPE_CHECKING, Optional
|
|
3
|
-
from django.db import models
|
|
4
2
|
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
5
|
import cv2
|
|
6
6
|
import numpy as np
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
|
-
from endoreg_db.models import
|
|
10
|
+
from endoreg_db.models import ImageClassificationAnnotation, VideoFile
|
|
9
11
|
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
14
|
+
|
|
12
15
|
# Unified Frame model
|
|
13
16
|
class Frame(models.Model):
|
|
14
17
|
video = models.ForeignKey(
|
|
@@ -25,61 +28,58 @@ class Frame(models.Model):
|
|
|
25
28
|
|
|
26
29
|
if TYPE_CHECKING:
|
|
27
30
|
image_classification_annotations: models.QuerySet["ImageClassificationAnnotation"]
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
video: models.ForeignKey["VideoFile"]
|
|
32
|
+
|
|
30
33
|
class Meta:
|
|
31
|
-
unique_together = (
|
|
32
|
-
ordering = [
|
|
34
|
+
unique_together = ("video", "frame_number")
|
|
35
|
+
ordering = ["video", "frame_number"]
|
|
33
36
|
|
|
34
37
|
@property
|
|
35
38
|
def file_path(self) -> Path:
|
|
36
39
|
"""
|
|
37
40
|
Return the absolute filesystem path to the frame image by combining the video's frame directory with the frame's relative path.
|
|
38
|
-
|
|
41
|
+
|
|
39
42
|
Returns:
|
|
40
43
|
Path: The absolute path to the frame image file.
|
|
41
44
|
"""
|
|
42
45
|
base_dir = self.video.get_frame_dir_path()
|
|
46
|
+
assert base_dir is not None, "Video frame directory path should not be None"
|
|
43
47
|
return base_dir / self.relative_path
|
|
44
|
-
|
|
48
|
+
|
|
45
49
|
@property
|
|
46
50
|
def predictions(self) -> models.QuerySet["ImageClassificationAnnotation"]:
|
|
47
51
|
"""
|
|
48
52
|
Return all image classification annotations for this frame that are linked to an information source of type "prediction".
|
|
49
|
-
|
|
53
|
+
|
|
50
54
|
Returns:
|
|
51
55
|
QuerySet: A queryset of related ImageClassificationAnnotation objects filtered to those whose information source type is "prediction".
|
|
52
56
|
"""
|
|
53
|
-
return self.image_classification_annotations.filter(
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
+
return self.image_classification_annotations.filter(information_source__information_source_types__name="prediction")
|
|
58
|
+
|
|
57
59
|
@property
|
|
58
60
|
def manual_annotations(self) -> models.QuerySet["ImageClassificationAnnotation"]:
|
|
59
61
|
"""
|
|
60
62
|
Return all manual image classification annotations associated with this frame.
|
|
61
|
-
|
|
63
|
+
|
|
62
64
|
Returns:
|
|
63
65
|
QuerySet: A queryset of related ImageClassificationAnnotation objects whose information source type is "manual_annotation".
|
|
64
66
|
"""
|
|
65
|
-
return self.image_classification_annotations.filter(
|
|
66
|
-
information_source__information_source_types__name="manual_annotation"
|
|
67
|
-
)
|
|
67
|
+
return self.image_classification_annotations.filter(information_source__information_source_types__name="manual_annotation")
|
|
68
68
|
|
|
69
69
|
@property
|
|
70
70
|
def has_predictions(self) -> bool:
|
|
71
71
|
"""
|
|
72
72
|
Returns True if the frame has any associated prediction annotations.
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
A prediction annotation is defined as an ImageClassificationAnnotation whose information source type is "prediction".
|
|
75
75
|
"""
|
|
76
76
|
return self.predictions.exists()
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
@property
|
|
79
79
|
def has_manual_annotations(self) -> bool:
|
|
80
80
|
"""
|
|
81
81
|
Returns True if the frame has any manual image classification annotations.
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
Manual annotations are identified as related ImageClassificationAnnotation objects whose information source type is named "manual_annotation".
|
|
84
84
|
"""
|
|
85
85
|
return self.manual_annotations.exists()
|
|
@@ -87,7 +87,7 @@ class Frame(models.Model):
|
|
|
87
87
|
def get_image(self) -> Optional[np.ndarray]:
|
|
88
88
|
"""
|
|
89
89
|
Load and return the frame image as a NumPy array using OpenCV.
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
Returns:
|
|
92
92
|
The image as a NumPy array if successfully loaded, or None if the file does not exist or cannot be read.
|
|
93
93
|
"""
|