megadetector 5.0.12__py3-none-any.whl → 5.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (40) hide show
  1. megadetector/api/batch_processing/api_core/server.py +1 -1
  2. megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
  3. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
  4. megadetector/api/batch_processing/api_core/server_utils.py +0 -4
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  6. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
  7. megadetector/classification/efficientnet/utils.py +0 -3
  8. megadetector/data_management/camtrap_dp_to_coco.py +0 -2
  9. megadetector/data_management/cct_json_utils.py +15 -6
  10. megadetector/data_management/coco_to_labelme.py +12 -1
  11. megadetector/data_management/databases/integrity_check_json_db.py +43 -27
  12. megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
  13. megadetector/data_management/ocr_tools.py +0 -4
  14. megadetector/data_management/read_exif.py +171 -43
  15. megadetector/data_management/rename_images.py +187 -0
  16. megadetector/data_management/wi_download_csv_to_coco.py +3 -2
  17. megadetector/data_management/yolo_output_to_md_output.py +7 -2
  18. megadetector/detection/process_video.py +360 -216
  19. megadetector/detection/pytorch_detector.py +17 -3
  20. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  21. megadetector/detection/tf_detector.py +3 -0
  22. megadetector/detection/video_utils.py +122 -30
  23. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  24. megadetector/postprocessing/classification_postprocessing.py +716 -0
  25. megadetector/postprocessing/compare_batch_results.py +101 -93
  26. megadetector/postprocessing/merge_detections.py +18 -7
  27. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  28. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  29. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  30. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  31. megadetector/utils/ct_utils.py +5 -4
  32. megadetector/utils/md_tests.py +311 -115
  33. megadetector/utils/path_utils.py +1 -0
  34. megadetector/utils/process_utils.py +6 -3
  35. megadetector/visualization/visualize_db.py +79 -77
  36. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  38. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/RECORD +40 -38
  39. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/top_level.txt +0 -0
  40. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
@@ -25,13 +25,14 @@ import argparse
25
25
  import itertools
26
26
  import json
27
27
  import shutil
28
+ import getpass
28
29
 
29
30
  from uuid import uuid1
30
31
 
31
32
  from megadetector.detection import run_detector_batch
32
33
  from megadetector.visualization import visualize_detector_output
33
34
  from megadetector.utils.ct_utils import args_to_object
34
- from megadetector.utils.path_utils import insert_before_extension
35
+ from megadetector.utils.path_utils import insert_before_extension, clean_path
35
36
  from megadetector.detection.video_utils import video_to_frames
36
37
  from megadetector.detection.video_utils import frames_to_video
37
38
  from megadetector.detection.video_utils import frame_results_to_video_results
@@ -46,95 +47,234 @@ class ProcessVideoOptions:
46
47
  Options controlling the behavior of process_video()
