megadetector 10.0.2__py3-none-any.whl → 10.0.4__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 (30) hide show
  1. megadetector/data_management/animl_to_md.py +158 -0
  2. megadetector/data_management/zamba_to_md.py +188 -0
  3. megadetector/detection/process_video.py +165 -946
  4. megadetector/detection/pytorch_detector.py +575 -276
  5. megadetector/detection/run_detector_batch.py +629 -202
  6. megadetector/detection/run_md_and_speciesnet.py +1319 -0
  7. megadetector/detection/video_utils.py +243 -107
  8. megadetector/postprocessing/classification_postprocessing.py +12 -1
  9. megadetector/postprocessing/combine_batch_outputs.py +2 -0
  10. megadetector/postprocessing/compare_batch_results.py +21 -2
  11. megadetector/postprocessing/merge_detections.py +16 -12
  12. megadetector/postprocessing/separate_detections_into_folders.py +1 -1
  13. megadetector/postprocessing/subset_json_detector_output.py +1 -3
  14. megadetector/postprocessing/validate_batch_results.py +25 -2
  15. megadetector/tests/__init__.py +0 -0
  16. megadetector/tests/test_nms_synthetic.py +335 -0
  17. megadetector/utils/ct_utils.py +69 -5
  18. megadetector/utils/extract_frames_from_video.py +303 -0
  19. megadetector/utils/md_tests.py +583 -524
  20. megadetector/utils/path_utils.py +4 -15
  21. megadetector/utils/wi_utils.py +20 -4
  22. megadetector/visualization/visualization_utils.py +1 -1
  23. megadetector/visualization/visualize_db.py +8 -22
  24. megadetector/visualization/visualize_detector_output.py +7 -5
  25. megadetector/visualization/visualize_video_output.py +607 -0
  26. {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/METADATA +134 -135
  27. {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/RECORD +30 -23
  28. {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/licenses/LICENSE +0 -0
  29. {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/top_level.txt +0 -0
  30. {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/WHEEL +0 -0
@@ -16,30 +16,21 @@ repeat detection elimination).
16
16
 
17
17
  import os
18
18
  import sys
19
- import tempfile
20
19
  import argparse
21
- import itertools
22
- import json
23
- import shutil
24
- import getpass
25
-
26
- from uuid import uuid1
27
20
 
28
21
  from megadetector.detection import run_detector_batch
29
- from megadetector.visualization import visualize_detector_output
30
22
  from megadetector.utils.ct_utils import args_to_object
31
23
  from megadetector.utils.ct_utils import dict_to_kvp_list, parse_kvp_list
32
- from megadetector.utils.path_utils import insert_before_extension, clean_path
33
- from megadetector.detection.video_utils import video_to_frames
34
- from megadetector.detection.video_utils import run_callback_on_frames
24
+ from megadetector.detection.video_utils import _filename_to_frame_number
25
+ from megadetector.detection.video_utils import find_videos
35
26
  from megadetector.detection.video_utils import run_callback_on_frames_for_folder
36
- from megadetector.detection.video_utils import frames_to_video
37
- from megadetector.detection.video_utils import frame_results_to_video_results
38
- from megadetector.detection.video_utils import FrameToVideoOptions
39
- from megadetector.detection.video_utils import _add_frame_numbers_to_results
40
- from megadetector.detection.video_utils import video_folder_to_frames
41
- from megadetector.detection.video_utils import default_fourcc
42
27
  from megadetector.detection.run_detector import load_detector
28
+ from megadetector.postprocessing.validate_batch_results import \
29
+ ValidateBatchResultsOptions, validate_batch_results
30
+
31
+ # Notes to self re: upcoming work on checkpointing
32
+ from megadetector.utils.ct_utils import split_list_into_fixed_size_chunks # noqa
33
+ from megadetector.detection.run_detector_batch import write_checkpoint, load_checkpoint # noqa
43
34
 
44
35
 
45
36
  #%% Classes
@@ -65,104 +56,23 @@ class ProcessVideoOptions:
65
56
  #: .json file to which we should write results
66
57
  self.output_json_file = None
67
58
 
68
- #: File to which we should write a video with boxes, only relevant if
69
- #: render_output_video is True
70
- self.output_video_file = None
71
-
72
- #: Folder to use for extracted frames; will use a folder in system temp space
73
- #: if this is None
74
- self.frame_folder = None
75
-
76
- #: Folder to use for rendered frames (if rendering output video); will use a folder
77
- #: in system temp space if this is None
78
- self.frame_rendering_folder = None
79
-
80
- #: Should we render a video with detection boxes?
81
- #:
82
- #: If processing a folder, this renders each input video to a separate
83
- #: video with detection boxes.
84
- self.render_output_video = False
85
-
86
- #: If we are rendering boxes to a new video, should we keep the temporary
87
- #: rendered frames?
88
- self.keep_rendered_frames = False
89
-
90
- #: Should we keep the extracted frames?
91
- self.keep_extracted_frames = False
92
-
93
- #: Should we delete the entire folder the extracted frames are written to?
94
- #:
95
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
96
- #: for no reason other than being paranoid about deleting folders.
97
- self.force_extracted_frame_folder_deletion = False
98
-
99
- #: Should we delete the entire folder the rendered frames are written to?
100
- #:
101
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
102
- #: for no reason other than being paranoid about deleting folders.
103
- self.force_rendered_frame_folder_deletion = False
104
-
105
- #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
106
- #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
107
- self.reuse_results_if_available = False
108
-
109
- #: If we've already split this video or folder of videos into frames, should we
110
- #: we re-use those extracted frames? Defaults to reprocessing.
111
- self.reuse_frames_if_available = False
112
-
113
59
  #: If [input_video_file] is a folder, should we search for videos recursively?
114
60
  self.recursive = False
115
61
 
116
62
  #: Enable additional debug console output
117
63
  self.verbose = False
118
64
 
119
- #: fourcc code to use for writing videos; only relevant if render_output_video is True
120
- self.fourcc = None
121
-
122
- #: force a specific frame rate for output videos; only relevant if render_output_video
123
- #: is True
124
- self.rendering_fs = None
125
-
126
- #: Confidence threshold to use for writing videos with boxes, only relevant if
127
- #: if render_output_video is True. Defaults to choosing a reasonable threshold
128
- #: based on the model version.
129
- self.rendering_confidence_threshold = None
130
-
131
65
  #: Detections below this threshold will not be included in the output file.
132
66
  self.json_confidence_threshold = 0.005
133
67
 
134
68
  #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
135
69
  #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
136
- #: typical value. Mutually exclusive with [frames_to_extract] and [time_sample].
70
+ #: typical value. Mutually exclusive with [time_sample].
137
71
  self.frame_sample = None
138
72
 
139
- #: Extract a specific set of frames (list of ints, or a single int). Mutually exclusive with
140
- #: [frame_sample] and [time_sample].
141
- self.frames_to_extract = None
142
-
143
- # Sample frames every N seconds. Mutually exclusive with [frame_sample] and [frames_to_extract].
73
+ #: Sample frames every N seconds. Mutually exclusive with [frame_sample]
144
74
  self.time_sample = None
145
75
 
146
- #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
147
- self.n_cores = 1
148
-
149
- #: For debugging only, stop processing after a certain number of frames.
150
- self.debug_max_frames = -1
151
-
152
- #: For debugging only, force on-disk frame extraction, even if it wouldn't otherwise be
153
- #: necessary
154
- self.force_on_disk_frame_extraction = False
155
-
156
- #: File containing non-standard categories, typically only used if you're running a non-MD
157
- #: detector.
158
- self.class_mapping_filename = None
159
-
160
- #: JPEG quality for frame output, from 0-100. Use None or -1 to let opencv decide.
161
- self.quality = 90
162
-
163
- #: Resize frames so they're at most this wide
164
- self.max_width = None
165
-
166
76
  #: Run the model at this image size (don't mess with this unless you know what you're
167
77
  #: getting into)... if you just want to pass smaller frames to MD, use max_width
168
78
  self.image_size = None
@@ -171,17 +81,23 @@ class ProcessVideoOptions:
171
81
  self.augment = False
172
82
 
173
83
  #: By default, a video with no frames (or no frames retrievable with the current parameters)
174
- #: is an error, this makes it a warning. This would apply if you request, e.g., the 100th
175
- #: frame from each video, but a video only has 50 frames.
84
+ #: is treated as a failure; this causes it to be treated as a video with no detections.
176
85
  self.allow_empty_videos = False
177
86
 
178
- #: When processing a folder of videos, should we include just a single representative
179
- #: frame result for each video (default), or every frame that was processed?
180
- self.include_all_processed_frames = False
181
-
182
87
  #: Detector-specific options
183
88
  self.detector_options = None
184
89
 
90
+ #: Write a checkpoint file (to resume processing later) every N videos;
91
+ #: set to -1 (default) to disable checkpointing
92
+ self.checkpoint_frequency = -1
93
+
94
+ #: Path to checkpoint file; None (default) for auto-generation based on output filename
95
+ self.checkpoint_path = None
96
+
97
+ #: Resume from a checkpoint file, or "auto" to use the most recent checkpoint in the
98
+ #: output directory
99
+ self.resume_from_checkpoint = None
100
+
185
101
  # ...class ProcessVideoOptions
186
102
 
187
103
 
@@ -197,369 +113,16 @@ def _validate_video_options(options):
197
113
  n_sampling_options_configured += 1
198
114
  if options.time_sample is not None:
199
115
  n_sampling_options_configured += 1
200
- if options.frames_to_extract is not None:
201
- n_sampling_options_configured += 1
202
116
 
203
117
  if n_sampling_options_configured > 1:
204
- raise ValueError('frame_sample, time_sample, and frames_to_extract are mutually exclusive')
118
+ raise ValueError('frame_sample and time_sample are mutually exclusive')
205
119
 
206
120
  return True
207
121
 
208
122
 
209
- def _select_temporary_output_folders(options):
123
+ def process_videos(options):
210
124
  """
211
- Choose folders in system temp space for writing temporary frames. Does not create folders,
212
- just defines them.
213
- """
214
-
215
- tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
216
-
217
- # If we create a folder like "process_camera_trap_video" in the system temp dir, it may
218
- # be the case that no one else can write to it, even to create user-specific subfolders.
219
- # If we create a uuid-named folder in the system temp dir, we make a mess.
220
- #
221
- # Compromise with "process_camera_trap_video-[user]".
222
- user_tempdir = tempdir + '-' + getpass.getuser()
223
-
224
- # I don't know whether it's possible for a username to contain characters that are
225
- # not valid filename characters, but just to be sure...
226
- user_tempdir = clean_path(user_tempdir)
227
-
228
- frame_output_folder = os.path.join(
229
- user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
230
-
231
- rendering_output_folder = os.path.join(
232
- tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
233
-
234
- temporary_folder_info = \
235
- {
236
- 'temp_folder_base':user_tempdir,
237
- 'frame_output_folder':frame_output_folder,
238
- 'rendering_output_folder':rendering_output_folder
239
- }
240
-
241
- return temporary_folder_info
242
-
243
- # ...def _create_frame_output_folders(...)
244
-
245
-
246
- def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
247
- """
248
- If necessary, delete rendered frames and/or the entire rendering output folder.
249
- """
250
-
251
- if rendering_output_folder is None:
252
- return
253
-
254
- caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
255
-
256
- # (Optionally) delete the temporary directory we used for rendered detection images
257
- if not options.keep_rendered_frames:
258
-
259
- try:
260
-
261
- # If (a) we're supposed to delete the temporary rendering folder no
262
- # matter where it is and (b) we created it in temp space, delete the
263
- # whole tree
264
- if options.force_rendered_frame_folder_deletion and \
265
- (not caller_provided_rendering_output_folder):
266
-
267
- if options.verbose:
268
- print('Recursively deleting rendered frame folder {}'.format(
269
- rendering_output_folder))
270
-
271
- shutil.rmtree(rendering_output_folder)
272
-
273
- # ...otherwise just delete the frames, but leave the folder in place
274
- else:
275
-
276
- if options.force_rendered_frame_folder_deletion:
277
- assert caller_provided_rendering_output_folder
278
- print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
279
- 'user-provided folder, only removing frames')
280
-
281
- for rendered_frame_fn in detected_frame_files:
282
- os.remove(rendered_frame_fn)
283
-
284
- except Exception as e:
285
- print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
286
- rendering_output_folder,str(e)))
287
- pass
288
-
289
- elif options.force_rendered_frame_folder_deletion:
290
-
291
- print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
292
- 'specified, not deleting')
293
-
294
- # ...def _clean_up_rendered_frames(...)
295
-
296
-
297
- def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
298
- """
299
- If necessary, delete extracted frames and/or the entire temporary frame folder.
300
- """
301
-
302
- if frame_output_folder is None:
303
- return
304
-
305
- caller_provided_frame_output_folder = (options.frame_folder is not None)
306
-
307
- if not options.keep_extracted_frames:
308
-
309
- try:
310
-
311
- # If (a) we're supposed to delete the temporary frame folder no
312
- # matter where it is and (b) we created it in temp space, delete the
313
- # whole tree.
314
- if options.force_extracted_frame_folder_deletion and \
315
- (not caller_provided_frame_output_folder):
316
-
317
- if options.verbose:
318
- print('Recursively deleting frame output folder {}'.format(frame_output_folder))
319
-
320
- shutil.rmtree(frame_output_folder)
321
-
322
- # ...otherwise just delete the frames, but leave the folder in place
323
- else:
324
-
325
- if frame_filenames is None:
326
- return
327
-
328
- if options.force_extracted_frame_folder_deletion:
329
- assert caller_provided_frame_output_folder
330
- print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
331
- 'user-provided folder, only removing frames')
332
-
333
- for extracted_frame_fn in frame_filenames:
334
- os.remove(extracted_frame_fn)
335
-
336
- except Exception as e:
337
- print('Warning: error removing extracted frames from folder {}:\n{}'.format(
338
- frame_output_folder,str(e)))
339
- pass
340
-
341
- elif options.force_extracted_frame_folder_deletion:
342
-
343
- print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
344
- 'specified, not deleting')
345
-
346
- # ...def _clean_up_extracted_frames
347
-
348
-
349
- def process_video(options):
350
- """
351
- Process a single video through MD, optionally writing a new video with boxes.
352
- Can also be used just to split a video into frames, without running a model.
353
-
354
- Args:
355
- options (ProcessVideoOptions): all the parameters used to control this process,
356
- including filenames; see ProcessVideoOptions for details
357
-
358
- Returns:
359
- dict: frame-level MegaDetector results, identical to what's in the output .json file
360
- """
361
-
362
- # Check for incompatible options
363
- _validate_video_options(options)
364
-
365
- if options.output_json_file is None:
366
- options.output_json_file = options.input_video_file + '.json'
367
-
368
- if options.render_output_video and (options.output_video_file is None):
369
- options.output_video_file = options.input_video_file + '.detections.mp4'
370
-
371
- if options.time_sample is not None:
372
- raise ValueError('Time-based sampling is not supported when processing a single video; ' + \
373
- 'consider processing a folder, or using frame_sample')
374
-
375
- if options.model_file == 'no_detection' and not options.keep_extracted_frames:
376
- print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
377
- return
378
-
379
- # Track whether frame and rendering folders were created by this script
380
- caller_provided_frame_output_folder = (options.frame_folder is not None)
381
- caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
382
-
383
- frame_output_folder = None
384
- frame_filenames = None
385
-
386
- # If we should re-use existing results, and the output file exists, don't bother running MD
387
- if (options.reuse_results_if_available and os.path.isfile(options.output_json_file)):
388
-
389
- print('Loading results from {}'.format(options.output_json_file))
390
- with open(options.output_json_file,'r') as f:
391
- results = json.load(f)
392
-
393
- # Run MD in memory if we don't need to generate frames
394
- #
395
- # Currently if we're generating an output video, we need to generate frames on disk first.
396
- elif (not options.keep_extracted_frames and \
397
- not options.render_output_video and \
398
- not options.force_on_disk_frame_extraction):
399
-
400
- # Run MegaDetector in memory
401
-
402
- if options.verbose:
403
- print('Running MegaDetector in memory for {}'.format(options.input_video_file))
404
-
405
- if options.frame_folder is not None:
406
- print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
407
- 'not; no raw frames will be written')
408
-
409
- detector = load_detector(options.model_file,detector_options=options.detector_options)
410
-
411
- def frame_callback(image_np,image_id):
412
- return detector.generate_detections_one_image(image_np,
413
- image_id,
414
- detection_threshold=options.json_confidence_threshold,
415
- augment=options.augment)
416
-
417
- frame_results = run_callback_on_frames(options.input_video_file,
418
- frame_callback,
419
- every_n_frames=options.frame_sample,
420
- verbose=options.verbose,
421
- frames_to_process=options.frames_to_extract)
422
-
423
- frame_results['results'] = _add_frame_numbers_to_results(frame_results['results'])
424
-
425
- run_detector_batch.write_results_to_file(
426
- frame_results['results'],
427
- options.output_json_file,
428
- relative_path_base=None,
429
- detector_file=options.model_file,
430
- custom_metadata={'video_frame_rate':frame_results['frame_rate']})
431
-
432
- # Extract frames and optionally run MegaDetector on those frames
433
- else:
434
-
435
- if options.verbose:
436
- print('Extracting frames for {}'.format(options.input_video_file))
437
-
438
- # This does not create any folders, just defines temporary folder names in
439
- # case we need them.
440
- temporary_folder_info = _select_temporary_output_folders(options)
441
-
442
- if (caller_provided_frame_output_folder):
443
- frame_output_folder = options.frame_folder
444
- else:
445
- frame_output_folder = temporary_folder_info['frame_output_folder']
446
-
447
- os.makedirs(frame_output_folder, exist_ok=True)
448
-
449
-
450
- ## Extract frames
451
-
452
- frame_filenames, fs = video_to_frames(
453
- options.input_video_file,
454
- frame_output_folder,
455
- every_n_frames=options.frame_sample,
456
- overwrite=(not options.reuse_frames_if_available),
457
- quality=options.quality,
458
- max_width=options.max_width,
459
- verbose=options.verbose,
460
- frames_to_extract=options.frames_to_extract,
461
- allow_empty_videos=options.allow_empty_videos)
462
-
463
- image_file_names = frame_filenames
464
- if options.debug_max_frames > 0:
465
- image_file_names = image_file_names[0:options.debug_max_frames]
466
-
467
- ## Run MegaDetector on those frames
468
-
469
- if options.model_file != 'no_detection':
470
-
471
- if options.verbose:
472
- print('Running MD for {}'.format(options.input_video_file))
473
-
474
- results = run_detector_batch.load_and_run_detector_batch(
475
- options.model_file,
476
- image_file_names,
477
- confidence_threshold=options.json_confidence_threshold,
478
- n_cores=options.n_cores,
479
- class_mapping_filename=options.class_mapping_filename,
480
- quiet=True,
481
- augment=options.augment,
482
- image_size=options.image_size,
483
- detector_options=options.detector_options)
484
-
485
- results = _add_frame_numbers_to_results(results)
486
-
487
- run_detector_batch.write_results_to_file(
488
- results,
489
- options.output_json_file,
490
- relative_path_base=frame_output_folder,
491
- detector_file=options.model_file,
492
- custom_metadata={'video_frame_rate':fs})
493
-
494
- # ...if we are/aren't keeping raw frames on disk
495
-
496
-
497
- ## (Optionally) render output video
498
-
499
- if options.render_output_video:
500
-
501
- ## Render detections to images
502
-
503
- if (caller_provided_rendering_output_folder):
504
- rendering_output_dir = options.frame_rendering_folder
505
- else:
506
- rendering_output_dir = temporary_folder_info['rendering_output_folder']
507
-
508
- os.makedirs(rendering_output_dir,exist_ok=True)
509
-
510
- detected_frame_files = visualize_detector_output.visualize_detector_output(
511
- detector_output_path=options.output_json_file,
512
- out_dir=rendering_output_dir,
513
- images_dir=frame_output_folder,
514
- confidence_threshold=options.rendering_confidence_threshold)
515
-
516
-
517
- ## Choose the frame rate at which we should render the output video
518
-
519
- if options.rendering_fs is not None:
520
- rendering_fs = options.rendering_fs
521
- elif options.frame_sample is None and options.time_sample is None:
522
- rendering_fs = fs
523
- elif options.frame_sample is not None:
524
- assert options.time_sample is None
525
- # If the original video was 30fps and we sampled every 10th frame,
526
- # render at 3fps
527
- rendering_fs = fs / options.frame_sample
528
- elif options.time_sample is not None:
529
- rendering_fs = options.time_sample
530
-
531
-
532
- ## Render the output video
533
-
534
- print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
535
- len(detected_frame_files), options.output_video_file,rendering_fs,fs))
536
- frames_to_video(detected_frame_files,
537
- rendering_fs,
538
- options.output_video_file,
539
- codec_spec=options.fourcc)
540
-
541
- # Possibly clean up rendered frames
542
- _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
543
-
544
- # ...if we're rendering video
545
-
546
-
547
- ## (Optionally) delete the extracted frames
548
-
549
- _clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
550
-
551
- # ...process_video()
552
-
553
-
554
- def process_video_folder(options):
555
- """
556
- Process a folder of videos through MD. Can also be used just to split a folder of
557
- videos into frames, without running a model.
558
-
559
- When this function is used to run MD, two .json files will get written, one with
560
- an entry for each *frame* (identical to what's created by process_video()), and
561
- one with an entry for each *video* (which is more suitable for, e.g., reading into
562
- Timelapse).
125
+ Process a video or folder of videos through MD.
563
126
 
564
127
  Args:
565
128
  options (ProcessVideoOptions): all the parameters used to control this process,
@@ -571,281 +134,137 @@ def process_video_folder(options):
571
134
  # Check for incompatible options
572
135
  _validate_video_options(options)
573
136
 
574
- assert os.path.isdir(options.input_video_file), \
575
- '{} is not a folder'.format(options.input_video_file)
576
-
577
- if options.model_file == 'no_detection' and not options.keep_extracted_frames:
578
- print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
579
- return
580
-
581
- if options.model_file != 'no_detection':
582
- assert options.output_json_file is not None, \
583
- 'When processing a folder, you must specify an output .json file'
584
- assert options.output_json_file.endswith('.json')
585
- video_json = options.output_json_file
586
- frames_json = options.output_json_file.replace('.json','.frames.json')
587
- os.makedirs(os.path.dirname(video_json),exist_ok=True)
588
-
589
- # Track whether frame and rendering folders were created by this script
590
- caller_provided_frame_output_folder = (options.frame_folder is not None)
591
- caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
592
-
593
- # This does not create any folders, just defines temporary folder names in
594
- # case we need them.
595
- temporary_folder_info = _select_temporary_output_folders(options)
596
-
597
- frame_output_folder = None
598
- image_file_names = None
599
- video_filename_to_fs = {}
137
+ assert options.output_json_file.endswith('.json'), \
138
+ 'Illegal output file {}'.format(options.output_json_file)
600
139
 
601
140
  if options.time_sample is not None:
602
141
  every_n_frames_param = -1 * options.time_sample
603
142
  else:
604
143
  every_n_frames_param = options.frame_sample
605
144
 
606
- # Run MD in memory if we don't need to generate frames
607
- #
608
- # Currently if we're generating an output video, we need to generate frames on disk first.
609
- if (not options.keep_extracted_frames and \
610
- not options.render_output_video and \
611
- not options.force_on_disk_frame_extraction):
145
+ if options.verbose:
146
+ print('Processing videos from input source {}'.format(options.input_video_file))
612
147
 
613
- if options.verbose:
614
- print('Running MegaDetector in memory for folder {}'.format(options.input_video_file))
148
+ detector = load_detector(options.model_file,detector_options=options.detector_options)
615
149
 
616
- if options.frame_folder is not None:
617
- print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
618
- 'not; no raw frames will be written')
150
+ def frame_callback(image_np,image_id):
151
+ return detector.generate_detections_one_image(image_np,
152
+ image_id,
153
+ detection_threshold=options.json_confidence_threshold,
154
+ augment=options.augment,
155
+ image_size=options.image_size,
156
+ verbose=options.verbose)
619
157
 
620
- detector = load_detector(options.model_file,detector_options=options.detector_options)
158
+ """
159
+ [md_results] will be dict with keys 'video_filenames' (list of str), 'frame_rates' (list of floats),
160
+ 'results' (list of list of dicts). 'video_filenames' will contain *relative* filenames.
161
+ 'results' is a list (one element per video) of lists (one element per frame) of whatever the
162
+ callback returns, typically (but not necessarily) dicts in the MD results format.
621
163
 
