megadetector 5.0.11__py3-none-any.whl → 5.0.12__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 (201) hide show
  1. megadetector/api/__init__.py +0 -0
  2. megadetector/api/batch_processing/__init__.py +0 -0
  3. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  4. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
  6. megadetector/api/batch_processing/api_core/server.py +294 -0
  7. megadetector/api/batch_processing/api_core/server_api_config.py +98 -0
  8. megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
  9. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
  10. megadetector/api/batch_processing/api_core/server_job_status_table.py +152 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +92 -0
  13. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  14. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
  15. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  16. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
  17. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  18. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  19. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  20. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  21. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +126 -0
  22. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  23. megadetector/api/synchronous/__init__.py +0 -0
  24. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  25. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
  26. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +266 -0
  27. megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
  28. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  29. megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
  30. megadetector/classification/__init__.py +0 -0
  31. megadetector/classification/aggregate_classifier_probs.py +108 -0
  32. megadetector/classification/analyze_failed_images.py +227 -0
  33. megadetector/classification/cache_batchapi_outputs.py +198 -0
  34. megadetector/classification/create_classification_dataset.py +627 -0
  35. megadetector/classification/crop_detections.py +516 -0
  36. megadetector/classification/csv_to_json.py +226 -0
  37. megadetector/classification/detect_and_crop.py +855 -0
  38. megadetector/classification/efficientnet/__init__.py +9 -0
  39. megadetector/classification/efficientnet/model.py +415 -0
  40. megadetector/classification/efficientnet/utils.py +610 -0
  41. megadetector/classification/evaluate_model.py +520 -0
  42. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  43. megadetector/classification/json_to_azcopy_list.py +63 -0
  44. megadetector/classification/json_validator.py +699 -0
  45. megadetector/classification/map_classification_categories.py +276 -0
  46. megadetector/classification/merge_classification_detection_output.py +506 -0
  47. megadetector/classification/prepare_classification_script.py +194 -0
  48. megadetector/classification/prepare_classification_script_mc.py +228 -0
  49. megadetector/classification/run_classifier.py +287 -0
  50. megadetector/classification/save_mislabeled.py +110 -0
  51. megadetector/classification/train_classifier.py +827 -0
  52. megadetector/classification/train_classifier_tf.py +725 -0
  53. megadetector/classification/train_utils.py +323 -0
  54. megadetector/data_management/__init__.py +0 -0
  55. megadetector/data_management/annotations/__init__.py +0 -0
  56. megadetector/data_management/annotations/annotation_constants.py +34 -0
  57. megadetector/data_management/camtrap_dp_to_coco.py +239 -0
  58. megadetector/data_management/cct_json_utils.py +395 -0
  59. megadetector/data_management/cct_to_md.py +176 -0
  60. megadetector/data_management/cct_to_wi.py +289 -0
  61. megadetector/data_management/coco_to_labelme.py +272 -0
  62. megadetector/data_management/coco_to_yolo.py +662 -0
  63. megadetector/data_management/databases/__init__.py +0 -0
  64. megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
  65. megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
  66. megadetector/data_management/databases/integrity_check_json_db.py +477 -0
  67. megadetector/data_management/databases/subset_json_db.py +115 -0
  68. megadetector/data_management/generate_crops_from_cct.py +149 -0
  69. megadetector/data_management/get_image_sizes.py +189 -0
  70. megadetector/data_management/importers/add_nacti_sizes.py +52 -0
  71. megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
  72. megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
  73. megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
  74. megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
  75. megadetector/data_management/importers/awc_to_json.py +191 -0
  76. megadetector/data_management/importers/bellevue_to_json.py +273 -0
  77. megadetector/data_management/importers/cacophony-thermal-importer.py +796 -0
  78. megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
  79. megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
  80. megadetector/data_management/importers/cct_field_adjustments.py +58 -0
  81. megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
  82. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
  83. megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
  84. megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
  85. megadetector/data_management/importers/ena24_to_json.py +276 -0
  86. megadetector/data_management/importers/filenames_to_json.py +386 -0
  87. megadetector/data_management/importers/helena_to_cct.py +283 -0
  88. megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
  89. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
  90. megadetector/data_management/importers/jb_csv_to_json.py +150 -0
  91. megadetector/data_management/importers/mcgill_to_json.py +250 -0
  92. megadetector/data_management/importers/missouri_to_json.py +490 -0
  93. megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
  94. megadetector/data_management/importers/noaa_seals_2019.py +181 -0
  95. megadetector/data_management/importers/pc_to_json.py +365 -0
  96. megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
  97. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
  98. megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
  99. megadetector/data_management/importers/rspb_to_json.py +356 -0
  100. megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
  101. megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
  102. megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
  103. megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
  104. megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
  105. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
  106. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
  107. megadetector/data_management/importers/sulross_get_exif.py +65 -0
  108. megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
  109. megadetector/data_management/importers/ubc_to_json.py +399 -0
  110. megadetector/data_management/importers/umn_to_json.py +507 -0
  111. megadetector/data_management/importers/wellington_to_json.py +263 -0
  112. megadetector/data_management/importers/wi_to_json.py +442 -0
  113. megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
  114. megadetector/data_management/labelme_to_coco.py +547 -0
  115. megadetector/data_management/labelme_to_yolo.py +272 -0
  116. megadetector/data_management/lila/__init__.py +0 -0
  117. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
  118. megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
  119. megadetector/data_management/lila/create_lila_blank_set.py +558 -0
  120. megadetector/data_management/lila/create_lila_test_set.py +152 -0
  121. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  122. megadetector/data_management/lila/download_lila_subset.py +178 -0
  123. megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
  124. megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
  125. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  126. megadetector/data_management/lila/lila_common.py +300 -0
  127. megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
  128. megadetector/data_management/ocr_tools.py +874 -0
  129. megadetector/data_management/read_exif.py +681 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/resize_coco_dataset.py +189 -0
  133. megadetector/data_management/wi_download_csv_to_coco.py +246 -0
  134. megadetector/data_management/yolo_output_to_md_output.py +441 -0
  135. megadetector/data_management/yolo_to_coco.py +676 -0
  136. megadetector/detection/__init__.py +0 -0
  137. megadetector/detection/detector_training/__init__.py +0 -0
  138. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  139. megadetector/detection/process_video.py +702 -0
  140. megadetector/detection/pytorch_detector.py +341 -0
  141. megadetector/detection/run_detector.py +779 -0
  142. megadetector/detection/run_detector_batch.py +1219 -0
  143. megadetector/detection/run_inference_with_yolov5_val.py +917 -0
  144. megadetector/detection/run_tiled_inference.py +934 -0
  145. megadetector/detection/tf_detector.py +189 -0
  146. megadetector/detection/video_utils.py +606 -0
  147. megadetector/postprocessing/__init__.py +0 -0
  148. megadetector/postprocessing/add_max_conf.py +64 -0
  149. megadetector/postprocessing/categorize_detections_by_size.py +163 -0
  150. megadetector/postprocessing/combine_api_outputs.py +249 -0
  151. megadetector/postprocessing/compare_batch_results.py +958 -0
  152. megadetector/postprocessing/convert_output_format.py +396 -0
  153. megadetector/postprocessing/load_api_results.py +195 -0
  154. megadetector/postprocessing/md_to_coco.py +310 -0
  155. megadetector/postprocessing/md_to_labelme.py +330 -0
  156. megadetector/postprocessing/merge_detections.py +401 -0
  157. megadetector/postprocessing/postprocess_batch_results.py +1902 -0
  158. megadetector/postprocessing/remap_detection_categories.py +170 -0
  159. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  160. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  161. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  162. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1631 -0
  163. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  164. megadetector/postprocessing/subset_json_detector_output.py +696 -0
  165. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  166. megadetector/taxonomy_mapping/__init__.py +0 -0
  167. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  168. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  169. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  170. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +590 -0
  171. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  172. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  173. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  174. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  175. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  176. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  177. megadetector/utils/__init__.py +0 -0
  178. megadetector/utils/azure_utils.py +178 -0
  179. megadetector/utils/ct_utils.py +612 -0
  180. megadetector/utils/directory_listing.py +246 -0
  181. megadetector/utils/md_tests.py +968 -0
  182. megadetector/utils/path_utils.py +1044 -0
  183. megadetector/utils/process_utils.py +157 -0
  184. megadetector/utils/sas_blob_utils.py +509 -0
  185. megadetector/utils/split_locations_into_train_val.py +228 -0
  186. megadetector/utils/string_utils.py +92 -0
  187. megadetector/utils/url_utils.py +323 -0
  188. megadetector/utils/write_html_image_list.py +225 -0
  189. megadetector/visualization/__init__.py +0 -0
  190. megadetector/visualization/plot_utils.py +293 -0
  191. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  192. megadetector/visualization/visualization_utils.py +1536 -0
  193. megadetector/visualization/visualize_db.py +550 -0
  194. megadetector/visualization/visualize_detector_output.py +405 -0
  195. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
  196. megadetector-5.0.12.dist-info/RECORD +199 -0
  197. megadetector-5.0.12.dist-info/top_level.txt +1 -0
  198. megadetector-5.0.11.dist-info/RECORD +0 -5
  199. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  200. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/LICENSE +0 -0
  201. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/WHEEL +0 -0
