endoreg-db 0.6.2__py3-none-any.whl → 0.6.4__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/data/__init__.py +14 -0
- endoreg_db/data/disease_classification/chronic_kidney_disease.yaml +2 -2
- endoreg_db/data/disease_classification_choice/chronic_kidney_disease.yaml +6 -6
- endoreg_db/data/distribution/numeric/data.yaml +1 -1
- endoreg_db/data/examination/examinations/data.yaml +22 -21
- endoreg_db/data/examination/type/data.yaml +12 -0
- endoreg_db/data/examination_indication/endoscopy.yaml +417 -1
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +157 -5
- endoreg_db/data/finding/data.yaml +18 -11
- endoreg_db/data/finding_intervention/endoscopy.yaml +26 -121
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +163 -0
- endoreg_db/data/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/information_source/endoscopy_guidelines.yaml +7 -0
- endoreg_db/data/medication_indication/anticoagulation.yaml +4 -4
- endoreg_db/data/pdf_type/data.yaml +9 -16
- endoreg_db/data/requirement/colonoscopy_indications.yaml +56 -0
- endoreg_db/data/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +38 -0
- endoreg_db/data/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/requirement/disease_renal.yaml +80 -0
- endoreg_db/data/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/requirement/lab_value.yaml +120 -0
- endoreg_db/data/requirement_operator/lab_operators.yaml +128 -0
- endoreg_db/data/requirement_operator/model_operators.yaml +90 -0
- endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +12 -0
- endoreg_db/data/requirement_set_type/data.yaml +20 -0
- endoreg_db/data/requirement_type/requirement_types.yaml +83 -0
- endoreg_db/data/risk/bleeding.yaml +26 -0
- endoreg_db/data/risk/thrombosis.yaml +37 -0
- endoreg_db/data/risk_type/data.yaml +27 -0
- endoreg_db/data/unit/time.yaml +36 -1
- endoreg_db/management/commands/load_base_db_data.py +14 -1
- endoreg_db/management/commands/load_center_data.py +46 -21
- endoreg_db/management/commands/load_examination_indication_data.py +49 -27
- endoreg_db/management/commands/load_requirement_data.py +156 -0
- endoreg_db/management/commands/load_risk_data.py +56 -0
- endoreg_db/mermaid/Overall_flow_patient_finding_intervention.md +10 -0
- endoreg_db/mermaid/anonymized_image_annotation.md +20 -0
- endoreg_db/mermaid/binary_classification_annotation.md +50 -0
- endoreg_db/mermaid/classification.md +8 -0
- endoreg_db/mermaid/examination.md +8 -0
- endoreg_db/mermaid/findings.md +7 -0
- endoreg_db/mermaid/image_classification.md +28 -0
- endoreg_db/mermaid/interventions.md +8 -0
- endoreg_db/mermaid/morphology.md +8 -0
- endoreg_db/mermaid/patient_creation.md +14 -0
- endoreg_db/mermaid/video_segmentation_annotation.md +17 -0
- endoreg_db/migrations/0009_requirementoperator_requirementsettype_and_more.py +154 -0
- endoreg_db/models/__init__.py +20 -0
- endoreg_db/models/ai_model/ai_model.py +0 -13
- endoreg_db/models/ai_model/model_meta.py +2 -12
- endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -2
- endoreg_db/models/data_file/base_classes/abstract_pdf.py +0 -9
- endoreg_db/models/data_file/base_classes/abstract_video.py +7 -8
- endoreg_db/models/data_file/base_classes/utils.py +0 -22
- endoreg_db/models/data_file/frame.py +1 -1
- endoreg_db/models/data_file/import_classes/raw_pdf.py +5 -11
- endoreg_db/models/data_file/import_classes/raw_video.py +6 -4
- endoreg_db/models/data_file/video/video.py +3 -3
- endoreg_db/models/disease.py +88 -19
- endoreg_db/models/event.py +108 -21
- endoreg_db/models/examination/examination_indication.py +108 -29
- endoreg_db/models/examination/examination_type.py +20 -6
- endoreg_db/models/information_source.py +37 -1
- endoreg_db/models/laboratory/lab_value.py +83 -32
- endoreg_db/models/requirement/__init__.py +11 -0
- endoreg_db/models/requirement/requirement.py +325 -0
- endoreg_db/models/requirement/requirement_evaluation/__init__.py +134 -0
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +102 -0
- endoreg_db/models/requirement/requirement_operator.py +58 -0
- endoreg_db/models/requirement/requirement_set.py +127 -0
- endoreg_db/models/risk/__init__.py +7 -0
- endoreg_db/models/risk/risk.py +72 -0
- endoreg_db/models/risk/risk_type.py +55 -0
- endoreg_db/serializers/raw_pdf_anony_text_validation.py +137 -0
- endoreg_db/serializers/raw_pdf_meta_validation.py +223 -0
- endoreg_db/serializers/raw_video_meta_validation.py +163 -1
- endoreg_db/serializers/video_segmentation.py +208 -126
- endoreg_db/urls.py +127 -14
- endoreg_db/utils/__init__.py +43 -0
- endoreg_db/utils/dataloader.py +38 -19
- endoreg_db/utils/hashs.py +1 -0
- endoreg_db/utils/paths.py +86 -0
- endoreg_db/views/raw_pdf_anony_text_validation_views.py +95 -0
- endoreg_db/views/raw_pdf_meta_validation_views.py +111 -0
- endoreg_db/views/raw_video_meta_validation_views.py +128 -18
- endoreg_db/views/video_segmentation_views.py +28 -11
- endoreg_db/views/views.py +107 -0
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/METADATA +1 -1
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/RECORD +96 -46
- endoreg_db/management/commands/load_name_data.py +0 -37
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/WHEEL +0 -0
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from rest_framework import serializers
|
|
3
|
-
from django.http import FileResponse, Http404,StreamingHttpResponse
|
|
4
|
-
from ..models import RawVideoFile,Label
|
|
3
|
+
from django.http import FileResponse, Http404, StreamingHttpResponse
|
|
4
|
+
from ..models import RawVideoFile, Label, LabelRawVideoSegment, RawVideoPredictionMeta
|
|
5
5
|
import subprocess, cv2
|
|
6
6
|
from django.conf import settings
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
class VideoFileSerializer(serializers.ModelSerializer):
|
|
11
|
-
|
|
12
|
-
|
|
13
10
|
"""
|
|
14
11
|
Serializer that dynamically handles video retrieval and streaming.
|
|
15
12
|
Ensures file returns the relative file path (not MEDIA_URL)
|
|
16
13
|
Computes full_video_path using the correct storage path (/home/admin/test-data)-need to change make it dynamic
|
|
17
14
|
Returns video_url for frontend integration
|
|
18
15
|
Serves the video file when needed
|
|
19
|
-
|
|
16
|
+
|
|
20
17
|
"""
|
|
21
18
|
|
|
22
19
|
video_url = serializers.SerializerMethodField()
|
|
@@ -24,53 +21,67 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
24
21
|
file = serializers.SerializerMethodField() # Override file to remove incorrect MEDIA_URL behavior,otherwise:Django's FileField automatically generates a URL based on MEDIA_URL
|
|
25
22
|
# Video dropdown field for frontend selection (currently shows video ID, but can be changed later)
|
|
26
23
|
video_selection_field = serializers.SerializerMethodField()
|
|
27
|
-
#classification_data = serializers.SerializerMethodField() #data from database (smooth prediction values but currently hardcoded one)
|
|
28
|
-
#The Meta class tells Django what data to include when serializing a RawVideoFile object.
|
|
24
|
+
# classification_data = serializers.SerializerMethodField() #data from database (smooth prediction values but currently hardcoded one)
|
|
25
|
+
# The Meta class tells Django what data to include when serializing a RawVideoFile object.
|
|
29
26
|
sequences = serializers.SerializerMethodField()
|
|
30
27
|
label_names = serializers.SerializerMethodField()
|
|
31
|
-
|
|
28
|
+
# Convert selected label frames into time segments (seconds)
|
|
32
29
|
label_time_segments = serializers.SerializerMethodField()
|
|
33
|
-
#label_predictions = serializers.SerializerMethodField()
|
|
30
|
+
# label_predictions = serializers.SerializerMethodField()
|
|
34
31
|
original_file_name = serializers.CharField()
|
|
35
32
|
duration = serializers.SerializerMethodField()
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
34
|
class Meta:
|
|
42
35
|
model = RawVideoFile
|
|
43
|
-
#he fields list defines which data should be included in the API response.
|
|
44
|
-
fields = [
|
|
45
|
-
|
|
36
|
+
# he fields list defines which data should be included in the API response.
|
|
37
|
+
fields = [
|
|
38
|
+
"id",
|
|
39
|
+
"original_file_name",
|
|
40
|
+
"file",
|
|
41
|
+
"duration",
|
|
42
|
+
"video_url",
|
|
43
|
+
"full_video_path",
|
|
44
|
+
"video_selection_field",
|
|
45
|
+
"label_names",
|
|
46
|
+
"sequences",
|
|
47
|
+
"label_time_segments",
|
|
48
|
+
] # Ensure computed fields are included
|
|
49
|
+
|
|
50
|
+
# @staticmethod #using @staticmethod makes it reusable without needing to create a serializer instance.
|
|
46
51
|
# Without @staticmethod, you would need to instantiate the serializer before calling the method, which is unnecessary her
|
|
47
|
-
def get_video_selection_field(self,obj):
|
|
52
|
+
def get_video_selection_field(self, obj):
|
|
48
53
|
"""
|
|
49
54
|
Returns the field used for video selection in the frontend dropdown.
|
|
50
55
|
Currently, it shows the video ID, but this can be changed easily later.
|
|
51
56
|
"""
|
|
52
57
|
return obj.id
|
|
53
|
-
|
|
54
|
-
def get_video_url(
|
|
58
|
+
|
|
59
|
+
def get_video_url(
|
|
60
|
+
self, obj
|
|
61
|
+
): # when we serialize a RawVideoFile object (video metadata), the get_video_url method is automatically invoked by DRF
|
|
55
62
|
"""
|
|
56
63
|
Returns the API endpoint where the frontend can fetch the video.
|
|
57
64
|
"""
|
|
58
65
|
if not obj.id:
|
|
59
66
|
return {"error": "Invalid video ID"}
|
|
60
|
-
|
|
61
|
-
request= self.context.get(
|
|
67
|
+
|
|
68
|
+
request = self.context.get(
|
|
69
|
+
"request"
|
|
70
|
+
) # Gets the request object (provided by DRF).
|
|
62
71
|
if request:
|
|
63
72
|
return request.build_absolute_uri(f"/api/video/{obj.id}/")
|
|
64
|
-
|
|
73
|
+
|
|
65
74
|
return {"error": "Video URL not avalaible"}
|
|
66
|
-
|
|
75
|
+
|
|
67
76
|
def get_duration(self, obj):
|
|
68
77
|
"""
|
|
69
78
|
Returns the total duration of the video in seconds.
|
|
70
79
|
If duration is not stored in the database, it extracts it dynamically using OpenCV.
|
|
71
80
|
"""
|
|
72
81
|
if hasattr(obj, "duration") and obj.duration:
|
|
73
|
-
return
|
|
82
|
+
return (
|
|
83
|
+
obj.duration
|
|
84
|
+
) # If duration is stored in the database, return it directly.
|
|
74
85
|
|
|
75
86
|
# Dynamically extract duration if not stored
|
|
76
87
|
video_path = obj.file.path
|
|
@@ -83,8 +94,9 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
83
94
|
total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
|
84
95
|
cap.release()
|
|
85
96
|
|
|
86
|
-
return
|
|
87
|
-
|
|
97
|
+
return (
|
|
98
|
+
round(total_frames / fps, 2) if fps > 0 else None
|
|
99
|
+
) # Return duration in seconds
|
|
88
100
|
|
|
89
101
|
def get_file(self, obj):
|
|
90
102
|
"""
|
|
@@ -92,56 +104,37 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
92
104
|
"""
|
|
93
105
|
if not obj.file:
|
|
94
106
|
return {"error": "No file associated with this entry"}
|
|
95
|
-
#obj.file.name is an attribute of FieldFile that returns the file path as a string and name is not the database attribute, it is an attribute of Django’s FieldFile object that holds the file path as a string.
|
|
96
|
-
if not hasattr(obj.file,
|
|
107
|
+
# obj.file.name is an attribute of FieldFile that returns the file path as a string and name is not the database attribute, it is an attribute of Django’s FieldFile object that holds the file path as a string.
|
|
108
|
+
if not hasattr(obj.file, "name") or not obj.file.name.strip():
|
|
97
109
|
return {"error": "Invalid file name"}
|
|
98
110
|
|
|
99
|
-
return str(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'''The error "muxer does not support non-seekable output"
|
|
103
|
-
happens because MP4 format requires seeking, but FFmpeg does not support writing MP4 directly to a non-seekable stream (like STDOUT).'''
|
|
111
|
+
return str(
|
|
112
|
+
obj.file.name
|
|
113
|
+
).strip() # Only return the file path, no URL,#obj.file returning a FieldFile object instead of a string
|
|
104
114
|
|
|
105
115
|
def get_full_video_path(self, obj):
|
|
106
116
|
"""
|
|
107
117
|
Constructs the absolute file path dynamically.
|
|
108
118
|
- Uses the actual storage directory (`/home/admin/test-data/`)
|
|
109
119
|
"""
|
|
120
|
+
from ..utils import STORAGE_DIR
|
|
110
121
|
if not obj.file:
|
|
111
122
|
return {"error": "No video file associated with this entry"}
|
|
112
|
-
|
|
123
|
+
|
|
113
124
|
video_relative_path = str(obj.file.name).strip() # Convert FieldFile to string
|
|
114
125
|
if not video_relative_path:
|
|
115
|
-
return {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return str(full_path) if full_path.exists() else {"error":f"file not found at: {full_path}"}
|
|
130
|
-
|
|
131
|
-
'''
|
|
132
|
-
ffmpeg_command = [
|
|
133
|
-
"ffmpeg", "-i", str(full_path), # Input video
|
|
134
|
-
"-vf", "drawtext=text='OUTSIDE':fontcolor=white:fontsize=24:x=(w-text_w)/2:y=30:enable='lt(t,10)'",
|
|
135
|
-
"-c:v", "libx264", "-preset", "ultrafast", # Encode quickly for streaming
|
|
136
|
-
"-f", "mp4", "-" # Output to STDOUT (no file saving)
|
|
137
|
-
]'''
|
|
138
|
-
'''ffmpeg_command = [
|
|
139
|
-
"ffmpeg", "-i", str(full_path), # Input video
|
|
140
|
-
"-vf", "drawtext=text='OUTSIDE':fontcolor=white:fontsize=24:x=(w-text_w)/2:y=30:enable='lt(t,10)'",
|
|
141
|
-
"-c:v", "libx264", "-preset", "ultrafast",
|
|
142
|
-
"-f", "mpegts", "pipe:1" # Output as MPEG-TS to STDOUT
|
|
143
|
-
]'''
|
|
144
|
-
|
|
126
|
+
return {
|
|
127
|
+
"error": "Video file path is empty or invalid"
|
|
128
|
+
} # none might cause, 500 error, Handle edge case where the file name is empty
|
|
129
|
+
|
|
130
|
+
full_path = STORAGE_DIR / video_relative_path
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
str(full_path)
|
|
134
|
+
if full_path.exists()
|
|
135
|
+
else {"error": f"file not found at: {full_path}"}
|
|
136
|
+
)
|
|
137
|
+
|
|
145
138
|
def get_sequences(self, obj):
|
|
146
139
|
"""
|
|
147
140
|
Extracts the sequences field from the RawVideoFile model.
|
|
@@ -152,8 +145,10 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
152
145
|
"kolonpolyp": [[91, 126]]
|
|
153
146
|
}
|
|
154
147
|
"""
|
|
155
|
-
return obj.sequences or {
|
|
156
|
-
|
|
148
|
+
return obj.sequences or {
|
|
149
|
+
"error": "no sequence found, check database first"
|
|
150
|
+
} # Get from sequences, return {} if missing
|
|
151
|
+
|
|
157
152
|
def get_label_names(self, obj):
|
|
158
153
|
"""
|
|
159
154
|
Extracts only label names from the sequences data.
|
|
@@ -162,7 +157,6 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
162
157
|
"""
|
|
163
158
|
sequences = self.get_sequences(obj)
|
|
164
159
|
return list(sequences.keys()) if sequences else []
|
|
165
|
-
|
|
166
160
|
|
|
167
161
|
def get_label_time_segments(self, obj):
|
|
168
162
|
"""
|
|
@@ -176,9 +170,15 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
176
170
|
- segment_start and segment_end (in frame index format, not divided by FPS)
|
|
177
171
|
"""
|
|
178
172
|
|
|
179
|
-
fps =
|
|
173
|
+
fps = (
|
|
174
|
+
obj.fps
|
|
175
|
+
if hasattr(obj, "fps") and obj.fps is not None
|
|
176
|
+
else obj.get_fps()
|
|
177
|
+
if hasattr(obj, "get_fps") and obj.get_fps() is not None
|
|
178
|
+
else 50
|
|
179
|
+
)
|
|
180
180
|
|
|
181
|
-
print("here is fps::::::::::::::::::.-----------::::::",fps)
|
|
181
|
+
print("here is fps::::::::::::::::::.-----------::::::", fps)
|
|
182
182
|
sequences = self.get_sequences(obj) # Fetch sequence data
|
|
183
183
|
readable_predictions = obj.readable_predictions # Predictions from DB
|
|
184
184
|
|
|
@@ -205,51 +205,64 @@ class VideoFileSerializer(serializers.ModelSerializer):
|
|
|
205
205
|
|
|
206
206
|
# Fetch predictions for frames within this range
|
|
207
207
|
for frame_num in range(start_frame, end_frame + 1):
|
|
208
|
-
if
|
|
208
|
+
if (
|
|
209
|
+
0 <= frame_num < len(readable_predictions)
|
|
210
|
+
): # Ensure index is valid
|
|
209
211
|
frame_filename = f"frame_{str(frame_num).zfill(7)}.jpg" # Frame filename format
|
|
210
|
-
frame_path =
|
|
212
|
+
frame_path = (
|
|
213
|
+
frame_dir / frame_filename
|
|
214
|
+
) # Full path to the frame
|
|
211
215
|
|
|
212
216
|
frame_data[frame_num] = {
|
|
213
217
|
"frame_filename": frame_filename,
|
|
214
218
|
"frame_file_path": str(frame_path),
|
|
215
|
-
"predictions": readable_predictions[frame_num]
|
|
219
|
+
"predictions": readable_predictions[frame_num],
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
# Store frame-wise predictions in frame_predictions
|
|
219
223
|
frame_predictions[frame_num] = readable_predictions[frame_num]
|
|
220
224
|
|
|
221
225
|
# Append the converted time segment
|
|
222
|
-
label_times.append(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
label_times.append(
|
|
227
|
+
{
|
|
228
|
+
"segment_start": start_frame, # Raw start frame (not divided by FPS)
|
|
229
|
+
"segment_end": end_frame, # Raw end frame (not divided by FPS)
|
|
230
|
+
"start_time": round(
|
|
231
|
+
start_time, 2
|
|
232
|
+
), # Converted start time in seconds
|
|
233
|
+
"end_time": round(end_time, 2), # Converted end time in seconds
|
|
234
|
+
"frames": frame_data, # Attach frame details
|
|
235
|
+
}
|
|
236
|
+
)
|
|
229
237
|
|
|
230
238
|
# Store time segments and frame_predictions under the label
|
|
231
239
|
time_segments[label] = {
|
|
232
240
|
"time_ranges": label_times,
|
|
233
|
-
"frame_predictions": frame_predictions # Ensure frame_predictions is correctly assigned
|
|
241
|
+
"frame_predictions": frame_predictions, # Ensure frame_predictions is correctly assigned
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
return time_segments
|
|
237
245
|
|
|
246
|
+
|
|
238
247
|
class VideoListSerializer(serializers.ModelSerializer):
|
|
239
248
|
"""
|
|
240
249
|
Minimal serializer to return only `id` and `original_file_name`
|
|
241
250
|
for the video selection dropdown in Vue.js.
|
|
242
251
|
"""
|
|
252
|
+
|
|
243
253
|
class Meta:
|
|
244
254
|
model = RawVideoFile
|
|
245
|
-
fields = [
|
|
246
|
-
|
|
255
|
+
fields = ["id", "original_file_name"] # Only fetch required fields
|
|
247
256
|
|
|
248
257
|
|
|
249
258
|
from pathlib import Path
|
|
250
259
|
from rest_framework import serializers
|
|
251
260
|
from django.http import FileResponse, Http404, StreamingHttpResponse
|
|
252
|
-
from ..models import
|
|
261
|
+
from ..models import (
|
|
262
|
+
RawVideoFile,
|
|
263
|
+
Label,
|
|
264
|
+
LabelRawVideoSegment,
|
|
265
|
+
) # Importing necessary models
|
|
253
266
|
import subprocess
|
|
254
267
|
from django.conf import settings
|
|
255
268
|
from django.db.models import Q # Import Q for better querying
|
|
@@ -260,24 +273,33 @@ class LabelSerializer(serializers.ModelSerializer):
|
|
|
260
273
|
Serializer for fetching labels from the `endoreg_db_label` table.
|
|
261
274
|
Includes `id` (for backend processing) and `name` (for dropdown display in Vue.js).
|
|
262
275
|
"""
|
|
276
|
+
|
|
263
277
|
class Meta:
|
|
264
278
|
model = Label
|
|
265
|
-
fields = [
|
|
279
|
+
fields = ["id", "name"]
|
|
266
280
|
|
|
267
281
|
|
|
268
282
|
class LabelSegmentSerializer(serializers.ModelSerializer):
|
|
269
283
|
"""
|
|
270
284
|
Serializer for retrieving label segments from `endoreg_db_labelrawvideosegment`.
|
|
271
285
|
"""
|
|
286
|
+
|
|
272
287
|
class Meta:
|
|
273
288
|
model = LabelRawVideoSegment
|
|
274
|
-
fields = [
|
|
289
|
+
fields = [
|
|
290
|
+
"id",
|
|
291
|
+
"video_id",
|
|
292
|
+
"label_id",
|
|
293
|
+
"start_frame_number",
|
|
294
|
+
"end_frame_number",
|
|
295
|
+
]
|
|
275
296
|
|
|
276
297
|
|
|
277
298
|
from django.db import transaction
|
|
278
299
|
|
|
279
300
|
from django.db import transaction
|
|
280
301
|
|
|
302
|
+
|
|
281
303
|
class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
282
304
|
"""
|
|
283
305
|
Serializer for updating label segments.
|
|
@@ -308,43 +330,83 @@ class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
|
308
330
|
|
|
309
331
|
for segment in data["segments"]:
|
|
310
332
|
if "start_frame_number" not in segment or "end_frame_number" not in segment:
|
|
311
|
-
raise serializers.ValidationError(
|
|
333
|
+
raise serializers.ValidationError(
|
|
334
|
+
"Each segment must have `start_frame_number` and `end_frame_number`."
|
|
335
|
+
)
|
|
312
336
|
|
|
313
337
|
if segment["start_frame_number"] > segment["end_frame_number"]:
|
|
314
|
-
raise serializers.ValidationError(
|
|
338
|
+
raise serializers.ValidationError(
|
|
339
|
+
"Start frame must be less than or equal to end frame."
|
|
340
|
+
)
|
|
315
341
|
|
|
316
342
|
return data
|
|
317
343
|
|
|
318
344
|
def save(self):
|
|
319
345
|
"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
346
|
+
Synchronizes label segments with updated frontend data.
|
|
347
|
+
|
|
348
|
+
This method compares the incoming segments with the current database entries for a given video and label.
|
|
349
|
+
It updates segments with modified end frame numbers, inserts new segments, and deletes existing segments
|
|
350
|
+
that are not present in the provided data. All operations are performed within a transaction to ensure
|
|
351
|
+
database consistency. A validation error is raised if no prediction metadata is found for the video.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
dict: A dictionary containing serialized updated segments, serialized new segments, and the count
|
|
355
|
+
of deleted segments.
|
|
328
356
|
"""
|
|
329
357
|
|
|
330
358
|
video_id = self.validated_data["video_id"]
|
|
331
359
|
label_id = self.validated_data["label_id"]
|
|
332
360
|
new_segments = self.validated_data["segments"]
|
|
333
361
|
|
|
334
|
-
# Fetch
|
|
335
|
-
|
|
362
|
+
# Fetch the correct `prediction_meta_id` based on `video_id`
|
|
363
|
+
prediction_meta_entry = RawVideoPredictionMeta.objects.filter(
|
|
364
|
+
video_id=video_id
|
|
365
|
+
).first()
|
|
366
|
+
if not prediction_meta_entry:
|
|
367
|
+
raise serializers.ValidationError(
|
|
368
|
+
{"error": "No prediction metadata found for this video."}
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
prediction_meta_id = (
|
|
372
|
+
prediction_meta_entry.id
|
|
373
|
+
) # Get the correct prediction_meta_id
|
|
374
|
+
|
|
375
|
+
existing_segments = LabelRawVideoSegment.objects.filter(
|
|
376
|
+
video_id=video_id, label_id=label_id
|
|
377
|
+
)
|
|
336
378
|
|
|
337
379
|
# Convert existing segments into a dictionary for quick lookup
|
|
338
380
|
# Key format: (start_frame_number, end_frame_number)
|
|
339
|
-
existing_segments_dict = {
|
|
381
|
+
existing_segments_dict = {
|
|
382
|
+
(float(seg.start_frame_number), float(seg.end_frame_number)): seg
|
|
383
|
+
for seg in existing_segments
|
|
384
|
+
}
|
|
340
385
|
|
|
341
386
|
# Prepare lists for batch processing
|
|
342
387
|
updated_segments = [] # Stores segments that need to be updated
|
|
343
388
|
new_entries = [] # Stores segments that need to be created
|
|
344
|
-
existing_keys = set(
|
|
345
|
-
|
|
389
|
+
existing_keys = set(
|
|
390
|
+
existing_segments_dict.keys()
|
|
391
|
+
) # Existing database segment keys
|
|
392
|
+
new_keys = set(
|
|
393
|
+
(float(seg["start_frame_number"]), float(seg["end_frame_number"]))
|
|
394
|
+
for seg in new_segments
|
|
395
|
+
) # New frontend segment keys
|
|
346
396
|
|
|
347
397
|
# Start a transaction to ensure database consistency
|
|
398
|
+
updated_segments = []
|
|
399
|
+
new_entries = []
|
|
400
|
+
existing_keys = set(existing_segments_dict.keys())
|
|
401
|
+
new_keys = set(
|
|
402
|
+
(float(seg["start_frame_number"]), float(seg["end_frame_number"]))
|
|
403
|
+
for seg in new_segments
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
print(f" Before Update: Found {existing_segments.count()} existing segments.")
|
|
407
|
+
print(f" New Segments Received: {len(new_segments)}")
|
|
408
|
+
print(f" Using prediction_meta_id: {prediction_meta_id}")
|
|
409
|
+
|
|
348
410
|
with transaction.atomic():
|
|
349
411
|
for segment in new_segments:
|
|
350
412
|
start_frame = float(segment["start_frame_number"])
|
|
@@ -358,7 +420,7 @@ class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
|
358
420
|
existing_segment = LabelRawVideoSegment.objects.filter(
|
|
359
421
|
video_id=video_id,
|
|
360
422
|
label_id=label_id,
|
|
361
|
-
start_frame_number=start_frame
|
|
423
|
+
start_frame_number=start_frame,
|
|
362
424
|
).first()
|
|
363
425
|
|
|
364
426
|
if existing_segment:
|
|
@@ -369,16 +431,32 @@ class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
|
369
431
|
updated_segments.append(existing_segment)
|
|
370
432
|
else:
|
|
371
433
|
# If no existing segment matches, create a new one
|
|
372
|
-
new_entries.append(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
434
|
+
new_entries.append(
|
|
435
|
+
LabelRawVideoSegment(
|
|
436
|
+
video_id=video_id,
|
|
437
|
+
label_id=label_id,
|
|
438
|
+
start_frame_number=start_frame,
|
|
439
|
+
end_frame_number=end_frame,
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
print(
|
|
443
|
+
f" Adding new segment: Start {start_frame} → End {end_frame}"
|
|
444
|
+
)
|
|
445
|
+
new_entries.append(
|
|
446
|
+
LabelRawVideoSegment(
|
|
447
|
+
video_id=video_id,
|
|
448
|
+
label_id=label_id,
|
|
449
|
+
start_frame_number=start_frame,
|
|
450
|
+
end_frame_number=end_frame,
|
|
451
|
+
prediction_meta_id=prediction_meta_id, # Assign correct prediction_meta_id
|
|
452
|
+
)
|
|
453
|
+
)
|
|
378
454
|
|
|
379
455
|
# Delete segments that are no longer present in the frontend data
|
|
380
456
|
segments_to_delete = existing_segments.exclude(
|
|
381
|
-
start_frame_number__in=[
|
|
457
|
+
start_frame_number__in=[
|
|
458
|
+
float(seg["start_frame_number"]) for seg in new_segments
|
|
459
|
+
]
|
|
382
460
|
)
|
|
383
461
|
deleted_count = segments_to_delete.count()
|
|
384
462
|
segments_to_delete.delete()
|
|
@@ -388,24 +466,32 @@ class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
|
388
466
|
LabelRawVideoSegment.objects.bulk_create(new_entries)
|
|
389
467
|
|
|
390
468
|
# Return the updated, new, and deleted segment information
|
|
391
|
-
print(
|
|
469
|
+
print(
|
|
470
|
+
"------------------------------,",
|
|
471
|
+
updated_segments,
|
|
472
|
+
"-----------------------",
|
|
473
|
+
new_segments,
|
|
474
|
+
"_-------",
|
|
475
|
+
deleted_count,
|
|
476
|
+
)
|
|
477
|
+
print(
|
|
478
|
+
f" After Update: Updated {len(updated_segments)} segments, Added {len(new_entries)}, Deleted {deleted_count}"
|
|
479
|
+
)
|
|
480
|
+
|
|
392
481
|
return {
|
|
393
|
-
"updated_segments": LabelSegmentSerializer(
|
|
482
|
+
"updated_segments": LabelSegmentSerializer(
|
|
483
|
+
updated_segments, many=True
|
|
484
|
+
).data,
|
|
394
485
|
"new_segments": LabelSegmentSerializer(new_entries, many=True).data,
|
|
395
|
-
"deleted_segments": deleted_count
|
|
486
|
+
"deleted_segments": deleted_count,
|
|
396
487
|
}
|
|
397
488
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
489
|
# Use StreamingHttpResponse to stream FFmpeg output to browser
|
|
405
|
-
|
|
490
|
+
# return StreamingHttpResponse(subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE).stdout, content_type="video/mp4")
|
|
491
|
+
|
|
492
|
+
# Ensure the path exists before returning
|
|
493
|
+
# return str(full_path) if full_path.exists() else None
|
|
406
494
|
|
|
407
|
-
# Ensure the path exists before returning
|
|
408
|
-
# return str(full_path) if full_path.exists() else None
|
|
409
495
|
|
|
410
496
|
''' def get_classification_data(self, obj):
|
|
411
497
|
"""
|
|
@@ -437,10 +523,6 @@ class LabelSegmentUpdateSerializer(serializers.Serializer):
|
|
|
437
523
|
return classifications'''
|
|
438
524
|
|
|
439
525
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
526
|
"""
|
|
445
527
|
await import('https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js');
|
|
446
528
|
const videoId = 1;
|
|
@@ -489,4 +571,4 @@ axios.put(`http://localhost:8000/api/video/${videoIdUpdate}/label/${labelIdUpdat
|
|
|
489
571
|
.catch(error => {
|
|
490
572
|
console.error(" Error Updating Segments:", error.response ? error.response.data : error);
|
|
491
573
|
});
|
|
492
|
-
"""
|
|
574
|
+
""" ""
|