megadetector 5.0.28__py3-none-any.whl → 10.0.0__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 (197) hide show
  1. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  2. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  3. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  4. megadetector/classification/aggregate_classifier_probs.py +3 -3
  5. megadetector/classification/analyze_failed_images.py +5 -5
  6. megadetector/classification/cache_batchapi_outputs.py +5 -5
  7. megadetector/classification/create_classification_dataset.py +11 -12
  8. megadetector/classification/crop_detections.py +10 -10
  9. megadetector/classification/csv_to_json.py +8 -8
  10. megadetector/classification/detect_and_crop.py +13 -15
  11. megadetector/classification/efficientnet/model.py +8 -8
  12. megadetector/classification/efficientnet/utils.py +6 -5
  13. megadetector/classification/evaluate_model.py +7 -7
  14. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  15. megadetector/classification/json_to_azcopy_list.py +1 -1
  16. megadetector/classification/json_validator.py +29 -32
  17. megadetector/classification/map_classification_categories.py +9 -9
  18. megadetector/classification/merge_classification_detection_output.py +12 -9
  19. megadetector/classification/prepare_classification_script.py +19 -19
  20. megadetector/classification/prepare_classification_script_mc.py +26 -26
  21. megadetector/classification/run_classifier.py +4 -4
  22. megadetector/classification/save_mislabeled.py +6 -6
  23. megadetector/classification/train_classifier.py +1 -1
  24. megadetector/classification/train_classifier_tf.py +9 -9
  25. megadetector/classification/train_utils.py +10 -10
  26. megadetector/data_management/annotations/annotation_constants.py +1 -2
  27. megadetector/data_management/camtrap_dp_to_coco.py +79 -46
  28. megadetector/data_management/cct_json_utils.py +103 -103
  29. megadetector/data_management/cct_to_md.py +49 -49
  30. megadetector/data_management/cct_to_wi.py +33 -33
  31. megadetector/data_management/coco_to_labelme.py +75 -75
  32. megadetector/data_management/coco_to_yolo.py +210 -193
  33. megadetector/data_management/databases/add_width_and_height_to_db.py +86 -12
  34. megadetector/data_management/databases/combine_coco_camera_traps_files.py +40 -40
  35. megadetector/data_management/databases/integrity_check_json_db.py +228 -200
  36. megadetector/data_management/databases/subset_json_db.py +33 -33
  37. megadetector/data_management/generate_crops_from_cct.py +88 -39
  38. megadetector/data_management/get_image_sizes.py +54 -49
  39. megadetector/data_management/labelme_to_coco.py +133 -125
  40. megadetector/data_management/labelme_to_yolo.py +159 -73
  41. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  42. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  43. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  44. megadetector/data_management/lila/download_lila_subset.py +21 -24
  45. megadetector/data_management/lila/generate_lila_per_image_labels.py +365 -107
  46. megadetector/data_management/lila/get_lila_annotation_counts.py +35 -33
  47. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  48. megadetector/data_management/lila/lila_common.py +73 -70
  49. megadetector/data_management/lila/test_lila_metadata_urls.py +28 -19
  50. megadetector/data_management/mewc_to_md.py +344 -340
  51. megadetector/data_management/ocr_tools.py +262 -255
  52. megadetector/data_management/read_exif.py +249 -227
  53. megadetector/data_management/remap_coco_categories.py +90 -28
  54. megadetector/data_management/remove_exif.py +81 -21
  55. megadetector/data_management/rename_images.py +187 -187
  56. megadetector/data_management/resize_coco_dataset.py +588 -120
  57. megadetector/data_management/speciesnet_to_md.py +41 -41
  58. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  59. megadetector/data_management/yolo_output_to_md_output.py +248 -122
  60. megadetector/data_management/yolo_to_coco.py +333 -191
  61. megadetector/detection/change_detection.py +832 -0
  62. megadetector/detection/process_video.py +340 -337
  63. megadetector/detection/pytorch_detector.py +358 -278
  64. megadetector/detection/run_detector.py +399 -186
  65. megadetector/detection/run_detector_batch.py +404 -377
  66. megadetector/detection/run_inference_with_yolov5_val.py +340 -327
  67. megadetector/detection/run_tiled_inference.py +257 -249
  68. megadetector/detection/tf_detector.py +24 -24
  69. megadetector/detection/video_utils.py +332 -295
  70. megadetector/postprocessing/add_max_conf.py +19 -11
  71. megadetector/postprocessing/categorize_detections_by_size.py +45 -45
  72. megadetector/postprocessing/classification_postprocessing.py +468 -433
  73. megadetector/postprocessing/combine_batch_outputs.py +23 -23
  74. megadetector/postprocessing/compare_batch_results.py +590 -525
  75. megadetector/postprocessing/convert_output_format.py +106 -102
  76. megadetector/postprocessing/create_crop_folder.py +347 -147
  77. megadetector/postprocessing/detector_calibration.py +173 -168
  78. megadetector/postprocessing/generate_csv_report.py +508 -499
  79. megadetector/postprocessing/load_api_results.py +48 -27
  80. megadetector/postprocessing/md_to_coco.py +133 -102
  81. megadetector/postprocessing/md_to_labelme.py +107 -90
  82. megadetector/postprocessing/md_to_wi.py +40 -40
  83. megadetector/postprocessing/merge_detections.py +92 -114
  84. megadetector/postprocessing/postprocess_batch_results.py +319 -301
  85. megadetector/postprocessing/remap_detection_categories.py +91 -38
  86. megadetector/postprocessing/render_detection_confusion_matrix.py +214 -205
  87. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  88. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  89. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +704 -679
  90. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  91. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  92. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  93. megadetector/postprocessing/validate_batch_results.py +70 -70
  94. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  95. megadetector/taxonomy_mapping/map_new_lila_datasets.py +18 -19
  96. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +54 -33
  97. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +67 -67
  98. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  99. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  100. megadetector/taxonomy_mapping/species_lookup.py +156 -74
  101. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  102. megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
  103. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  104. megadetector/utils/ct_utils.py +1049 -211
  105. megadetector/utils/directory_listing.py +21 -77
  106. megadetector/utils/gpu_test.py +22 -22
  107. megadetector/utils/md_tests.py +632 -529
  108. megadetector/utils/path_utils.py +1520 -431
  109. megadetector/utils/process_utils.py +41 -41
  110. megadetector/utils/split_locations_into_train_val.py +62 -62
  111. megadetector/utils/string_utils.py +148 -27
  112. megadetector/utils/url_utils.py +489 -176
  113. megadetector/utils/wi_utils.py +2658 -2526
  114. megadetector/utils/write_html_image_list.py +137 -137
  115. megadetector/visualization/plot_utils.py +34 -30
  116. megadetector/visualization/render_images_with_thumbnails.py +39 -74
  117. megadetector/visualization/visualization_utils.py +487 -435
  118. megadetector/visualization/visualize_db.py +232 -198
  119. megadetector/visualization/visualize_detector_output.py +82 -76
  120. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/METADATA +5 -2
  121. megadetector-10.0.0.dist-info/RECORD +139 -0
  122. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/WHEEL +1 -1
  123. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  124. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  125. megadetector/api/batch_processing/api_core/batch_service/score.py +0 -439
  126. megadetector/api/batch_processing/api_core/server.py +0 -294
  127. megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
  128. megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
  129. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  130. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
  131. megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
  132. megadetector/api/batch_processing/api_core/server_utils.py +0 -88
  133. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  134. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  135. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  136. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  137. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  138. megadetector/api/synchronous/__init__.py +0 -0
  139. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  140. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
  141. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
  142. megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
  143. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  144. megadetector/api/synchronous/api_core/tests/load_test.py +0 -110
  145. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  146. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  147. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  148. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  149. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  150. megadetector/data_management/importers/awc_to_json.py +0 -191
  151. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  152. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  153. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  154. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  155. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  156. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  157. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  158. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  159. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  160. megadetector/data_management/importers/ena24_to_json.py +0 -276
  161. megadetector/data_management/importers/filenames_to_json.py +0 -386
  162. megadetector/data_management/importers/helena_to_cct.py +0 -283
  163. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  164. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  165. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  166. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  167. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  168. megadetector/data_management/importers/missouri_to_json.py +0 -490
  169. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  170. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  171. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  172. megadetector/data_management/importers/pc_to_json.py +0 -365
  173. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  174. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  175. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  176. megadetector/data_management/importers/rspb_to_json.py +0 -356
  177. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  178. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  179. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  180. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  181. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  182. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  183. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  184. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  185. megadetector/data_management/importers/ubc_to_json.py +0 -399
  186. megadetector/data_management/importers/umn_to_json.py +0 -507
  187. megadetector/data_management/importers/wellington_to_json.py +0 -263
  188. megadetector/data_management/importers/wi_to_json.py +0 -442
  189. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  190. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  191. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  192. megadetector/utils/azure_utils.py +0 -178
  193. megadetector/utils/sas_blob_utils.py +0 -509
  194. megadetector-5.0.28.dist-info/RECORD +0 -209
  195. /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
  196. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
  197. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,7 @@ from tqdm import tqdm