47
48
  """
48
49
 
49
- #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
50
- model_file = 'MDV5A'
51
-
52
- #: Video (of folder of videos) to process
53
- input_video_file = ''
54
-
55
- #: .json file to which we should write results
56
- output_json_file = None
57
-
58
- #: File to which we should write a video with boxes, only relevant if
59
- #: render_output_video is True
60
- output_video_file = None
61
-
62
- #: Folder to use for extracted frames; will use a folder in system temp space
63
- #: if this is None
64
- frame_folder = None
50
+ def __init__(self):
51
+
52
+ #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
53
+ self.model_file = 'MDV5A'
54
+
55
+ #: Video (of folder of videos) to process
56
+ self.input_video_file = ''
65
57
 
66
- # Folder to use for rendered frames (if rendering output video); will use a folder
67
- #: in system temp space if this is None
68
- frame_rendering_folder = None
58
+ #: .json file to which we should write results
59
+ self.output_json_file = None
60
+
61
+ #: File to which we should write a video with boxes, only relevant if
62
+ #: render_output_video is True
63
+ self.output_video_file = None
64
+
65
+ #: Folder to use for extracted frames; will use a folder in system temp space
66
+ #: if this is None
67
+ self.frame_folder = None
68
+
69
+ # Folder to use for rendered frames (if rendering output video); will use a folder
70
+ #: in system temp space if this is None
71
+ self.frame_rendering_folder = None
72
+
73
+ #: Should we render a video with detection boxes?
74
+ #:
75
+ #: Only supported when processing a single video, not a folder.
76
+ self.render_output_video = False
77
+
78
+ #: If we are rendering boxes to a new video, should we keep the temporary
79
+ #: rendered frames?
80
+ self.keep_rendered_frames = False
81
+
82
+ #: Should we keep the extracted frames?
83
+ self.keep_extracted_frames = False
84
+
85
+ #: Should we delete the entire folder the extracted frames are written to?
86
+ #:
87
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
88
+ #: for no reason other than being paranoid about deleting folders.
89
+ self.force_extracted_frame_folder_deletion = False
90
+
91
+ #: Should we delete the entire folder the rendered frames are written to?
92
+ #:
93
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
94
+ #: for no reason other than being paranoid about deleting folders.
95
+ self.force_rendered_frame_folder_deletion = False
96
+
97
+ #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
98
+ #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
99
+ self.reuse_results_if_available = False
100
+
101
+ #: If we've already split this video or folder of videos into frames, should we
102
+ #: we re-use those extracted frames? Defaults to reprocessing.
103
+ self.reuse_frames_if_available = False
104
+
105
+ #: If [input_video_file] is a folder, should we search for videos recursively?
106
+ self.recursive = False
107
+
108
+ #: Enable additional debug console output
109
+ self.verbose = False
110
+
111
+ #: fourcc code to use for writing videos; only relevant if render_output_video is True
112
+ self.fourcc = None
69
113
 
70
- #: Should we render a video with detection boxes?
71
- #:
72
- #: Only supported when processing a single video, not a folder.
73
- render_output_video = False
114
+ #: Confidence threshold to use for writing videos with boxes, only relevant if
115
+ #: if render_output_video is True. Defaults to choosing a reasonable threshold
116
+ #: based on the model version.
117
+ self.rendering_confidence_threshold = None
118
+
119
+ #: Detections below this threshold will not be included in the output file.
120
+ self.json_confidence_threshold = 0.005
121
+
122
+ #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
123
+ #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
124
+ #: typical value.
125
+ self.frame_sample = None
126
+
127
+ #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
128
+ self.n_cores = 1
74
129
 
75
- #: If we are rendering boxes to a new video, should we keep the temporary
76
- #: rendered frames?
77
- keep_rendered_frames = False
130
+ #: For debugging only, stop processing after a certain number of frames.
131
+ self.debug_max_frames = -1
132
+
133
+ #: File containing non-standard categories, typically only used if you're running a non-MD
134
+ #: detector.
135
+ self.class_mapping_filename = None
136
+
137
+ #: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
138
+ self.quality = 90
139
+
140
+ #: Resize frames so they're at most this wide
141
+ self.max_width = 1600
78
142
 
79
- #: Should we keep the extracted frames?
80
- keep_extracted_frames = False
143
+ # ...class ProcessVideoOptions
144
+
145
+
146
+ #%% Functions
147
+
148
+ def _select_temporary_output_folders(options):
149
+ """
150
+ Choose folders in system temp space for writing temporary frames. Does not create folders,
151
+ just defines them.
152
+ """
81
153
 
82
- #: Should we delete the entire folder the extracted frames are written to?
83
- #:
84
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
85
- #: for no reason other than being paranoid about deleting folders.
86
- force_extracted_frame_folder_deletion = False
154
+ tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
87
155
 
88
- #: Should we delete the entire folder the rendered frames are written to?
89
- #:
90
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
91
- #: for no reason other than being paranoid about deleting folders.
92
- force_rendered_frame_folder_deletion = False
93
-
94
- #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
95
- #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
96
- reuse_results_if_available = False
156
+ # If we create a folder like "process_camera_trap_video" in the system temp dir, it may
157
+ # be the case that no one else can write to it, even to create user-specific subfolders.
158
+ # If we create a uuid-named folder in the system temp dir, we make a mess.
159
+ #
160
+ # Compromise with "process_camera_trap_video-[user]".
161
+ user_tempdir = tempdir + '-' + getpass.getuser()
97
162
 
98
- #: If we've already split this video or folder of videos into frames, should we
99
- #: we re-use those extracted frames? Defaults to reprocessing.
100
- reuse_frames_if_available = False
163
+ # I don't know whether it's possible for a username to contain characters that are
164
+ # not valid filename characters, but just to be sure...
165
+ user_tempdir = clean_path(user_tempdir)
101
166
 
102
- #: If [input_video_file] is a folder, should we search for videos recursively?
103
- recursive = False
167
+ frame_output_folder = os.path.join(
168
+ user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
104
169
 
105
- #: Enable additional debug console output
106
- verbose = False
170
+ rendering_output_folder = os.path.join(
171
+ tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
172
+
173
+ temporary_folder_info = \
174
+ {
175
+ 'temp_folder_base':user_tempdir,
176
+ 'frame_output_folder':frame_output_folder,
177
+ 'rendering_output_folder':rendering_output_folder
178
+ }
107
179
 
108
- #: fourcc code to use for writing videos; only relevant if render_output_video is True
109
- fourcc = None
180
+ return temporary_folder_info
110
181
 
111
- #: Confidence threshold to use for writing videos with boxes, only relevant if
112
- #: if render_output_video is True. Defaults to choosing a reasonable threshold
113
- #: based on the model version.
114
- rendering_confidence_threshold = None
115
-
116
- #: Detections below this threshold will not be included in the output file.
117
- json_confidence_threshold = 0.005
182
+ # ...def _create_frame_output_folders(...)
183
+
184
+
185
+ def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
186
+ """
187
+ If necessary, delete rendered frames and/or the entire rendering output folder.
188
+ """
118
189
 
119
- #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
120
- #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
121
- #: typical value.
122
- frame_sample = None
190
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
123
191
 
124
- #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
125
- n_cores = 1
192
+ # (Optionally) delete the temporary directory we used for rendered detection images
193
+ if not options.keep_rendered_frames:
194
+
195
+ try:
196
+
197
+ # If (a) we're supposed to delete the temporary rendering folder no
198
+ # matter where it is and (b) we created it in temp space, delete the
199
+ # whole tree
200
+ if options.force_rendered_frame_folder_deletion and \
201
+ (not caller_provided_rendering_output_folder):
202
+
203
+ if options.verbose:
204
+ print('Recursively deleting rendered frame folder {}'.format(
205
+ rendering_output_folder))
206
+
207
+ shutil.rmtree(rendering_output_folder)
208
+
209
+ # ...otherwise just delete the frames, but leave the folder in place
210
+ else:
211
+
212
+ if options.force_rendered_frame_folder_deletion:
213
+ assert caller_provided_rendering_output_folder
214
+ print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
215
+ 'user-provided folder, only removing frames')
216
+
217
+ for rendered_frame_fn in detected_frame_files:
218
+ os.remove(rendered_frame_fn)
219
+
220
+ except Exception as e:
221
+ print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
222
+ rendering_output_folder,str(e)))
223
+ pass
126
224
 
127
- #: For debugging only, stop processing after a certain number of frames.
128
- debug_max_frames = -1
129
-
130
- #: File containing non-standard categories, typically only used if you're running a non-MD
131
- #: detector.
132
- class_mapping_filename = None
225
+ elif options.force_rendered_frame_folder_deletion:
226
+
227
+ print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
228
+ 'specified, not deleting')
229
+
230
+ # ...def _clean_up_rendered_frames(...)
133
231
 
134
- # ...class ProcessVideoOptions
135
232
 
233
+ def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
234
+ """
235
+ If necessary, delete extracted frames and/or the entire temporary frame folder.
236
+ """
237
+
238
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
239
+
240
+ if not options.keep_extracted_frames:
241
+
242
+ try:
243
+
244
+ # If (a) we're supposed to delete the temporary frame folder no
245
+ # matter where it is and (b) we created it in temp space, delete the
246
+ # whole tree.
247
+ if options.force_extracted_frame_folder_deletion and \
248
+ (not caller_provided_frame_output_folder):
249
+
250
+ if options.verbose:
251
+ print('Recursively deleting frame output folder {}'.format(frame_output_folder))
252
+
253
+ shutil.rmtree(frame_output_folder)
254
+
255
+ # ...otherwise just delete the frames, but leave the folder in place
256
+ else:
257
+
258
+ if options.force_extracted_frame_folder_deletion:
259
+ assert caller_provided_frame_output_folder
260
+ print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
261
+ 'user-provided folder, only removing frames')
262
+
263
+ for extracted_frame_fn in frame_filenames:
264
+ os.remove(extracted_frame_fn)
265
+
266
+ except Exception as e:
267
+ print('Warning: error removing extracted frames from folder {}:\n{}'.format(
268
+ frame_output_folder,str(e)))
269
+ pass
270
+
271
+ elif options.force_extracted_frame_folder_deletion:
272
+
273
+ print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
274
+ 'specified, not deleting')
275
+
276
+ # ...def _clean_up_extracted_frames
136
277
 
137
- #%% Functions
138
278
 
139
279
  def process_video(options):
140
280
  """
