megadetector 5.0.8__py3-none-any.whl → 5.0.9__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 (190) hide show
  1. api/__init__.py +0 -0
  2. api/batch_processing/__init__.py +0 -0
  3. api/batch_processing/api_core/__init__.py +0 -0
  4. api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. api/batch_processing/api_core/batch_service/score.py +0 -1
  6. api/batch_processing/api_core/server_job_status_table.py +0 -1
  7. api/batch_processing/api_core_support/__init__.py +0 -0
  8. api/batch_processing/api_core_support/aggregate_results_manually.py +0 -1
  9. api/batch_processing/api_support/__init__.py +0 -0
  10. api/batch_processing/api_support/summarize_daily_activity.py +0 -1
  11. api/batch_processing/data_preparation/__init__.py +0 -0
  12. api/batch_processing/data_preparation/manage_local_batch.py +65 -65
  13. api/batch_processing/data_preparation/manage_video_batch.py +8 -8
  14. api/batch_processing/integration/digiKam/xmp_integration.py +0 -1
  15. api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  16. api/batch_processing/postprocessing/__init__.py +0 -0
  17. api/batch_processing/postprocessing/add_max_conf.py +12 -12
  18. api/batch_processing/postprocessing/categorize_detections_by_size.py +32 -14
  19. api/batch_processing/postprocessing/combine_api_outputs.py +68 -54
  20. api/batch_processing/postprocessing/compare_batch_results.py +113 -43
  21. api/batch_processing/postprocessing/convert_output_format.py +41 -16
  22. api/batch_processing/postprocessing/load_api_results.py +16 -17
  23. api/batch_processing/postprocessing/md_to_coco.py +31 -21
  24. api/batch_processing/postprocessing/md_to_labelme.py +52 -22
  25. api/batch_processing/postprocessing/merge_detections.py +14 -14
  26. api/batch_processing/postprocessing/postprocess_batch_results.py +246 -174
  27. api/batch_processing/postprocessing/remap_detection_categories.py +32 -25
  28. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +60 -27
  29. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +53 -44
  30. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +25 -14
  31. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +242 -158
  32. api/batch_processing/postprocessing/separate_detections_into_folders.py +159 -114
  33. api/batch_processing/postprocessing/subset_json_detector_output.py +146 -169
  34. api/batch_processing/postprocessing/top_folders_to_bottom.py +77 -43
  35. api/synchronous/__init__.py +0 -0
  36. api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  37. api/synchronous/api_core/animal_detection_api/api_backend.py +0 -2
  38. api/synchronous/api_core/animal_detection_api/api_frontend.py +266 -268
  39. api/synchronous/api_core/animal_detection_api/config.py +35 -35
  40. api/synchronous/api_core/tests/__init__.py +0 -0
  41. api/synchronous/api_core/tests/load_test.py +109 -109
  42. classification/__init__.py +0 -0
  43. classification/aggregate_classifier_probs.py +21 -24
  44. classification/analyze_failed_images.py +11 -13
  45. classification/cache_batchapi_outputs.py +51 -51
  46. classification/create_classification_dataset.py +69 -68
  47. classification/crop_detections.py +54 -53
  48. classification/csv_to_json.py +97 -100
  49. classification/detect_and_crop.py +105 -105
  50. classification/evaluate_model.py +43 -42
  51. classification/identify_mislabeled_candidates.py +47 -46
  52. classification/json_to_azcopy_list.py +10 -10
  53. classification/json_validator.py +72 -71
  54. classification/map_classification_categories.py +44 -43
  55. classification/merge_classification_detection_output.py +68 -68
  56. classification/prepare_classification_script.py +157 -154
  57. classification/prepare_classification_script_mc.py +228 -228
  58. classification/run_classifier.py +27 -26
  59. classification/save_mislabeled.py +30 -30
  60. classification/train_classifier.py +20 -20
  61. classification/train_classifier_tf.py +21 -22
  62. classification/train_utils.py +10 -10
  63. data_management/__init__.py +0 -0
  64. data_management/annotations/__init__.py +0 -0
  65. data_management/annotations/annotation_constants.py +18 -31
  66. data_management/camtrap_dp_to_coco.py +238 -0
  67. data_management/cct_json_utils.py +102 -59
  68. data_management/cct_to_md.py +176 -158
  69. data_management/cct_to_wi.py +247 -219
  70. data_management/coco_to_labelme.py +272 -263
  71. data_management/coco_to_yolo.py +79 -58
  72. data_management/databases/__init__.py +0 -0
  73. data_management/databases/add_width_and_height_to_db.py +20 -16
  74. data_management/databases/combine_coco_camera_traps_files.py +35 -31
  75. data_management/databases/integrity_check_json_db.py +62 -24
  76. data_management/databases/subset_json_db.py +24 -15
  77. data_management/generate_crops_from_cct.py +27 -45
  78. data_management/get_image_sizes.py +188 -162
  79. data_management/importers/add_nacti_sizes.py +8 -8
  80. data_management/importers/add_timestamps_to_icct.py +78 -78
  81. data_management/importers/animl_results_to_md_results.py +158 -158
  82. data_management/importers/auckland_doc_test_to_json.py +9 -9
  83. data_management/importers/auckland_doc_to_json.py +8 -8
  84. data_management/importers/awc_to_json.py +7 -7
  85. data_management/importers/bellevue_to_json.py +15 -15
  86. data_management/importers/cacophony-thermal-importer.py +13 -13
  87. data_management/importers/carrizo_shrubfree_2018.py +8 -8
  88. data_management/importers/carrizo_trail_cam_2017.py +8 -8
  89. data_management/importers/cct_field_adjustments.py +9 -9
  90. data_management/importers/channel_islands_to_cct.py +10 -10
  91. data_management/importers/eMammal/copy_and_unzip_emammal.py +1 -0
  92. data_management/importers/ena24_to_json.py +7 -7
  93. data_management/importers/filenames_to_json.py +8 -8
  94. data_management/importers/helena_to_cct.py +7 -7
  95. data_management/importers/idaho-camera-traps.py +7 -7
  96. data_management/importers/idfg_iwildcam_lila_prep.py +10 -10
  97. data_management/importers/jb_csv_to_json.py +9 -9
  98. data_management/importers/mcgill_to_json.py +8 -8
  99. data_management/importers/missouri_to_json.py +18 -18
  100. data_management/importers/nacti_fieldname_adjustments.py +10 -10
  101. data_management/importers/noaa_seals_2019.py +7 -7
  102. data_management/importers/pc_to_json.py +7 -7
  103. data_management/importers/plot_wni_giraffes.py +7 -7
  104. data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -359
  105. data_management/importers/prepare_zsl_imerit.py +7 -7
  106. data_management/importers/rspb_to_json.py +8 -8
  107. data_management/importers/save_the_elephants_survey_A.py +8 -8
  108. data_management/importers/save_the_elephants_survey_B.py +9 -9
  109. data_management/importers/snapshot_safari_importer.py +26 -26
  110. data_management/importers/snapshot_safari_importer_reprise.py +665 -665
  111. data_management/importers/snapshot_serengeti_lila.py +14 -14
  112. data_management/importers/sulross_get_exif.py +8 -9
  113. data_management/importers/timelapse_csv_set_to_json.py +11 -11
  114. data_management/importers/ubc_to_json.py +13 -13
  115. data_management/importers/umn_to_json.py +7 -7
  116. data_management/importers/wellington_to_json.py +8 -8
  117. data_management/importers/wi_to_json.py +9 -9
  118. data_management/importers/zamba_results_to_md_results.py +181 -181
  119. data_management/labelme_to_coco.py +65 -24
  120. data_management/labelme_to_yolo.py +8 -8
  121. data_management/lila/__init__.py +0 -0
  122. data_management/lila/add_locations_to_island_camera_traps.py +9 -9
  123. data_management/lila/add_locations_to_nacti.py +147 -147
  124. data_management/lila/create_lila_blank_set.py +13 -13
  125. data_management/lila/create_lila_test_set.py +8 -8
  126. data_management/lila/create_links_to_md_results_files.py +106 -106
  127. data_management/lila/download_lila_subset.py +44 -110
  128. data_management/lila/generate_lila_per_image_labels.py +55 -42
  129. data_management/lila/get_lila_annotation_counts.py +18 -15
  130. data_management/lila/get_lila_image_counts.py +11 -11
  131. data_management/lila/lila_common.py +96 -33
  132. data_management/lila/test_lila_metadata_urls.py +132 -116
  133. data_management/ocr_tools.py +173 -128
  134. data_management/read_exif.py +110 -97
  135. data_management/remap_coco_categories.py +83 -83
  136. data_management/remove_exif.py +58 -62
  137. data_management/resize_coco_dataset.py +30 -23
  138. data_management/wi_download_csv_to_coco.py +246 -239
  139. data_management/yolo_output_to_md_output.py +86 -73
  140. data_management/yolo_to_coco.py +300 -60
  141. detection/__init__.py +0 -0
  142. detection/detector_training/__init__.py +0 -0
  143. detection/process_video.py +85 -33
  144. detection/pytorch_detector.py +43 -25
  145. detection/run_detector.py +157 -72
  146. detection/run_detector_batch.py +179 -113
  147. detection/run_inference_with_yolov5_val.py +108 -48
  148. detection/run_tiled_inference.py +111 -40
  149. detection/tf_detector.py +51 -29
  150. detection/video_utils.py +606 -521
  151. docs/source/conf.py +43 -0
  152. md_utils/__init__.py +0 -0
  153. md_utils/azure_utils.py +9 -9
  154. md_utils/ct_utils.py +228 -68
  155. md_utils/directory_listing.py +59 -64
  156. md_utils/md_tests.py +968 -871
  157. md_utils/path_utils.py +460 -134
  158. md_utils/process_utils.py +157 -133
  159. md_utils/sas_blob_utils.py +20 -20
  160. md_utils/split_locations_into_train_val.py +45 -32
  161. md_utils/string_utils.py +33 -10
  162. md_utils/url_utils.py +176 -60
  163. md_utils/write_html_image_list.py +40 -33
  164. md_visualization/__init__.py +0 -0
  165. md_visualization/plot_utils.py +102 -109
  166. md_visualization/render_images_with_thumbnails.py +34 -34
  167. md_visualization/visualization_utils.py +597 -291
  168. md_visualization/visualize_db.py +76 -48
  169. md_visualization/visualize_detector_output.py +61 -42
  170. {megadetector-5.0.8.dist-info → megadetector-5.0.9.dist-info}/METADATA +13 -7
  171. megadetector-5.0.9.dist-info/RECORD +224 -0
  172. {megadetector-5.0.8.dist-info → megadetector-5.0.9.dist-info}/top_level.txt +1 -0
  173. taxonomy_mapping/__init__.py +0 -0
  174. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +342 -335
  175. taxonomy_mapping/map_new_lila_datasets.py +154 -154
  176. taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -134
  177. taxonomy_mapping/preview_lila_taxonomy.py +591 -591
  178. taxonomy_mapping/retrieve_sample_image.py +12 -12
  179. taxonomy_mapping/simple_image_download.py +11 -11
  180. taxonomy_mapping/species_lookup.py +10 -10
  181. taxonomy_mapping/taxonomy_csv_checker.py +18 -18
  182. taxonomy_mapping/taxonomy_graph.py +47 -47
  183. taxonomy_mapping/validate_lila_category_mappings.py +83 -76
  184. data_management/cct_json_to_filename_json.py +0 -89
  185. data_management/cct_to_csv.py +0 -140
  186. data_management/databases/remove_corrupted_images_from_db.py +0 -191
  187. detection/detector_training/copy_checkpoints.py +0 -43
  188. megadetector-5.0.8.dist-info/RECORD +0 -205
  189. {megadetector-5.0.8.dist-info → megadetector-5.0.9.dist-info}/LICENSE +0 -0
  190. {megadetector-5.0.8.dist-info → megadetector-5.0.9.dist-info}/WHEEL +0 -0