21
21
  from functools import partial
22
22
  from inspect import signature
23
23
 
24
- from megadetector.utils import path_utils
24
+ from megadetector.utils import path_utils
25
25
  from megadetector.utils.ct_utils import sort_list_of_dicts_by_key
26
26
  from megadetector.visualization import visualization_utils as vis_utils
27
27
 
@@ -37,15 +37,15 @@ def is_video_file(s,video_extensions=VIDEO_EXTENSIONS):
37
37
  Checks a file's extension against a set of known video file
38
38
  extensions to determine whether it's a video file. Performs a
39
39
  case-insensitive comparison.
40
-
40
+
41
41
  Args:
42
42
  s (str): filename to check for probable video-ness
43
43
  video_extensions (list, optional): list of video file extensions
44
-
44
+
45
45
  Returns:
46
46
  bool: True if this looks like a video file, else False
47
47
  """
48
-
48
+
49
49
  ext = os.path.splitext(s)[1]
50
50
  return ext.lower() in video_extensions
51
51
 
@@ -54,49 +54,49 @@ def find_video_strings(strings):
54
54
  """
55
55
  Given a list of strings that are potentially video file names, looks for
56
56
  strings that actually look like video file names (based on extension).
57
-
57
+
58
58
  Args:
59
59
  strings (list): list of strings to check for video-ness
60
-
60
+
61
61
  Returns:
62
62
  list: a subset of [strings] that looks like they are video filenames
63
63
  """
64
-
64
+
65
65
  return [s for s in strings if is_video_file(s.lower())]
66
66
 
67
67
 
68
- def find_videos(dirname,
68
+ def find_videos(dirname,
69
69
  recursive=False,
70
70
  convert_slashes=True,
71
71
  return_relative_paths=False):
72
72
  """
73
73
  Finds all files in a directory that look like video file names.
74
-
74
+
75
75
  Args:
76
76
  dirname (str): folder to search for video files
77
77
  recursive (bool, optional): whether to search [dirname] recursively
78
78
  convert_slashes (bool, optional): forces forward slashes in the returned files,
79
79
  otherwise uses the native path separator
80
- return_relative_paths (bool, optional): forces the returned filenames to be
80
+ return_relative_paths (bool, optional): forces the returned filenames to be
81
81
  relative to [dirname], otherwise returns absolute paths
82
-
82
+
83
83
  Returns:
84
84
  A list of filenames within [dirname] that appear to be videos
85
85
  """
86
-
86
+
87
87
  if recursive:
88
88
  files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
89
89
  else:
90
90
  files = glob.glob(os.path.join(dirname, '*.*'))
91
-
91
+
92
92
  files = [fn for fn in files if os.path.isfile(fn)]
93
-
93
+
94
94
  if return_relative_paths:
95
95
  files = [os.path.relpath(fn,dirname) for fn in files]
96
96
 
97
97
  if convert_slashes:
98
98
  files = [fn.replace('\\', '/') for fn in files]
99
-
99
+
100
100
  return find_video_strings(files)
101
101
 
102
102
 
@@ -104,30 +104,30 @@ def find_videos(dirname,
104
104
 
105
105
  # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
106
106
 
107
- def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
107
+ def frames_to_video(images, fs, output_file_name, codec_spec=default_fourcc):
108
108
  """
109
109
  Given a list of image files and a sample rate, concatenates those images into
110
110
  a video and writes to a new video file.
111
-
111
+
112
112
  Args:
113
113
  images (list): a list of frame file names to concatenate into a video
114
- Fs (float): the frame rate in fps
114
+ fs (float): the frame rate in fps
115
115
  output_file_name (str): the output video file, no checking is performed to make
116
116
  sure the extension is compatible with the codec