@@ -154,35 +294,25 @@ def process_video(options):
154
294
  if options.render_output_video and (options.output_video_file is None):
155
295
  options.output_video_file = options.input_video_file + '.detections.mp4'
156
296
 
157
- tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
158
- os.makedirs(tempdir,exist_ok=True)
159
-
160
- # TODO:
161
- #
162
- # This is a lazy fix to an issue... if multiple users run this script, the
163
- # "process_camera_trap_video" folder is owned by the first person who creates it, and others
164
- # can't write to it. I could create uniquely-named folders, but I philosophically prefer
165
- # to put all the individual UUID-named folders within a larger folder, so as to be a
166
- # good tempdir citizen. So, the lazy fix is to make this world-writable.
167
- try:
168
- os.chmod(tempdir,0o777)
169
- except Exception:
170
- pass
297
+ # Track whether frame and rendering folders were created by this script
298
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
299
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
171
300
 
172
- if options.frame_folder is not None:
301
+ # This does not create any folders, just defines temporary folder names in
302
+ # case we need them.
303
+ temporary_folder_info = _select_temporary_output_folders(options)
304
+
305
+ if (caller_provided_frame_output_folder):
173
306
  frame_output_folder = options.frame_folder
174
307
  else:
175
- frame_output_folder = os.path.join(
176
- tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
177
-
178
- # TODO: keep track of whether we created this folder, delete if we're deleting the extracted
179
- # frames and we created the folder, and the output files aren't in the same folder. For now,
180
- # we're just deleting the extracted frames and leaving the empty folder around in this case.
308
+ frame_output_folder = temporary_folder_info['frame_output_folder']
309
+
181
310
  os.makedirs(frame_output_folder, exist_ok=True)
182
311
 
183
312
  frame_filenames, Fs = video_to_frames(
184
313
  options.input_video_file, frame_output_folder,
185
- every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available))
314
+ every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available),
315
+ quality=options.quality, max_width=options.max_width, verbose=options.verbose)
186
316
 
