megadetector 5.0.15__py3-none-any.whl → 5.0.17__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/importers/snapshot_safari_importer_reprise.py +28 -16
- 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 +270 -127
- megadetector/detection/pytorch_detector.py +13 -11
- megadetector/detection/run_detector.py +9 -2
- megadetector/detection/run_detector_batch.py +8 -1
- megadetector/detection/run_inference_with_yolov5_val.py +58 -10
- megadetector/detection/tf_detector.py +8 -2
- megadetector/detection/video_utils.py +214 -18
- megadetector/postprocessing/md_to_coco.py +31 -9
- megadetector/postprocessing/postprocess_batch_results.py +23 -7
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +5 -2
- 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/ct_utils.py +48 -0
- megadetector/utils/md_tests.py +231 -56
- megadetector/utils/path_utils.py +2 -2
- megadetector/utils/torch_test.py +32 -0
- megadetector/utils/url_utils.py +101 -4
- megadetector/visualization/visualization_utils.py +21 -6
- megadetector/visualization/visualize_db.py +16 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.17.dist-info}/LICENSE +0 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.17.dist-info}/METADATA +5 -7
- {megadetector-5.0.15.dist-info → megadetector-5.0.17.dist-info}/RECORD +34 -32
- {megadetector-5.0.15.dist-info → megadetector-5.0.17.dist-info}/WHEEL +1 -1
- {megadetector-5.0.15.dist-info → megadetector-5.0.17.dist-info}/top_level.txt +0 -0
|
@@ -29,9 +29,9 @@ from megadetector.data_management.yolo_output_to_md_output import read_classes_f
|
|
|
29
29
|
|
|
30
30
|
def _filename_to_image_id(fn):
|
|
31
31
|
"""
|
|
32
|
-
Image IDs can't have spaces in them,
|
|
32
|
+
Image IDs can't have spaces in them, replace spaces with underscores
|
|
33
33
|
"""
|
|
34
|
-
return fn.replace(' ','_')
|
|
34
|
+
return fn.replace(' ','_').replace('\\','/')
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _process_image(fn_abs,input_folder,category_id_to_name):
|
|
@@ -40,7 +40,9 @@ def _process_image(fn_abs,input_folder,category_id_to_name):
|
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
# Create the image object for this image
|
|
43
|
-
|
|
43
|
+
#
|
|
44
|
+
# Always use forward slashes in image filenames and IDs
|
|
45
|
+
fn_relative = os.path.relpath(fn_abs,input_folder).replace('\\','/')
|
|
44
46
|
image_id = _filename_to_image_id(fn_relative)
|
|
45
47
|
|
|
46
48
|
# This is done in a separate loop now
|
|
@@ -51,7 +53,7 @@ def _process_image(fn_abs,input_folder,category_id_to_name):
|
|
|
51
53
|
# image_ids.add(image_id)
|
|
52
54
|
|
|
53
55
|
im = {}
|
|
54
|
-
im['file_name'] = fn_relative
|
|
56
|
+
im['file_name'] = fn_relative
|
|
55
57
|
im['id'] = image_id
|
|
56
58
|
|
|
57
59
|
annotations_this_image = []
|
|
@@ -393,7 +395,8 @@ def yolo_to_coco(input_folder,
|
|
|
393
395
|
pool_type='thread',
|
|
394
396
|
recursive=True,
|
|
395
397
|
exclude_string=None,
|
|
396
|
-
include_string=None
|
|
398
|
+
include_string=None,
|
|
399
|
+
overwrite_handling='overwrite'):
|
|
397
400
|
"""
|
|
398
401
|
Converts a YOLO-formatted dataset to a COCO-formatted dataset.
|
|
399
402
|
|
|
@@ -427,6 +430,8 @@ def yolo_to_coco(input_folder,
|
|
|
427
430
|
recursive (bool, optional): whether to recurse into [input_folder]
|
|
428
431
|
exclude_string (str, optional): exclude any images whose filename contains a string
|
|
429
432
|
include_string (str, optional): include only images whose filename contains a string
|
|
433
|
+
overwrite_handling (bool, optional): behavior if output_file exists ('load', 'overwrite', or
|
|
434
|
+
'error')
|
|
430
435
|
|
|
431
436
|
Returns:
|
|
432
437
|
dict: COCO-formatted data, the same as what's written to [output_file]
|
|
@@ -441,7 +446,21 @@ def yolo_to_coco(input_folder,
|
|
|
441
446
|
('no_annotations','empty_annotations','skip','error'), \
|
|
442
447
|
'Unrecognized empty image handling spec: {}'.format(empty_image_handling)
|
|
443
448
|
|
|
444
|
-
|
|
449
|
+
if (output_file is not None) and os.path.isfile(output_file):
|
|
450
|
+
|
|
451
|
+
if overwrite_handling == 'overwrite':
|
|
452
|
+
print('Warning: output file {} exists, over-writing'.format(output_file))
|
|
453
|
+
elif overwrite_handling == 'load':
|
|
454
|
+
print('Output file {} exists, loading and returning'.format(output_file))
|
|
455
|
+
with open(output_file,'r') as f:
|
|
456
|
+
d = json.load(f)
|
|
457
|
+
return d
|
|
458
|
+
elif overwrite_handling == 'error':
|
|
459
|
+
raise ValueError('Output file {} exists'.format(output_file))
|
|
460
|
+
else:
|
|
461
|
+
raise ValueError('Unrecognized overwrite_handling value: {}'.format(overwrite_handling))
|
|
462
|
+
|
|
463
|
+
|
|
445
464
|
## Read class names
|
|
446
465
|
|
|
447
466
|
category_id_to_name = load_yolo_class_list(class_name_file)
|
|
@@ -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,118 @@ 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
|
-
|
|
327
|
-
temporary_folder_info = _select_temporary_output_folders(options)
|
|
328
|
-
|
|
329
|
-
if (caller_provided_frame_output_folder):
|
|
330
|
-
frame_output_folder = options.frame_folder
|
|
331
|
-
else:
|
|
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
|
-
|
|
347
|
+
frame_output_folder = None
|
|
348
|
+
frame_filenames = None
|
|
358
349
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
os.path.isfile(options.output_json_file):
|
|
350
|
+
# If we should re-use existing results, and the output file exists, don't bother running MD
|
|
351
|
+
if (options.reuse_results_if_available and os.path.isfile(options.output_json_file)):
|
|
352
|
+
|
|
363
353
|
print('Loading results from {}'.format(options.output_json_file))
|
|
364
354
|
with open(options.output_json_file,'r') as f:
|
|
365
355
|
results = json.load(f)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
356
|
+
|
|
357
|
+
# Run MD in memory if we don't need to generate frames
|
|
358
|
+
#
|
|
359
|
+
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
360
|
+
elif (not options.keep_extracted_frames and \
|
|
361
|
+
not options.render_output_video and \
|
|
362
|
+
not options.force_on_disk_frame_extraction):
|
|
363
|
+
|
|
364
|
+
# Run MegaDetector in memory
|
|
365
|
+
|
|
366
|
+
if options.verbose:
|
|
367
|
+
print('Running MegaDetector in memory for {}'.format(options.input_video_file))
|
|
368
|
+
|
|
369
|
+
if options.frame_folder is not None:
|
|
370
|
+
print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
|
|
371
|
+
'not; no raw frames will be written')
|
|
372
|
+
|
|
373
|
+
detector = load_detector(options.model_file)
|
|
374
|
+
|
|
375
|
+
def frame_callback(image_np,image_id):
|
|
376
|
+
return detector.generate_detections_one_image(image_np,
|
|
377
|
+
image_id,
|
|
378
|
+
detection_threshold=options.json_confidence_threshold,
|
|
379
|
+
augment=options.augment)
|
|
380
|
+
|
|
381
|
+
frame_results = run_callback_on_frames(options.input_video_file,
|
|
382
|
+
frame_callback,
|
|
383
|
+
every_n_frames=options.frame_sample,
|
|
384
|
+
verbose=options.verbose,
|
|
385
|
+
frames_to_process=options.frames_to_extract)
|
|
386
|
+
|
|
387
|
+
frame_results['results'] = _add_frame_numbers_to_results(frame_results['results'])
|
|
378
388
|
|
|
379
389
|
run_detector_batch.write_results_to_file(
|
|
380
|
-
results,
|
|
381
|
-
|
|
390
|
+
frame_results['results'],
|
|
391
|
+
options.output_json_file,
|
|
392
|
+
relative_path_base=None,
|
|
382
393
|
detector_file=options.model_file,
|
|
383
|
-
custom_metadata={'video_frame_rate':
|
|
394
|
+
custom_metadata={'video_frame_rate':frame_results['frame_rate']})
|
|
384
395
|
|
|
396
|
+
# Extract frames and optionally run MegaDetector on those frames
|
|
397
|
+
else:
|
|
398
|
+
|
|
399
|
+
if options.verbose:
|
|
400
|
+
print('Extracting frames for {}'.format(options.input_video_file))
|
|
401
|
+
|
|
402
|
+
# This does not create any folders, just defines temporary folder names in
|
|
403
|
+
# case we need them.
|
|
404
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
405
|
+
|
|
406
|
+
if (caller_provided_frame_output_folder):
|
|
407
|
+
frame_output_folder = options.frame_folder
|
|
408
|
+
else:
|
|
409
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
410
|
+
|
|
411
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
## Extract frames
|
|
415
|
+
|
|
416
|
+
frame_filenames, Fs = video_to_frames(
|
|
417
|
+
options.input_video_file,
|
|
418
|
+
frame_output_folder,
|
|
419
|
+
every_n_frames=options.frame_sample,
|
|
420
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
421
|
+
quality=options.quality,
|
|
422
|
+
max_width=options.max_width,
|
|
423
|
+
verbose=options.verbose,
|
|
424
|
+
frames_to_extract=options.frames_to_extract,
|
|
425
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
426
|
+
|
|
427
|
+
image_file_names = frame_filenames
|
|
428
|
+
if options.debug_max_frames > 0:
|
|
429
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
430
|
+
|
|
431
|
+
## Run MegaDetector on those frames
|
|
432
|
+
|
|
433
|
+
if options.model_file != 'no_detection':
|
|
434
|
+
|
|
435
|
+
if options.verbose:
|
|
436
|
+
print('Running MD for {}'.format(options.input_video_file))
|
|
437
|
+
|
|
438
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
439
|
+
options.model_file,
|
|
440
|
+
image_file_names,
|
|
441
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
442
|
+
n_cores=options.n_cores,
|
|
443
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
444
|
+
quiet=True,
|
|
445
|
+
augment=options.augment,
|
|
446
|
+
image_size=options.image_size)
|
|
447
|
+
|
|
448
|
+
results = _add_frame_numbers_to_results(results)
|
|
449
|
+
|
|
450
|
+
run_detector_batch.write_results_to_file(
|
|
451
|
+
results,
|
|
452
|
+
options.output_json_file,
|
|
453
|
+
relative_path_base=frame_output_folder,
|
|
454
|
+
detector_file=options.model_file,
|
|
455
|
+
custom_metadata={'video_frame_rate':Fs})
|
|
456
|
+
|
|
457
|
+
# ...if we are/aren't keeping raw frames on disk
|
|
458
|
+
|
|
385
459
|
|
|
386
460
|
## (Optionally) render output video
|
|
387
461
|
|
|
@@ -470,81 +544,141 @@ def process_video_folder(options):
|
|
|
470
544
|
# case we need them.
|
|
471
545
|
temporary_folder_info = _select_temporary_output_folders(options)
|
|
472
546
|
|
|
547
|
+
frame_output_folder = None
|
|
548
|
+
image_file_names = None
|
|
549
|
+
video_filename_to_fs = {}
|
|
473
550
|
|
|
474
|
-
|
|
551
|
+
# Run MD in memory if we don't need to generate frames
|
|
552
|
+
#
|
|
553
|
+
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
554
|
+
if (not options.keep_extracted_frames and \
|
|
555
|
+
not options.render_output_video and \
|
|
556
|
+
not options.force_on_disk_frame_extraction):
|
|
557
|
+
|
|
558
|
+
if options.verbose:
|
|
559
|
+
print('Running MegaDetector in memory for folder {}'.format(options.input_video_file))
|
|
560
|
+
|
|
561
|
+
if options.frame_folder is not None:
|
|
562
|
+
print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
|
|
563
|
+
'not; no raw frames will be written')
|
|
564
|
+
|
|
565
|
+
detector = load_detector(options.model_file)
|
|
566
|
+
|
|
567
|
+
def frame_callback(image_np,image_id):
|
|
568
|
+
return detector.generate_detections_one_image(image_np,
|
|
569
|
+
image_id,
|
|
570
|
+
detection_threshold=options.json_confidence_threshold,
|
|
571
|
+
augment=options.augment)
|
|
572
|
+
|
|
573
|
+
md_results = run_callback_on_frames_for_folder(input_video_folder=options.input_video_file,
|
|
574
|
+
frame_callback=frame_callback,
|
|
575
|
+
every_n_frames=options.frame_sample,
|
|
576
|
+
verbose=options.verbose)
|
|
577
|
+
|
|
578
|
+
video_results = md_results['results']
|
|
579
|
+
|
|
580
|
+
for i_video,video_filename in enumerate(md_results['video_filenames']):
|
|
581
|
+
assert video_filename not in video_filename_to_fs
|
|
582
|
+
video_filename_to_fs[video_filename] = md_results['frame_rates'][i_video]
|
|
583
|
+
|
|
584
|
+
all_frame_results = []
|
|
585
|
+
|
|
586
|
+
# r = video_results[0]
|
|
587
|
+
for frame_results in video_results:
|
|
588
|
+
_add_frame_numbers_to_results(frame_results)
|
|
589
|
+
all_frame_results.extend(frame_results)
|
|
590
|
+
|
|
591
|
+
run_detector_batch.write_results_to_file(
|
|
592
|
+
all_frame_results,
|
|
593
|
+
frames_json,
|
|
594
|
+
relative_path_base=None,
|
|
595
|
+
detector_file=options.model_file)
|
|
475
596
|
|
|
476
|
-
if caller_provided_frame_output_folder:
|
|
477
|
-
frame_output_folder = options.frame_folder
|
|
478
597
|
else:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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))
|
|
598
|
+
|
|
599
|
+
## Split every video into frames
|
|
600
|
+
|
|
601
|
+
if options.verbose:
|
|
602
|
+
print('Extracting frames for folder {}'.format(options.input_video_file))
|
|
603
|
+
|
|
604
|
+
if caller_provided_frame_output_folder:
|
|
605
|
+
frame_output_folder = options.frame_folder
|
|
503
606
|
else:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
607
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
608
|
+
|
|
609
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
610
|
+
|
|
611
|
+
frame_filenames, Fs, video_filenames = \
|
|
612
|
+
video_folder_to_frames(input_folder=options.input_video_file,
|
|
613
|
+
output_folder_base=frame_output_folder,
|
|
614
|
+
recursive=options.recursive,
|
|
615
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
616
|
+
n_threads=options.n_cores,
|
|
617
|
+
every_n_frames=options.frame_sample,
|
|
618
|
+
verbose=options.verbose,
|
|
619
|
+
quality=options.quality,
|
|
620
|
+
max_width=options.max_width,
|
|
621
|
+
frames_to_extract=options.frames_to_extract,
|
|
622
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
510
623
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
## Run MegaDetector on the extracted frames
|
|
518
|
-
|
|
519
|
-
if options.reuse_results_if_available and \
|
|
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)
|
|
624
|
+
for i_video,video_filename in enumerate(video_filenames):
|
|
625
|
+
assert video_filename not in video_filename_to_fs
|
|
626
|
+
video_filename_to_fs[video_filename] = Fs[i_video]
|
|
627
|
+
|
|
628
|
+
print('Extracted frames for {} videos'.format(len(set(video_filenames))))
|
|
629
|
+
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
536
630
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
631
|
+
if len(image_file_names) == 0:
|
|
632
|
+
if len(video_filenames) == 0:
|
|
633
|
+
print('No videos found in folder {}'.format(options.input_video_file))
|
|
634
|
+
else:
|
|
635
|
+
print('No frames extracted from folder {}, this may be due to an '\
|
|
636
|
+
'unsupported video codec'.format(options.input_video_file))
|
|
637
|
+
return
|
|
542
638
|
|
|
639
|
+
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
640
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
641
|
+
|
|
642
|
+
if options.model_file == 'no_detection':
|
|
643
|
+
assert options.keep_extracted_frames, \
|
|
644
|
+
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
## Run MegaDetector on the extracted frames
|
|
649
|
+
|
|
650
|
+
if options.reuse_results_if_available and \
|
|
651
|
+
os.path.isfile(frames_json):
|
|
652
|
+
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
653
|
+
with open(frames_json,'r') as f:
|
|
654
|
+
results = json.load(f)
|
|
655
|
+
else:
|
|
656
|
+
print('Running MegaDetector')
|
|
657
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
658
|
+
options.model_file,
|
|
659
|
+
image_file_names,
|
|
660
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
661
|
+
n_cores=options.n_cores,
|
|
662
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
663
|
+
quiet=True,
|
|
664
|
+
augment=options.augment,
|
|
665
|
+
image_size=options.image_size)
|
|
666
|
+
|
|
667
|
+
_add_frame_numbers_to_results(results)
|
|
668
|
+
|
|
669
|
+
run_detector_batch.write_results_to_file(
|
|
670
|
+
results,
|
|
671
|
+
frames_json,
|
|
672
|
+
relative_path_base=frame_output_folder,
|
|
673
|
+
detector_file=options.model_file)
|
|
674
|
+
|
|
675
|
+
# ...if we're running MD on in-memory frames vs. extracting frames to disk
|
|
543
676
|
|
|
544
677
|
## Convert frame-level results to video-level results
|
|
545
678
|
|
|
546
679
|
print('Converting frame-level results to video-level results')
|
|
547
|
-
frame_results_to_video_results(frames_json,video_json
|
|
680
|
+
frame_results_to_video_results(frames_json,video_json,
|
|
681
|
+
video_filename_to_frame_rate=video_filename_to_fs)
|
|
548
682
|
|
|
549
683
|
|
|
550
684
|
## (Optionally) render output videos
|
|
@@ -646,13 +780,13 @@ def process_video_folder(options):
|
|
|
646
780
|
|
|
647
781
|
def options_to_command(options):
|
|
648
782
|
"""
|
|
649
|
-
Convert a ProcessVideoOptions
|
|
783
|
+
Convert a ProcessVideoOptions object to a corresponding command line.
|
|
650
784
|
|
|
651
785
|
Args:
|
|
652
786
|
options (ProcessVideoOptions): the options set to render as a command line
|
|
653
787
|
|
|
654
788
|
Returns:
|
|
655
|
-
str: the command line
|
|
789
|
+
str: the command line corresponding to [options]
|
|
656
790
|
|
|
657
791
|
:meta private:
|
|
658
792
|
"""
|
|
@@ -725,8 +859,8 @@ if False:
|
|
|
725
859
|
#%% Process a folder of videos
|
|
726
860
|
|
|
727
861
|
model_file = 'MDV5A'
|
|
728
|
-
|
|
729
|
-
input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
862
|
+
input_dir = r'g:\temp\test-videos'
|
|
863
|
+
# input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
730
864
|
output_base = r'g:\temp\video_test'
|
|
731
865
|
frame_folder = os.path.join(output_base,'frames')
|
|
732
866
|
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
@@ -744,25 +878,26 @@ if False:
|
|
|
744
878
|
options.recursive = True
|
|
745
879
|
options.reuse_frames_if_available = False
|
|
746
880
|
options.reuse_results_if_available = False
|
|
747
|
-
options.quality = 90
|
|
881
|
+
options.quality = None # 90
|
|
748
882
|
options.frame_sample = 10
|
|
749
|
-
options.max_width = 1280
|
|
883
|
+
options.max_width = None # 1280
|
|
750
884
|
options.n_cores = 4
|
|
751
885
|
options.verbose = True
|
|
752
|
-
options.render_output_video =
|
|
886
|
+
options.render_output_video = False
|
|
753
887
|
options.frame_folder = frame_folder
|
|
754
888
|
options.frame_rendering_folder = rendering_folder
|
|
755
|
-
options.keep_extracted_frames =
|
|
756
|
-
options.keep_rendered_frames =
|
|
889
|
+
options.keep_extracted_frames = False
|
|
890
|
+
options.keep_rendered_frames = False
|
|
757
891
|
options.force_extracted_frame_folder_deletion = False
|
|
758
892
|
options.force_rendered_frame_folder_deletion = False
|
|
759
893
|
options.fourcc = 'mp4v'
|
|
894
|
+
options.force_on_disk_frame_extraction = True
|
|
760
895
|
# options.rendering_confidence_threshold = 0.15
|
|
761
896
|
|
|
762
897
|
cmd = options_to_command(options); print(cmd)
|
|
763
898
|
|
|
764
899
|
# import clipboard; clipboard.copy(cmd)
|
|
765
|
-
|
|
900
|
+
process_video_folder(options)
|
|
766
901
|
|
|
767
902
|
|
|
768
903
|
#%% Process a single video
|
|
@@ -988,6 +1123,10 @@ def main():
|
|
|
988
1123
|
parser.add_argument('--augment',
|
|
989
1124
|
action='store_true',
|
|
990
1125
|
help='Enable image augmentation')
|
|
1126
|
+
|
|
1127
|
+
parser.add_argument('--allow_empty_videos',
|
|
1128
|
+
action='store_true',
|
|
1129
|
+
help='By default, videos with no retrievable frames cause an error, this makes it a warning')
|
|
991
1130
|
|
|
992
1131
|
if len(sys.argv[1:]) == 0:
|
|
993
1132
|
parser.print_help()
|
|
@@ -1000,6 +1139,10 @@ def main():
|
|
|
1000
1139
|
if os.path.isdir(options.input_video_file):
|
|
1001
1140
|
process_video_folder(options)
|
|
1002
1141
|
else:
|
|
1142
|
+
assert os.path.isfile(options.input_video_file), \
|
|
1143
|
+
'{} is not a valid file or folder name'.format(options.input_video_file)
|
|
1144
|
+
assert not options.recursive, \
|
|
1145
|
+
'--recursive is only meaningful when processing a folder'
|
|
1003
1146
|
process_video(options)
|
|
1004
1147
|
|
|
1005
1148
|
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)
|