117
- codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
118
- and generally works on Windows, but when this fails (which is around 50% of the time
117
+ codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
118
+ and generally works on Windows, but when this fails (which is around 50% of the time
119
119
  on Linux), mp4v is a good second choice
120
120
  """
121
-
121
+
122
122
  if codec_spec is None:
123
123
  codec_spec = 'h264'
124
-
124
+
125
125
  if len(images) == 0:
126
126
  print('Warning: no frames to render')
127
127
  return
128
128
 
129
129
  os.makedirs(os.path.dirname(output_file_name),exist_ok=True)
130
-
130
+
131
131
  # Determine the width and height from the first image
132
132
  frame = cv2.imread(images[0])
133
133
  cv2.imshow('video',frame)
@@ -135,7 +135,7 @@ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
135
135
 
136
136
  # Define the codec and create VideoWriter object
137
137
  fourcc = cv2.VideoWriter_fourcc(*codec_spec)
138
- out = cv2.VideoWriter(output_file_name, fourcc, Fs, (width, height))
138
+ out = cv2.VideoWriter(output_file_name, fourcc, fs, (width, height))
139
139
 
140
140
  for image in images:
141
141
  frame = cv2.imread(image)
@@ -148,40 +148,41 @@ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
148
148
  def get_video_fs(input_video_file):
149
149
  """
150
150
  Retrieves the frame rate of [input_video_file].
151
-
151
+
152
152
  Args:
153
153
  input_video_file (str): video file for which we want the frame rate
154
-
154
+
155
155
  Returns:
156
156
  float: the frame rate of [input_video_file]
157
157
  """
158
-
159
- assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
158
+
159
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
160
160
  vidcap = cv2.VideoCapture(input_video_file)
161
- Fs = vidcap.get(cv2.CAP_PROP_FPS)
161
+ fs = vidcap.get(cv2.CAP_PROP_FPS)
162
162
  vidcap.release()
163
- return Fs
163
+ return fs
164
164
 
165
165
 
166
166
  def _frame_number_to_filename(frame_number):
167
167
  """
168
168
  Ensures that frame images are given consistent filenames.
169
169
  """
170
-
170
+
171
171
  return 'frame{:06d}.jpg'.format(frame_number)
172
172
 
173
173
 
174
174
  def _filename_to_frame_number(filename):
175
175
  """
176
- Extract the frame number from a filename that was created using
176
+ Extract the frame number from a filename that was created using
177
177
  _frame_number_to_filename.
178
-
178
+
179
179
  Args:
180
180
  filename (str): a filename created with _frame_number_to_filename.
181
+
181
182
  Returns:
182
183
  int: the frame number extracted from [filename]
183
184
  """
184
-
185
+
185
186
  filename = os.path.basename(filename)
186
187
  match = re.search(r'frame(\d+)\.jpg', filename)
187
188
  if match is None:
@@ -189,9 +190,9 @@ def _filename_to_frame_number(filename):
189
190
  frame_number = match.group(1)
190
191
  try:
191
192
  frame_number = int(frame_number)
192
- except:
193
+ except Exception:
193
194
  raise ValueError('Filename {} does contain a valid frame number'.format(filename))
194
-
195
+
195
196
  return frame_number
196
197
 
197
198
 
@@ -199,38 +200,38 @@ def _add_frame_numbers_to_results(results):
199
200
  """
200
201
  Given the 'images' list from a set of MD results that was generated on video frames,
201
202
  add a 'frame_number' field to each image, and return the list, sorted by frame number.
202
-
203
+
203
204
  Args:
204
- results (list): list of image dicts
205
+ results (list): list of image dicts
205
206
  """
206
-
207
+
207
208
  # Add video-specific fields to the results
208
209
  for im in results:
209
210
  fn = im['file']
210
211
  frame_number = _filename_to_frame_number(fn)
211
212
  im['frame_number'] = frame_number
212
-
213
+
213
214
  results = sort_list_of_dicts_by_key(results,'frame_number')
214
215
  return results
215
-
216
216
 
217
- def run_callback_on_frames(input_video_file,
217
+
218
+ def run_callback_on_frames(input_video_file,
218
219
  frame_callback,
219
- every_n_frames=None,
220
- verbose=False,
220
+ every_n_frames=None,
221
+ verbose=False,
221
222
  frames_to_process=None,
222
223
  allow_empty_videos=False):
223
224
  """
224
225
  Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
225
226
  [input_video_file].
226
-
227
+
227
228
  Args:
228
229
  input_video_file (str): video file to process
229
- frame_callback (function): callback to run on frames, should take an np.array and a string and
230
+ frame_callback (function): callback to run on frames, should take an np.array and a string and
230
231
  return a single value. callback should expect PIL-formatted (RGB) images.
231
232
  every_n_frames (float, optional): sample every Nth frame starting from the first frame;
232
233
  if this is None or 1, every frame is processed. If this is a negative value, it's
233
- interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
234
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
234
235
  rate. Mutually exclusive with frames_to_process.
235
236
  verbose (bool, optional): enable additional debug console output
236
237
  frames_to_process (list of int, optional): process this specific set of frames;
@@ -239,43 +240,43 @@ def run_callback_on_frames(input_video_file,
239
240
  a single frame number.
240
241
  allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
241
242
  frames (by default, this is an error).
242
-
243
+
243
244
  Returns:
244
245
  dict: dict with keys 'frame_filenames' (list), 'frame_rate' (float), 'results' (list).
245
246
  'frame_filenames' are synthetic filenames (e.g. frame000000.jpg); 'results' are
246
247
  in the same format used in the 'images' array in the MD results format.
247
248
  """
248
-
249
+
249
250
  assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
250
-
251
+
251
252
  if isinstance(frames_to_process,int):
252
253
  frames_to_process = [frames_to_process]
253
-
254
+
254
255
  if (frames_to_process is not None) and (every_n_frames is not None):
255
256
  raise ValueError('frames_to_process and every_n_frames are mutually exclusive')
256
-
257
+
257
258
  vidcap = cv2.VideoCapture(input_video_file)
258
259
  n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
259
260
  frame_rate = vidcap.get(cv2.CAP_PROP_FPS)
260
-
261
+
261
262
  if verbose:
262
263
  print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,frame_rate))
263
264
 
264
265
  frame_filenames = []
265
266
  results = []
266
-
267
+
267
268
  if (every_n_frames is not None) and (every_n_frames < 0):
268
269
  every_n_seconds = abs(every_n_frames)
269
270
  every_n_frames = int(every_n_seconds * frame_rate)
270
271
  if verbose:
271
272
  print('Interpreting a time sampling rate of {} hz as a frame interval of {}'.format(
272
273
  every_n_seconds,every_n_frames))
273
-
274
+
274
275
  # frame_number = 0
275
276
  for frame_number in range(0,n_frames):
276
277
 
277
278
  success,image = vidcap.read()
278
-
279
+
279
280
  if not success:
280
281
  assert image is None
281
282
  if verbose:
@@ -291,88 +292,88 @@ def run_callback_on_frames(input_video_file,
291
292
  break
292
293
  if frame_number not in frames_to_process:
293
294
  continue
294
-
295
- frame_filename_relative = _frame_number_to_filename(frame_number)
295
+
296
+ frame_filename_relative = _frame_number_to_filename(frame_number)
296
297
  frame_filenames.append(frame_filename_relative)
297
-
298
- image_np = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
298
+
299
+ image_np = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
299
300
  frame_results = frame_callback(image_np,frame_filename_relative)
300
301
  results.append(frame_results)
301
-
302
- # ...for each frame
303
-
302
+
303
+ # ...for each frame
304
+
304
305
  if len(frame_filenames) == 0:
305
306
  if allow_empty_videos:
306
307
  print('Warning: found no frames in file {}'.format(input_video_file))
307
308
  else:
308
309
  raise Exception('Error: found no frames in file {}'.format(input_video_file))
309
-
310
+
310
311
  if verbose:
311
312
  print('\nProcessed {} of {} frames for {}'.format(
312
313
  len(frame_filenames),n_frames,input_video_file))
313
314
 
314
- vidcap.release()
315
+ vidcap.release()
315
316
  to_return = {}
316
317
  to_return['frame_filenames'] = frame_filenames
317
318
  to_return['frame_rate'] = frame_rate
318
319
  to_return['results'] = results
319
-
320
+
320
321
  return to_return
321
322
 
322
323
  # ...def run_callback_on_frames(...)
323
324
 
324
325
 
325
- def run_callback_on_frames_for_folder(input_video_folder,
326
+ def run_callback_on_frames_for_folder(input_video_folder,
326
327
  frame_callback,
327
- every_n_frames=None,
328
- verbose=False,
328
+ every_n_frames=None,
329
+ verbose=False,
329
330
  allow_empty_videos=False,
330
331
  recursive=True):
331
332
  """
332
- Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
333
+ Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
333
334
  all videos in [input_video_folder].
334
-
335
+
335
336
  Args:
336
337
  input_video_folder (str): video folder to process
337
- frame_callback (function): callback to run on frames, should take an np.array and a string and
338
+ frame_callback (function): callback to run on frames, should take an np.array and a string and
338
339
  return a single value. callback should expect PIL-formatted (RGB) images.
339
340
  every_n_frames (int, optional): sample every Nth frame starting from the first frame;
340
- if this is None or 1, every frame is processed. If this is a negative value, it's
341
- interpreted as a sampling rate in seconds, which is rounded to the nearest frame
341
+ if this is None or 1, every frame is processed. If this is a negative value, it's
342
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame
342
343
  sampling rate.
343
344
  verbose (bool, optional): enable additional debug console output
344
345
  allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
345
346
  frames (by default, this is an error).
346
347
  recursive (bool, optional): recurse into [input_video_folder]
347
-
348
+
348
349
  Returns:
349
350
  dict: dict with keys 'video_filenames' (list of str), 'frame_rates' (list of floats),
350
351
  'results' (list of list of dicts). 'video_filenames' will contain *relative* filenames.
351
352
  """
352
-
353
+
353
354
  to_return = {'video_filenames':[],'frame_rates':[],'results':[]}
354
-
355
+
355
356
  # Recursively enumerate video files
356
357
  input_files_full_paths = find_videos(input_video_folder,
357
358
  recursive=recursive,
358
359
  convert_slashes=True,
359
360
  return_relative_paths=False)
360
361
  print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_video_folder))
361
-
362
+
362
363
  if len(input_files_full_paths) == 0:
363
364
  return to_return
364
-
365
+
365
366
  # Process each video
366
-
367
+
367
368
  # video_fn_abs = input_files_full_paths[0]
368
369
  for video_fn_abs in tqdm(input_files_full_paths):
369
370
  video_results = run_callback_on_frames(input_video_file=video_fn_abs,
370
371
  frame_callback=frame_callback,
371
- every_n_frames=every_n_frames,
372
- verbose=verbose,
372
+ every_n_frames=every_n_frames,
373
+ verbose=verbose,
373
374
  frames_to_process=None,
374
375
  allow_empty_videos=allow_empty_videos)
375
-
376
+
376
377
  """
377
378
  dict: dict with keys 'frame_filenames' (list), 'frame_rate' (float), 'results' (list).
378
379
  'frame_filenames' are synthetic filenames (e.g. frame000000.jpg); 'results' are
@@ -386,42 +387,42 @@ def run_callback_on_frames_for_folder(input_video_folder,
386
387
  assert r['file'].startswith('frame')
387
388
  r['file'] = video_filename_relative + '/' + r['file']
388
389
  to_return['results'].append(video_results['results'])
389
-
390
+
390
391
  # ...for each video
391
-
392
+
392
393
  n_videos = len(input_files_full_paths)
393
394
  assert len(to_return['video_filenames']) == n_videos
394
395
  assert len(to_return['frame_rates']) == n_videos
395
396
  assert len(to_return['results']) == n_videos
396
-
397
+
397
398
  return to_return
398
399
 
399
400
  # ...def run_callback_on_frames_for_folder(...)
400
401
 
401
-
402
- def video_to_frames(input_video_file,
403
- output_folder,
404
- overwrite=True,
405
- every_n_frames=None,
406
- verbose=False,
402
+
403
+ def video_to_frames(input_video_file,
404
+ output_folder,
405
+ overwrite=True,
406
+ every_n_frames=None,
407
+ verbose=False,
407
408
  quality=None,
408
- max_width=None,
409
+ max_width=None,
409
410
  frames_to_extract=None,
410
411
  allow_empty_videos=False):
411
412
  """
412
413
  Renders frames from [input_video_file] to .jpg files in [output_folder].
413
-
414
+
414
415
  With help from:
415
-
416
+
416
417
  https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
417
-
418
+
418
419
  Args:
419
420
  input_video_file (str): video file to split into frames
420
421
  output_folder (str): folder to put frame images in
421
422
  overwrite (bool, optional): whether to overwrite existing frame images
422
423
  every_n_frames (int, optional): sample every Nth frame starting from the first frame;
423
424
  if this is None or 1, every frame is extracted. If this is a negative value, it's
424
- interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
425
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
425
426
  rate. Mutually exclusive with frames_to_extract.
426
427
  verbose (bool, optional): enable additional debug console output
427
428
  quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
@@ -430,60 +431,65 @@ def video_to_frames(input_video_file,
430
431
  frames_to_extract (list of int, optional): extract this specific set of frames;
431
432
  mutually exclusive with every_n_frames. If all values are beyond the length
432
433
  of the video, no frames are extracted. Can also be a single int, specifying
433
- a single frame number.
434
+ a single frame number. In the special case where frames_to_extract
435
+ is [], this function still reads video frame rates and verifies that videos
436
+ are readable, but no frames are extracted.
434
437
  allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
435
438
  frames (by default, this is an error).
436
-
439
+
437
440
  Returns:
438
441
  tuple: length-2 tuple containing (list of frame filenames,frame rate)
439
442
  """
440
-
443
+
441
444
  assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
442
-
445
+
443
446
  if quality is not None and quality < 0:
444
447
  quality = None
445
-
448
+
446
449
  if isinstance(frames_to_extract,int):
447
450
  frames_to_extract = [frames_to_extract]
448
-
451
+
449
452
  if (frames_to_extract is not None) and (every_n_frames is not None):
450
453
  raise ValueError('frames_to_extract and every_n_frames are mutually exclusive')
451
-
452
- os.makedirs(output_folder,exist_ok=True)
453
-
454
+
455
+ bypass_extraction = ((frames_to_extract is not None) and (len(frames_to_extract) == 0))
456
+
457
+ if not bypass_extraction:
458
+ os.makedirs(output_folder,exist_ok=True)
459
+
454
460
  vidcap = cv2.VideoCapture(input_video_file)
455
461
  n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
456
- Fs = vidcap.get(cv2.CAP_PROP_FPS)
457
-
462
+ fs = vidcap.get(cv2.CAP_PROP_FPS)
463
+
458
464
  if (every_n_frames is not None) and (every_n_frames < 0):
459
465
  every_n_seconds = abs(every_n_frames)
460
- every_n_frames = int(every_n_seconds * Fs)
466
+ every_n_frames = int(every_n_seconds * fs)
461
467
  if verbose:
462
468
  print('Interpreting a time sampling rate of {} hz as a frame interval of {}'.format(
463
469
  every_n_seconds,every_n_frames))
464
-
470
+
465
471
  # If we're not over-writing, check whether all frame images already exist
466
- if overwrite == False:
467
-
472
+ if (not overwrite) and (not bypass_extraction):
473
+
468
474
  missing_frame_number = None
469
475
  missing_frame_filename = None
470
476
  frame_filenames = []
471
477
  found_existing_frame = False
472
-
478
+
473
479
  for frame_number in range(0,n_frames):
474
-
480
+
475
481
  if every_n_frames is not None:
476
482
  assert frames_to_extract is None, \
477
483
  'Internal error: frames_to_extract and every_n_frames are exclusive'
478
484
  if (frame_number % every_n_frames) != 0:
479
485
  continue
480
-
486
+
481
487
  if frames_to_extract is not None:
482
488
  assert every_n_frames is None, \
483
489
  'Internal error: frames_to_extract and every_n_frames are exclusive'
484
490
  if frame_number not in frames_to_extract:
485
491
  continue
486
-
492
+
487
493
  frame_filename = _frame_number_to_filename(frame_number)
488
494
  frame_filename = os.path.join(output_folder,frame_filename)
489
495
  frame_filenames.append(frame_filename)
@@ -494,39 +500,38 @@ def video_to_frames(input_video_file,
494
500
  missing_frame_number = frame_number
495
501
  missing_frame_filename = frame_filename
496
502
  break
497
-
503
+
498
504
  if verbose and missing_frame_number is not None:
499
505
  print('Missing frame {} ({}) for video {}'.format(
500
506
  missing_frame_number,
501
507
  missing_frame_filename,
502
508
  input_video_file))
503
-
509
+
504
510
  # OpenCV seems to over-report the number of frames by 1 in some cases, or fails
505
511
  # to read the last frame; either way, I'm allowing one missing frame.
506
512
  allow_last_frame_missing = True
507
-
513
+
508
514
  # This doesn't have to mean literally the last frame number, it just means that if
509
515
  # we find this frame or later, we consider the video done
510
516
  last_expected_frame_number = n_frames-1
511
517
  if every_n_frames is not None:
512
518
  last_expected_frame_number -= (every_n_frames*2)
513
-
519
+
514
520
  # When specific frames are requested, if anything is missing, reprocess the video
515
521
  if (frames_to_extract is not None) and (missing_frame_number is not None):
516
-
517
522
  pass
518
-
523
+
519
524
  # If no frames are missing, or only frames very close to the end of the video are "missing",
520
525
  # skip this video
521
526
  elif (missing_frame_number is None) or \
522
527
  (allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
523
-
528
+
524
529
  if verbose:
525
530
  print('Skipping video {}, all output frames exist'.format(input_video_file))
526
- return frame_filenames,Fs
527
-
531
+ return frame_filenames,fs
532
+
528
533
  else:
529
-
534
+
530
535
  # If we found some frames, but not all, print a message
531
536
  if verbose and found_existing_frame:
532
537
  print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
@@ -534,17 +539,17 @@ def video_to_frames(input_video_file,
534
539
  missing_frame_number,
535
540
  missing_frame_filename,
536
541
  last_expected_frame_number))
537
-
542
+
538
543
  # ...if we need to check whether to skip this video entirely
539
-
544
+
540
545
  if verbose:
541
- print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,Fs))
546
+ print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,fs))
542
547
 
543
548
  frame_filenames = []
544
549
 
545
- # YOLOv5 does some totally bananas monkey-patching of opencv, which causes
546
- # problems if we try to supply a third parameter to imwrite (to specify JPEG
547
- # quality). Detect this case, and ignore the quality parameter if it looks
550
+ # YOLOv5 does some totally bananas monkey-patching of opencv, which causes
551
+ # problems if we try to supply a third parameter to imwrite (to specify JPEG
552
+ # quality). Detect this case, and ignore the quality parameter if it looks
548
553
  # like imwrite has been messed with.
549
554
  #
550
555
  # See:
@@ -552,7 +557,7 @@ def video_to_frames(input_video_file,
552
557
  # https://github.com/ultralytics/yolov5/issues/7285
553
558
  imwrite_patched = False
554
559
  n_imwrite_parameters = None
555
-
560
+
556
561
  try:
557
562
  # calling signature() on the native cv2.imwrite function will
558
563
  # fail, so an exception here is a good thing. In fact I don't think
@@ -562,15 +567,19 @@ def video_to_frames(input_video_file,
562
567
  n_imwrite_parameters = len(sig.parameters)
563
568
  except Exception:
564
569
  pass
565
-
570
+
566
571
  if (n_imwrite_parameters is not None) and (n_imwrite_parameters < 3):
567
572
  imwrite_patched = True
568
573
  if verbose and (quality is not None):
569
574
  print('Warning: quality value supplied, but YOLOv5 has mucked with cv2.imwrite, ignoring quality')
570
-
575
+
571
576
  # for frame_number in tqdm(range(0,n_frames)):
572
577
  for frame_number in range(0,n_frames):
573
578
 
579
+ # Special handling for the case where we're just doing dummy reads
580
+ if bypass_extraction:
581
+ break
582
+
574
583
  success,image = vidcap.read()
575
584
  if not success:
576
585
  assert image is None
@@ -587,40 +596,40 @@ def video_to_frames(input_video_file,
587
596
  break
588
597
  if frame_number not in frames_to_extract:
589
598
  continue
590
-
599
+
591
600
  # Has resizing been requested?
592
601
  if max_width is not None:
593
-
594
- # image.shape is h/w/dims
602
+
603
+ # image.shape is h/w/dims
595
604
  input_shape = image.shape
596
605
  assert input_shape[2] == 3
597
606
  input_width = input_shape[1]
598
-
607
+
599
608
  # Is resizing necessary?
600
609
  if input_width > max_width:
601
-
610
+
602
611
  scale = max_width / input_width
603
612
  assert scale <= 1.0
604
-
613
+
605
614
  # INTER_AREA is recommended for size reduction
606
615
  image = cv2.resize(image, (0,0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
607
-
616
+
608
617
  # ...if we need to deal with resizing
609
-
610
- frame_filename_relative = _frame_number_to_filename(frame_number)
618
+
619
+ frame_filename_relative = _frame_number_to_filename(frame_number)
611
620
  frame_filename = os.path.join(output_folder,frame_filename_relative)
612
621
  frame_filenames.append(frame_filename)
613
-
614
- if overwrite == False and os.path.isfile(frame_filename):
622
+
623
+ if (not overwrite) and (os.path.isfile(frame_filename)):
615
624
  # print('Skipping frame {}'.format(frame_filename))
616
- pass
625
+ pass
617
626
  else:
618
627
  try:
619
628
  if frame_filename.isascii():
620
-
629
+
621
630
  if quality is None or imwrite_patched:
622
631
  cv2.imwrite(os.path.normpath(frame_filename),image)
623
- else:
632
+ else:
624
633
  cv2.imwrite(os.path.normpath(frame_filename),image,
625
634
  [int(cv2.IMWRITE_JPEG_QUALITY), quality])
626
635
  else:
@@ -639,19 +648,19 @@ def video_to_frames(input_video_file,
639
648
  print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
640
649
 
641
650
  # ...for each frame
642
-
651
+
643
652
  if len(frame_filenames) == 0:
644
653
  if allow_empty_videos:
645
- print('Warning: found no frames in file {}'.format(input_video_file))
654
+ print('Warning: no frames extracted from file {}'.format(input_video_file))
646
655
  else:
647
- raise Exception('Error: found no frames in file {}'.format(input_video_file))
648
-
656
+ raise Exception('Error: no frames extracted from file {}'.format(input_video_file))
657
+
649
658
  if verbose:
650
659
  print('\nExtracted {} of {} frames for {}'.format(
651
660
  len(frame_filenames),n_frames,input_video_file))
652
661
 
653
- vidcap.release()
654
- return frame_filenames,Fs
662
+ vidcap.release()
663
+ return frame_filenames,fs
655
664
 
656
665
  # ...def video_to_frames(...)
657
666
 
@@ -660,11 +669,11 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
660
669
  every_n_frames,overwrite,verbose,quality,max_width,
661
670
  frames_to_extract,allow_empty_videos):
662
671
  """
663
- Internal function to call video_to_frames for a single video in the context of
664
- video_folder_to_frames; makes sure the right output folder exists, then calls
672
+ Internal function to call video_to_frames for a single video in the context of
673
+ video_folder_to_frames; makes sure the right output folder exists, then calls
665
674
  video_to_frames.
666
- """
667
-
675
+ """
676
+
668
677
  input_fn_absolute = os.path.join(input_folder,relative_fn)
669
678
  assert os.path.isfile(input_fn_absolute),\
670
679
  'Could not find file {}'.format(input_fn_absolute)
@@ -684,26 +693,26 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
684
693
  max_width=max_width,
685
694
  frames_to_extract=frames_to_extract,
686
695
  allow_empty_videos=allow_empty_videos)
687
-
696
+
688
697
  return frame_filenames,fs
689
698
 
690
699
 
691
700
  def video_folder_to_frames(input_folder,
692
- output_folder_base,
693
- recursive=True,
701
+ output_folder_base,
702
+ recursive=True,
694
703
  overwrite=True,
695
704
  n_threads=1,
696
705
  every_n_frames=None,
697
706
  verbose=False,
698
707
  parallelization_uses_threads=True,
699
708
  quality=None,
700
- max_width=None,
709
+ max_width=None,
701
710
  frames_to_extract=None,
702
711
  allow_empty_videos=False):
703
712
  """
704
- For every video file in input_folder, creates a folder within output_folder_base, and
713
+ For every video file in input_folder, creates a folder within output_folder_base, and
705
714
  renders frame of that video to images in that folder.
706
-
715
+
707
716
  Args:
708
717
  input_folder (str): folder to process
709
718
  output_folder_base (str): root folder for output images; subfolders will be
@@ -714,7 +723,7 @@ def video_folder_to_frames(input_folder,
714
723
  parallelism
715
724
  every_n_frames (int, optional): sample every Nth frame starting from the first frame;
716
725
  if this is None or 1, every frame is extracted. If this is a negative value, it's
717
- interpreted as a sampling rate in seconds, which is rounded to the nearest frame
726
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame
718
727
  sampling rate. Mutually exclusive with frames_to_extract.
719
728
  verbose (bool, optional): enable additional debug console output
720
729
  parallelization_uses_threads (bool, optional): whether to use threads (True) or
@@ -723,20 +732,22 @@ def video_folder_to_frames(input_folder,
723
732
  to the opencv default (typically 95).
724
733
  max_width (int, optional): resize frames to be no wider than [max_width]
725
734
  frames_to_extract (list of int, optional): extract this specific set of frames from
726
- each video; mutually exclusive with every_n_frames. If all values are beyond
727
- the length of a video, no frames are extracted. Can also be a single int,
728
- specifying a single frame number.
735
+ each video; mutually exclusive with every_n_frames. If all values are beyond
736
+ the length of a video, no frames are extracted. Can also be a single int,
737
+ specifying a single frame number. In the special case where frames_to_extract
738
+ is [], this function still reads video frame rates and verifies that videos
739
+ are readable, but no frames are extracted.
729
740
  allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
730
741
  frames (by default, this is an error).
731
-
742
+
732
743
  Returns:
733
744
  tuple: a length-3 tuple containing:
734
- - list of lists of frame filenames; the Nth list of frame filenames corresponds to
745
+ - list of lists of frame filenames; the Nth list of frame filenames corresponds to
735
746
  the Nth video
736
747
  - list of video frame rates; the Nth value corresponds to the Nth video
737
- - list of video filenames
748
+ - list of video filenames
738
749
  """
739
-
750
+
740
751
  # Recursively enumerate video files
741
752
  if verbose:
742
753
  print('Enumerating videos in {}'.format(input_folder))
@@ -745,119 +756,141 @@ def video_folder_to_frames(input_folder,
745
756
  print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
746
757
  if len(input_files_full_paths) == 0:
747
758
  return [],[],[]
748
-
759
+
749
760
  input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
750
761
  input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
751
-
752
- os.makedirs(output_folder_base,exist_ok=True)
753
-
762
+
763
+ os.makedirs(output_folder_base,exist_ok=True)
764
+
754
765
  frame_filenames_by_video = []
755
766
  fs_by_video = []
756
-
767
+
757
768
  if n_threads == 1:
758
769
  # For each video
759
770
  #
760
771
  # input_fn_relative = input_files_relative_paths[0]
761
772
  for input_fn_relative in tqdm(input_files_relative_paths):
762
-
773
+
763
774
  frame_filenames,fs = \
764
- _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
765
- every_n_frames,overwrite,verbose,quality,max_width,
766
- frames_to_extract,allow_empty_videos)
775
+ _video_to_frames_for_folder(input_fn_relative,
776
+ input_folder,
777
+ output_folder_base,
778
+ every_n_frames,
779
+ overwrite,
780
+ verbose,
781
+ quality,
782
+ max_width,
783
+ frames_to_extract,
784
+ allow_empty_videos)
767
785
  frame_filenames_by_video.append(frame_filenames)
768
786
  fs_by_video.append(fs)
769
787
  else:
770
- if parallelization_uses_threads:
771
- print('Starting a worker pool with {} threads'.format(n_threads))
772
- pool = ThreadPool(n_threads)
773
- else:
774
- print('Starting a worker pool with {} processes'.format(n_threads))
775
- pool = Pool(n_threads)
776
- process_video_with_options = partial(_video_to_frames_for_folder,
777
- input_folder=input_folder,
778
- output_folder_base=output_folder_base,
779
- every_n_frames=every_n_frames,
780
- overwrite=overwrite,
781
- verbose=verbose,
782
- quality=quality,
783
- max_width=max_width,
784
- frames_to_extract=frames_to_extract,
785
- allow_empty_videos=allow_empty_videos)
786
- results = list(tqdm(pool.imap(
787
- partial(process_video_with_options),input_files_relative_paths),
788
- total=len(input_files_relative_paths)))
788
+ pool = None
789
+ results = None
790
+ try:
791
+ if parallelization_uses_threads:
792
+ print('Starting a worker pool with {} threads'.format(n_threads))
793
+ pool = ThreadPool(n_threads)
794
+ else:
795
+ print('Starting a worker pool with {} processes'.format(n_threads))
796
+ pool = Pool(n_threads)
797
+ process_video_with_options = partial(_video_to_frames_for_folder,
798
+ input_folder=input_folder,
799
+ output_folder_base=output_folder_base,
800
+ every_n_frames=every_n_frames,
801
+ overwrite=overwrite,
802
+ verbose=verbose,
803
+ quality=quality,
804
+ max_width=max_width,
805
+ frames_to_extract=frames_to_extract,
806
+ allow_empty_videos=allow_empty_videos)
807
+ results = list(tqdm(pool.imap(
808
+ partial(process_video_with_options),input_files_relative_paths),
809
+ total=len(input_files_relative_paths)))
810
+ finally:
811
+ pool.close()
812
+ pool.join()
813
+ print("Pool closed and joined for video processing")
789
814
  frame_filenames_by_video = [x[0] for x in results]
790
815
  fs_by_video = [x[1] for x in results]
791
-
816
+
792
817
  return frame_filenames_by_video,fs_by_video,input_files_full_paths
793
-
818
+
794
819
  # ...def video_folder_to_frames(...)
795
820
 
796
821
 
797
822
  class FrameToVideoOptions:
798
823
  """
799
824
  Options controlling the conversion of frame-level results to video-level results via
800
- frame_results_to_video_results()
825
+ frame_results_to_video_results()
801
826
  """
802
-
827
+
803
828
  def __init__(self):
804
-
829
+
805
830
  #: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
806
831
  #: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
807
832
  self.nth_highest_confidence = 1
808
-
833
+
809
834
  #: Should we include just a single representative frame result for each video (default), or
810
835
  #: every frame that was processed?
811
836
  self.include_all_processed_frames = False
812
-
813
- #: What to do if a file referred to in a .json results file appears not to be a
837
+
838
+ #: What to do if a file referred to in a .json results file appears not to be a
814
839
  #: video; can be 'error' or 'skip_with_warning'
815
840
  self.non_video_behavior = 'error'
816
-
841
+
842
+ #: Are frame rates required?
843
+ self.frame_rates_are_required = False
844
+
817
845
 
818
846
  def frame_results_to_video_results(input_file,
819
847
  output_file,
820
848
  options=None,
821
849
  video_filename_to_frame_rate=None):
822
850
  """
823
- Given an MD results file produced at the *frame* level, corresponding to a directory
824
- created with video_folder_to_frames, maps those frame-level results back to the
851
+ Given an MD results file produced at the *frame* level, corresponding to a directory
852
+ created with video_folder_to_frames, maps those frame-level results back to the
825
853
  video level for use in Timelapse.
826
-
854
+
827
855
  Preserves everything in the input .json file other than the images.
828
-
856
+
829
857
  Args:
830
858
  input_file (str): the frame-level MD results file to convert to video-level results
831
859
  output_file (str): the .json file to which we should write video-level results
832
860
  options (FrameToVideoOptions, optional): parameters for converting frame-level results
833
- to video-level results, see FrameToVideoOptions for details
834
- video_filename_to_frame_rate (dict): maps (relative) video path names to frame rates,
835
- used only to populate the output file
861
+ to video-level results, see FrameToVideoOptions for details
862
+ video_filename_to_frame_rate (dict, optional): maps (relative) video path names to frame
863
+ rates, used only to populate the output file
836
864
  """
837
865
 
838
866
  if options is None:
839
867
  options = FrameToVideoOptions()
840
-
868
+
869
+ if options.frame_rates_are_required:
870
+ assert video_filename_to_frame_rate is not None, \
871
+ 'You specified that frame rates are required, but you did not ' + \
872
+ 'supply video_filename_to_frame_rate'
873
+
841
874
  # Load results
842
875
  with open(input_file,'r') as f:
843
876
  input_data = json.load(f)
844
877
 
845
878
  images = input_data['images']
846
879
  detection_categories = input_data['detection_categories']
847
-
848
-
880
+
881
+
849
882
  ## Break into videos
850
-
851
- video_to_frame_info = defaultdict(list)
852
-
883
+
884
+ video_to_frame_info = defaultdict(list)
885
+
853
886
  # im = images[0]
854
887
  for im in tqdm(images):
855
-
888
+
856
889
  fn = im['file']
857
890
  video_name = os.path.dirname(fn)
858
-
891
+
859
892
  if not is_video_file(video_name):
860
-
893
+
861
894
  if options.non_video_behavior == 'error':
862
895
  raise ValueError('{} is not a video file'.format(video_name))
863
896
  elif options.non_video_behavior == 'skip_with_warning':
@@ -866,74 +899,78 @@ def frame_results_to_video_results(input_file,
866
899
  else:
867
900
  raise ValueError('Unrecognized non-video handling behavior: {}'.format(
868
901
  options.non_video_behavior))
869
-
902
+
870
903
  # Attach video-specific fields to the output, specifically attach the frame
871
- # number to both the video and each detection. Only the frame number for the
904
+ # number to both the video and each detection. Only the frame number for the
872
905
  # canonical detection will end up in the video-level output file.
873
906
  frame_number = _filename_to_frame_number(fn)
874
907
  im['frame_number'] = frame_number
875
- for detection in im['detections']:
908
+ for detection in im['detections']:
876
909
  detection['frame_number'] = frame_number
877
-
910
+
878
911
  video_to_frame_info[video_name].append(im)
879
-
912
+
880
913
  # ...for each frame referred to in the results file
881
-
914
+
882
915
  print('Found {} unique videos in {} frame-level results'.format(
883
916
  len(video_to_frame_info),len(images)))
884
-
917
+
885
918
  output_images = []
886
-
887
-
919
+
920
+
888
921
  ## For each video...
889
-
922
+
890
923
  # video_name = list(video_to_frame_info.keys())[0]
891
924
  for video_name in tqdm(video_to_frame_info):
892
-
925
+
893
926
  # Prepare the output representation for this video
894
927
  im_out = {}
895
928
  im_out['file'] = video_name
896
-
897
- if (video_filename_to_frame_rate is not None) and \
898
- (video_name in video_filename_to_frame_rate):
899
- im_out['frame_rate'] = video_filename_to_frame_rate[video_name]
900
-
929
+
930
+ if (video_filename_to_frame_rate is not None):
931
+
932
+ if options.frame_rates_are_required:
933
+ assert video_name in video_filename_to_frame_rate, \
934
+ 'Could not determine frame rate for {}'.format(video_name)
935
+ if video_name in video_filename_to_frame_rate:
936
+ im_out['frame_rate'] = video_filename_to_frame_rate[video_name]
937
+
901
938
  # Find all detections for this video
902
939
  all_detections_this_video = []
903
-
940
+
904
941
  frames = video_to_frame_info[video_name]
905
-
942
+
906
943
  # frame = frames[0]
907
944
  for frame in frames:
908
- if ('detections' in frame) and (frame['detections'] is not None):
945
+ if ('detections' in frame) and (frame['detections'] is not None):
909
946
  all_detections_this_video.extend(frame['detections'])
910
-
947
+
911
948
  # Should we keep detections for all frames?
912
949
  if (options.include_all_processed_frames):
913
-
950
+
914
951
  im_out['detections'] = all_detections_this_video
915
-
952
+
916
953
  # ...or should we keep just a canonical detection for each category?
917
954
  else:
918
-
955
+
919
956
  canonical_detections = []
920
-
957
+
921
958
  # category_id = list(detection_categories.keys())[0]
922
959
  for category_id in detection_categories:
923
-
960
+
924
961
  category_detections = [det for det in all_detections_this_video if \
925
962
  det['category'] == category_id]
926
-
963
+
927
964
  # Find the nth-highest-confidence video to choose a confidence value
928
965
  if len(category_detections) >= options.nth_highest_confidence:
929
-
930
- category_detections_by_confidence = sorted(category_detections,
966
+
967
+ category_detections_by_confidence = sorted(category_detections,
931
968
  key = lambda i: i['conf'],reverse=True)
932
969
  canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
933
970
  canonical_detections.append(canonical_detection)
934
-
971
+
935
972
  im_out['detections'] = canonical_detections
936
-
973
+
937
974
  # 'max_detection_conf' is no longer included in output files by default
938
975
  if False:
939
976
  im_out['max_detection_conf'] = 0
@@ -942,19 +979,19 @@ def frame_results_to_video_results(input_file,
942
979
  im_out['max_detection_conf'] = max(confidences)
943
980
 
944
981
  # ...if we're keeping output for all frames / canonical frames
945
-
982
+
946
983
  output_images.append(im_out)
947
-
984
+
948
985
  # ...for each video
949
-
986
+
950
987
  output_data = input_data
951
988
  output_data['images'] = output_images
952
989
  s = json.dumps(output_data,indent=1)
953
-
990
+
954
991
  # Write the output file
955
992
  with open(output_file,'w') as f:
956
993
  f.write(s)
957
-
994
+
958
995
  # ...def frame_results_to_video_results(...)
959
996
 
960
997
 
@@ -965,40 +1002,40 @@ if False:
965
1002
  pass
966
1003
 
967
1004
  #%% Constants
968
-
1005
+
969
1006
  input_folder = r'G:\temp\usu-long\data'
970
1007
  frame_folder_base = r'g:\temp\usu-long-single-frames'
971
1008
  assert os.path.isdir(input_folder)
972
-
973
-
1009
+
1010
+
974
1011
  #%% Split videos into frames
975
-
1012
+
976
1013
  frame_filenames_by_video,fs_by_video,video_filenames = \
977
1014
  video_folder_to_frames(input_folder,
978
1015
  frame_folder_base,
979
1016
  recursive=True,
980
1017
  overwrite=True,
981
- n_threads=10,
1018
+ n_threads=10,
982
1019
  every_n_frames=None,
983
- verbose=True,
1020
+ verbose=True,
984
1021
  parallelization_uses_threads=True,
985
- quality=None,
986
- max_width=None,
1022
+ quality=None,
1023
+ max_width=None,
987
1024
  frames_to_extract=150)
988
-
989
-
1025
+
1026
+
990
1027
  #%% Constants for detection tests
991
-
1028
+
992
1029
  detected_frame_folder_base = r'e:\video_test\detected_frames'
993
1030
  rendered_videos_folder_base = r'e:\video_test\rendered_videos'
994
1031
  os.makedirs(detected_frame_folder_base,exist_ok=True)
995
1032
  os.makedirs(rendered_videos_folder_base,exist_ok=True)
996
1033
  results_file = r'results.json'
997
1034
  confidence_threshold = 0.75
998
-
999
-
1000
- #%% Load detector output
1001
-
1035
+
1036
+
1037
+ #%% Load detector output
1038
+
1002
1039
  with open(results_file,'r') as f:
1003
1040
  detection_results = json.load(f)
1004
1041
  detections = detection_results['images']
@@ -1008,11 +1045,11 @@ if False:
1008
1045
 
1009
1046
 
1010
1047
  #%% List image files, break into folders
1011
-
1048
+
1012
1049
  frame_files = path_utils.find_images(frame_folder_base,True)
1013
1050
  frame_files = [s.replace('\\','/') for s in frame_files]
1014
1051
  print('Enumerated {} total frames'.format(len(frame_files)))
1015
-
1052
+
1016
1053
  # Find unique folders
1017
1054
  folders = set()
1018
1055
  # fn = frame_files[0]
@@ -1020,57 +1057,57 @@ if False:
1020
1057
  folders.add(os.path.dirname(fn))
1021
1058
  folders = [s.replace('\\','/') for s in folders]
1022
1059
  print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
1023
-
1024
-
1060
+
1061
+
1025
1062
  #%% Render detector frames
1026
-
1063
+
1027
1064
  # folder = list(folders)[0]
1028
1065
  for folder in folders:
1029
-
1066
+
1030
1067
  frame_files_this_folder = [fn for fn in frame_files if folder in fn]
1031
1068
  folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
1032
1069
  detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
1033
1070
  print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
1034
1071
  assert len(frame_files_this_folder) == len(detection_results_this_folder)
1035
-
1072
+
1036
1073
  rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
1037
1074
  os.makedirs(rendered_frame_output_folder,exist_ok=True)
1038
-
1075
+
1039
1076
  # d = detection_results_this_folder[0]
1040
1077
  for d in tqdm(detection_results_this_folder):
1041
-
1078
+
1042
1079
  input_file = os.path.join(frame_folder_base,d['file'])
1043
1080
  output_file = os.path.join(detected_frame_folder_base,d['file'])
1044
1081
  os.makedirs(os.path.dirname(output_file),exist_ok=True)
1045
1082
  vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
1046
1083
  confidence_threshold)
1047
-
1084
+
1048
1085
  # ...for each file in this folder
1049
-
1086
+
1050
1087
  # ...for each folder
1051
1088
 
1052
1089
 
1053
1090
  #%% Render output videos
1054
-
1091
+
1055
1092
  # folder = list(folders)[0]
1056
1093
  for folder in tqdm(folders):
1057
-
1094
+
1058
1095
  folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
1059
1096
  rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
1060
1097
  assert os.path.isdir(rendered_detector_output_folder)
1061
-
1098
+
1062
1099
  frame_files_relative = os.listdir(rendered_detector_output_folder)
1063
1100
  frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
1064
1101
  for s in frame_files_relative]
1065
-
1102
+
1066
1103
  output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
1067
1104
  os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
1068
-
1105
+
1069
1106
  original_video_filename = output_video_filename.replace(
1070
1107
  rendered_videos_folder_base,input_folder)
1071
1108
  assert os.path.isfile(original_video_filename)
1072
- Fs = get_video_fs(original_video_filename)
1073
-
1074
- frames_to_video(frame_files_absolute, Fs, output_video_filename)
1109
+ fs = get_video_fs(original_video_filename)
1110
+
1111
+ frames_to_video(frame_files_absolute, fs, output_video_filename)
1075
1112
 
1076
1113
  # ...for each video