187
317
  image_file_names = frame_filenames
188
318
  if options.debug_max_frames > 0:
@@ -198,8 +328,8 @@ def process_video(options):
198
328
  options.model_file, image_file_names,
199
329
  confidence_threshold=options.json_confidence_threshold,
200
330
  n_cores=options.n_cores,
201
- quiet=(not options.verbose),
202
- class_mapping_filename=options.class_mapping_filename)
331
+ class_mapping_filename=options.class_mapping_filename,
332
+ quiet=True)
203
333
 
204
334
  run_detector_batch.write_results_to_file(
205
335
  results, options.output_json_file,
@@ -213,15 +343,11 @@ def process_video(options):
213
343
  if options.render_output_video:
214
344
 
215
345
  # Render detections to images
216
- if options.frame_rendering_folder is not None:
346
+ if (caller_provided_rendering_output_folder):
217
347
  rendering_output_dir = options.frame_rendering_folder
218
348
  else:
219
- rendering_output_dir = os.path.join(
220
- tempdir, os.path.basename(options.input_video_file) + '_detections')
349
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
221
350
 
222
- # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
223
- # frames and we created the folder, and the output files aren't in the same folder. For now,
224
- # we're just deleting the rendered frames and leaving the empty folder around in this case.
225
351
  os.makedirs(rendering_output_dir,exist_ok=True)
226
352
 
227
353
  detected_frame_files = visualize_detector_output.visualize_detector_output(
@@ -236,44 +362,20 @@ def process_video(options):
236
362
  else:
237
363
  rendering_fs = Fs / options.frame_sample
238
364
 
239
- print('Rendering video to {} at {} fps (original video {} fps)'.format(
240
- options.output_video_file,rendering_fs,Fs))
241
- frames_to_video(detected_frame_files, rendering_fs, options.output_video_file, codec_spec=options.fourcc)
365
+ print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
366
+ len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
367
+ frames_to_video(detected_frame_files, rendering_fs, options.output_video_file,
368
+ codec_spec=options.fourcc)
242
369
 
243
- # Delete the temporary directory we used for detection images
244
- if not options.keep_rendered_frames:
245
- try:
246
- if options.force_rendered_frame_folder_deletion:
247
- shutil.rmtree(rendering_output_dir)
248
- else:
249
- for rendered_frame_fn in detected_frame_files:
250
- os.remove(rendered_frame_fn)
251
- except Exception as e:
252
- print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
253
- rendering_output_dir,str(e)))
254
- pass
370
+ # Possibly clean up rendered frames
371
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
255
372
 