622
- def frame_callback(image_np,image_id):
623
- return detector.generate_detections_one_image(image_np,
624
- image_id,
625
- detection_threshold=options.json_confidence_threshold,
626
- augment=options.augment)
164
+ For failed videos, the frame rate will be represented by -1, and "results"
165
+ will be a dict with at least the key "failure".
166
+ """
167
+ if os.path.isfile(options.input_video_file):
627
168
 
628
- md_results = run_callback_on_frames_for_folder(input_video_folder=options.input_video_file,
169
+ video_folder = os.path.dirname(options.input_video_file)
170
+ video_bn = os.path.basename(options.input_video_file)
171
+ md_results = run_callback_on_frames_for_folder(input_video_folder=video_folder,
629
172
  frame_callback=frame_callback,
630
173
  every_n_frames=every_n_frames_param,
631
- verbose=options.verbose)
632
-
633
- video_results = md_results['results']
634
-
635
- for i_video,video_filename in enumerate(md_results['video_filenames']):
636
- video_filename = video_filename.replace('\\','/')
637
- assert video_filename not in video_filename_to_fs
638
- video_filename_to_fs[video_filename] = md_results['frame_rates'][i_video]
639
-
640
- all_frame_results = []
641
-
642
- # r = video_results[0]
643
- for frame_results in video_results:
644
- _add_frame_numbers_to_results(frame_results)
645
- all_frame_results.extend(frame_results)
646
-
647
- run_detector_batch.write_results_to_file(
648
- all_frame_results,
649
- frames_json,
650
- relative_path_base=None,
651
- detector_file=options.model_file)
174
+ verbose=options.verbose,
175
+ files_to_process_relative=[video_bn],
176
+ allow_empty_videos=options.allow_empty_videos)
652
177
 
653
178
  else:
654
179
 
655
- ## Split every video into frames
656
-
657
- if options.verbose:
658
- print('Extracting frames for folder {}'.format(options.input_video_file))
659
-
660
- if caller_provided_frame_output_folder:
661
- frame_output_folder = options.frame_folder
662
- else:
663
- frame_output_folder = temporary_folder_info['frame_output_folder']
664
-
665
- os.makedirs(frame_output_folder, exist_ok=True)
666
-
667
- frame_filenames, fs, video_filenames = \
668
- video_folder_to_frames(input_folder=options.input_video_file,
669
- output_folder_base=frame_output_folder,
670
- recursive=options.recursive,
671
- overwrite=(not options.reuse_frames_if_available),
672
- n_threads=options.n_cores,
673
- every_n_frames=every_n_frames_param,
674
- verbose=options.verbose,
675
- quality=options.quality,
676
- max_width=options.max_width,
677
- frames_to_extract=options.frames_to_extract,
678
- allow_empty_videos=options.allow_empty_videos)
679
-
680
- for i_video,video_filename_abs in enumerate(video_filenames):
681
- video_filename_relative = os.path.relpath(video_filename_abs,options.input_video_file)
682
- video_filename_relative = video_filename_relative.replace('\\','/')
683
- assert video_filename_relative not in video_filename_to_fs
684
- video_filename_to_fs[video_filename_relative] = fs[i_video]
685
-
686
- print('Extracted frames for {} videos'.format(len(set(video_filenames))))
687
- image_file_names = list(itertools.chain.from_iterable(frame_filenames))
688
-
689
- if len(image_file_names) == 0:
690
- if len(video_filenames) == 0:
691
- print('No videos found in folder {}'.format(options.input_video_file))
692
- else:
693
- print('No frames extracted from folder {}, this may be due to an '\
694
- 'unsupported video codec'.format(options.input_video_file))
695
- return
696
-
697
- if options.debug_max_frames is not None and options.debug_max_frames > 0:
698
- image_file_names = image_file_names[0:options.debug_max_frames]
699
-
700
- if options.model_file == 'no_detection':
701
- assert options.keep_extracted_frames, \
702
- 'Internal error: keep_extracted_frames not set, but no model specified'
703
- return
704
-
705
-
706
- ## Run MegaDetector on the extracted frames
707
-
708
- if options.reuse_results_if_available and \
709
- os.path.isfile(frames_json):
710
-
711
- print('Bypassing inference, loading results from {}'.format(frames_json))
712
- with open(frames_json,'r') as f:
713
- results = json.load(f)
714
-
715
- else:
180
+ assert os.path.isdir(options.input_video_file), \
181
+ '{} is neither a file nor a folder'.format(options.input_video_file)
716
182
 
717
- print('Running MegaDetector')
183
+ video_folder = options.input_video_file
718
184
 
719
- results = run_detector_batch.load_and_run_detector_batch(
720
- options.model_file,
721
- image_file_names,
722
- confidence_threshold=options.json_confidence_threshold,
723
- n_cores=options.n_cores,
724
- class_mapping_filename=options.class_mapping_filename,
725
- quiet=True,
726
- augment=options.augment,
727
- image_size=options.image_size,
728
- detector_options=options.detector_options)
185
+ md_results = run_callback_on_frames_for_folder(input_video_folder=options.input_video_file,
186
+ frame_callback=frame_callback,
187
+ every_n_frames=every_n_frames_param,
188
+ verbose=options.verbose,
189
+ recursive=options.recursive,
190
+ allow_empty_videos=options.allow_empty_videos)
729
191
 
730
- _add_frame_numbers_to_results(results)
192
+ # ...whether we're processing a file or a folder
731
193
 
732
- run_detector_batch.write_results_to_file(
733
- results,
734
- frames_json,
735
- relative_path_base=frame_output_folder,
736
- detector_file=options.model_file)
194
+ print('Finished running MD on videos')
737
195
 
738
- # ...if we're re-using existing results / running MD
196
+ video_results = md_results['results']
197
+ video_filenames = md_results['video_filenames']
198
+ video_frame_rates = md_results['frame_rates']
739
199
 
740
- # ...if we're running MD on in-memory frames vs. extracting frames to disk
200
+ assert len(video_results) == len(video_filenames)
201
+ assert len(video_results) == len(video_frame_rates)
741
202
 
742
- ## Convert frame-level results to video-level results
203
+ video_list_md_format = []
743
204
 
744
- frame_to_video_options = FrameToVideoOptions()
745
- frame_to_video_options.include_all_processed_frames = options.include_all_processed_frames
205
+ # i_video = 0; results_this_video = video_results[i_video]
206
+ for i_video,results_this_video in enumerate(video_results):
746
207
 
747
- print('Converting frame-level results to video-level results')
748
- frame_results_to_video_results(frames_json,
749
- video_json,
750
- options=frame_to_video_options,
751
- video_filename_to_frame_rate=video_filename_to_fs)
208
+ video_fn = video_filenames[i_video]
752
209
 
210
+ im = {}
211
+ im['file'] = video_fn
212
+ im['frame_rate'] = video_frame_rates[i_video]
213
+ im['frames_processed'] = []
753
214
 
754
- ## (Optionally) render output videos
215
+ if isinstance(results_this_video,dict):
755
216
 
756
- if options.render_output_video:
217
+ assert 'failure' in results_this_video
218
+ im['failure'] = results_this_video['failure']
219
+ im['detections'] = None
757
220
 
758
- # Render detections to images
759
- if (caller_provided_rendering_output_folder):
760
- rendering_output_dir = options.frame_rendering_folder
761
- else:
762
- rendering_output_dir = temporary_folder_info['rendering_output_folder']
763
-
764
- os.makedirs(rendering_output_dir,exist_ok=True)
765
-
766
- detected_frame_files = visualize_detector_output.visualize_detector_output(
767
- detector_output_path=frames_json,
768
- out_dir=rendering_output_dir,
769
- images_dir=frame_output_folder,
770
- confidence_threshold=options.rendering_confidence_threshold,
771
- preserve_path_structure=True,
772
- output_image_width=-1)
773
- detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
774
-
775
- # Choose an output folder
776
- output_folder_is_input_folder = False
777
- if options.output_video_file is not None:
778
- if os.path.isfile(options.output_video_file):
779
- raise ValueError('Rendering videos for a folder, but an existing file was specified as output')
780
- elif options.output_video_file == options.input_video_file:
781
- output_folder_is_input_folder = True
782
- output_video_folder = options.input_video_file
783
- else:
784
- os.makedirs(options.output_video_file,exist_ok=True)
785
- output_video_folder = options.output_video_file
786
221
  else:
787
- output_folder_is_input_folder = True
788
- output_video_folder = options.input_video_file
789
222
 
790
- # For each video
791
- #
792
- # TODO: parallelize this loop
793
- #
794
- # i_video=0; input_video_file_abs = video_filenames[i_video]
795
- for i_video,input_video_file_abs in enumerate(video_filenames):
223
+ im['detections'] = []
796
224
 
797
- video_fs = fs[i_video]
225
+ # results_one_frame = results_this_video[0]
226
+ for results_one_frame in results_this_video:
798
227
 
799
- if options.rendering_fs is not None:
800
- rendering_fs = options.rendering_fs
801
- elif options.frame_sample is None:
802
- rendering_fs = video_fs
803
- else:
804
- # If the original video was 30fps and we sampled every 10th frame,
805
- # render at 3fps
806
- rendering_fs = video_fs / options.frame_sample
228
+ assert results_one_frame['file'].startswith(video_fn)
807
229
 
808
- input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
809
- video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
230
+ frame_number = _filename_to_frame_number(results_one_frame['file'])
810
231
 
811
- video_frame_output_folder = video_frame_output_folder.replace('\\','/')
812
- assert os.path.isdir(video_frame_output_folder), \
813
- 'Could not find frame folder for video {}'.format(input_video_file_relative)
232
+ assert frame_number not in im['frames_processed'], \
233
+ 'Received the same frame twice for video {}'.format(im['file'])
814
234
 
815
- # Find the corresponding rendered frame folder
816
- video_frame_files = [fn for fn in detected_frame_files if \
817
- fn.startswith(video_frame_output_folder)]
818
- assert len(video_frame_files) > 0, 'Could not find rendered frames for video {}'.format(
819
- input_video_file_relative)
235
+ im['frames_processed'].append(frame_number)
820
236
 
821
- # Select the output filename for the rendered video
822
- if output_folder_is_input_folder:
823
- video_output_file = insert_before_extension(input_video_file_abs,'annotated','_')
824
- else:
825
- video_output_file = os.path.join(output_video_folder,input_video_file_relative)
237
+ for det in results_one_frame['detections']:
238
+ det['frame_number'] = frame_number
826
239
 
827
- os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
240
+ # This is a no-op if there were no above-threshold detections
241
+ # in this frame
242
+ im['detections'].extend(results_one_frame['detections'])
828
243
 
829
- # Create the output video
830
- print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
831
- input_video_file_relative,video_output_file,rendering_fs,video_fs))
832
- frames_to_video(video_frame_files,
833
- rendering_fs,
834
- video_output_file,
835
- codec_spec=options.fourcc)
244
+ # ...for each frame
836
245
 
837
- # ...for each video
246
+ # ...was this a failed video?
838
247
 
839
- # Possibly clean up rendered frames
840
- _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
248
+ im['frames_processed'] = sorted(im['frames_processed'])
841
249
 
842
- # ...if we're rendering video
250
+ video_list_md_format.append(im)
843
251
 
252
+ # ...for each video
844
253
 
845
- ## (Optionally) delete the extracted frames
846
- _clean_up_extracted_frames(options, frame_output_folder, image_file_names)
254
+ run_detector_batch.write_results_to_file(
255
+ video_list_md_format,
256
+ options.output_json_file,
257
+ relative_path_base=None,
258
+ detector_file=options.model_file)
847
259
 
848
- # ...process_video_folder()
260
+ validation_options = ValidateBatchResultsOptions()
261
+ validation_options.raise_errors = True
262
+ validation_options.check_image_existence = True
263
+ validation_options.return_data = False
264
+ validation_options.relative_path_base = video_folder
265
+ validate_batch_results(options.output_json_file,options=validation_options)
266
+
267
+ # ...process_videos()
849
268
 
850
269
 
851
270
  def options_to_command(options):
@@ -860,62 +279,21 @@ def options_to_command(options):
860
279
 
861
280
  :meta private:
862
281
  """
