megadetector 5.0.15__py3-none-any.whl → 5.0.16__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.
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +387 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +3 -3
- megadetector/data_management/lila/test_lila_metadata_urls.py +2 -2
- megadetector/data_management/remove_exif.py +61 -36
- megadetector/data_management/yolo_to_coco.py +25 -6
- megadetector/detection/process_video.py +259 -126
- megadetector/detection/pytorch_detector.py +13 -11
- megadetector/detection/run_detector.py +9 -2
- megadetector/detection/run_detector_batch.py +7 -0
- megadetector/detection/run_inference_with_yolov5_val.py +58 -10
- megadetector/detection/tf_detector.py +8 -2
- megadetector/detection/video_utils.py +201 -16
- megadetector/postprocessing/md_to_coco.py +31 -9
- megadetector/postprocessing/postprocess_batch_results.py +19 -3
- megadetector/postprocessing/subset_json_detector_output.py +22 -12
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +3 -3
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +2 -1
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +1 -1
- megadetector/taxonomy_mapping/simple_image_download.py +5 -0
- megadetector/taxonomy_mapping/species_lookup.py +1 -1
- megadetector/utils/md_tests.py +196 -49
- megadetector/utils/path_utils.py +2 -2
- megadetector/utils/url_utils.py +7 -1
- megadetector/visualization/visualize_db.py +16 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/LICENSE +0 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/METADATA +2 -2
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/RECORD +29 -28
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/WHEEL +1 -1
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/top_level.txt +0 -0
|
@@ -32,11 +32,14 @@ from megadetector.visualization import visualize_detector_output
|
|
|
32
32
|
from megadetector.utils.ct_utils import args_to_object
|
|
33
33
|
from megadetector.utils.path_utils import insert_before_extension, clean_path
|
|
34
34
|
from megadetector.detection.video_utils import video_to_frames
|
|
35
|
+
from megadetector.detection.video_utils import run_callback_on_frames
|
|
36
|
+
from megadetector.detection.video_utils import run_callback_on_frames_for_folder
|
|
35
37
|
from megadetector.detection.video_utils import frames_to_video
|
|
36
38
|
from megadetector.detection.video_utils import frame_results_to_video_results
|
|
37
39
|
from megadetector.detection.video_utils import _add_frame_numbers_to_results
|
|
38
40
|
from megadetector.detection.video_utils import video_folder_to_frames
|
|
39
41
|
from megadetector.detection.video_utils import default_fourcc
|
|
42
|
+
from megadetector.detection.run_detector import load_detector
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
#%% Classes
|
|
@@ -76,7 +79,8 @@ class ProcessVideoOptions:
|
|
|
76
79
|
|
|
77
80
|
#: Should we render a video with detection boxes?
|
|
78
81
|
#:
|
|
79
|
-
#:
|
|
82
|
+
#: If processing a folder, this renders each input video to a separate
|
|
83
|
+
#: video with detection boxes.
|
|
80
84
|
self.render_output_video = False
|
|
81
85
|
|
|
82
86
|
#: If we are rendering boxes to a new video, should we keep the temporary
|
|
@@ -142,6 +146,10 @@ class ProcessVideoOptions:
|
|
|
142
146
|
#: For debugging only, stop processing after a certain number of frames.
|
|
143
147
|
self.debug_max_frames = -1
|
|
144
148
|
|
|
149
|
+
#: For debugging only, force on-disk frame extraction, even if it wouldn't otherwise be
|
|
150
|
+
#: necessary
|
|
151
|
+
self.force_on_disk_frame_extraction = False
|
|
152
|
+
|
|
145
153
|
#: File containing non-standard categories, typically only used if you're running a non-MD
|
|
146
154
|
#: detector.
|
|
147
155
|
self.class_mapping_filename = None
|
|
@@ -158,6 +166,11 @@ class ProcessVideoOptions:
|
|
|
158
166
|
|
|
159
167
|
#: Enable image augmentation
|
|
160
168
|
self.augment = False
|
|
169
|
+
|
|
170
|
+
#: By default, a video with no frames (or no frames retrievable with the current parameters)
|
|
171
|
+
#: is an error, this makes it a warning. This would apply if you request, e.g., the 100th
|
|
172
|
+
#: frame from each video, but a video only has 50 frames.
|
|
173
|
+
self.allow_empty_videos = False
|
|
161
174
|
|
|
162
175
|
# ...class ProcessVideoOptions
|
|
163
176
|
|
|
@@ -206,6 +219,9 @@ def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_fil
|
|
|
206
219
|
If necessary, delete rendered frames and/or the entire rendering output folder.
|
|
207
220
|
"""
|
|
208
221
|
|
|
222
|
+
if rendering_output_folder is None:
|
|
223
|
+
return
|
|
224
|
+
|
|
209
225
|
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
210
226
|
|
|
211
227
|
# (Optionally) delete the temporary directory we used for rendered detection images
|
|
@@ -254,6 +270,9 @@ def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
|
254
270
|
If necessary, delete extracted frames and/or the entire temporary frame folder.
|
|
255
271
|
"""
|
|
256
272
|
|
|
273
|
+
if frame_output_folder is None:
|
|
274
|
+
return
|
|
275
|
+
|
|
257
276
|
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
258
277
|
|
|
259
278
|
if not options.keep_extracted_frames:
|
|
@@ -274,6 +293,9 @@ def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
|
274
293
|
# ...otherwise just delete the frames, but leave the folder in place
|
|
275
294
|
else:
|
|
276
295
|
|
|
296
|
+
if frame_filenames is None:
|
|
297
|
+
return
|
|
298
|
+
|
|
277
299
|
if options.force_extracted_frame_folder_deletion:
|
|
278
300
|
assert caller_provided_frame_output_folder
|
|
279
301
|
print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
|
|
@@ -322,66 +344,117 @@ def process_video(options):
|
|
|
322
344
|
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
323
345
|
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
324
346
|
|
|
325
|
-
|
|
326
|
-
# case we need them.
|
|
327
|
-
temporary_folder_info = _select_temporary_output_folders(options)
|
|
347
|
+
frame_output_folder = None
|
|
328
348
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
333
|
-
|
|
334
|
-
os.makedirs(frame_output_folder, exist_ok=True)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
## Extract frames
|
|
338
|
-
|
|
339
|
-
frame_filenames, Fs = video_to_frames(
|
|
340
|
-
options.input_video_file,
|
|
341
|
-
frame_output_folder,
|
|
342
|
-
every_n_frames=options.frame_sample,
|
|
343
|
-
overwrite=(not options.reuse_frames_if_available),
|
|
344
|
-
quality=options.quality,
|
|
345
|
-
max_width=options.max_width,
|
|
346
|
-
verbose=options.verbose,
|
|
347
|
-
frames_to_extract=options.frames_to_extract)
|
|
348
|
-
|
|
349
|
-
image_file_names = frame_filenames
|
|
350
|
-
if options.debug_max_frames > 0:
|
|
351
|
-
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
352
|
-
|
|
353
|
-
if options.model_file == 'no_detection':
|
|
354
|
-
assert options.keep_extracted_frames, \
|
|
355
|
-
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
## Run MegaDetector
|
|
360
|
-
|
|
361
|
-
if options.reuse_results_if_available and \
|
|
362
|
-
os.path.isfile(options.output_json_file):
|
|
349
|
+
# If we should re-use existing results, and the output file exists, don't bother running MD
|
|
350
|
+
if (options.reuse_results_if_available and os.path.isfile(options.output_json_file)):
|
|
351
|
+
|
|
363
352
|
print('Loading results from {}'.format(options.output_json_file))
|
|
364
353
|
with open(options.output_json_file,'r') as f:
|
|
365
354
|
results = json.load(f)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
355
|
+
|
|
356
|
+
# Run MD in memory if we don't need to generate frames
|
|
357
|
+
#
|
|
358
|
+
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
359
|
+
elif (not options.keep_extracted_frames and \
|
|
360
|
+
not options.render_output_video and \
|
|
361
|
+
not options.force_on_disk_frame_extraction):
|
|
362
|
+
|
|
363
|
+
# Run MegaDetector in memory
|
|
364
|
+
|
|
365
|
+
if options.verbose:
|
|
366
|
+
print('Running MegaDetector in memory for {}'.format(options.input_video_file))
|
|
367
|
+
|
|
368
|
+
if options.frame_folder is not None:
|
|
369
|
+
print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
|
|
370
|
+
'not; no raw frames will be written')
|
|
371
|
+
|
|
372
|
+
detector = load_detector(options.model_file)
|
|
373
|
+
|
|
374
|
+
def frame_callback(image_np,image_id):
|
|
375
|
+
return detector.generate_detections_one_image(image_np,
|
|
376
|
+
image_id,
|
|
377
|
+
detection_threshold=options.json_confidence_threshold,
|
|
378
|
+
augment=options.augment)
|
|
379
|
+
|
|
380
|
+
frame_results = run_callback_on_frames(options.input_video_file,
|
|
381
|
+
frame_callback,
|
|
382
|
+
every_n_frames=options.frame_sample,
|
|
383
|
+
verbose=options.verbose,
|
|
384
|
+
frames_to_process=options.frames_to_extract)
|
|
385
|
+
|
|
386
|
+
_add_frame_numbers_to_results(frame_results['results'])
|
|
378
387
|
|
|
379
388
|
run_detector_batch.write_results_to_file(
|
|
380
|
-
results,
|
|
381
|
-
|
|
389
|
+
frame_results['results'],
|
|
390
|
+
options.output_json_file,
|
|
391
|
+
relative_path_base=None,
|
|
382
392
|
detector_file=options.model_file,
|
|
383
|
-
custom_metadata={'video_frame_rate':
|
|
393
|
+
custom_metadata={'video_frame_rate':frame_results['frame_rate']})
|
|
384
394
|
|
|
395
|
+
# Extract frames and optionally run MegaDetector on those frames
|
|
396
|
+
else:
|
|
397
|
+
|
|
398
|
+
if options.verbose:
|
|
399
|
+
print('Extracting frames for {}'.format(options.input_video_file))
|
|
400
|
+
|
|
401
|
+
# This does not create any folders, just defines temporary folder names in
|
|
402
|
+
# case we need them.
|
|
403
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
404
|
+
|
|
405
|
+
if (caller_provided_frame_output_folder):
|
|
406
|
+
frame_output_folder = options.frame_folder
|
|
407
|
+
else:
|
|
408
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
409
|
+
|
|
410
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
## Extract frames
|
|
414
|
+
|
|
415
|
+
frame_filenames, Fs = video_to_frames(
|
|
416
|
+
options.input_video_file,
|
|
417
|
+
frame_output_folder,
|
|
418
|
+
every_n_frames=options.frame_sample,
|
|
419
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
420
|
+
quality=options.quality,
|
|
421
|
+
max_width=options.max_width,
|
|
422
|
+
verbose=options.verbose,
|
|
423
|
+
frames_to_extract=options.frames_to_extract,
|
|
424
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
425
|
+
|
|
426
|
+
image_file_names = frame_filenames
|
|
427
|
+
if options.debug_max_frames > 0:
|
|
428
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
429
|
+
|
|
430
|
+
## Run MegaDetector on those frames
|
|
431
|
+
|
|
432
|
+
if options.model_file != 'no_detection':
|
|
433
|
+
|
|
434
|
+
if options.verbose:
|
|
435
|
+
print('Running MD for {}'.format(options.input_video_file))
|
|
436
|
+
|
|
437
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
438
|
+
options.model_file,
|
|
439
|
+
image_file_names,
|
|
440
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
441
|
+
n_cores=options.n_cores,
|
|
442
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
443
|
+
quiet=True,
|
|
444
|
+
augment=options.augment,
|
|
445
|
+
image_size=options.image_size)
|
|
446
|
+
|
|
447
|
+
_add_frame_numbers_to_results(results)
|
|
448
|
+
|
|
449
|
+
run_detector_batch.write_results_to_file(
|
|
450
|
+
results,
|
|
451
|
+
options.output_json_file,
|
|
452
|
+
relative_path_base=frame_output_folder,
|
|
453
|
+
detector_file=options.model_file,
|
|
454
|
+
custom_metadata={'video_frame_rate':Fs})
|
|
455
|
+
|
|
456
|
+
# ...if we are/aren't keeping raw frames on disk
|
|
457
|
+
|
|
385
458
|
|
|
386
459
|
## (Optionally) render output video
|
|
387
460
|
|
|
@@ -470,76 +543,127 @@ def process_video_folder(options):
|
|
|
470
543
|
# case we need them.
|
|
471
544
|
temporary_folder_info = _select_temporary_output_folders(options)
|
|
472
545
|
|
|
546
|
+
frame_output_folder = None
|
|
547
|
+
image_file_names = None
|
|
473
548
|
|
|
474
|
-
|
|
549
|
+
# Run MD in memory if we don't need to generate frames
|
|
550
|
+
#
|
|
551
|
+
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
552
|
+
if (not options.keep_extracted_frames and \
|
|
553
|
+
not options.render_output_video and \
|
|
554
|
+
not options.force_on_disk_frame_extraction):
|
|
555
|
+
|
|
556
|
+
if options.verbose:
|
|
557
|
+
print('Running MegaDetector in memory for folder {}'.format(options.input_video_file))
|
|
558
|
+
|
|
559
|
+
if options.frame_folder is not None:
|
|
560
|
+
print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
|
|
561
|
+
'not; no raw frames will be written')
|
|
562
|
+
|
|
563
|
+
detector = load_detector(options.model_file)
|
|
564
|
+
|
|
565
|
+
def frame_callback(image_np,image_id):
|
|
566
|
+
return detector.generate_detections_one_image(image_np,
|
|
567
|
+
image_id,
|
|
568
|
+
detection_threshold=options.json_confidence_threshold,
|
|
569
|
+
augment=options.augment)
|
|
570
|
+
|
|
571
|
+
md_results = run_callback_on_frames_for_folder(input_video_folder=options.input_video_file,
|
|
572
|
+
frame_callback=frame_callback,
|
|
573
|
+
every_n_frames=options.frame_sample,
|
|
574
|
+
verbose=options.verbose)
|
|
575
|
+
|
|
576
|
+
video_results = md_results['results']
|
|
577
|
+
|
|
578
|
+
all_frame_results = []
|
|
579
|
+
|
|
580
|
+
# r = video_results[0]
|
|
581
|
+
for frame_results in video_results:
|
|
582
|
+
_add_frame_numbers_to_results(frame_results)
|
|
583
|
+
all_frame_results.extend(frame_results)
|
|
584
|
+
|
|
585
|
+
run_detector_batch.write_results_to_file(
|
|
586
|
+
all_frame_results,
|
|
587
|
+
frames_json,
|
|
588
|
+
relative_path_base=None,
|
|
589
|
+
detector_file=options.model_file,
|
|
590
|
+
custom_metadata={'video_frame_rate':md_results['frame_rates']})
|
|
475
591
|
|
|
476
|
-
if caller_provided_frame_output_folder:
|
|
477
|
-
frame_output_folder = options.frame_folder
|
|
478
592
|
else:
|
|
479
|
-
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
480
|
-
|
|
481
|
-
os.makedirs(frame_output_folder, exist_ok=True)
|
|
482
|
-
|
|
483
|
-
print('Extracting frames')
|
|
484
|
-
|
|
485
|
-
frame_filenames, Fs, video_filenames = \
|
|
486
|
-
video_folder_to_frames(input_folder=options.input_video_file,
|
|
487
|
-
output_folder_base=frame_output_folder,
|
|
488
|
-
recursive=options.recursive,
|
|
489
|
-
overwrite=(not options.reuse_frames_if_available),
|
|
490
|
-
n_threads=options.n_cores,
|
|
491
|
-
every_n_frames=options.frame_sample,
|
|
492
|
-
verbose=options.verbose,
|
|
493
|
-
quality=options.quality,
|
|
494
|
-
max_width=options.max_width,
|
|
495
|
-
frames_to_extract=options.frames_to_extract)
|
|
496
|
-
|
|
497
|
-
print('Extracted frames for {} videos'.format(len(set(video_filenames))))
|
|
498
|
-
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
499
|
-
|
|
500
|
-
if len(image_file_names) == 0:
|
|
501
|
-
if len(video_filenames) == 0:
|
|
502
|
-
print('No videos found in folder {}'.format(options.input_video_file))
|
|
503
|
-
else:
|
|
504
|
-
print('No frames extracted from folder {}, this may be due to an '\
|
|
505
|
-
'unsupported video codec'.format(options.input_video_file))
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
509
|
-
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
510
593
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
594
|
+
## Split every video into frames
|
|
595
|
+
|
|
596
|
+
if caller_provided_frame_output_folder:
|
|
597
|
+
frame_output_folder = options.frame_folder
|
|
598
|
+
else:
|
|
599
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
600
|
+
|
|
601
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
518
602
|
|
|
519
|
-
|
|
520
|
-
os.path.isfile(frames_json):
|
|
521
|
-
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
522
|
-
results = None
|
|
523
|
-
else:
|
|
524
|
-
print('Running MegaDetector')
|
|
525
|
-
results = run_detector_batch.load_and_run_detector_batch(
|
|
526
|
-
options.model_file,
|
|
527
|
-
image_file_names,
|
|
528
|
-
confidence_threshold=options.json_confidence_threshold,
|
|
529
|
-
n_cores=options.n_cores,
|
|
530
|
-
class_mapping_filename=options.class_mapping_filename,
|
|
531
|
-
quiet=True,
|
|
532
|
-
augment=options.augment,
|
|
533
|
-
image_size=options.image_size)
|
|
534
|
-
|
|
535
|
-
_add_frame_numbers_to_results(results)
|
|
603
|
+
print('Extracting frames')
|
|
536
604
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
605
|
+
frame_filenames, Fs, video_filenames = \
|
|
606
|
+
video_folder_to_frames(input_folder=options.input_video_file,
|
|
607
|
+
output_folder_base=frame_output_folder,
|
|
608
|
+
recursive=options.recursive,
|
|
609
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
610
|
+
n_threads=options.n_cores,
|
|
611
|
+
every_n_frames=options.frame_sample,
|
|
612
|
+
verbose=options.verbose,
|
|
613
|
+
quality=options.quality,
|
|
614
|
+
max_width=options.max_width,
|
|
615
|
+
frames_to_extract=options.frames_to_extract,
|
|
616
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
617
|
+
|
|
618
|
+
print('Extracted frames for {} videos'.format(len(set(video_filenames))))
|
|
619
|
+
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
620
|
+
|
|
621
|
+
if len(image_file_names) == 0:
|
|
622
|
+
if len(video_filenames) == 0:
|
|
623
|
+
print('No videos found in folder {}'.format(options.input_video_file))
|
|
624
|
+
else:
|
|
625
|
+
print('No frames extracted from folder {}, this may be due to an '\
|
|
626
|
+
'unsupported video codec'.format(options.input_video_file))
|
|
627
|
+
return
|
|
542
628
|
|
|
629
|
+
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
630
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
631
|
+
|
|
632
|
+
if options.model_file == 'no_detection':
|
|
633
|
+
assert options.keep_extracted_frames, \
|
|
634
|
+
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
## Run MegaDetector on the extracted frames
|
|
639
|
+
|
|
640
|
+
if options.reuse_results_if_available and \
|
|
641
|
+
os.path.isfile(frames_json):
|
|
642
|
+
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
643
|
+
with open(frames_json,'r') as f:
|
|
644
|
+
results = json.load(f)
|
|
645
|
+
else:
|
|
646
|
+
print('Running MegaDetector')
|
|
647
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
648
|
+
options.model_file,
|
|
649
|
+
image_file_names,
|
|
650
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
651
|
+
n_cores=options.n_cores,
|
|
652
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
653
|
+
quiet=True,
|
|
654
|
+
augment=options.augment,
|
|
655
|
+
image_size=options.image_size)
|
|
656
|
+
|
|
657
|
+
_add_frame_numbers_to_results(results)
|
|
658
|
+
|
|
659
|
+
run_detector_batch.write_results_to_file(
|
|
660
|
+
results,
|
|
661
|
+
frames_json,
|
|
662
|
+
relative_path_base=frame_output_folder,
|
|
663
|
+
detector_file=options.model_file,
|
|
664
|
+
custom_metadata={'video_frame_rate':Fs})
|
|
665
|
+
|
|
666
|
+
# ...if we're running MD on in-memory frames vs. extracting frames to disk
|
|
543
667
|
|
|
544
668
|
## Convert frame-level results to video-level results
|
|
545
669
|
|
|
@@ -646,13 +770,13 @@ def process_video_folder(options):
|
|
|
646
770
|
|
|
647
771
|
def options_to_command(options):
|
|
648
772
|
"""
|
|
649
|
-
Convert a ProcessVideoOptions
|
|
773
|
+
Convert a ProcessVideoOptions object to a corresponding command line.
|
|
650
774
|
|
|
651
775
|
Args:
|
|
652
776
|
options (ProcessVideoOptions): the options set to render as a command line
|
|
653
777
|
|
|
654
778
|
Returns:
|
|
655
|
-
str: the command line
|
|
779
|
+
str: the command line corresponding to [options]
|
|
656
780
|
|
|
657
781
|
:meta private:
|
|
658
782
|
"""
|
|
@@ -725,8 +849,8 @@ if False:
|
|
|
725
849
|
#%% Process a folder of videos
|
|
726
850
|
|
|
727
851
|
model_file = 'MDV5A'
|
|
728
|
-
|
|
729
|
-
input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
852
|
+
input_dir = r'g:\temp\test-videos'
|
|
853
|
+
# input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
730
854
|
output_base = r'g:\temp\video_test'
|
|
731
855
|
frame_folder = os.path.join(output_base,'frames')
|
|
732
856
|
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
@@ -744,25 +868,26 @@ if False:
|
|
|
744
868
|
options.recursive = True
|
|
745
869
|
options.reuse_frames_if_available = False
|
|
746
870
|
options.reuse_results_if_available = False
|
|
747
|
-
options.quality = 90
|
|
871
|
+
options.quality = None # 90
|
|
748
872
|
options.frame_sample = 10
|
|
749
|
-
options.max_width = 1280
|
|
873
|
+
options.max_width = None # 1280
|
|
750
874
|
options.n_cores = 4
|
|
751
875
|
options.verbose = True
|
|
752
|
-
options.render_output_video =
|
|
876
|
+
options.render_output_video = False
|
|
753
877
|
options.frame_folder = frame_folder
|
|
754
878
|
options.frame_rendering_folder = rendering_folder
|
|
755
|
-
options.keep_extracted_frames =
|
|
756
|
-
options.keep_rendered_frames =
|
|
879
|
+
options.keep_extracted_frames = False
|
|
880
|
+
options.keep_rendered_frames = False
|
|
757
881
|
options.force_extracted_frame_folder_deletion = False
|
|
758
882
|
options.force_rendered_frame_folder_deletion = False
|
|
759
883
|
options.fourcc = 'mp4v'
|
|
884
|
+
options.force_on_disk_frame_extraction = True
|
|
760
885
|
# options.rendering_confidence_threshold = 0.15
|
|
761
886
|
|
|
762
887
|
cmd = options_to_command(options); print(cmd)
|
|
763
888
|
|
|
764
889
|
# import clipboard; clipboard.copy(cmd)
|
|
765
|
-
|
|
890
|
+
process_video_folder(options)
|
|
766
891
|
|
|
767
892
|
|
|
768
893
|
#%% Process a single video
|
|
@@ -988,6 +1113,10 @@ def main():
|
|
|
988
1113
|
parser.add_argument('--augment',
|
|
989
1114
|
action='store_true',
|
|
990
1115
|
help='Enable image augmentation')
|
|
1116
|
+
|
|
1117
|
+
parser.add_argument('--allow_empty_videos',
|
|
1118
|
+
action='store_true',
|
|
1119
|
+
help='By default, videos with no retrievable frames cause an error, this makes it a warning')
|
|
991
1120
|
|
|
992
1121
|
if len(sys.argv[1:]) == 0:
|
|
993
1122
|
parser.print_help()
|
|
@@ -1000,6 +1129,10 @@ def main():
|
|
|
1000
1129
|
if os.path.isdir(options.input_video_file):
|
|
1001
1130
|
process_video_folder(options)
|
|
1002
1131
|
else:
|
|
1132
|
+
assert os.path.isfile(options.input_video_file), \
|
|
1133
|
+
'{} is not a valid file or folder name'.format(options.input_video_file)
|
|
1134
|
+
assert not options.recursive, \
|
|
1135
|
+
'--recursive is only meaningful when processing a folder'
|
|
1003
1136
|
process_video(options)
|
|
1004
1137
|
|
|
1005
1138
|
if __name__ == '__main__':
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
pytorch_detector.py
|
|
4
4
|
|
|
5
|
-
Module to run MegaDetector v5
|
|
5
|
+
Module to run MegaDetector v5.
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -131,7 +131,7 @@ class PTDetector:
|
|
|
131
131
|
try:
|
|
132
132
|
self.model = PTDetector._load_model(model_path, self.device)
|
|
133
133
|
except Exception as e:
|
|
134
|
-
# In a very
|
|
134
|
+
# In a very esoteric scenario where an old version of YOLOv5 is used to run
|
|
135
135
|
# newer models, we run into an issue because the "Model" class became
|
|
136
136
|
# "DetectionModel". New YOLOv5 code handles this case by just setting them
|
|
137
137
|
# to be the same, so doing that via monkey-patch doesn't seem *that* rude.
|
|
@@ -180,7 +180,8 @@ class PTDetector:
|
|
|
180
180
|
|
|
181
181
|
return model
|
|
182
182
|
|
|
183
|
-
def generate_detections_one_image(self,
|
|
183
|
+
def generate_detections_one_image(self,
|
|
184
|
+
img_original,
|
|
184
185
|
image_id='unknown',
|
|
185
186
|
detection_threshold=0.00001,
|
|
186
187
|
image_size=None,
|
|
@@ -190,7 +191,8 @@ class PTDetector:
|
|
|
190
191
|
Applies the detector to an image.
|
|
191
192
|
|
|
192
193
|
Args:
|
|
193
|
-
img_original (Image): the PIL Image object
|
|
194
|
+
img_original (Image): the PIL Image object (or numpy array) on which we should run the
|
|
195
|
+
detector, with EXIF rotation already handled.
|
|
194
196
|
image_id (str, optional): a path to identify the image; will be in the "file" field
|
|
195
197
|
of the output object
|
|
196
198
|
detection_threshold (float, optional): only detections above this confidence threshold
|
|
@@ -209,20 +211,20 @@ class PTDetector:
|
|
|
209
211
|
- 'failure' (a failure string, or None if everything went fine)
|
|
210
212
|
"""
|
|
211
213
|
|
|
212
|
-
result = {
|
|
213
|
-
'file': image_id
|
|
214
|
-
}
|
|
214
|
+
result = {'file': image_id }
|
|
215
215
|
detections = []
|
|
216
216
|
max_conf = 0.0
|
|
217
217
|
|
|
218
218
|
if detection_threshold is None:
|
|
219
|
+
|
|
219
220
|
detection_threshold = 0
|
|
220
221
|
|
|
221
222
|
try:
|
|
222
223
|
|
|
223
|
-
|
|
224
|
+
if not isinstance(img_original,np.ndarray):
|
|
225
|
+
img_original = np.asarray(img_original)
|
|
224
226
|
|
|
225
|
-
#
|
|
227
|
+
# Padded resize
|
|
226
228
|
target_size = PTDetector.IMAGE_SIZE
|
|
227
229
|
|
|
228
230
|
# Image size can be an int (which translates to a square target size) or (h,w)
|
|
@@ -267,9 +269,9 @@ class PTDetector:
|
|
|
267
269
|
|
|
268
270
|
# NMS
|
|
269
271
|
if self.device == 'mps':
|
|
270
|
-
# As of
|
|
272
|
+
# As of PyTorch 1.13.0.dev20220824, nms is not implemented for MPS.
|
|
271
273
|
#
|
|
272
|
-
# Send
|
|
274
|
+
# Send predictions back to the CPU for NMS.
|
|
273
275
|
pred = non_max_suppression(prediction=pred.cpu(), conf_thres=detection_threshold)
|
|
274
276
|
else:
|
|
275
277
|
pred = non_max_suppression(prediction=pred, conf_thres=detection_threshold)
|
|
@@ -114,6 +114,8 @@ DETECTION_FILENAME_INSERT = '_detections'
|
|
|
114
114
|
# automatic model download to the system temp folder, or they will use the paths specified in the
|
|
115
115
|
# $MDV4, $MDV5A, or $MDV5B environment variables if they exist.
|
|
116
116
|
downloadable_models = {
|
|
117
|
+
'MDV2':'https://lila.science/public/models/megadetector/megadetector_v2.pb',
|
|
118
|
+
'MDV3':'https://lila.science/public/models/megadetector/megadetector_v3.pb',
|
|
117
119
|
'MDV4':'https://github.com/agentmorris/MegaDetector/releases/download/v4.1/md_v4.1.0.pb',
|
|
118
120
|
'MDV5A':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5a.0.0.pt',
|
|
119
121
|
'MDV5B':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5b.0.0.pt'
|
|
@@ -197,7 +199,7 @@ def get_detector_metadata_from_version_string(detector_version):
|
|
|
197
199
|
return DETECTOR_METADATA[detector_version]
|
|
198
200
|
|
|
199
201
|
|
|
200
|
-
def get_detector_version_from_filename(detector_filename):
|
|
202
|
+
def get_detector_version_from_filename(detector_filename,accept_first_match=True):
|
|
201
203
|
r"""
|
|
202
204
|
Gets the version number component of the detector from the model filename.
|
|
203
205
|
|
|
@@ -215,6 +217,8 @@ def get_detector_version_from_filename(detector_filename):
|
|
|
215
217
|
|
|
216
218
|
Args:
|
|
217
219
|
detector_filename (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt
|
|
220
|
+
accept_first_match (bool, optional): if multiple candidates match the filename, choose the
|
|
221
|
+
first one, otherwise returns the string "multiple"
|
|
218
222
|
|
|
219
223
|
Returns:
|
|
220
224
|
str: a detector version string, e.g. "v5a.0.0", or "multiple" if I'm confused
|
|
@@ -230,7 +234,10 @@ def get_detector_version_from_filename(detector_filename):
|
|
|
230
234
|
return 'unknown'
|
|
231
235
|
elif len(matches) > 1:
|
|
232
236
|
print('Warning: multiple MegaDetector versions for model file {}'.format(detector_filename))
|
|
233
|
-
|
|
237
|
+
if accept_first_match:
|
|
238
|
+
return model_string_to_model_version[matches[0]]
|
|
239
|
+
else:
|
|
240
|
+
return 'multiple'
|
|
234
241
|
else:
|
|
235
242
|
return model_string_to_model_version[matches[0]]
|
|
236
243
|
|
|
@@ -923,6 +923,13 @@ def write_results_to_file(results,
|
|
|
923
923
|
'info': info
|
|
924
924
|
}
|
|
925
925
|
|
|
926
|
+
# Create the folder where the output file belongs; this will fail if
|
|
927
|
+
# this is a relative path with no folder component
|
|
928
|
+
try:
|
|
929
|
+
os.makedirs(os.path.dirname(output_file),exist_ok=True)
|
|
930
|
+
except Exception:
|
|
931
|
+
pass
|
|
932
|
+
|
|
926
933
|
with open(output_file, 'w') as f:
|
|
927
934
|
json.dump(final_output, f, indent=1, default=str)
|
|
928
935
|
print('Output file saved at {}'.format(output_file))
|