256
373
  # ...if we're rendering video
257
374
 
258
375
 
259
376
  ## (Optionally) delete the extracted frames
377
+ _clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
260
378
 
261
- if not options.keep_extracted_frames:
262
-
263
- try:
264
- if options.force_extracted_frame_folder_deletion:
265
- print('Recursively deleting frame output folder {}'.format(frame_output_folder))
266
- shutil.rmtree(frame_output_folder)
267
- else:
268
- for extracted_frame_fn in frame_filenames:
269
- os.remove(extracted_frame_fn)
270
- except Exception as e:
271
- print('Warning: error removing extracted frames from folder {}:\n{}'.format(
272
- frame_output_folder,str(e)))
273
- pass
274
-
275
- return results
276
-
277
379
  # ...process_video()
278
380
 
279
381
 
@@ -299,24 +401,22 @@ def process_video_folder(options):
299
401
  frames_json = options.output_json_file.replace('.json','.frames.json')
300
402
  os.makedirs(os.path.dirname(video_json),exist_ok=True)
301
403
 
404
+ # Track whether frame and rendering folders were created by this script
405
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
406
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
407
+
408
+ # This does not create any folders, just defines temporary folder names in
409
+ # case we need them.
410
+ temporary_folder_info = _select_temporary_output_folders(options)
411
+
302
412
 
303
413
  ## Split every video into frames
304
414
 
305
- if options.frame_folder is not None:
415
+ if caller_provided_frame_output_folder:
306
416
  frame_output_folder = options.frame_folder
307
417
  else:
308
- tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
309
- os.makedirs(tempdir,exist_ok=True)
418
+ frame_output_folder = temporary_folder_info['frame_output_folder']
310
419
 
311
- # TODO: see above; this is a lazy fix to a permissions issue
312
- try:
313
- os.chmod(tempdir,0o777)
314
- except Exception:
315
- pass
316
-
317
- frame_output_folder = os.path.join(
318
- tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
319
-
320
420
  os.makedirs(frame_output_folder, exist_ok=True)
321
421
 
322
422
  print('Extracting frames')
@@ -325,8 +425,11 @@ def process_video_folder(options):
325
425
  output_folder_base=frame_output_folder,
326
426
  recursive=options.recursive,
327
427
  overwrite=(not options.reuse_frames_if_available),
328
- n_threads=options.n_cores,every_n_frames=options.frame_sample,
329
- verbose=options.verbose)
428
+ n_threads=options.n_cores,
429
+ every_n_frames=options.frame_sample,
430
+ verbose=options.verbose,
431
+ quality=options.quality,
432
+ max_width=options.max_width)
330
433
 
331
434
  image_file_names = list(itertools.chain.from_iterable(frame_filenames))
332
435
 
@@ -346,7 +449,7 @@ def process_video_folder(options):
346
449
 
347
450
  if options.reuse_results_if_available and \
348
451
  os.path.isfile(frames_json):
349
- print('Loading results from {}'.format(frames_json))
452
+ print('Bypassing inference, loading results from {}'.format(frames_json))
350
453
  results = None