282
+
863
283
  cmd = 'python process_video.py'
864
284
  cmd += ' "' + options.model_file + '"'
865
285
  cmd += ' "' + options.input_video_file + '"'
866
286
 
867
287
  if options.recursive:
868
288
  cmd += ' --recursive'
869
- if options.frame_folder is not None:
870
- cmd += ' --frame_folder' + ' "' + options.frame_folder + '"'
871
- if options.frame_rendering_folder is not None:
872
- cmd += ' --frame_rendering_folder' + ' "' + options.frame_rendering_folder + '"'
873
289
  if options.output_json_file is not None:
874
290
  cmd += ' --output_json_file' + ' "' + options.output_json_file + '"'
875
- if options.output_video_file is not None:
876
- cmd += ' --output_video_file' + ' "' + options.output_video_file + '"'
877
- if options.keep_extracted_frames:
878
- cmd += ' --keep_extracted_frames'
879
- if options.reuse_results_if_available:
880
- cmd += ' --reuse_results_if_available'
881
- if options.reuse_frames_if_available:
882
- cmd += ' --reuse_frames_if_available'
883
- if options.render_output_video:
884
- cmd += ' --render_output_video'
885
- if options.keep_rendered_frames:
886
- cmd += ' --keep_rendered_frames'
887
- if options.rendering_confidence_threshold is not None:
888
- cmd += ' --rendering_confidence_threshold ' + str(options.rendering_confidence_threshold)
889
291
  if options.json_confidence_threshold is not None:
890
292
  cmd += ' --json_confidence_threshold ' + str(options.json_confidence_threshold)
891
- if options.n_cores is not None:
892
- cmd += ' --n_cores ' + str(options.n_cores)
893
293
  if options.frame_sample is not None:
894
294
  cmd += ' --frame_sample ' + str(options.frame_sample)
895
- if options.frames_to_extract is not None:
896
- cmd += ' --frames_to_extract '
897
- if isinstance(options.frames_to_extract,int):
898
- frames_to_extract = [options.frames_to_extract]
899
- else:
900
- frames_to_extract = options.frames_to_extract
901
- for frame_number in frames_to_extract:
902
- cmd += ' {}'.format(frame_number)
903
- if options.debug_max_frames is not None:
904
- cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
905
- if options.class_mapping_filename is not None:
906
- cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
907
- if options.fourcc is not None:
908
- cmd += ' --fourcc ' + options.fourcc
909
- if options.quality is not None:
910
- cmd += ' --quality ' + str(options.quality)
911
- if options.max_width is not None:
912
- cmd += ' --max_width ' + str(options.max_width)
913
295
  if options.verbose:
914
296
  cmd += ' --verbose'
915
- if options.force_extracted_frame_folder_deletion:
916
- cmd += ' --force_extracted_frame_folder_deletion'
917
- if options.force_rendered_frame_folder_deletion:
918
- cmd += ' --force_rendered_frame_folder_deletion'
919
297
  if options.detector_options is not None and len(options.detector_options) > 0:
920
298
  cmd += '--detector_options {}'.format(dict_to_kvp_list(options.detector_options))
921
299
 
@@ -930,145 +308,58 @@ if False:
930
308
 
931
309
  #%% Process a folder of videos
932
310
 
311
+ import os
312
+ from megadetector.detection.process_video import \
313
+ process_videos, ProcessVideoOptions
314
+
933
315
  model_file = 'MDV5A'
934
- # input_dir = r'g:\temp\test-videos'
935
- # input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
936
- input_dir = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/video-samples')
316
+ input_dir = r"G:\temp\md-test-images\video-samples"
937
317
  assert os.path.isdir(input_dir)
938
318
 
939
- output_base = r'g:\temp\video_test'
940
- os.makedirs(output_base,exist_ok=True)
941
-
942
- frame_folder = os.path.join(output_base,'frames')
943
- rendering_folder = os.path.join(output_base,'rendered-frames')
944
- output_json_file = os.path.join(output_base,'video-test.json')
945
- output_video_folder = os.path.join(output_base,'output_videos')
946
-
319
+ output_json_file = os.path.join(input_dir,'mdv5a-video.json')
947
320
 
948
321
  print('Processing folder {}'.format(input_dir))
949
322
 
950
323
  options = ProcessVideoOptions()
324
+ options.json_confidence_threshold = 0.05
951
325
  options.model_file = model_file