@@ -0,0 +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 functools import partial
21
+
22
+ from megadetector.utils import path_utils
23
+ from megadetector.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
File without changes
@@ -0,0 +1,64 @@
1
+ """
2
+
3
+ add_max_conf.py
4
+
5
+ The MD output format included a "max_detection_conf" field with each image
6
+ up to and including version 1.2; it was removed as of version 1.3 (it's
7
+ redundant with the individual detection confidence values).
8
+
9
+ Just in case someone took a dependency on that field, this script allows you
10
+ to add it back to an existing .json file.
11
+
12
+ """
13
+
14
+ #%% Imports and constants
15
+
16
+ import os
17
+ import json
18
+ from megadetector.utils import ct_utils
19
+
20
+
21
+ #%% Main function
22
+
23
+ def add_max_conf(input_file,output_file):
24
+
25
+ assert os.path.isfile(input_file), "Can't find input file {}".format(input_file)
26
+
27
+ with open(input_file,'r') as f:
28
+ d = json.load(f)
29
+
30
+ for im in d['images']:
31
+
32
+ max_conf = ct_utils.get_max_conf(im)
33
+
34
+ if 'max_detection_conf' in im:
35
+ assert abs(max_conf - im['max_detection_conf']) < 0.00001
36
+ else:
37
+ im['max_detection_conf'] = max_conf
38
+
39
+ with open(output_file,'w') as f:
40
+ json.dump(d,f,indent=1)
41
+
42
+
43
+ #%% Driver
44
+
45
+ import sys,argparse
46
+
47
+ def main():
48
+
49
+ parser = argparse.ArgumentParser()
50
+ parser.add_argument('input_file',type=str,
51
+ help='Input .json file')
52
+ parser.add_argument('output_file',type=str,
53
+ help='Output .json file')
54
+
55
+ if len(sys.argv[1:]) == 0:
56
+ parser.print_help()
57
+ parser.exit()
58
+
59
+ args = parser.parse_args()
60
+ add_max_conf(args.input_file, args.output_file)
61
+
62
+ if __name__ == '__main__':
63
+ main()
64
+