detection/video_utils.py CHANGED
@@ -1,521 +1,606 @@
1
- ########
2
- #
3
- # video_utils.py
4
- #
5
- # Utilities for splitting, rendering, and assembling videos.
6
- #
7
- ########
8
-
9
- #%% Constants, imports, environment
10
-
11
- import os
12
- import cv2
13
- import glob
14
- import json
15
-
16
- from collections import defaultdict
17
- from multiprocessing.pool import ThreadPool
18
- from multiprocessing.pool import Pool
19
- from tqdm import tqdm
20
- from typing import Container,Iterable,List
21
- from functools import partial
22
-
23
- from md_utils import path_utils
24
-
25
- from md_visualization import visualization_utils as vis_utils
26
-
27
- default_fourcc = 'h264'
28
-
29
-
30
- #%% Path utilities
31
-
32
- VIDEO_EXTENSIONS = ('.mp4','.avi','.mpeg','.mpg')
33
-
34
- def is_video_file(s: str, video_extensions: Container[str] = VIDEO_EXTENSIONS
35
- ) -> bool:
36
- """
37
- Checks a file's extension against a hard-coded set of video file
38
- extensions.
39
- """
40
-
41
- ext = os.path.splitext(s)[1]
42
- return ext.lower() in video_extensions
43
-
44
-
45
- def find_video_strings(strings: Iterable[str]) -> List[str]:
46
- """
47
- Given a list of strings that are potentially video file names, looks for
48
- strings that actually look like video file names (based on extension).
49
- """
50
-
51
- return [s for s in strings if is_video_file(s.lower())]
52
-
53
-
54
- def find_videos(dirname: str, recursive: bool = False,
55
- convert_slashes: bool=False,
56
- return_relative_paths: bool=False) -> List[str]:
57
- """
58
- Finds all files in a directory that look like video file names. Returns
59
- absolute paths unless return_relative_paths is set. Uses the native
60
- path separator unless convert_slashes is set.
61
- """
62
-
63
- if recursive:
64
- files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
65
- else:
66
- files = glob.glob(os.path.join(dirname, '*.*'))
67
-
68
- if return_relative_paths:
69
- files = [os.path.relpath(fn,dirname) for fn in files]
70
-
71
- if convert_slashes:
72
- files = [fn.replace('\\', '/') for fn in files]
73
-
74
- return find_video_strings(files)
75
-
76
-
77
- #%% Function for rendering frames to video and vice-versa
78
-
79
- # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
80
-
81
- def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
82
- """
83
- Given a list of image files and a sample rate, concatenate those images into
84
- a video and write to [output_file_name].
85
-
86
- Note to self: h264 is a sensible default and generally works on Windows, but when this
87
- fails (which is around 50% of the time on Linux), I fall back to mp4v.
88
- """
89
-
90
- if codec_spec is None:
91
- codec_spec = 'h264'
92
-
93
- if len(images) == 0:
94
- return
95
-
96
- # Determine the width and height from the first image
97
- frame = cv2.imread(images[0])
98
- cv2.imshow('video',frame)
99
- height, width, channels = frame.shape
100
-
101
- # Define the codec and create VideoWriter object
102
- fourcc = cv2.VideoWriter_fourcc(*codec_spec)
103
- out = cv2.VideoWriter(output_file_name, fourcc, Fs, (width, height))
104
-
105
- for image in images:
106
- frame = cv2.imread(image)
107
- out.write(frame)
108
-
109
- out.release()
110
- cv2.destroyAllWindows()
111
-
112
-
113
- def get_video_fs(input_video_file):
114
- """
115
- Get the frame rate of [input_video_file]
116
- """
117
-
118
- assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
119
- vidcap = cv2.VideoCapture(input_video_file)
120
- Fs = vidcap.get(cv2.CAP_PROP_FPS)
121
- vidcap.release()
122
- return Fs
123
-
124
-
125
- def frame_number_to_filename(frame_number):
126
- return 'frame{:06d}.jpg'.format(frame_number)
127
-
128
-
129
- def video_to_frames(input_video_file, output_folder, overwrite=True,
130
- every_n_frames=None, verbose=False):
131
- """
132
- Render every frame of [input_video_file] to a .jpg in [output_folder]
133
-
134
- With help from:
135
-
136
- https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
137
- """
138
-
139
- assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
140
-
141
- vidcap = cv2.VideoCapture(input_video_file)
142
- n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
143
- Fs = vidcap.get(cv2.CAP_PROP_FPS)
144
-
145
- # If we're not over-writing, check whether all frame images already exist
146
- if overwrite == False:
147
-
148
- missing_frame_number = None
149
- frame_filenames = []
150
-
151
- for frame_number in range(0,n_frames):
152
-
153
- if every_n_frames is not None:
154
- if frame_number % every_n_frames != 0:
155
- continue
156
-
157
- frame_filename = frame_number_to_filename(frame_number)
158
- frame_filename = os.path.join(output_folder,frame_filename)
159
- frame_filenames.append(frame_filename)
160
- if os.path.isfile(frame_filename):
161
- continue
162
- else:
163
- missing_frame_number = frame_number
164
- break
165
-
166
- # OpenCV seems to over-report the number of frames by 1 in some cases, or fails
167
- # to read the last frame; either way, I'm allowing one missing frame.
168
- allow_last_frame_missing = True
169
-
170
- if missing_frame_number is None or \
171
- (allow_last_frame_missing and (missing_frame_number == n_frames-1)):
172
- if verbose:
173
- print('Skipping video {}, all output frames exist'.format(input_video_file))
174
- return frame_filenames,Fs
175
- else:
176
- pass
177
- # print("Rendering video {}, couldn't find frame {}".format(
178
- # input_video_file,missing_frame_number))
179
-
180
- # ...if we need to check whether to skip this video entirely
181
-
182
- if verbose:
183
- print('Reading {} frames at {} Hz from {}'.format(n_frames,Fs,input_video_file))
184
-
185
- frame_filenames = []
186
-
187
- # for frame_number in tqdm(range(0,n_frames)):
188
- for frame_number in range(0,n_frames):
189
-
190
- success,image = vidcap.read()
191
- if not success:
192
- assert image is None
193
- if verbose:
194
- print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
195
- break
196
-
197
- if every_n_frames is not None:
198
- if frame_number % every_n_frames != 0:
199
- continue
200
-
201
- frame_filename = frame_number_to_filename(frame_number)
202
- frame_filename = os.path.join(output_folder,frame_filename)
203
- frame_filenames.append(frame_filename)
204
-
205
- if overwrite == False and os.path.isfile(frame_filename):
206
- # print('Skipping frame {}'.format(frame_filename))
207
- pass
208
- else:
209
- try:
210
- if frame_filename.isascii():
211
- cv2.imwrite(os.path.normpath(frame_filename),image)
212
- else:
213
- is_success, im_buf_arr = cv2.imencode('.jpg', image)
214
- im_buf_arr.tofile(frame_filename)
215
- assert os.path.isfile(frame_filename), \
216
- 'Output frame {} unavailable'.format(frame_filename)
217
- except KeyboardInterrupt:
218
- vidcap.release()
219
- raise
220
- except Exception as e:
221
- print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
222
-
223
- if verbose:
224
- print('\nExtracted {} of {} frames'.format(len(frame_filenames),n_frames))
225
-
226
- vidcap.release()
227
- return frame_filenames,Fs
228
-
229
-
230
- def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,every_n_frames,overwrite,verbose):
231
- """
232
- Internal function to call video_to_frames in the context of video_folder_to_frames;
233
- makes sure the right output folder exists, then calls video_to_frames.
234
- """
235
-
236
- input_fn_absolute = os.path.join(input_folder,relative_fn)
237
- assert os.path.isfile(input_fn_absolute),\
238
- 'Could not find file {}'.format(input_fn_absolute)
239
-
240
- # Create the target output folder
241
- output_folder_video = os.path.join(output_folder_base,relative_fn)
242
- os.makedirs(output_folder_video,exist_ok=True)
243
-
244
- # Render frames
245
- # input_video_file = input_fn_absolute; output_folder = output_folder_video
246
- frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
247
- overwrite=overwrite,every_n_frames=every_n_frames,
248
- verbose=verbose)
249
-
250
- return frame_filenames,fs
251
-
252
-
253
- def video_folder_to_frames(input_folder:str, output_folder_base:str,
254
- recursive:bool=True, overwrite:bool=True,
255
- n_threads:int=1, every_n_frames:int=None,
256
- verbose=False, parallelization_uses_threads=True):
257
- """
258
- For every video file in input_folder, create a folder within output_folder_base, and
259
- render every frame of the video to .jpg in that folder.
260
-
261
- return frame_filenames_by_video,fs_by_video,input_files_full_paths
262
- """
263
-
264
- # Recursively enumerate video files
265
- input_files_full_paths = find_videos(input_folder,recursive=recursive)
266
- print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
267
- if len(input_files_full_paths) == 0:
268
- return [],[],[]
269
-
270
- input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
271
- input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
272
-
273
- os.makedirs(output_folder_base,exist_ok=True)
274
-
275
- frame_filenames_by_video = []
276
- fs_by_video = []
277
-
278
- if n_threads == 1:
279
- # For each video
280
- #
281
- # input_fn_relative = input_files_relative_paths[0]
282
- for input_fn_relative in tqdm(input_files_relative_paths):
283
-
284
- frame_filenames,fs = \
285
- _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
286
- every_n_frames,overwrite,verbose)
287
- frame_filenames_by_video.append(frame_filenames)
288
- fs_by_video.append(fs)
289
- else:
290
- if parallelization_uses_threads:
291
- print('Starting a worker pool with {} threads'.format(n_threads))
292
- pool = ThreadPool(n_threads)
293
- else:
294
- print('Starting a worker pool with {} processes'.format(n_threads))
295
- pool = Pool(n_threads)
296
- process_video_with_options = partial(_video_to_frames_for_folder,
297
- input_folder=input_folder,
298
- output_folder_base=output_folder_base,
299
- every_n_frames=every_n_frames,
300
- overwrite=overwrite,
301
- verbose=verbose)
302
- results = list(tqdm(pool.imap(
303
- partial(process_video_with_options),input_files_relative_paths),
304
- total=len(input_files_relative_paths)))
305
- frame_filenames_by_video = [x[0] for x in results]
306
- fs_by_video = [x[1] for x in results]
307
-
308
- return frame_filenames_by_video,fs_by_video,input_files_full_paths
309
-
310
-
311
- class FrameToVideoOptions:
312
-
313
- # One-indexed, i.e. "1" means "use the confidence value from the highest-confidence frame"
314
- nth_highest_confidence = 1
315
-
316
- # 'error' or 'skip_with_warning'
317
- non_video_behavior = 'error'
318
-
319
-
320
- def frame_results_to_video_results(input_file,output_file,options:FrameToVideoOptions = None):
321
- """
322
- Given an API output file produced at the *frame* level, corresponding to a directory
323
- created with video_folder_to_frames, map those frame-level results back to the
324
- video level for use in Timelapse.
325
-
326
- Preserves everything in the input .json file other than the images.
327
- """
328
-
329
- if options is None:
330
- options = FrameToVideoOptions()
331
-
332
- # Load results
333
- with open(input_file,'r') as f:
334
- input_data = json.load(f)
335
-
336
- images = input_data['images']
337
- detection_categories = input_data['detection_categories']
338
-
339
- ## Break into videos
340
-
341
- video_to_frames = defaultdict(list)
342
-
343
- # im = images[0]
344
- for im in tqdm(images):
345
-
346
- fn = im['file']
347
- video_name = os.path.dirname(fn)
348
- if not is_video_file(video_name):
349
- if options.non_video_behavior == 'error':
350
- raise ValueError('{} is not a video file'.format(video_name))
351
- elif options.non_video_behavior == 'skip_with_warning':
352
- print('Warning: {} is not a video file'.format(video_name))
353
- continue
354
- else:
355
- raise ValueError('Unrecognized non-video handling behavior: {}'.format(
356
- options.non_video_behavior))
357
- video_to_frames[video_name].append(im)
358
-
359
- print('Found {} unique videos in {} frame-level results'.format(
360
- len(video_to_frames),len(images)))
361
-
362
- output_images = []
363
-
364
- ## For each video...
365
-
366
- # video_name = list(video_to_frames.keys())[0]
367
- for video_name in tqdm(video_to_frames):
368
-
369
- frames = video_to_frames[video_name]
370
-
371
- all_detections_this_video = []
372
-
373
- # frame = frames[0]
374
- for frame in frames:
375
- if frame['detections'] is not None:
376
- all_detections_this_video.extend(frame['detections'])
377
-
378
- # At most one detection for each category for the whole video
379
- canonical_detections = []
380
-
381
- # category_id = list(detection_categories.keys())[0]
382
- for category_id in detection_categories:
383
-
384
- category_detections = [det for det in all_detections_this_video if \
385
- det['category'] == category_id]
386
-
387
- # Find the nth-highest-confidence video to choose a confidence value
388
- if len(category_detections) >= options.nth_highest_confidence:
389
-
390
- category_detections_by_confidence = sorted(category_detections,
391
- key = lambda i: i['conf'],reverse=True)
392
- canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
393
- canonical_detections.append(canonical_detection)
394
-
395
- # Prepare the output representation for this video
396
- im_out = {}
397
- im_out['file'] = video_name
398
- im_out['detections'] = canonical_detections
399
-
400
- # 'max_detection_conf' is no longer included in output files by default
401
- if False:
402
- im_out['max_detection_conf'] = 0
403
- if len(canonical_detections) > 0:
404
- confidences = [d['conf'] for d in canonical_detections]
405
- im_out['max_detection_conf'] = max(confidences)
406
-
407
- output_images.append(im_out)
408
-
409
- # ...for each video
410
-
411
- output_data = input_data
412
- output_data['images'] = output_images
413
- s = json.dumps(output_data,indent=1)
414
-
415
- # Write the output file
416
- with open(output_file,'w') as f:
417
- f.write(s)
418
-
419
-
420
- #%% Test driver
421
-
422
- if False:
423
-
424
- #%% Constants
425
-
426
- Fs = 30.01
427
- confidence_threshold = 0.75
428
- input_folder = 'z:\\'
429
- frame_folder_base = r'e:\video_test\frames'
430
- detected_frame_folder_base = r'e:\video_test\detected_frames'
431
- rendered_videos_folder_base = r'e:\video_test\rendered_videos'
432
-
433
- results_file = r'results.json'
434
- os.makedirs(detected_frame_folder_base,exist_ok=True)
435
- os.makedirs(rendered_videos_folder_base,exist_ok=True)
436
-
437
-
438
- #%% Split videos into frames
439
-
440
- frame_filenames_by_video,fs_by_video,video_filenames = \
441
- video_folder_to_frames(input_folder,frame_folder_base,recursive=True)
442
-
443
-
444
- #%% List image files, break into folders
445
-
446
- frame_files = path_utils.find_images(frame_folder_base,True)
447
- frame_files = [s.replace('\\','/') for s in frame_files]
448
- print('Enumerated {} total frames'.format(len(frame_files)))
449
-
450
- Fs = 30.01
451
- # Find unique folders
452
- folders = set()
453
- # fn = frame_files[0]
454
- for fn in frame_files:
455
- folders.add(os.path.dirname(fn))
456
- folders = [s.replace('\\','/') for s in folders]
457
- print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
458
-
459
-
460
- #%% Load detector output
461
-
462
- with open(results_file,'r') as f:
463
- detection_results = json.load(f)
464
- detections = detection_results['images']
465
- detector_label_map = detection_results['detection_categories']
466
- for d in detections:
467
- d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
468
-
469
-
470
- #%% Render detector frames
471
-
472
- # folder = list(folders)[0]
473
- for folder in folders:
474
-
475
- frame_files_this_folder = [fn for fn in frame_files if folder in fn]
476
- folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
477
- detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
478
- print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
479
- assert len(frame_files_this_folder) == len(detection_results_this_folder)
480
-
481
- rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
482
- os.makedirs(rendered_frame_output_folder,exist_ok=True)
483
-
484
- # d = detection_results_this_folder[0]
485
- for d in tqdm(detection_results_this_folder):
486
-
487
- input_file = os.path.join(frame_folder_base,d['file'])
488
- output_file = os.path.join(detected_frame_folder_base,d['file'])
489
- os.makedirs(os.path.dirname(output_file),exist_ok=True)
490
- vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
491
- confidence_threshold)
492
-
493
- # ...for each file in this folder
494
-
495
- # ...for each folder
496
-
497
-
498
- #%% Render output videos
499
-
500
- # folder = list(folders)[0]
501
- for folder in tqdm(folders):
502
-
503
- folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
504
- rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
505
- assert os.path.isdir(rendered_detector_output_folder)
506
-
507
- frame_files_relative = os.listdir(rendered_detector_output_folder)
508
- frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
509
- for s in frame_files_relative]
510
-
511
- output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
512
- os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
513
-
514
- original_video_filename = output_video_filename.replace(
515
- rendered_videos_folder_base,input_folder)
516
- assert os.path.isfile(original_video_filename)
517
- Fs = get_video_fs(original_video_filename)
518
-
519
- frames_to_video(frame_files_absolute, Fs, output_video_filename)
520
-
521
- # ...for each video
1
+ """
2
+
3
+ video_utils.py
4
+
5
+ Utilities for splitting, rendering, and assembling videos.
6
+
7
+ """
8
+
9
+ #%% Constants, imports, environment
10
+
11
+ import os
12
+ import cv2
13
+ import glob
14
+ import json
15
+
16
+ from collections import defaultdict
17
+ from multiprocessing.pool import ThreadPool
18
+ from multiprocessing.pool import Pool
19
+ from tqdm import tqdm
20
+ from functools import partial
21
+
22
+ from md_utils import path_utils
23
+ from md_visualization import visualization_utils as vis_utils
24
+
25
+ default_fourcc = 'h264'
26
+
27
+
28
+ #%% Path utilities
29
+
30
+ VIDEO_EXTENSIONS = ('.mp4','.avi','.mpeg','.mpg')
31
+
32
+ def is_video_file(s,video_extensions=VIDEO_EXTENSIONS):
33
+ """
34
+ Checks a file's extension against a set of known video file
35
+ extensions to determine whether it's a video file. Performs a
36
+ case-insensitive comparison.
37
+
38
+ Args:
39
+ s (str): filename to check for probable video-ness
40
+ video_extensions (list, optional): list of video file extensions
41
+
42
+ Returns:
43
+ bool: True if this looks like a video file, else False
44
+ """
45
+
46
+ ext = os.path.splitext(s)[1]
47
+ return ext.lower() in video_extensions
48
+
49
+
50
+ def find_video_strings(strings):
51
+ """
52
+ Given a list of strings that are potentially video file names, looks for
53
+ strings that actually look like video file names (based on extension).
54
+
55
+ Args:
56
+ strings (list): list of strings to check for video-ness
57
+
58
+ Returns:
59
+ list: a subset of [strings] that looks like they are video filenames
60
+ """
61
+
62
+ return [s for s in strings if is_video_file(s.lower())]
63
+
64
+
65
+ def find_videos(dirname,
66
+ recursive=False,
67
+ convert_slashes=True,
68
+ return_relative_paths=False):
69
+ """
70
+ Finds all files in a directory that look like video file names.
71
+
72
+ Args:
73
+ dirname (str): folder to search for video files
74
+ recursive (bool, optional): whether to search [dirname] recursively
75
+ convert_slashes (bool, optional): forces forward slashes in the returned files,
76
+ otherwise uses the native path separator
77
+ return_relative_paths (bool, optional): forces the returned filenames to be
78
+ relative to [dirname], otherwise returns absolute paths
79
+
80
+ Returns:
81
+ A list of filenames within [dirname] that appear to be videos
82
+ """
83
+
84
+ if recursive:
85
+ files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
86
+ else:
87
+ files = glob.glob(os.path.join(dirname, '*.*'))
88
+
89
+ if return_relative_paths:
90
+ files = [os.path.relpath(fn,dirname) for fn in files]
91
+
92
+ if convert_slashes:
93
+ files = [fn.replace('\\', '/') for fn in files]
94
+
95
+ return find_video_strings(files)
96
+
97
+
98
+ #%% Function for rendering frames to video and vice-versa
99
+
100
+ # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
101
+
102
+ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
103
+ """
104
+ Given a list of image files and a sample rate, concatenates those images into
105
+ a video and writes to a new video file.
106
+
107
+ Args:
108
+ images (list): a list of frame file names to concatenate into a video
109
+ Fs (float): the frame rate in fps
110
+ output_file_name (str): the output video file, no checking is performed to make
111
+ sure the extension is compatible with the codec
112
+ codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
113
+ and generally works on Windows, but when this fails (which is around 50% of the time
114
+ on Linux), mp4v is a good second choice
115
+ """
116
+
117
+ if codec_spec is None:
118
+ codec_spec = 'h264'
119
+
120
+ if len(images) == 0:
121
+ return
122
+
123
+ # Determine the width and height from the first image
124
+ frame = cv2.imread(images[0])
125
+ cv2.imshow('video',frame)
126
+ height, width, channels = frame.shape
127
+
128
+ # Define the codec and create VideoWriter object
129
+ fourcc = cv2.VideoWriter_fourcc(*codec_spec)
130
+ out = cv2.VideoWriter(output_file_name, fourcc, Fs, (width, height))
131
+
132
+ for image in images:
133
+ frame = cv2.imread(image)
134
+ out.write(frame)
135
+
136
+ out.release()
137
+ cv2.destroyAllWindows()
138
+
139
+
140
+ def get_video_fs(input_video_file):
141
+ """
142
+ Retrieves the frame rate of [input_video_file].
143
+
144
+ Args:
145
+ input_video_file (str): video file for which we want the frame rate
146
+
147
+ Returns:
148
+ float: the frame rate of [input_video_file]
149
+ """
150
+
151
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
152
+ vidcap = cv2.VideoCapture(input_video_file)
153
+ Fs = vidcap.get(cv2.CAP_PROP_FPS)
154
+ vidcap.release()
155
+ return Fs
156
+
157
+
158
+ def _frame_number_to_filename(frame_number):
159
+ """
160
+ Ensures that frame images are given consistent filenames.
161
+ """
162
+
163
+ return 'frame{:06d}.jpg'.format(frame_number)
164
+
165
+
166
+ def video_to_frames(input_video_file, output_folder, overwrite=True,
167
+ every_n_frames=None, verbose=False):
168
+ """
169
+ Renders frames from [input_video_file] to a .jpg in [output_folder].
170
+
171
+ With help from:
172
+
173
+ https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
174
+
175
+ Args:
176
+ input_video_file (str): video file to split into frames
177
+ output_folder (str): folder to put frame images in
178
+ overwrite (bool, optional): whether to overwrite existing frame images
179
+ every_n_frames (int, optional): sample every Nth frame starting from the first frame;
180
+ if this is None or 1, every frame is extracted
181
+ verbose (bool, optional): enable additional debug console output
182
+
183
+ Returns:
184
+ tuple: length-2 tuple containing (list of frame filenames,frame rate)
185
+ """
186
+
187
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
188
+
189
+ vidcap = cv2.VideoCapture(input_video_file)
190
+ n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
191
+ Fs = vidcap.get(cv2.CAP_PROP_FPS)
192
+
193
+ # If we're not over-writing, check whether all frame images already exist
194
+ if overwrite == False:
195
+
196
+ missing_frame_number = None
197
+ frame_filenames = []
198
+
199
+ for frame_number in range(0,n_frames):
200
+
201
+ if every_n_frames is not None:
202
+ if frame_number % every_n_frames != 0:
203
+ continue
204
+
205
+ frame_filename = _frame_number_to_filename(frame_number)
206
+ frame_filename = os.path.join(output_folder,frame_filename)
207
+ frame_filenames.append(frame_filename)
208
+ if os.path.isfile(frame_filename):
209
+ continue
210
+ else:
211
+ missing_frame_number = frame_number
212
+ break
213
+
214
+ # OpenCV seems to over-report the number of frames by 1 in some cases, or fails
215
+ # to read the last frame; either way, I'm allowing one missing frame.
216
+ allow_last_frame_missing = True
217
+
218
+ if missing_frame_number is None or \
219
+ (allow_last_frame_missing and (missing_frame_number == n_frames-1)):
220
+ if verbose:
221
+ print('Skipping video {}, all output frames exist'.format(input_video_file))
222
+ return frame_filenames,Fs
223
+ else:
224
+ pass
225
+ # print("Rendering video {}, couldn't find frame {}".format(
226
+ # input_video_file,missing_frame_number))
227
+
228
+ # ...if we need to check whether to skip this video entirely
229
+
230
+ if verbose:
231
+ print('Reading {} frames at {} Hz from {}'.format(n_frames,Fs,input_video_file))
232
+
233
+ frame_filenames = []
234
+
235
+ # for frame_number in tqdm(range(0,n_frames)):
236
+ for frame_number in range(0,n_frames):
237
+
238
+ success,image = vidcap.read()
239
+ if not success:
240
+ assert image is None
241
+ if verbose:
242
+ print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
243
+ break
244
+
245
+ if every_n_frames is not None:
246
+ if frame_number % every_n_frames != 0:
247
+ continue
248
+
249
+ frame_filename = _frame_number_to_filename(frame_number)
250
+ frame_filename = os.path.join(output_folder,frame_filename)
251
+ frame_filenames.append(frame_filename)
252
+
253
+ if overwrite == False and os.path.isfile(frame_filename):
254
+ # print('Skipping frame {}'.format(frame_filename))
255
+ pass
256
+ else:
257
+ try:
258
+ if frame_filename.isascii():
259
+ cv2.imwrite(os.path.normpath(frame_filename),image)
260
+ else:
261
+ is_success, im_buf_arr = cv2.imencode('.jpg', image)
262
+ im_buf_arr.tofile(frame_filename)
263
+ assert os.path.isfile(frame_filename), \
264
+ 'Output frame {} unavailable'.format(frame_filename)
265
+ except KeyboardInterrupt:
266
+ vidcap.release()
267
+ raise
268
+ except Exception as e:
269
+ print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
270
+
271
+ if verbose:
272
+ print('\nExtracted {} of {} frames'.format(len(frame_filenames),n_frames))
273
+
274
+ vidcap.release()
275
+ return frame_filenames,Fs
276
+
277
+ # ...def video_to_frames(...)
278
+
279
+
280
+ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,every_n_frames,overwrite,verbose):
281
+ """
282
+ Internal function to call video_to_frames in the context of video_folder_to_frames;
283
+ makes sure the right output folder exists, then calls video_to_frames.
284
+ """
285
+
286
+ input_fn_absolute = os.path.join(input_folder,relative_fn)
287
+ assert os.path.isfile(input_fn_absolute),\
288
+ 'Could not find file {}'.format(input_fn_absolute)
289
+
290
+ # Create the target output folder
291
+ output_folder_video = os.path.join(output_folder_base,relative_fn)
292
+ os.makedirs(output_folder_video,exist_ok=True)
293
+
294
+ # Render frames
295
+ # input_video_file = input_fn_absolute; output_folder = output_folder_video
296
+ frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
297
+ overwrite=overwrite,every_n_frames=every_n_frames,
298
+ verbose=verbose)
299
+
300
+ return frame_filenames,fs
301
+
302
+
303
+ def video_folder_to_frames(input_folder, output_folder_base,
304
+ recursive=True, overwrite=True,
305
+ n_threads=1, every_n_frames=None,
306
+ verbose=False, parallelization_uses_threads=True):
307
+ """
308
+ For every video file in input_folder, creates a folder within output_folder_base, and
309
+ renders frame of that video to images in that folder.
310
+
311
+ Args:
312
+ input_folder (str): folder to process
313
+ output_folder_base (str): root folder for output images; subfolders will be
314
+ created for each input video
315
+ recursive (bool, optional): whether to recursively process videos in [input_folder]
316
+ overwrite (bool, optional): whether to overwrite existing frame images
317
+ n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
318
+ parallelism
319
+ every_n_frames (int, optional): sample every Nth frame starting from the first frame;
320
+ if this is None or 1, every frame is extracted
321
+ verbose (bool, optional): enable additional debug console output
322
+ parallelization_uses_threads (bool, optional): whether to use threads (True) or
323
+ processes (False) for parallelization; ignored if n_threads <= 1
324
+
325
+ Returns:
326
+ tuple: a length-3 tuple containing:
327
+ - list of lists of frame filenames; the Nth list of frame filenames corresponds to
328
+ the Nth video
329
+ - list of video frame rates; the Nth value corresponds to the Nth video
330
+ - list of video filenames
331
+ """
332
+
333
+ # Recursively enumerate video files
334
+ input_files_full_paths = find_videos(input_folder,recursive=recursive)
335
+ print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
336
+ if len(input_files_full_paths) == 0:
337
+ return [],[],[]
338
+
339
+ input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
340
+ input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
341
+
342
+ os.makedirs(output_folder_base,exist_ok=True)
343
+
344
+ frame_filenames_by_video = []
345
+ fs_by_video = []
346
+
347
+ if n_threads == 1:
348
+ # For each video
349
+ #
350
+ # input_fn_relative = input_files_relative_paths[0]
351
+ for input_fn_relative in tqdm(input_files_relative_paths):
352
+
353
+ frame_filenames,fs = \
354
+ _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
355
+ every_n_frames,overwrite,verbose)
356
+ frame_filenames_by_video.append(frame_filenames)
357
+ fs_by_video.append(fs)
358
+ else:
359
+ if parallelization_uses_threads:
360
+ print('Starting a worker pool with {} threads'.format(n_threads))
361
+ pool = ThreadPool(n_threads)
362
+ else:
363
+ print('Starting a worker pool with {} processes'.format(n_threads))
364
+ pool = Pool(n_threads)
365
+ process_video_with_options = partial(_video_to_frames_for_folder,
366
+ input_folder=input_folder,
367
+ output_folder_base=output_folder_base,
368
+ every_n_frames=every_n_frames,
369
+ overwrite=overwrite,
370
+ verbose=verbose)
371
+ results = list(tqdm(pool.imap(
372
+ partial(process_video_with_options),input_files_relative_paths),
373
+ total=len(input_files_relative_paths)))
374
+ frame_filenames_by_video = [x[0] for x in results]
375
+ fs_by_video = [x[1] for x in results]
376
+
377
+ return frame_filenames_by_video,fs_by_video,input_files_full_paths
378
+
379
+ # ...def video_folder_to_frames(...)
380
+
381
+
382
+ class FrameToVideoOptions:
383
+ """
384
+ Options controlling the conversion of frame-level results to video-level results via
385
+ frame_results_to_video_results()
386
+ """
387
+
388
+ #: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
389
+ #: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
390
+ nth_highest_confidence = 1
391
+
392
+ #: What to do if a file referred to in a .json results file appears not to be a
393
+ #: video; can be 'error' or 'skip_with_warning'
394
+ non_video_behavior = 'error'
395
+
396
+
397
+ def frame_results_to_video_results(input_file,output_file,options=None):
398
+ """
399
+ Given an MD results file produced at the *frame* level, corresponding to a directory
400
+ created with video_folder_to_frames, maps those frame-level results back to the
401
+ video level for use in Timelapse.
402
+
403
+ Preserves everything in the input .json file other than the images.
404
+
405
+ Args:
406
+ input_file (str): the frame-level MD results file to convert to video-level results
407
+ output_file (str): the .json file to which we should write video-level results
408
+ options (FrameToVideoOptions, optional): parameters for converting frame-level results
409
+ to video-level results, see FrameToVideoOptions for details
410
+ """
411
+
412
+ if options is None:
413
+ options = FrameToVideoOptions()
414
+
415
+ # Load results
416
+ with open(input_file,'r') as f:
417
+ input_data = json.load(f)
418
+
419
+ images = input_data['images']
420
+ detection_categories = input_data['detection_categories']
421
+
422
+ ## Break into videos
423
+
424
+ video_to_frames = defaultdict(list)
425
+
426
+ # im = images[0]
427
+ for im in tqdm(images):
428
+
429
+ fn = im['file']
430
+ video_name = os.path.dirname(fn)
431
+ if not is_video_file(video_name):
432
+ if options.non_video_behavior == 'error':
433
+ raise ValueError('{} is not a video file'.format(video_name))
434
+ elif options.non_video_behavior == 'skip_with_warning':
435
+ print('Warning: {} is not a video file'.format(video_name))
436
+ continue
437
+ else:
438
+ raise ValueError('Unrecognized non-video handling behavior: {}'.format(
439
+ options.non_video_behavior))
440
+ video_to_frames[video_name].append(im)
441
+
442
+ print('Found {} unique videos in {} frame-level results'.format(
443
+ len(video_to_frames),len(images)))
444
+
445
+ output_images = []
446
+
447
+ ## For each video...
448
+
449
+ # video_name = list(video_to_frames.keys())[0]
450
+ for video_name in tqdm(video_to_frames):
451
+
452
+ frames = video_to_frames[video_name]
453
+
454
+ all_detections_this_video = []
455
+
456
+ # frame = frames[0]
457
+ for frame in frames:
458
+ if frame['detections'] is not None:
459
+ all_detections_this_video.extend(frame['detections'])
460
+
461
+ # At most one detection for each category for the whole video
462
+ canonical_detections = []
463
+
464
+ # category_id = list(detection_categories.keys())[0]
465
+ for category_id in detection_categories:
466
+
467
+ category_detections = [det for det in all_detections_this_video if \
468
+ det['category'] == category_id]
469
+
470
+ # Find the nth-highest-confidence video to choose a confidence value
471
+ if len(category_detections) >= options.nth_highest_confidence:
472
+
473
+ category_detections_by_confidence = sorted(category_detections,
474
+ key = lambda i: i['conf'],reverse=True)
475
+ canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
476
+ canonical_detections.append(canonical_detection)
477
+
478
+ # Prepare the output representation for this video
479
+ im_out = {}
480
+ im_out['file'] = video_name
481
+ im_out['detections'] = canonical_detections
482
+
483
+ # 'max_detection_conf' is no longer included in output files by default
484
+ if False:
485
+ im_out['max_detection_conf'] = 0
486
+ if len(canonical_detections) > 0:
487
+ confidences = [d['conf'] for d in canonical_detections]
488
+ im_out['max_detection_conf'] = max(confidences)
489
+
490
+ output_images.append(im_out)
491
+
492
+ # ...for each video
493
+
494
+ output_data = input_data
495
+ output_data['images'] = output_images
496
+ s = json.dumps(output_data,indent=1)
497
+
498
+ # Write the output file
499
+ with open(output_file,'w') as f:
500
+ f.write(s)
501
+
502
+ # ...def frame_results_to_video_results(...)
503
+
504
+
505
+ #%% Test driver
506
+
507
+ if False:
508
+
509
+ #%% Constants
510
+
511
+ Fs = 30.01
512
+ confidence_threshold = 0.75
513
+ input_folder = 'z:\\'
514
+ frame_folder_base = r'e:\video_test\frames'
515
+ detected_frame_folder_base = r'e:\video_test\detected_frames'
516
+ rendered_videos_folder_base = r'e:\video_test\rendered_videos'
517
+
518
+ results_file = r'results.json'
519
+ os.makedirs(detected_frame_folder_base,exist_ok=True)
520
+ os.makedirs(rendered_videos_folder_base,exist_ok=True)
521
+
522
+
523
+ #%% Split videos into frames
524
+
525
+ frame_filenames_by_video,fs_by_video,video_filenames = \
526
+ video_folder_to_frames(input_folder,frame_folder_base,recursive=True)
527
+
528
+
529
+ #%% List image files, break into folders
530
+
531
+ frame_files = path_utils.find_images(frame_folder_base,True)
532
+ frame_files = [s.replace('\\','/') for s in frame_files]
533
+ print('Enumerated {} total frames'.format(len(frame_files)))
534
+
535
+ Fs = 30.01
536
+ # Find unique folders
537
+ folders = set()
538
+ # fn = frame_files[0]
539
+ for fn in frame_files:
540
+ folders.add(os.path.dirname(fn))
541
+ folders = [s.replace('\\','/') for s in folders]
542
+ print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
543
+
544
+
545
+ #%% Load detector output
546
+
547
+ with open(results_file,'r') as f:
548
+ detection_results = json.load(f)
549
+ detections = detection_results['images']
550
+ detector_label_map = detection_results['detection_categories']
551
+ for d in detections:
552
+ d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
553
+
554
+
555
+ #%% Render detector frames
556
+
557
+ # folder = list(folders)[0]
558
+ for folder in folders:
559
+
560
+ frame_files_this_folder = [fn for fn in frame_files if folder in fn]
561
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
562
+ detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
563
+ print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
564
+ assert len(frame_files_this_folder) == len(detection_results_this_folder)
565
+
566
+ rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
567
+ os.makedirs(rendered_frame_output_folder,exist_ok=True)
568
+
569
+ # d = detection_results_this_folder[0]
570
+ for d in tqdm(detection_results_this_folder):
571
+
572
+ input_file = os.path.join(frame_folder_base,d['file'])
573
+ output_file = os.path.join(detected_frame_folder_base,d['file'])
574
+ os.makedirs(os.path.dirname(output_file),exist_ok=True)
575
+ vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
576
+ confidence_threshold)
577
+
578
+ # ...for each file in this folder
579
+
580
+ # ...for each folder
581
+
582
+
583
+ #%% Render output videos
584
+
585
+ # folder = list(folders)[0]
586
+ for folder in tqdm(folders):
587
+
588
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
589
+ rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
590
+ assert os.path.isdir(rendered_detector_output_folder)
591
+
592
+ frame_files_relative = os.listdir(rendered_detector_output_folder)
593
+ frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
594
+ for s in frame_files_relative]
595
+
596
+ output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
597
+ os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
598
+
599
+ original_video_filename = output_video_filename.replace(
600
+ rendered_videos_folder_base,input_folder)
601
+ assert os.path.isfile(original_video_filename)
602
+ Fs = get_video_fs(original_video_filename)
603
+
604
+ frames_to_video(frame_files_absolute, Fs, output_video_filename)
605
+
606
+ # ...for each video