952
326
  options.input_video_file = input_dir
953
- options.output_video_file = output_video_folder
954
327
  options.output_json_file = output_json_file
955
328
  options.recursive = True
956
- options.reuse_frames_if_available = False
957
- options.reuse_results_if_available = False
958
- options.quality = None # 90
959
- options.frame_sample = 10
960
- options.max_width = None # 1280
961
- options.n_cores = 4
329
+ # options.frame_sample = 10
330
+ options.time_sample = 2
962
331
  options.verbose = True
963
- options.render_output_video = False
964
- options.frame_folder = frame_folder
965
- options.frame_rendering_folder = rendering_folder
966
- options.keep_extracted_frames = False
967
- options.keep_rendered_frames = False
968
- options.force_extracted_frame_folder_deletion = False
969
- options.force_rendered_frame_folder_deletion = False
970
- options.fourcc = 'mp4v'
971
- options.force_on_disk_frame_extraction = False
972
- # options.rendering_confidence_threshold = 0.15
973
-
974
- cmd = options_to_command(options); print(cmd)
975
332
 
976
- # import clipboard; clipboard.copy(cmd)
977
- process_video_folder(options)
333
+ process_videos(options)
978
334
 
979
335
 
980
336
  #%% Process a single video
981
337
 
982
- fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
983
- assert os.path.isfile(fn)
984
- model_file = 'MDV5A'
985
- input_video_file = fn
986
-
987
- output_base = r'g:\temp\video_test'
988
- frame_folder = os.path.join(output_base,'frames')
989
- rendering_folder = os.path.join(output_base,'rendered-frames')
990
- output_json_file = os.path.join(output_base,'video-test.json')
991
- output_video_file = os.path.join(output_base,'output_video.mp4')
992
-
993
- options = ProcessVideoOptions()
994
- options.model_file = model_file
995
- options.input_video_file = input_video_file
996
- options.render_output_video = True
997
- options.output_video_file = output_video_file
998
- options.output_json_file = output_json_file
999
- options.verbose = True
1000
- options.quality = 75
1001
- options.frame_sample = 10
1002
- options.max_width = 1600
1003
- options.frame_folder = frame_folder
1004
- options.frame_rendering_folder = rendering_folder
1005
- options.keep_extracted_frames = False
1006
- options.keep_rendered_frames = False
1007
- options.force_extracted_frame_folder_deletion = True
1008
- options.force_rendered_frame_folder_deletion = True
1009
- options.fourcc = 'mp4v'
1010
- # options.rendering_confidence_threshold = 0.15
1011
-
1012
- cmd = options_to_command(options); print(cmd)
1013
-
1014
- # import clipboard; clipboard.copy(cmd)
1015
- process_video(options)
1016
-
1017
-
1018
- #%% Extract specific frames from a single video, no detection
338
+ import os
339
+ from megadetector.detection.process_video import \
340
+ process_videos, ProcessVideoOptions
341
+ from megadetector.detection.video_utils import find_videos
1019
342
 