351
454
  else:
352
455
  print('Running MegaDetector')
@@ -354,8 +457,8 @@ def process_video_folder(options):
354
457
  options.model_file, image_file_names,
355
458
  confidence_threshold=options.json_confidence_threshold,
356
459
  n_cores=options.n_cores,
357
- quiet=(not options.verbose),
358
- class_mapping_filename=options.class_mapping_filename)
460
+ class_mapping_filename=options.class_mapping_filename,
461
+ quiet=True)
359
462
 
360
463
  run_detector_batch.write_results_to_file(
361
464
  results, frames_json,
@@ -373,26 +476,23 @@ def process_video_folder(options):
373
476
  ## (Optionally) render output videos
374
477
 
375
478
  if options.render_output_video:
376
-
479
+
377
480
  # Render detections to images
378
- if options.frame_rendering_folder is not None:
379
- frame_rendering_output_dir = options.frame_rendering_folder
481
+ if (caller_provided_rendering_output_folder):
482
+ rendering_output_dir = options.frame_rendering_folder
380
483
  else:
381
- frame_rendering_output_dir = os.path.join(
382
- tempdir, os.path.basename(options.input_video_file) + '_detections')
383
-
384
- # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
385
- # frames and we created the folder, and the output files aren't in the same folder. For now,
386
- # we're just deleting the rendered frames and leaving the empty folder around in this case.
387
- os.makedirs(frame_rendering_output_dir,exist_ok=True)
484
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
485
+
486
+ os.makedirs(rendering_output_dir,exist_ok=True)
388
487
 
389
488
  detected_frame_files = visualize_detector_output.visualize_detector_output(
390
489
  detector_output_path=frames_json,
391
- out_dir=frame_rendering_output_dir,
490
+ out_dir=rendering_output_dir,
392
491
  images_dir=frame_output_folder,
393
492
  confidence_threshold=options.rendering_confidence_threshold,
394
493
  preserve_path_structure=True,
395
494
  output_image_width=-1)
495
+ detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
396
496
 
397
497
  # Choose an output folder
398
498
  output_folder_is_input_folder = False
@@ -424,7 +524,9 @@ def process_video_folder(options):
424
524
  rendering_fs = video_fs / options.frame_sample
425
525
 
426
526
  input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
427
- video_frame_output_folder = os.path.join(frame_rendering_output_dir,input_video_file_relative)
527
+ video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
528
+
529
+ video_frame_output_folder = video_frame_output_folder.replace('\\','/')
428
530
  assert os.path.isdir(video_frame_output_folder), \
429
531
  'Could not find frame folder for video {}'.format(input_video_file_relative)
430
532
 
@@ -450,42 +552,29 @@ def process_video_folder(options):
450
552
  # ...for each video
451
553
 
452
554
  # Possibly clean up rendered frames
453
- if not options.keep_rendered_frames:
454
- try:
455
- if options.force_rendered_frame_folder_deletion:
456
- shutil.rmtree(frame_rendering_output_dir)
457
- else:
458
- for rendered_frame_fn in detected_frame_files:
459
- os.remove(rendered_frame_fn)
460
- except Exception as e:
461
- print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
462
- frame_rendering_output_dir,str(e)))
463
- pass
555
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
464
556
 
465
557
  # ...if we're rendering video
466
558
 
467
559
 
468
560
  ## (Optionally) delete the extracted frames
469
-
470
- if not options.keep_extracted_frames:
471
- try:
472
- print('Deleting frame cache')
473
- if options.force_extracted_frame_folder_deletion:
474
- print('Recursively deleting frame output folder {}'.format(frame_output_folder))
475
- shutil.rmtree(frame_output_folder)
476
- else:
477
- for frame_fn in image_file_names:
478
- os.remove(frame_fn)
479
- except Exception as e:
480
- print('Warning: error deleting frames from folder {}:\n{}'.format(
481
- frame_output_folder,str(e)))
482
- pass
561
+ _clean_up_extracted_frames(options, frame_output_folder, image_file_names)
483
562
 
484
563
  # ...process_video_folder()
485
564
 
486
565
 
487
566
  def options_to_command(options):
