megadetector 10.0.13__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 megadetector might be problematic. Click here for more details.
- megadetector/__init__.py +0 -0
- megadetector/api/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +626 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +853 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +608 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +696 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +509 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/animl_to_md.py +161 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +33 -0
- megadetector/data_management/camtrap_dp_to_coco.py +270 -0
- megadetector/data_management/cct_json_utils.py +566 -0
- megadetector/data_management/cct_to_md.py +184 -0
- megadetector/data_management/cct_to_wi.py +293 -0
- megadetector/data_management/coco_to_labelme.py +284 -0
- megadetector/data_management/coco_to_yolo.py +702 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
- megadetector/data_management/databases/integrity_check_json_db.py +528 -0
- megadetector/data_management/databases/subset_json_db.py +195 -0
- megadetector/data_management/generate_crops_from_cct.py +200 -0
- megadetector/data_management/get_image_sizes.py +164 -0
- megadetector/data_management/labelme_to_coco.py +559 -0
- megadetector/data_management/labelme_to_yolo.py +349 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/create_lila_blank_set.py +556 -0
- megadetector/data_management/lila/create_lila_test_set.py +187 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +182 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +319 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
- megadetector/data_management/mewc_to_md.py +344 -0
- megadetector/data_management/ocr_tools.py +873 -0
- megadetector/data_management/read_exif.py +964 -0
- megadetector/data_management/remap_coco_categories.py +195 -0
- megadetector/data_management/remove_exif.py +156 -0
- megadetector/data_management/rename_images.py +194 -0
- megadetector/data_management/resize_coco_dataset.py +663 -0
- megadetector/data_management/speciesnet_to_md.py +41 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +594 -0
- megadetector/data_management/yolo_to_coco.py +876 -0
- megadetector/data_management/zamba_to_md.py +188 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/change_detection.py +840 -0
- megadetector/detection/process_video.py +479 -0
- megadetector/detection/pytorch_detector.py +1451 -0
- megadetector/detection/run_detector.py +1267 -0
- megadetector/detection/run_detector_batch.py +2159 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
- megadetector/detection/run_md_and_speciesnet.py +1494 -0
- megadetector/detection/run_tiled_inference.py +1038 -0
- megadetector/detection/tf_detector.py +209 -0
- megadetector/detection/video_utils.py +1379 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +72 -0
- megadetector/postprocessing/categorize_detections_by_size.py +166 -0
- megadetector/postprocessing/classification_postprocessing.py +1752 -0
- megadetector/postprocessing/combine_batch_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +2110 -0
- megadetector/postprocessing/convert_output_format.py +403 -0
- megadetector/postprocessing/create_crop_folder.py +629 -0
- megadetector/postprocessing/detector_calibration.py +570 -0
- megadetector/postprocessing/generate_csv_report.py +522 -0
- megadetector/postprocessing/load_api_results.py +223 -0
- megadetector/postprocessing/md_to_coco.py +428 -0
- megadetector/postprocessing/md_to_labelme.py +351 -0
- megadetector/postprocessing/md_to_wi.py +41 -0
- megadetector/postprocessing/merge_detections.py +392 -0
- megadetector/postprocessing/postprocess_batch_results.py +2077 -0
- megadetector/postprocessing/remap_detection_categories.py +226 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
- megadetector/postprocessing/separate_detections_into_folders.py +795 -0
- megadetector/postprocessing/subset_json_detector_output.py +964 -0
- megadetector/postprocessing/top_folders_to_bottom.py +238 -0
- megadetector/postprocessing/validate_batch_results.py +332 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +213 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +224 -0
- megadetector/taxonomy_mapping/species_lookup.py +1008 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/tests/__init__.py +0 -0
- megadetector/tests/test_nms_synthetic.py +335 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/ct_utils.py +1857 -0
- megadetector/utils/directory_listing.py +199 -0
- megadetector/utils/extract_frames_from_video.py +307 -0
- megadetector/utils/gpu_test.py +125 -0
- megadetector/utils/md_tests.py +2072 -0
- megadetector/utils/path_utils.py +2832 -0
- megadetector/utils/process_utils.py +172 -0
- megadetector/utils/split_locations_into_train_val.py +237 -0
- megadetector/utils/string_utils.py +234 -0
- megadetector/utils/url_utils.py +825 -0
- megadetector/utils/wi_platform_utils.py +968 -0
- megadetector/utils/wi_taxonomy_utils.py +1759 -0
- megadetector/utils/write_html_image_list.py +239 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +309 -0
- megadetector/visualization/render_images_with_thumbnails.py +243 -0
- megadetector/visualization/visualization_utils.py +1940 -0
- megadetector/visualization/visualize_db.py +630 -0
- megadetector/visualization/visualize_detector_output.py +479 -0
- megadetector/visualization/visualize_video_output.py +705 -0
- megadetector-10.0.13.dist-info/METADATA +134 -0
- megadetector-10.0.13.dist-info/RECORD +147 -0
- megadetector-10.0.13.dist-info/WHEEL +5 -0
- megadetector-10.0.13.dist-info/licenses/LICENSE +19 -0
- megadetector-10.0.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
visualize_video_output.py
|
|
4
|
+
|
|
5
|
+
Render a folder of videos with bounding boxes to a new folder, based on a
|
|
6
|
+
detector output file.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
#%% Imports
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import random
|
|
15
|
+
import cv2
|
|
16
|
+
|
|
17
|
+
from multiprocessing.pool import ThreadPool
|
|
18
|
+
from multiprocessing.pool import Pool
|
|
19
|
+
from functools import partial
|
|
20
|
+
from tqdm import tqdm
|
|
21
|
+
from PIL import Image
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from megadetector.data_management.annotations.annotation_constants import detector_bbox_category_id_to_name
|
|
25
|
+
from megadetector.detection.video_utils import run_callback_on_frames, default_fourcc, is_video_file
|
|
26
|
+
from megadetector.utils.path_utils import path_is_abs
|
|
27
|
+
from megadetector.utils.wi_taxonomy_utils import load_md_or_speciesnet_file
|
|
28
|
+
from megadetector.visualization.visualization_utils import render_detection_bounding_boxes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#%% Constants
|
|
32
|
+
|
|
33
|
+
# This will only be used if a category mapping is not available in the results file
|
|
34
|
+
DEFAULT_DETECTOR_LABEL_MAP = {
|
|
35
|
+
str(k): v for k, v in detector_bbox_category_id_to_name.items()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
DEFAULT_CLASSIFICATION_THRESHOLD = 0.4
|
|
39
|
+
DEFAULT_DETECTION_THRESHOLD = 0.15
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
#%% Classes
|
|
43
|
+
|
|
44
|
+
class VideoVisualizationOptions:
|
|
45
|
+
"""
|
|
46
|
+
Options controlling the behavior of visualize_video_output()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
|
|
51
|
+
#: Confidence threshold for including detections
|
|
52
|
+
self.confidence_threshold = DEFAULT_DETECTION_THRESHOLD
|
|
53
|
+
|
|
54
|
+
#: Sample N videos to process (-1 for all videos)
|
|
55
|
+
self.sample = -1
|
|
56
|
+
|
|
57
|
+
#: Random seed for sampling
|
|
58
|
+
self.random_seed = None
|
|
59
|
+
|
|
60
|
+
#: Confidence threshold for including classifications
|
|
61
|
+
self.classification_confidence_threshold = DEFAULT_CLASSIFICATION_THRESHOLD
|
|
62
|
+
|
|
63
|
+
#: Frame rate for output videos. Either a float (fps) or 'auto' to calculate
|
|
64
|
+
#: based on detection frame intervals
|
|
65
|
+
self.rendering_fs = 'auto'
|
|
66
|
+
|
|
67
|
+
#: Fourcc codec specification for video encoding
|
|
68
|
+
self.fourcc = default_fourcc
|
|
69
|
+
|
|
70
|
+
#: Skip frames before first and after last above-threshold detection
|
|
71
|
+
self.trim_to_detections = False
|
|
72
|
+
|
|
73
|
+
#: By default, output videos use the same extension as input videos,
|
|
74
|
+
#: use this to force a particular extension
|
|
75
|
+
self.output_extension = None
|
|
76
|
+
|
|
77
|
+
#: By default, relative paths are preserved in the output folder; this
|
|
78
|
+
#: flattens the output structure.
|
|
79
|
+
self.flatten_output = False
|
|
80
|
+
|
|
81
|
+
#: When flatten_output is True, path separators will be replaced with this
|
|
82
|
+
#: string.
|
|
83
|
+
self.path_separator_replacement = '#'
|
|
84
|
+
|
|
85
|
+
#: Don't render videos below this length
|
|
86
|
+
self.min_output_length_seconds = None
|
|
87
|
+
|
|
88
|
+
#: Enable parallel processing of videos
|
|
89
|
+
self.parallelize_rendering = True
|
|
90
|
+
|
|
91
|
+
#: Number of concurrent workers (None = default based on CPU count)
|
|
92
|
+
self.parallelize_rendering_n_cores = 8
|
|
93
|
+
|
|
94
|
+
#: Use threads (True) vs processes (False) for parallelization
|
|
95
|
+
self.parallelize_rendering_with_threads = True
|
|
96
|
+
|
|
97
|
+
# ...class VideoVisualizationOptions
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#%% Support functions
|
|
101
|
+
|
|
102
|
+
def _get_video_output_framerate(video_entry, original_framerate, rendering_fs='auto'):
|
|
103
|
+
"""
|
|
104
|
+
Calculate the appropriate output frame rate for a video based on detection frame numbers.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
video_entry (dict): video entry from results file containing detections
|
|
108
|
+
original_framerate (float): original frame rate of the video
|
|
109
|
+
rendering_fs (str or float): 'auto' for automatic calculation, negative float for
|
|
110
|
+
speedup factor, positive float for explicit fps
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
float: calculated output frame rate
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if rendering_fs != 'auto':
|
|
117
|
+
|
|
118
|
+
if float(rendering_fs) < 0:
|
|
119
|
+
|
|
120
|
+
# Negative value means speedup factor
|
|
121
|
+
speedup_factor = abs(float(rendering_fs))
|
|
122
|
+
if ('detections' not in video_entry) or (len(video_entry['detections']) == 0):
|
|
123
|
+
# This is a bit arbitrary, but a reasonable thing to do when we have no basis
|
|
124
|
+
# to determine the output frame rate
|
|
125
|
+
return original_framerate * speedup_factor
|
|
126
|
+
|
|
127
|
+
frame_numbers = []
|
|
128
|
+
for detection in video_entry['detections']:
|
|
129
|
+
if 'frame_number' in detection:
|
|
130
|
+
frame_numbers.append(detection['frame_number'])
|
|
131
|
+
|
|
132
|
+
if len(frame_numbers) < 2:
|
|
133
|
+
# This is a bit arbitrary, but a reasonable thing to do when we have no basis
|
|
134
|
+
# to determine the output frame rate
|
|
135
|
+
return original_framerate * speedup_factor
|
|
136
|
+
|
|
137
|
+
frame_numbers = sorted(set(frame_numbers))
|
|
138
|
+
first_interval = frame_numbers[1] - frame_numbers[0]
|
|
139
|
+
|
|
140
|
+
# Calculate base output frame rate based on first interval, then apply speedup
|
|
141
|
+
base_output_fps = original_framerate / first_interval
|
|
142
|
+
return base_output_fps * speedup_factor
|
|
143
|
+
|
|
144
|
+
else:
|
|
145
|
+
|
|
146
|
+
# Positive value means explicit fps
|
|
147
|
+
return float(rendering_fs)
|
|
148
|
+
|
|
149
|
+
# ...if we're using an explicit/speedup-based frame rate
|
|
150
|
+
|
|
151
|
+
# ...if we aren't in "auto" frame rate mode
|
|
152
|
+
|
|
153
|
+
# Auto mode
|
|
154
|
+
if 'detections' not in video_entry or len(video_entry['detections']) == 0:
|
|
155
|
+
return original_framerate
|
|
156
|
+
|
|
157
|
+
frame_numbers = []
|
|
158
|
+
for detection in video_entry['detections']:
|
|
159
|
+
if 'frame_number' in detection:
|
|
160
|
+
frame_numbers.append(detection['frame_number'])
|
|
161
|
+
|
|
162
|
+
if len(frame_numbers) < 2:
|
|
163
|
+
return original_framerate
|
|
164
|
+
|
|
165
|
+
frame_numbers = sorted(set(frame_numbers))
|
|
166
|
+
first_interval = frame_numbers[1] - frame_numbers[0]
|
|
167
|
+
|
|
168
|
+
# Calculate output frame rate based on first interval
|
|
169
|
+
output_fps = original_framerate / first_interval
|
|
170
|
+
|
|
171
|
+
return output_fps
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _get_frames_to_process(video_entry, confidence_threshold, trim_to_detections=False):
|
|
175
|
+
"""
|
|
176
|
+
Get list of frame numbers that have detections for this video.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
video_entry (dict): video entry from results file
|
|
180
|
+
confidence_threshold (float): minimum confidence for detections to be considered
|
|
181
|
+
trim_to_detections (bool): if True, only include frames between first and last
|
|
182
|
+
above-threshold detections (inclusive)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
list: sorted list of unique frame numbers to process
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
if 'detections' not in video_entry:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
if 'frames_processed' in video_entry:
|
|
192
|
+
frame_numbers = set(video_entry['frames_processed'])
|
|
193
|
+
else:
|
|
194
|
+
frame_numbers = set()
|
|
195
|
+
|
|
196
|
+
for detection in video_entry['detections']:
|
|
197
|
+
|
|
198
|
+
if 'frame_number' in detection:
|
|
199
|
+
# If this file includes the list of frames processed (required as of format
|
|
200
|
+
# version 1.5), every frame with detections should be included in that list
|
|
201
|
+
if 'frames_processed' in video_entry:
|
|
202
|
+
if detection['frame_number'] not in frame_numbers:
|
|
203
|
+
print('Warning: frames_processed field present in {}, but frame {} is missing'.\
|
|
204
|
+
format(video_entry['file'],detection['frame_number']))
|
|
205
|
+
frame_numbers.add(detection['frame_number'])
|
|
206
|
+
else:
|
|
207
|
+
print('Warning: detections in {} lack frame numbers'.format(video_entry['file']))
|
|
208
|
+
|
|
209
|
+
# ...for each detection
|
|
210
|
+
|
|
211
|
+
frame_numbers = sorted(list(frame_numbers))
|
|
212
|
+
|
|
213
|
+
if trim_to_detections and (len(frame_numbers) > 0):
|
|
214
|
+
|
|
215
|
+
# Find first and last frames with above-threshold detections
|
|
216
|
+
|
|
217
|
+
above_threshold_frames = set()
|
|
218
|
+
for detection in video_entry['detections']:
|
|
219
|
+
if detection['conf'] >= confidence_threshold:
|
|
220
|
+
above_threshold_frames.add(detection['frame_number'])
|
|
221
|
+
|
|
222
|
+
if len(above_threshold_frames) > 0:
|
|
223
|
+
|
|
224
|
+
above_threshold_frames = sorted(list(above_threshold_frames))
|
|
225
|
+
first_detection_frame = above_threshold_frames[0]
|
|
226
|
+
last_detection_frame = above_threshold_frames[-1]
|
|
227
|
+
|
|
228
|
+
# Return all frames between first and last above-threshold detections (inclusive)
|
|
229
|
+
trimmed_frames = []
|
|
230
|
+
for frame_num in frame_numbers:
|
|
231
|
+
if (first_detection_frame <= frame_num) and (frame_num <= last_detection_frame):
|
|
232
|
+
trimmed_frames.append(frame_num)
|
|
233
|
+
return trimmed_frames
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
# No above-threshold detections, return empty list
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
# ...if we're supposed to be trimming to non-empty frames
|
|
240
|
+
|
|
241
|
+
return frame_numbers
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _get_detections_for_frame(video_entry, frame_number, confidence_threshold):
|
|
245
|
+
"""
|
|
246
|
+
Get all detections for a specific frame that meet confidence thresholds.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
video_entry (dict): video entry from results file
|
|
250
|
+
frame_number (int): frame number to get detections for
|
|
251
|
+
confidence_threshold (float): minimum detection confidence
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
list: list of detection dictionaries for this frame
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
if 'detections' not in video_entry:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
frame_detections = []
|
|
261
|
+
|
|
262
|
+
for detection in video_entry['detections']:
|
|
263
|
+
if ((detection['frame_number'] == frame_number) and
|
|
264
|
+
(detection['conf'] >= confidence_threshold)):
|
|
265
|
+
frame_detections.append(detection)
|
|
266
|
+
|
|
267
|
+
return frame_detections
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _process_video(video_entry,
|
|
271
|
+
detector_label_map,
|
|
272
|
+
classification_label_map,
|
|
273
|
+
options,
|
|
274
|
+
video_dir,
|
|
275
|
+
out_dir):
|
|
276
|
+
"""
|
|
277
|
+
Process a single video, rendering detections on frames and creating output video.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
video_entry (dict): video entry from results file
|
|
281
|
+
detector_label_map (dict): mapping of detection category IDs to names
|
|
282
|
+
classification_label_map (dict): mapping of classification category IDs to names
|
|
283
|
+
options (VideoVisualizationOptions): processing options
|
|
284
|
+
video_dir (str): input video directory
|
|
285
|
+
out_dir (str): output directory
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
dict: processing result information, with at least keys 'file, 'error', 'success',
|
|
289
|
+
'frames_processed'.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
result = {
|
|
293
|
+
'file': video_entry['file'],
|
|
294
|
+
'success': False,
|
|
295
|
+
'error': None,
|
|
296
|
+
'frames_processed': 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Handle failure cases
|
|
300
|
+
if ('failure' in video_entry) and (video_entry['failure'] is not None):
|
|
301
|
+
result['error'] = 'Ignoring failed video: {}'.format(video_entry['failure'])
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
# Construct input and output paths
|
|
305
|
+
if video_dir is None:
|
|
306
|
+
input_video_path = video_entry['file']
|
|
307
|
+
assert path_is_abs(input_video_path), \
|
|
308
|
+
'Absolute paths are required when no video base dir is supplied'
|
|
309
|
+
else:
|
|
310
|
+
assert not path_is_abs(video_entry['file']), \
|
|
311
|
+
'Relative paths are required when a video base dir is supplied'
|
|
312
|
+
input_video_path = os.path.join(video_dir, video_entry['file'])
|
|
313
|
+
|
|
314
|
+
if not os.path.exists(input_video_path):
|
|
315
|
+
result['error'] = 'Video not found: {}'.format(input_video_path)
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
output_fn_relative = video_entry['file']
|
|
319
|
+
|
|
320
|
+
if options.flatten_output:
|
|
321
|
+
output_fn_relative = output_fn_relative.replace('\\','/')
|
|
322
|
+
output_fn_relative = \
|
|
323
|
+
output_fn_relative.replace('/',options.path_separator_replacement)
|
|
324
|
+
|
|
325
|
+
if options.output_extension is not None:
|
|
326
|
+
ext = options.output_extension
|
|
327
|
+
if not ext.startswith('.'):
|
|
328
|
+
ext = '.' + ext
|
|
329
|
+
output_fn_relative = os.path.splitext(output_fn_relative)[0] + ext
|
|
330
|
+
|
|
331
|
+
output_fn_abs = os.path.join(out_dir, output_fn_relative)
|
|
332
|
+
parent_dir = os.path.dirname(output_fn_abs)
|
|
333
|
+
if len(parent_dir) > 0:
|
|
334
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
335
|
+
|
|
336
|
+
# Get frames to process
|
|
337
|
+
frames_to_process = _get_frames_to_process(video_entry,
|
|
338
|
+
options.confidence_threshold,
|
|
339
|
+
options.trim_to_detections)
|
|
340
|
+
if len(frames_to_process) == 0:
|
|
341
|
+
result['error'] = 'No frames with detections to process'
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
# Determine output frame rate
|
|
345
|
+
original_framerate = video_entry['frame_rate']
|
|
346
|
+
output_framerate = _get_video_output_framerate(video_entry,
|
|
347
|
+
original_framerate,
|
|
348
|
+
options.rendering_fs)
|
|
349
|
+
|
|
350
|
+
# Bail early if this video is below the output length limit
|
|
351
|
+
if options.min_output_length_seconds is not None:
|
|
352
|
+
output_length = len(frames_to_process) / output_framerate
|
|
353
|
+
if output_length < options.min_output_length_seconds:
|
|
354
|
+
print('Skipping video {}, {}s is below minimum length ({}s)'.format(
|
|
355
|
+
video_entry['file'],output_length,options.min_output_length_seconds))
|
|
356
|
+
result['error'] = 'Skipped, below minimum length'
|
|
357
|
+
return result
|
|
358
|
+
|
|
359
|
+
# Storage for rendered frames
|
|
360
|
+
rendered_frames = []
|
|
361
|
+
|
|
362
|
+
def frame_callback(frame_array, frame_id):
|
|
363
|
+
"""
|
|
364
|
+
Callback function for processing each frame.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
frame_array (np.array): frame image data
|
|
368
|
+
frame_id (str): frame identifier (unused)
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
np.array: processed frame
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
# Extract frame number from the current processing context
|
|
375
|
+
current_frame_idx = len(rendered_frames)
|
|
376
|
+
if current_frame_idx >= len(frames_to_process):
|
|
377
|
+
print('Warning: received an extra frame (index {} of {}) for video {}'.format(
|
|
378
|
+
current_frame_idx,len(frames_to_process),video_entry['file']
|
|
379
|
+
))
|
|
380
|
+
return frame_array
|
|
381
|
+
|
|
382
|
+
current_frame_number = frames_to_process[current_frame_idx]
|
|
383
|
+
|
|
384
|
+
# Convert numpy array to PIL Image
|
|
385
|
+
if frame_array.dtype != np.uint8:
|
|
386
|
+
frame_array = (frame_array * 255).astype(np.uint8)
|
|
387
|
+
|
|
388
|
+
# Convert from BGR (OpenCV) to RGB (PIL) if needed
|
|
389
|
+
if len(frame_array.shape) == 3 and frame_array.shape[2] == 3:
|
|
390
|
+
frame_array = cv2.cvtColor(frame_array, cv2.COLOR_BGR2RGB)
|
|
391
|
+
|
|
392
|
+
pil_image = Image.fromarray(frame_array)
|
|
393
|
+
|
|
394
|
+
# Get detections for this frame
|
|
395
|
+
frame_detections = _get_detections_for_frame(
|
|
396
|
+
video_entry,
|
|
397
|
+
current_frame_number,
|
|
398
|
+
options.confidence_threshold
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Render detections on the frame
|
|
402
|
+
if frame_detections:
|
|
403
|
+
render_detection_bounding_boxes(
|
|
404
|
+
frame_detections,
|
|
405
|
+
pil_image,
|
|
406
|
+
detector_label_map,
|
|
407
|
+
classification_label_map,
|
|
408
|
+
classification_confidence_threshold=options.classification_confidence_threshold
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Convert back to numpy array for video writing
|
|
412
|
+
frame_array = np.array(pil_image)
|
|
413
|
+
if (len(frame_array.shape) == 3) and (frame_array.shape[2] == 3):
|
|
414
|
+
frame_array = cv2.cvtColor(frame_array, cv2.COLOR_RGB2BGR)
|
|
415
|
+
|
|
416
|
+
rendered_frames.append(frame_array)
|
|
417
|
+
return frame_array
|
|
418
|
+
|
|
419
|
+
# ...def frame_callback(...)
|
|
420
|
+
|
|
421
|
+
# Process video frames
|
|
422
|
+
try:
|
|
423
|
+
run_callback_on_frames(
|
|
424
|
+
input_video_path,
|
|
425
|
+
frame_callback,
|
|
426
|
+
frames_to_process=frames_to_process,
|
|
427
|
+
verbose=False
|
|
428
|
+
)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
import traceback
|
|
431
|
+
trace = traceback.format_exc()
|
|
432
|
+
result['error'] = 'Error processing video frames: {} ({})'.format(str(e),trace)
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
# Write output video
|
|
436
|
+
if len(rendered_frames) > 0:
|
|
437
|
+
|
|
438
|
+
video_writer = None
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
|
|
442
|
+
# Get frame dimensions
|
|
443
|
+
height, width = rendered_frames[0].shape[:2]
|
|
444
|
+
|
|
445
|
+
# Create VideoWriter
|
|
446
|
+
fourcc = cv2.VideoWriter_fourcc(*options.fourcc)
|
|
447
|
+
video_writer = cv2.VideoWriter(output_fn_abs, fourcc, output_framerate, (width, height))
|
|
448
|
+
|
|
449
|
+
if not video_writer.isOpened():
|
|
450
|
+
result['error'] = 'Failed to open video writer for {}'.format(output_fn_abs)
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
# Write frames
|
|
454
|
+
for frame in rendered_frames:
|
|
455
|
+
video_writer.write(frame)
|
|
456
|
+
|
|
457
|
+
result['success'] = True
|
|
458
|
+
result['frames_processed'] = len(rendered_frames)
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
|
|
462
|
+
result['error'] = 'Error writing output video: {}'.format(str(e))
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
finally:
|
|
466
|
+
|
|
467
|
+
if video_writer is not None:
|
|
468
|
+
try:
|
|
469
|
+
video_writer.release()
|
|
470
|
+
except Exception as e:
|
|
471
|
+
print('Warning: failed to release video writer for file {}: {}'.format(
|
|
472
|
+
video_entry['file'],str(e)))
|
|
473
|
+
|
|
474
|
+
# ...try/except
|
|
475
|
+
|
|
476
|
+
else:
|
|
477
|
+
|
|
478
|
+
result['error'] = 'No frames were processed for video {}'.format(video_entry['file'])
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
# ...def _process_video(...)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
#%% Main function
|
|
486
|
+
|
|
487
|
+
def visualize_video_output(detector_output_path,
|
|
488
|
+
out_dir,
|
|
489
|
+
video_dir,
|
|
490
|
+
options=None):
|
|
491
|
+
"""
|
|
492
|
+
Renders videos with bounding boxes based on detector output.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
detector_output_path (str): path to .json file containing detection results
|
|
496
|
+
out_dir (str): output directory for rendered videos
|
|
497
|
+
video_dir (str): input video directory
|
|
498
|
+
options (VideoVisualizationOptions, optional): processing options
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
list: list of processing results for each video
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
if options is None:
|
|
505
|
+
options = VideoVisualizationOptions()
|
|
506
|
+
|
|
507
|
+
# Validate that input and output directories are different
|
|
508
|
+
if (video_dir is not None) and (os.path.abspath(out_dir) == os.path.abspath(video_dir)):
|
|
509
|
+
raise ValueError('Output directory cannot be the same as video directory')
|
|
510
|
+
|
|
511
|
+
# Load results file
|
|
512
|
+
print('Loading results from {}'.format(detector_output_path))
|
|
513
|
+
results_data = load_md_or_speciesnet_file(detector_output_path)
|
|
514
|
+
|
|
515
|
+
# Get label mappings
|
|
516
|
+
detector_label_map = results_data.get('detection_categories', DEFAULT_DETECTOR_LABEL_MAP)
|
|
517
|
+
classification_label_map = results_data.get('classification_categories', {})
|
|
518
|
+
|
|
519
|
+
# Filter to video entries only
|
|
520
|
+
video_entries = []
|
|
521
|
+
for entry in results_data['images']:
|
|
522
|
+
if is_video_file(entry['file']):
|
|
523
|
+
video_entries.append(entry)
|
|
524
|
+
|
|
525
|
+
print('Found {} videos in results file'.format(len(video_entries)))
|
|
526
|
+
|
|
527
|
+
# Apply sampling if requested
|
|
528
|
+
if (options.sample > 0) and (len(video_entries) > options.sample):
|
|
529
|
+
if options.random_seed is not None:
|
|
530
|
+
random.seed(options.random_seed)
|
|
531
|
+
n_videos_available = len(video_entries)
|
|
532
|
+
video_entries = random.sample(video_entries, options.sample)
|
|
533
|
+
print('Sampled {} of {} videos for processing'.format(
|
|
534
|
+
len(video_entries),n_videos_available))
|
|
535
|
+
|
|
536
|
+
# Create output directory
|
|
537
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
538
|
+
|
|
539
|
+
# Process each video
|
|
540
|
+
results = []
|
|
541
|
+
|
|
542
|
+
if options.parallelize_rendering:
|
|
543
|
+
|
|
544
|
+
if options.parallelize_rendering_with_threads:
|
|
545
|
+
worker_string = 'threads'
|
|
546
|
+
else:
|
|
547
|
+
worker_string = 'processes'
|
|
548
|
+
|
|
549
|
+
pool = None
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
|
|
553
|
+
if options.parallelize_rendering_n_cores is None:
|
|
554
|
+
if options.parallelize_rendering_with_threads:
|
|
555
|
+
pool = ThreadPool()
|
|
556
|
+
else:
|
|
557
|
+
pool = Pool()
|
|
558
|
+
else:
|
|
559
|
+
if options.parallelize_rendering_with_threads:
|
|
560
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
561
|
+
else:
|
|
562
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
563
|
+
print('Processing videos with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
564
|
+
worker_string))
|
|
565
|
+
results = list(tqdm(pool.imap(
|
|
566
|
+
partial(_process_video,
|
|
567
|
+
detector_label_map=detector_label_map,
|
|
568
|
+
classification_label_map=classification_label_map,
|
|
569
|
+
options=options,
|
|
570
|
+
video_dir=video_dir,
|
|
571
|
+
out_dir=out_dir),
|
|
572
|
+
video_entries), total=len(video_entries), desc='Processing videos'))
|
|
573
|
+
finally:
|
|
574
|
+
|
|
575
|
+
if pool is not None:
|
|
576
|
+
pool.close()
|
|
577
|
+
pool.join()
|
|
578
|
+
print('Pool closed and joined for video output visualization')
|
|
579
|
+
|
|
580
|
+
else:
|
|
581
|
+
|
|
582
|
+
for video_entry in tqdm(video_entries, desc='Processing videos'):
|
|
583
|
+
|
|
584
|
+
result = _process_video(
|
|
585
|
+
video_entry,
|
|
586
|
+
detector_label_map,
|
|
587
|
+
classification_label_map,
|
|
588
|
+
options,
|
|
589
|
+
video_dir,
|
|
590
|
+
out_dir
|
|
591
|
+
)
|
|
592
|
+
results.append(result)
|
|
593
|
+
|
|
594
|
+
# ...for each video
|
|
595
|
+
|
|
596
|
+
# Print summary
|
|
597
|
+
successful = sum(1 for r in results if r['success'])
|
|
598
|
+
failed = len(results) - successful
|
|
599
|
+
total_frames = sum(r['frames_processed'] for r in results if r['success'])
|
|
600
|
+
|
|
601
|
+
print('\nProcessing complete:')
|
|
602
|
+
print(f' Successfully processed: {successful} videos')
|
|
603
|
+
print(f' Failed: {failed} videos')
|
|
604
|
+
print(f' Total frames rendered: {total_frames}')
|
|
605
|
+
|
|
606
|
+
return results
|
|
607
|
+
|
|
608
|
+
# ...def visualize_video_output(...)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
#%% Command-line driver
|
|
612
|
+
|
|
613
|
+
def main():
|
|
614
|
+
"""
|
|
615
|
+
Command-line driver for visualize_video_output
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
parser = argparse.ArgumentParser(
|
|
619
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
620
|
+
description='Render videos with bounding boxes predicted by a detector above '
|
|
621
|
+
'a confidence threshold, and save the rendered videos.')
|
|
622
|
+
|
|
623
|
+
parser.add_argument(
|
|
624
|
+
'detector_output_path',
|
|
625
|
+
type=str,
|
|
626
|
+
help='Path to json output file of the detector')
|
|
627
|
+
|
|
628
|
+
parser.add_argument(
|
|
629
|
+
'out_dir',
|
|
630
|
+
type=str,
|
|
631
|
+
help='Path to directory where the rendered videos will be saved. '
|
|
632
|
+
'The directory will be created if it does not exist.')
|
|
633
|
+
|
|
634
|
+
parser.add_argument(
|
|
635
|
+
'video_dir',
|
|
636
|
+
type=str,
|
|
637
|
+
help='Path to directory containing the input videos')
|
|
638
|
+
|
|
639
|
+
parser.add_argument(
|
|
640
|
+
'--confidence_threshold',
|
|
641
|
+
type=float,
|
|
642
|
+
default=DEFAULT_DETECTION_THRESHOLD,
|
|
643
|
+
help='Confidence threshold above which detections will be rendered')
|
|
644
|
+
|
|
645
|
+
parser.add_argument(
|
|
646
|
+
'--sample',
|
|
647
|
+
type=int,
|
|
648
|
+
default=-1,
|
|
649
|
+
help='Number of videos to randomly sample for processing. '
|
|
650
|
+
'Set to -1 to process all videos')
|
|
651
|
+
|
|
652
|
+
parser.add_argument(
|
|
653
|
+
'--random_seed',
|
|
654
|
+
type=int,
|
|
655
|
+
default=None,
|
|
656
|
+
help='Random seed for reproducible sampling')
|
|
657
|
+
|
|
658
|
+
parser.add_argument(
|
|
659
|
+
'--classification_confidence_threshold',
|
|
660
|
+
type=float,
|
|
661
|
+
default=DEFAULT_CLASSIFICATION_THRESHOLD,
|
|
662
|
+
help='Value between 0 and 1, indicating the confidence threshold '
|
|
663
|
+
'above which classifications will be rendered')
|
|
664
|
+
|
|
665
|
+
parser.add_argument(
|
|
666
|
+
'--rendering_fs',
|
|
667
|
+
default='auto',
|
|
668
|
+
help='Frame rate for output videos. Use "auto" to calculate based on '
|
|
669
|
+
'detection frame intervals, positive float for explicit fps, '
|
|
670
|
+
'or negative float for speedup factor (e.g. -2.0 = 2x faster)')
|
|
671
|
+
|
|
672
|
+
parser.add_argument(
|
|
673
|
+
'--fourcc',
|
|
674
|
+
type=str,
|
|
675
|
+
default=default_fourcc,
|
|
676
|
+
help='Fourcc codec specification for video encoding')
|
|
677
|
+
|
|
678
|
+
parser.add_argument(
|
|
679
|
+
'--trim_to_detections',
|
|
680
|
+
action='store_true',
|
|
681
|
+
help='Skip frames before first and after last above-threshold detection')
|
|
682
|
+
|
|
683
|
+
args = parser.parse_args()
|
|
684
|
+
|
|
685
|
+
# Create options object
|
|
686
|
+
options = VideoVisualizationOptions()
|
|
687
|
+
options.confidence_threshold = args.confidence_threshold
|
|
688
|
+
options.sample = args.sample
|
|
689
|
+
options.random_seed = args.random_seed
|
|
690
|
+
options.classification_confidence_threshold = args.classification_confidence_threshold
|
|
691
|
+
options.rendering_fs = args.rendering_fs
|
|
692
|
+
options.fourcc = args.fourcc
|
|
693
|
+
options.trim_to_detections = args.trim_to_detections
|
|
694
|
+
|
|
695
|
+
# Run visualization
|
|
696
|
+
visualize_video_output(
|
|
697
|
+
args.detector_output_path,
|
|
698
|
+
args.out_dir,
|
|
699
|
+
args.video_dir,
|
|
700
|
+
options
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
if __name__ == '__main__':
|
|
705
|
+
main()
|