1020
- fn = r'g:\temp\test-videos\person_and_dog\DSCF0064.AVI'
1021
- assert os.path.isfile(fn)
1022
- model_file = 'no_detection'
1023
- input_video_file = fn
1024
-
1025
- output_base = r'g:\temp\video_test'
1026
- frame_folder = os.path.join(output_base,'frames')
1027
- output_video_file = os.path.join(output_base,'output_videos.mp4')
1028
-
1029
- options = ProcessVideoOptions()
1030
- options.model_file = model_file
1031
- options.input_video_file = input_video_file
1032
- options.verbose = True
1033
- options.quality = 90
1034
- options.frame_sample = None
1035
- options.frames_to_extract = [0,100]
1036
- options.max_width = None
1037
- options.frame_folder = frame_folder
1038
- options.keep_extracted_frames = True
1039
-
1040
- cmd = options_to_command(options); print(cmd)
1041
-
1042
- # import clipboard; clipboard.copy(cmd)
1043
- process_video(options)
1044
-
1045
-
1046
- #%% Extract specific frames from a folder, no detection
343
+ model_file = 'MDV5A'
344
+ input_dir = r"G:\temp\md-test-images\video-samples"
345
+ assert os.path.isdir(input_dir)
346
+ video_fn_abs = find_videos(input_dir)[0]
1047
347
 
1048
- fn = r'g:\temp\test-videos\person_and_dog'
1049
- assert os.path.isdir(fn)
1050
- model_file = 'no_detection'
1051
- input_video_file = fn
348
+ output_json_file = os.path.join(input_dir,'mdv5a-single-video.json')
1052
349
 
1053
- output_base = r'g:\temp\video_test'
1054
- frame_folder = os.path.join(output_base,'frames')
1055
- output_video_file = os.path.join(output_base,'output_videos.mp4')
350
+ print('Processing video {}'.format(video_fn_abs))
1056
351
 
1057
352
  options = ProcessVideoOptions()
353
+ options.json_confidence_threshold = 0.05
1058
354
  options.model_file = model_file
1059
- options.input_video_file = input_video_file
355
+ options.input_video_file = video_fn_abs
356
+ options.output_json_file = output_json_file
357
+ options.recursive = True
358
+ # options.frame_sample = 10
359
+ options.time_sample = 2
1060
360
  options.verbose = True
1061
- options.quality = 90
1062
- options.frame_sample = None
1063
- options.frames_to_extract = [0,100]
1064
- options.max_width = None
1065
- options.frame_folder = frame_folder
1066
- options.keep_extracted_frames = True
1067
-
1068
- cmd = options_to_command(options); print(cmd)
1069
361
 
1070
- # import clipboard; clipboard.copy(cmd)
1071
- process_video(options)
362
+ process_videos(options)
1072
363
 
1073
364
 
1074
365
  #%% Command-line driver
@@ -1092,111 +383,23 @@ def main(): # noqa
1092
383
  help='recurse into [input_video_file]; only meaningful if a folder '\
1093
384
  'is specified as input')
1094
385
 
1095
- parser.add_argument('--frame_folder', type=str, default=None,
1096
- help='folder to use for intermediate frame storage, defaults to a folder '\
1097
- 'in the system temporary folder')
1098
-
1099
- parser.add_argument('--frame_rendering_folder', type=str, default=None,
1100
- help='folder to use for rendered frame storage, defaults to a folder in '\
1101
- 'the system temporary folder')
1102
-
1103
386
  parser.add_argument('--output_json_file', type=str,
1104
387
  default=None, help='.json output file, defaults to [video file].json')
1105
388
 