567
+ """
568
+ Convert a ProcessVideoOptions obejct to a corresponding command line.
488
569
 
570
+ Args:
571
+ options (ProcessVideoOptions): the options set to render as a command line
572
+
573
+ Returns:
574
+ str: the command line coresponding to [options]
575
+
576
+ :meta private:
577
+ """
489
578
  cmd = 'python process_video.py'
490
579
  cmd += ' "' + options.model_file + '"'
491
580
  cmd += ' "' + options.input_video_file + '"'
@@ -524,6 +613,16 @@ def options_to_command(options):
524
613
  cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
525
614
  if options.fourcc is not None:
526
615
  cmd += ' --fourcc ' + options.fourcc
616
+ if options.quality is not None:
617
+ cmd += ' --quality ' + str(options.quality)
618
+ if options.max_width is not None:
619
+ cmd += ' --max_width ' + str(options.max_width)
620
+ if options.verbose:
621
+ cmd += ' --verbose'
622
+ if options.force_extracted_frame_folder_deletion:
623
+ cmd += ' --force_extracted_frame_folder_deletion'
624
+ if options.force_rendered_frame_folder_deletion:
625
+ cmd += ' --force_rendered_frame_folder_deletion'
527
626
 
528
627
  return cmd
529
628
 
@@ -535,11 +634,12 @@ if False:
535
634
  #%% Process a folder of videos
536
635
 
537
636
  model_file = 'MDV5A'
538
- input_dir = r'c:\git\MegaDetector\test_images\test_images'
539
- frame_folder = r'g:\temp\video_test\frames'
540
- rendering_folder = r'g:\temp\video_test\rendered-frames'
541
- output_json_file = r'g:\temp\video_test\video-test.json'
542
- output_video_folder = r'g:\temp\video_test\output_videos'
637
+ input_dir = r'g:\temp\test-videos'
638
+ output_base = r'g:\temp\video_test'
639
+ frame_folder = os.path.join(output_base,'frames')
640
+ rendering_folder = os.path.join(output_base,'rendered-frames')
641
+ output_json_file = os.path.join(output_base,'video-test.json')
642
+ output_video_folder = os.path.join(output_base,'output_videos')
543
643
 
544
644
  print('Processing folder {}'.format(input_dir))
545
645
 
@@ -547,21 +647,31 @@ if False:
547
647
  options.model_file = model_file
548
648
  options.input_video_file = input_dir
549
649
  options.output_video_file = output_video_folder
550
- options.frame_folder = frame_folder
551
650
  options.output_json_file = output_json_file
552
- options.frame_rendering_folder = rendering_folder
553
- options.render_output_video = True
554
- options.keep_extracted_frames = True
555
- options.keep_rendered_frames = True
556
651
  options.recursive = True
557
- options.reuse_frames_if_available = True
558
- options.reuse_results_if_available = True
652
+ options.reuse_frames_if_available = False
653
+ options.reuse_results_if_available = False
654
+ options.quality = 90
655
+ options.frame_sample = 10
656
+ options.max_width = 1280
657
+ options.n_cores = 5
658
+ options.verbose = True
659
+ options.render_output_video = True
660
+
661
+ options.frame_folder = None # frame_folder
662
+ options.frame_rendering_folder = None # rendering_folder
663
+
664
+ options.keep_extracted_frames = False
665
+ options.keep_rendered_frames = False
666
+ options.force_extracted_frame_folder_deletion = True
667
+ options.force_rendered_frame_folder_deletion = True
668
+
559
669
  # options.confidence_threshold = 0.15
560
- # options.fourcc = 'mp4v'
670
+ options.fourcc = 'mp4v'
561
671
 
562
- cmd = options_to_command(options)
563
- print(cmd)
564
- # import clipboard; clipboard.copy(cmd)
672
+ cmd = options_to_command(options); print(cmd)
673
+
674
+ import clipboard; clipboard.copy(cmd)
565
675
 
566
676
  if False:
567
677
  process_video_folder(options)
@@ -569,23 +679,42 @@ if False:
569
679
 
570
680
  #%% Process a single video
571
681
 
572
- fn = os.path.expanduser('~/tmp/video-test/test-video.mp4')
682
+ fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
573
683
  model_file = 'MDV5A'
