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.

Files changed (147) hide show
  1. megadetector/__init__.py +0 -0
  2. megadetector/api/__init__.py +0 -0
  3. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  7. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  8. megadetector/classification/__init__.py +0 -0
  9. megadetector/classification/aggregate_classifier_probs.py +108 -0
  10. megadetector/classification/analyze_failed_images.py +227 -0
  11. megadetector/classification/cache_batchapi_outputs.py +198 -0
  12. megadetector/classification/create_classification_dataset.py +626 -0
  13. megadetector/classification/crop_detections.py +516 -0
  14. megadetector/classification/csv_to_json.py +226 -0
  15. megadetector/classification/detect_and_crop.py +853 -0
  16. megadetector/classification/efficientnet/__init__.py +9 -0
  17. megadetector/classification/efficientnet/model.py +415 -0
  18. megadetector/classification/efficientnet/utils.py +608 -0
  19. megadetector/classification/evaluate_model.py +520 -0
  20. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  21. megadetector/classification/json_to_azcopy_list.py +63 -0
  22. megadetector/classification/json_validator.py +696 -0
  23. megadetector/classification/map_classification_categories.py +276 -0
  24. megadetector/classification/merge_classification_detection_output.py +509 -0
  25. megadetector/classification/prepare_classification_script.py +194 -0
  26. megadetector/classification/prepare_classification_script_mc.py +228 -0
  27. megadetector/classification/run_classifier.py +287 -0
  28. megadetector/classification/save_mislabeled.py +110 -0
  29. megadetector/classification/train_classifier.py +827 -0
  30. megadetector/classification/train_classifier_tf.py +725 -0
  31. megadetector/classification/train_utils.py +323 -0
  32. megadetector/data_management/__init__.py +0 -0
  33. megadetector/data_management/animl_to_md.py +161 -0
  34. megadetector/data_management/annotations/__init__.py +0 -0
  35. megadetector/data_management/annotations/annotation_constants.py +33 -0
  36. megadetector/data_management/camtrap_dp_to_coco.py +270 -0
  37. megadetector/data_management/cct_json_utils.py +566 -0
  38. megadetector/data_management/cct_to_md.py +184 -0
  39. megadetector/data_management/cct_to_wi.py +293 -0
  40. megadetector/data_management/coco_to_labelme.py +284 -0
  41. megadetector/data_management/coco_to_yolo.py +702 -0
  42. megadetector/data_management/databases/__init__.py +0 -0
  43. megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
  44. megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
  45. megadetector/data_management/databases/integrity_check_json_db.py +528 -0
  46. megadetector/data_management/databases/subset_json_db.py +195 -0
  47. megadetector/data_management/generate_crops_from_cct.py +200 -0
  48. megadetector/data_management/get_image_sizes.py +164 -0
  49. megadetector/data_management/labelme_to_coco.py +559 -0
  50. megadetector/data_management/labelme_to_yolo.py +349 -0
  51. megadetector/data_management/lila/__init__.py +0 -0
  52. megadetector/data_management/lila/create_lila_blank_set.py +556 -0
  53. megadetector/data_management/lila/create_lila_test_set.py +187 -0
  54. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  55. megadetector/data_management/lila/download_lila_subset.py +182 -0
  56. megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
  57. megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
  58. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  59. megadetector/data_management/lila/lila_common.py +319 -0
  60. megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
  61. megadetector/data_management/mewc_to_md.py +344 -0
  62. megadetector/data_management/ocr_tools.py +873 -0
  63. megadetector/data_management/read_exif.py +964 -0
  64. megadetector/data_management/remap_coco_categories.py +195 -0
  65. megadetector/data_management/remove_exif.py +156 -0
  66. megadetector/data_management/rename_images.py +194 -0
  67. megadetector/data_management/resize_coco_dataset.py +663 -0
  68. megadetector/data_management/speciesnet_to_md.py +41 -0
  69. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  70. megadetector/data_management/yolo_output_to_md_output.py +594 -0
  71. megadetector/data_management/yolo_to_coco.py +876 -0
  72. megadetector/data_management/zamba_to_md.py +188 -0
  73. megadetector/detection/__init__.py +0 -0
  74. megadetector/detection/change_detection.py +840 -0
  75. megadetector/detection/process_video.py +479 -0
  76. megadetector/detection/pytorch_detector.py +1451 -0
  77. megadetector/detection/run_detector.py +1267 -0
  78. megadetector/detection/run_detector_batch.py +2159 -0
  79. megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
  80. megadetector/detection/run_md_and_speciesnet.py +1494 -0
  81. megadetector/detection/run_tiled_inference.py +1038 -0
  82. megadetector/detection/tf_detector.py +209 -0
  83. megadetector/detection/video_utils.py +1379 -0
  84. megadetector/postprocessing/__init__.py +0 -0
  85. megadetector/postprocessing/add_max_conf.py +72 -0
  86. megadetector/postprocessing/categorize_detections_by_size.py +166 -0
  87. megadetector/postprocessing/classification_postprocessing.py +1752 -0
  88. megadetector/postprocessing/combine_batch_outputs.py +249 -0
  89. megadetector/postprocessing/compare_batch_results.py +2110 -0
  90. megadetector/postprocessing/convert_output_format.py +403 -0
  91. megadetector/postprocessing/create_crop_folder.py +629 -0
  92. megadetector/postprocessing/detector_calibration.py +570 -0
  93. megadetector/postprocessing/generate_csv_report.py +522 -0
  94. megadetector/postprocessing/load_api_results.py +223 -0
  95. megadetector/postprocessing/md_to_coco.py +428 -0
  96. megadetector/postprocessing/md_to_labelme.py +351 -0
  97. megadetector/postprocessing/md_to_wi.py +41 -0
  98. megadetector/postprocessing/merge_detections.py +392 -0
  99. megadetector/postprocessing/postprocess_batch_results.py +2077 -0
  100. megadetector/postprocessing/remap_detection_categories.py +226 -0
  101. megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
  102. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
  103. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
  104. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
  105. megadetector/postprocessing/separate_detections_into_folders.py +795 -0
  106. megadetector/postprocessing/subset_json_detector_output.py +964 -0
  107. megadetector/postprocessing/top_folders_to_bottom.py +238 -0
  108. megadetector/postprocessing/validate_batch_results.py +332 -0
  109. megadetector/taxonomy_mapping/__init__.py +0 -0
  110. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  111. megadetector/taxonomy_mapping/map_new_lila_datasets.py +213 -0
  112. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
  113. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
  114. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  115. megadetector/taxonomy_mapping/simple_image_download.py +224 -0
  116. megadetector/taxonomy_mapping/species_lookup.py +1008 -0
  117. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  118. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  119. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  120. megadetector/tests/__init__.py +0 -0
  121. megadetector/tests/test_nms_synthetic.py +335 -0
  122. megadetector/utils/__init__.py +0 -0
  123. megadetector/utils/ct_utils.py +1857 -0
  124. megadetector/utils/directory_listing.py +199 -0
  125. megadetector/utils/extract_frames_from_video.py +307 -0
  126. megadetector/utils/gpu_test.py +125 -0
  127. megadetector/utils/md_tests.py +2072 -0
  128. megadetector/utils/path_utils.py +2832 -0
  129. megadetector/utils/process_utils.py +172 -0
  130. megadetector/utils/split_locations_into_train_val.py +237 -0
  131. megadetector/utils/string_utils.py +234 -0
  132. megadetector/utils/url_utils.py +825 -0
  133. megadetector/utils/wi_platform_utils.py +968 -0
  134. megadetector/utils/wi_taxonomy_utils.py +1759 -0
  135. megadetector/utils/write_html_image_list.py +239 -0
  136. megadetector/visualization/__init__.py +0 -0
  137. megadetector/visualization/plot_utils.py +309 -0
  138. megadetector/visualization/render_images_with_thumbnails.py +243 -0
  139. megadetector/visualization/visualization_utils.py +1940 -0
  140. megadetector/visualization/visualize_db.py +630 -0
  141. megadetector/visualization/visualize_detector_output.py +479 -0
  142. megadetector/visualization/visualize_video_output.py +705 -0
  143. megadetector-10.0.13.dist-info/METADATA +134 -0
  144. megadetector-10.0.13.dist-info/RECORD +147 -0
  145. megadetector-10.0.13.dist-info/WHEEL +5 -0
  146. megadetector-10.0.13.dist-info/licenses/LICENSE +19 -0
  147. 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()