1106
- parser.add_argument('--output_video_file', type=str,
1107
- default=None, help='video output file (or folder), defaults to '\
1108
- '[video file].mp4 for files, or [video file]_annotated for folders')
1109
-
1110
- parser.add_argument('--keep_extracted_frames',
1111
- action='store_true', help='Disable the deletion of extracted frames')
1112
-
1113
- parser.add_argument('--reuse_frames_if_available',
1114
- action='store_true',
1115
- help="Don't extract frames that are already available in the frame extraction folder")
1116
-
1117
- parser.add_argument('--reuse_results_if_available',
1118
- action='store_true',
1119
- help='If the output .json files exists, and this flag is set,'\
1120
- 'we\'ll skip running MegaDetector')
1121
-
1122
- parser.add_argument('--render_output_video', action='store_true',
1123
- help='enable video output rendering (not rendered by default)')
1124
-
1125
- parser.add_argument('--fourcc', default=default_fourcc,
1126
- help=f'fourcc code to use for video encoding (default {default_fourcc}), ' + \
1127
- 'only used if render_output_video is True')
1128
-
1129
- parser.add_argument('--keep_rendered_frames',
1130
- action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
1131
-
1132
- parser.add_argument('--force_extracted_frame_folder_deletion',
1133
- action='store_true', help='By default, when keep_extracted_frames is False, we '\
1134
- 'delete the frames, but leave the (probably-empty) folder in place. This option '\
1135
- 'forces deletion of the folder as well. Use at your own risk; does not check '\
1136
- 'whether other files were present in the folder.')
1137
-
1138
- parser.add_argument('--force_rendered_frame_folder_deletion',
1139
- action='store_true', help='By default, when keep_rendered_frames is False, we '\
1140
- 'delete the frames, but leave the (probably-empty) folder in place. This option '\
1141
- 'forces deletion of the folder as well. Use at your own risk; does not check '\
1142
- 'whether other files were present in the folder.')
1143
-
1144
- parser.add_argument('--rendering_confidence_threshold', type=float,
1145
- default=None,
1146
- help="don't render boxes with confidence below this threshold " + \
1147
- "(defaults to choosing based on the MD version)")
1148
-
1149
- parser.add_argument('--rendering_fs', type=float,
1150
- default=None,
1151
- help='force a specific frame rate for output videos (only relevant when using '\
1152
- '--render_output_video) (defaults to the original frame rate)')
1153
-
1154
389
  parser.add_argument('--json_confidence_threshold', type=float,
1155
390
  default=default_options.json_confidence_threshold,
1156
391
  help="don't include boxes in the .json file with confidence "\
1157
392
  'below this threshold (default {})'.format(
1158
393
  default_options.json_confidence_threshold))
1159
394
 
1160
- parser.add_argument('--n_cores', type=int,
1161
- default=default_options.n_cores,
1162
- help='Number of cores to use for frame separation and detection. '\
1163
- 'If using a GPU, this option will be respected for frame separation but '\
1164
- 'ignored for detection. Only relevant to frame separation when processing '\
1165
- 'a folder. Default {}.'.format(default_options.n_cores))
1166
-
1167
395
  parser.add_argument('--frame_sample', type=int,
1168
396
  default=None, help='process every Nth frame (defaults to every frame), mutually exclusive '\
1169
- 'with --frames_to_extract and --time_sample.')
1170
-
1171
- parser.add_argument('--frames_to_extract', nargs='+', type=int,
1172
- default=None, help='extract specific frames (one or more ints), mutually exclusive '\
1173
- 'with --frame_sample and --time_sample.')
397
+ 'with --time_sample.')
1174
398
 
1175
399
  parser.add_argument('--time_sample', type=float,
1176
400
  default=None, help='process frames every N seconds; this is converted to a '\
1177
401
  'frame sampling rate, so it may not be exactly the requested interval in seconds. '\
1178
- 'mutually exclusive with --frame_sample and --frames_to_extract.')
1179
-
1180
- parser.add_argument('--quality', type=int,
1181
- default=default_options.quality,
1182
- help=f'JPEG quality for extracted frames (defaults to {default_options.quality}), ' + \
1183
- 'use -1 to force no quality setting')
1184
-
1185
- parser.add_argument('--max_width', type=int,
1186
- default=default_options.max_width,
1187
- help='Resize frames larger than this before writing (defaults to {})'.format(
1188
- default_options.max_width))
1189
-
1190
- parser.add_argument('--debug_max_frames', type=int,
1191
- default=-1, help='Trim to N frames for debugging (impacts model execution, '\
1192
- 'not frame rendering)')
1193
-
1194
- parser.add_argument('--class_mapping_filename',
1195
- type=str,
1196
- default=None, help='Use a non-default class mapping, supplied in a .json file '\
1197
- 'with a dictionary mapping int-strings to strings. This will also disable '\
1198
- 'the addition of "1" to all category IDs, so your class mapping should start '\
1199
- 'at zero.')
402
+ 'mutually exclusive with --frame_sample')
1200
403
 
1201
404
  parser.add_argument('--verbose', action='store_true',
1202
405
  help='Enable additional debug output')
@@ -1211,12 +414,6 @@ def main(): # noqa
1211
414
  action='store_true',
1212
415
  help='Enable image augmentation')
1213
416
 
1214
- parser.add_argument('--include_all_processed_frames',
1215
- action='store_true',
1216
- help='When processing a folder of videos, this flag indicates that the output '\
1217
- 'should include results for every frame that was processed, rather than just '\
1218
- 'one representative frame for each detection category per video.')
1219
-
1220
417
  parser.add_argument('--allow_empty_videos',
1221
418
  action='store_true',
1222
419
  help='By default, videos with no retrievable frames cause an error, this makes it a warning')
@@ -1228,6 +425,28 @@ def main(): # noqa
1228
425
  default='',
1229
426
  help='Detector-specific options, as a space-separated list of key-value pairs')
1230
427
 
428
+ parser.add_argument(
429
+ '--checkpoint_frequency',
430
+ type=int,
431
+ default=default_options.checkpoint_frequency,
432
+ help='Write a checkpoint file (to resume processing later) every N videos; ' + \
433
+ 'set to -1 to disable checkpointing (default {})'.format(
434
+ default_options.checkpoint_frequency))
435
+
436
+ parser.add_argument(
437
+ '--checkpoint_path',
438
+ type=str,
439
+ default=None,
440
+ help='Path to checkpoint file; defaults to a file in the same directory ' + \
441
+ 'as the output file')
442
+
443
+ parser.add_argument(
444
+ '--resume_from_checkpoint',
445
+ type=str,
446
+ default=None,
447
+ help='Resume from a specific checkpoint file, or "auto" to resume from the ' + \
448
+ 'most recent checkpoint in the output directory')
449
+
1231
450
  if len(sys.argv[1:]) == 0:
1232
451
  parser.print_help()
1233
452
  parser.exit()
@@ -1239,13 +458,13 @@ def main(): # noqa
1239
458
  options.detector_options = parse_kvp_list(args.detector_options)
1240
459
 
1241
460
  if os.path.isdir(options.input_video_file):
1242
- process_video_folder(options)
461
+ process_videos(options)
1243
462
  else:
1244
463
  assert os.path.isfile(options.input_video_file), \
1245
464
  '{} is not a valid file or folder name'.format(options.input_video_file)
1246
465
  assert not options.recursive, \
1247
466
  '--recursive is only meaningful when processing a folder'
1248
- process_video(options)
467
+ process_videos(options)
1249
468
 
1250
469
  if __name__ == '__main__':
1251
470
  main()