574
684
  input_video_file = fn
575
- frame_folder = os.path.expanduser('~/tmp/video-test/frames')
576
- rendering_folder = os.path.expanduser('~/tmp/video-test/rendered-frames')
685
+
686
+ output_base = r'g:\temp\video_test'
687
+ frame_folder = os.path.join(output_base,'frames')
688
+ rendering_folder = os.path.join(output_base,'rendered-frames')
689
+ output_json_file = os.path.join(output_base,'video-test.json')
690
+ output_video_file = os.path.join(output_base,'output_videos.mp4')
577
691
 
578
692
  options = ProcessVideoOptions()
579
693
  options.model_file = model_file
580
694
  options.input_video_file = input_video_file
581
- options.frame_folder = frame_folder
582
- options.frame_rendering_folder = rendering_folder
583
695
  options.render_output_video = True
584
- options.output_video_file = os.path.expanduser('~/tmp/video-test/detections.mp4')
696
+ options.output_video_file = output_video_file
697
+
698
+ options.verbose = True
699
+
700
+ options.quality = 75
701
+ options.frame_sample = None # 10
702
+ options.max_width = 600
703
+
704
+ options.frame_folder = None # frame_folder
705
+ options.frame_rendering_folder = None # rendering_folder
585
706
 
586
- cmd = options_to_command(options)
587
- print(cmd)
588
- # import clipboard; clipboard.copy(cmd)
707
+ options.keep_extracted_frames = False
708
+ options.keep_rendered_frames = False
709
+ options.force_extracted_frame_folder_deletion = True
710
+ options.force_rendered_frame_folder_deletion = True
711
+
712
+ # options.confidence_threshold = 0.15
713
+ options.fourcc = 'mp4v'
714
+
715
+ cmd = options_to_command(options); print(cmd)
716
+
717
+ import clipboard; clipboard.copy(cmd)
589
718
 
590
719
  if False:
591
720
  process_video(options)
@@ -640,7 +769,8 @@ def main():
640
769
  help='enable video output rendering (not rendered by default)')
641
770
 
642
771
  parser.add_argument('--fourcc', default=default_fourcc,
643
- help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(default_fourcc))
772
+ help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
773
+ default_fourcc))
644
774
 
645
775
  parser.add_argument('--keep_rendered_frames',
646
776
  action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
@@ -666,7 +796,7 @@ def main():
666
796
  default_options.json_confidence_threshold))
667
797
 
668
798
  parser.add_argument('--n_cores', type=int,
669
- default=1, help='number of cores to use for frame separation and detection. '\
799
+ default=1, help='Number of cores to use for frame separation and detection. '\
670
800
  'If using a GPU, this option will be respected for frame separation but '\
671
801
  'ignored for detection. Only relevant to frame separation when processing '\
672
802
  'a folder.')
@@ -674,8 +804,18 @@ def main():
674
804
  parser.add_argument('--frame_sample', type=int,
675
805
  default=None, help='process every Nth frame (defaults to every frame)')
676
806
 
807
+ parser.add_argument('--quality', type=int,
808
+ default=default_options.quality,
809
+ help='JPEG quality for extracted frames (defaults to {})'.format(
810
+ default_options.quality))
811
+
812
+ parser.add_argument('--max_width', type=int,
813
+ default=default_options.max_width,
814
+ help='Resize frames larger than this before writing (defaults to {})'.format(
815
+ default_options.max_width))
816
+
677
817
  parser.add_argument('--debug_max_frames', type=int,
678
- default=-1, help='trim to N frames for debugging (impacts model execution, '\
818
+ default=-1, help='Trim to N frames for debugging (impacts model execution, '\
679
819
  'not frame rendering)')
680
820
 
681
821
  parser.add_argument('--class_mapping_filename',
@@ -685,6 +825,10 @@ def main():
685
825
  'the addition of "1" to all category IDs, so your class mapping should start '\
686
826
  'at zero.')
687
827
 
828
+ parser.add_argument('--verbose', action='store_true',
829
+ help='Enable additional debug output')
830
+
831
+
688
832
  if len(sys.argv[1:]) == 0:
689
833
  parser.print_help()
690
834
  parser.exit()