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
|
@@ -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
|
|
|
@@ -846,7 +846,7 @@ def write_results_to_file(results,
|
|
|
846
846
|
https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format
|
|
847
847
|
|
|
848
848
|
Args:
|
|
849
|
-
results (list):
|
|
849
|
+
results (list): list of dict, each dict represents detections on one image
|
|
850
850
|
output_file (str): path to JSON output file, should end in '.json'
|
|
851
851
|
relative_path_base (str, optional): path to a directory as the base for relative paths, can
|
|
852
852
|
be None if the paths in [results] are absolute
|
|
@@ -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))
|
|
@@ -106,7 +106,9 @@ class YoloInferenceOptions:
|
|
|
106
106
|
|
|
107
107
|
#: Image size to use; this is a single int, which in ultralytics's terminology means
|
|
108
108
|
#: "scale the long side of the image to this size, and preserve aspect ratio".
|
|
109
|
-
|
|
109
|
+
#:
|
|
110
|
+
#: If None, will choose based on whether augmentation is enabled.
|
|
111
|
+
self.image_size = None
|
|
110
112
|
|
|
111
113
|
#: Detections below this threshold will not be included in the output file
|
|
112
114
|
self.conf_thres = '0.001'
|
|
@@ -276,10 +278,10 @@ def run_inference_with_yolo_val(options):
|
|
|
276
278
|
|
|
277
279
|
if options.input_folder is not None:
|
|
278
280
|
options.input_folder = options.input_folder.replace('\\','/')
|
|
281
|
+
|
|
279
282
|
|
|
280
|
-
|
|
281
283
|
##%% Other input handling
|
|
282
|
-
|
|
284
|
+
|
|
283
285
|
if isinstance(options.yolo_category_id_to_name,str):
|
|
284
286
|
|
|
285
287
|
assert os.path.isfile(options.yolo_category_id_to_name)
|
|
@@ -328,7 +330,9 @@ def run_inference_with_yolo_val(options):
|
|
|
328
330
|
image_files_relative = None
|
|
329
331
|
image_files_absolute = None
|
|
330
332
|
|
|
333
|
+
# If the caller just provided a folder, not a list of files...
|
|
331
334
|
if options.image_filename_list is None:
|
|
335
|
+
|
|
332
336
|
assert options.input_folder is not None and os.path.isdir(options.input_folder), \
|
|
333
337
|
'Could not find input folder {}'.format(options.input_folder)
|
|
334
338
|
image_files_relative = path_utils.find_images(options.input_folder,
|
|
@@ -337,18 +341,23 @@ def run_inference_with_yolo_val(options):
|
|
|
337
341
|
convert_slashes=True)
|
|
338
342
|
image_files_absolute = [os.path.join(options.input_folder,fn) for \
|
|
339
343
|
fn in image_files_relative]
|
|
344
|
+
|
|
340
345
|
else:
|
|
341
346
|
|
|
342
|
-
|
|
347
|
+
# If the caller provided a list of image files (rather than a filename pointing
|
|
348
|
+
# to a list of image files)...
|
|
349
|
+
if is_iterable(options.image_filename_list) and not isinstance(options.image_filename_list,str):
|
|
343
350
|
|
|
344
351
|
image_files_relative = options.image_filename_list
|
|
345
352
|
|
|
353
|
+
# If the caller provided a filename pointing to a list of image files...
|
|
346
354
|
else:
|
|
355
|
+
|
|
347
356
|
assert isinstance(options.image_filename_list,str), \
|
|
348
357
|
'Unrecognized image filename list object type: {}'.format(options.image_filename_list)
|
|
349
358
|
assert os.path.isfile(options.image_filename_list), \
|
|
350
359
|
'Could not find image filename list file: {}'.format(options.image_filename_list)
|
|
351
|
-
ext = os.path.splitext(options.image_filename_list).lower()
|
|
360
|
+
ext = os.path.splitext(options.image_filename_list)[-1].lower()
|
|
352
361
|
assert ext in ('.json','.txt'), \
|
|
353
362
|
'Unrecognized image filename list file extension: {}'.format(options.image_filename_list)
|
|
354
363
|
if ext == '.json':
|
|
@@ -364,8 +373,11 @@ def run_inference_with_yolo_val(options):
|
|
|
364
373
|
# ...whether the image filename list was supplied as list vs. a filename
|
|
365
374
|
|
|
366
375
|
if options.input_folder is None:
|
|
376
|
+
|
|
367
377
|
image_files_absolute = image_files_relative
|
|
378
|
+
|
|
368
379
|
else:
|
|
380
|
+
|
|
369
381
|
# The list should be relative filenames
|
|
370
382
|
for fn in image_files_relative:
|
|
371
383
|
assert not path_is_abs(fn), \
|
|
@@ -373,12 +385,14 @@ def run_inference_with_yolo_val(options):
|
|
|
373
385
|
|
|
374
386
|
image_files_absolute = \
|
|
375
387
|
[os.path.join(options.input_folder,fn) for fn in image_files_relative]
|
|
388
|
+
|
|
376
389
|
for fn in image_files_absolute:
|
|
377
390
|
assert os.path.isfile(fn), 'Could not find image file {}'.format(fn)
|
|
378
391
|
|
|
379
392
|
# ...whether the caller supplied a list of filenames
|
|
380
393
|
|
|
381
394
|
image_files_absolute = [fn.replace('\\','/') for fn in image_files_absolute]
|
|
395
|
+
|
|
382
396
|
del image_files_relative
|
|
383
397
|
|
|
384
398
|
|
|
@@ -549,6 +563,7 @@ def run_inference_with_yolo_val(options):
|
|
|
549
563
|
for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
|
|
550
564
|
|
|
551
565
|
ext = os.path.splitext(image_fn)[1]
|
|
566
|
+
image_fn_without_extension = os.path.splitext(image_fn)[0]
|
|
552
567
|
|
|
553
568
|
# YOLO .json output identifies images by the base filename without the extension
|
|
554
569
|
image_id = str(i_image).zfill(10)
|
|
@@ -557,12 +572,25 @@ def run_inference_with_yolo_val(options):
|
|
|
557
572
|
symlink_full_path = os.path.join(symlink_folder_inner,symlink_name)
|
|
558
573
|
link_full_paths.append(symlink_full_path)
|
|
559
574
|
|
|
575
|
+
# If annotation files exist, link those too; only useful if we're reading the computed
|
|
576
|
+
# mAP value, but it doesn't hurt.
|
|
577
|
+
annotation_fn = image_fn_without_extension + '.txt'
|
|
578
|
+
annotation_file_exists = False
|
|
579
|
+
if os.path.isfile(annotation_fn):
|
|
580
|
+
annotation_file_exists = True
|
|
581
|
+
annotation_symlink_name = image_id + '.txt'
|
|
582
|
+
annotation_symlink_full_path = os.path.join(symlink_folder_inner,annotation_symlink_name)
|
|
583
|
+
|
|
560
584
|
try:
|
|
561
585
|
|
|
562
586
|
if options.use_symlinks:
|
|
563
587
|
path_utils.safe_create_link(image_fn,symlink_full_path)
|
|
588
|
+
if annotation_file_exists:
|
|
589
|
+
path_utils.safe_create_link(annotation_fn,annotation_symlink_full_path)
|
|
564
590
|
else:
|
|
565
591
|
shutil.copyfile(image_fn,symlink_full_path)
|
|
592
|
+
if annotation_file_exists:
|
|
593
|
+
shutil.copyfile(annotation_fn,annotation_symlink_full_path)
|
|
566
594
|
|
|
567
595
|
except Exception as e:
|
|
568
596
|
|
|
@@ -648,7 +676,15 @@ def run_inference_with_yolo_val(options):
|
|
|
648
676
|
|
|
649
677
|
##%% Prepare Python command or YOLO CLI command
|
|
650
678
|
|
|
651
|
-
|
|
679
|
+
if options.image_size is None:
|
|
680
|
+
if options.augment:
|
|
681
|
+
image_size = default_image_size_with_augmentation
|
|
682
|
+
else:
|
|
683
|
+
image_size = default_image_size_with_no_augmentation
|
|
684
|
+
else:
|
|
685
|
+
image_size = options.image_size
|
|
686
|
+
|
|
687
|
+
image_size_string = str(round(image_size))
|
|
652
688
|
|
|
653
689
|
if options.model_type == 'yolov5':
|
|
654
690
|
|
|
@@ -659,6 +695,9 @@ def run_inference_with_yolo_val(options):
|
|
|
659
695
|
cmd += ' --device "{}" --save-json'.format(options.device_string)
|
|
660
696
|
cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
|
|
661
697
|
|
|
698
|
+
# This is the NMS IoU threshold
|
|
699
|
+
# cmd += ' --iou-thres 0.6'
|
|
700
|
+
|
|
662
701
|
if options.augment:
|
|
663
702
|
cmd += ' --augment'
|
|
664
703
|
|
|
@@ -837,7 +876,7 @@ def run_inference_with_yolo_val(options):
|
|
|
837
876
|
_clean_up_temporary_folders(options,
|
|
838
877
|
symlink_folder,yolo_results_folder,
|
|
839
878
|
symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
|
|
840
|
-
|
|
879
|
+
|
|
841
880
|
# ...def run_inference_with_yolo_val()
|
|
842
881
|
|
|
843
882
|
|
|
@@ -856,7 +895,7 @@ def main():
|
|
|
856
895
|
help='model file name')
|
|
857
896
|
parser.add_argument(
|
|
858
897
|
'input_folder',type=str,
|
|
859
|
-
help='folder on which to recursively run the model')
|
|
898
|
+
help='folder on which to recursively run the model, or a .json or .txt file containing a list of absolute image paths')
|
|
860
899
|
parser.add_argument(
|
|
861
900
|
'output_file',type=str,
|
|
862
901
|
help='.json file where output will be written')
|
|
@@ -967,7 +1006,15 @@ def main():
|
|
|
967
1006
|
|
|
968
1007
|
if args.yolo_dataset_file is not None:
|
|
969
1008
|
options.yolo_category_id_to_name = args.yolo_dataset_file
|
|
970
|
-
|
|
1009
|
+
|
|
1010
|
+
# The function convention is that input_folder should be None when we want to use a list of
|
|
1011
|
+
# absolute paths, but the CLI convention is that the required argument is always valid, whether
|
|
1012
|
+
# it's a folder or a list of absolute paths.
|
|
1013
|
+
if os.path.isfile(options.input_folder):
|
|
1014
|
+
assert options.image_filename_list is None, \
|
|
1015
|
+
'image_filename_list should not be specified when input_folder is a file'
|
|
1016
|
+
options.image_filename_list = options.input_folder
|
|
1017
|
+
options.input_folder = None
|
|
971
1018
|
|
|
972
1019
|
options.recursive = (not options.nonrecursive)
|
|
973
1020
|
options.remove_symlink_folder = (not options.no_remove_symlink_folder)
|
|
@@ -980,6 +1027,7 @@ def main():
|
|
|
980
1027
|
del options.no_remove_yolo_results_folder
|
|
981
1028
|
del options.no_use_symlinks
|
|
982
1029
|
del options.augment_enabled
|
|
1030
|
+
del options.yolo_dataset_file
|
|
983
1031
|
|
|
984
1032
|
print(options.__dict__)
|
|
985
1033
|
|
|
@@ -1001,7 +1049,7 @@ if False:
|
|
|
1001
1049
|
yolo_working_folder = r'c:\git\yolov5-tegus'
|
|
1002
1050
|
dataset_file = r'g:\temp\dataset.yaml'
|
|
1003
1051
|
|
|
1004
|
-
# This only impacts the output file name, it's not passed to the inference
|
|
1052
|
+
# This only impacts the output file name, it's not passed to the inference function
|
|
1005
1053
|
job_name = 'yolo-inference-test'
|
|
1006
1054
|
|
|
1007
1055
|
model_name = os.path.splitext(os.path.basename(model_filename))[0]
|
|
@@ -110,7 +110,10 @@ class TFDetector:
|
|
|
110
110
|
Runs the detector on a single image.
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
if isinstance(image,np.ndarray):
|
|
114
|
+
np_im = image
|
|
115
|
+
else:
|
|
116
|
+
np_im = np.asarray(image, np.uint8)
|
|
114
117
|
im_w_batch_dim = np.expand_dims(np_im, axis=0)
|
|
115
118
|
|
|
116
119
|
# need to change the above line to the following if supporting a batch size > 1 and resizing to the same size
|
|
@@ -136,7 +139,8 @@ class TFDetector:
|
|
|
136
139
|
Runs the detector on an image.
|
|
137
140
|
|
|
138
141
|
Args:
|
|
139
|
-
image (Image): the PIL Image object on which we should run the detector
|
|
142
|
+
image (Image): the PIL Image object (or numpy array) on which we should run the detector, with
|
|
143
|
+
EXIF rotation already handled.
|
|
140
144
|
image_id (str): a path to identify the image; will be in the "file" field of the output object
|
|
141
145
|
detection_threshold (float): only detections above this threshold will be included in the return
|
|
142
146
|
value
|
|
@@ -166,6 +170,7 @@ class TFDetector:
|
|
|
166
170
|
result = { 'file': image_id }
|
|
167
171
|
|
|
168
172
|
try:
|
|
173
|
+
|
|
169
174
|
b_box, b_score, b_class = self._generate_detections_one_image(image)
|
|
170
175
|
|
|
171
176
|
# our batch size is 1; need to loop the batch dim if supporting batch size > 1
|
|
@@ -190,6 +195,7 @@ class TFDetector:
|
|
|
190
195
|
result['detections'] = detections_cur_image
|
|
191
196
|
|
|
192
197
|
except Exception as e:
|
|
198
|
+
|
|
193
199
|
result['failure'] = FAILURE_INFER
|
|
194
200
|
print('TFDetector: image {} failed during inference: {}'.format(image_id, str(e)))
|
|
195
201
|
|
|
@@ -22,6 +22,7 @@ from functools import partial
|
|
|
22
22
|
from inspect import signature
|
|
23
23
|
|
|
24
24
|
from megadetector.utils import path_utils
|
|
25
|
+
from megadetector.utils.ct_utils import sort_list_of_dicts_by_key
|
|
25
26
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
26
27
|
|
|
27
28
|
default_fourcc = 'h264'
|
|
@@ -88,14 +89,14 @@ def find_videos(dirname,
|
|
|
88
89
|
else:
|
|
89
90
|
files = glob.glob(os.path.join(dirname, '*.*'))
|
|
90
91
|
|
|
92
|
+
files = [fn for fn in files if os.path.isfile(fn)]
|
|
93
|
+
|
|
91
94
|
if return_relative_paths:
|
|
92
95
|
files = [os.path.relpath(fn,dirname) for fn in files]
|
|
93
96
|
|
|
94
97
|
if convert_slashes:
|
|
95
98
|
files = [fn.replace('\\', '/') for fn in files]
|
|
96
99
|
|
|
97
|
-
files = [fn for fn in files if os.path.isfile(fn)]
|
|
98
|
-
|
|
99
100
|
return find_video_strings(files)
|
|
100
101
|
|
|
101
102
|
|
|
@@ -197,7 +198,7 @@ def _filename_to_frame_number(filename):
|
|
|
197
198
|
def _add_frame_numbers_to_results(results):
|
|
198
199
|
"""
|
|
199
200
|
Given the 'images' list from a set of MD results that was generated on video frames,
|
|
200
|
-
add a 'frame_number' field to each image.
|
|
201
|
+
add a 'frame_number' field to each image, and return the list, sorted by frame number.
|
|
201
202
|
|
|
202
203
|
Args:
|
|
203
204
|
results (list): list of image dicts
|
|
@@ -208,8 +209,186 @@ def _add_frame_numbers_to_results(results):
|
|
|
208
209
|
fn = im['file']
|
|
209
210
|
frame_number = _filename_to_frame_number(fn)
|
|
210
211
|
im['frame_number'] = frame_number
|
|
212
|
+
|
|
213
|
+
results = sort_list_of_dicts_by_key(results,'frame_number')
|
|
214
|
+
return results
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def run_callback_on_frames(input_video_file,
|
|
218
|
+
frame_callback,
|
|
219
|
+
every_n_frames=None,
|
|
220
|
+
verbose=False,
|
|
221
|
+
frames_to_process=None,
|
|
222
|
+
allow_empty_videos=False):
|
|
223
|
+
"""
|
|
224
|
+
Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
|
|
225
|
+
[input_video_file].
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
input_video_file (str): video file to process
|
|
229
|
+
frame_callback (function): callback to run on frames, should take an np.array and a string and
|
|
230
|
+
return a single value. callback should expect PIL-formatted (RGB) images.
|
|
231
|
+
every_n_frames (int, optional): sample every Nth frame starting from the first frame;
|
|
232
|
+
if this is None or 1, every frame is processed. Mutually exclusive with
|
|
233
|
+
frames_to_process.
|
|
234
|
+
verbose (bool, optional): enable additional debug console output
|
|
235
|
+
frames_to_process (list of int, optional): process this specific set of frames;
|
|
236
|
+
mutually exclusive with every_n_frames. If all values are beyond the length
|
|
237
|
+
of the video, no frames are extracted. Can also be a single int, specifying
|
|
238
|
+
a single frame number.
|
|
239
|
+
allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
|
|
240
|
+
frames (by default, this is an error).
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
dict: dict with keys 'frame_filenames' (list), 'frame_rate' (float), 'results' (list).
|
|
244
|
+
'frame_filenames' are synthetic filenames (e.g. frame000000.jpg); 'results' are
|
|
245
|
+
in the same format used in the 'images' array in the MD results format.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
|
|
249
|
+
|
|
250
|
+
if isinstance(frames_to_process,int):
|
|
251
|
+
frames_to_process = [frames_to_process]
|
|
252
|
+
|
|
253
|
+
if (frames_to_process is not None) and (every_n_frames is not None):
|
|
254
|
+
raise ValueError('frames_to_process and every_n_frames are mutually exclusive')
|
|
255
|
+
|
|
256
|
+
vidcap = cv2.VideoCapture(input_video_file)
|
|
257
|
+
n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
258
|
+
frame_rate = vidcap.get(cv2.CAP_PROP_FPS)
|
|
259
|
+
|
|
260
|
+
if verbose:
|
|
261
|
+
print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,frame_rate))
|
|
262
|
+
|
|
263
|
+
frame_filenames = []
|
|
264
|
+
results = []
|
|
265
|
+
|
|
266
|
+
# frame_number = 0
|
|
267
|
+
for frame_number in range(0,n_frames):
|
|
268
|
+
|
|
269
|
+
success,image = vidcap.read()
|
|
270
|
+
|
|
271
|
+
if not success:
|
|
272
|
+
assert image is None
|
|
273
|
+
if verbose:
|
|
274
|
+
print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
if every_n_frames is not None:
|
|
278
|
+
if frame_number % every_n_frames != 0:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
if frames_to_process is not None:
|
|
282
|
+
if frame_number > max(frames_to_process):
|
|
283
|
+
break
|
|
284
|
+
if frame_number not in frames_to_process:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
frame_filename_relative = _frame_number_to_filename(frame_number)
|
|
288
|
+
frame_filenames.append(frame_filename_relative)
|
|
289
|
+
|
|
290
|
+
image_np = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
291
|
+
frame_results = frame_callback(image_np,frame_filename_relative)
|
|
292
|
+
results.append(frame_results)
|
|
293
|
+
|
|
294
|
+
# ...for each frame
|
|
295
|
+
|
|
296
|
+
if len(frame_filenames) == 0:
|
|
297
|
+
if allow_empty_videos:
|
|
298
|
+
print('Warning: found no frames in file {}'.format(input_video_file))
|
|
299
|
+
else:
|
|
300
|
+
raise Exception('Error: found no frames in file {}'.format(input_video_file))
|
|
301
|
+
|
|
302
|
+
if verbose:
|
|
303
|
+
print('\nProcessed {} of {} frames for {}'.format(
|
|
304
|
+
len(frame_filenames),n_frames,input_video_file))
|
|
305
|
+
|
|
306
|
+
vidcap.release()
|
|
307
|
+
to_return = {}
|
|
308
|
+
to_return['frame_filenames'] = frame_filenames
|
|
309
|
+
to_return['frame_rate'] = frame_rate
|
|
310
|
+
to_return['results'] = results
|
|
311
|
+
|
|
312
|
+
return to_return
|
|
313
|
+
|
|
314
|
+
# ...def run_callback_on_frames(...)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def run_callback_on_frames_for_folder(input_video_folder,
|
|
318
|
+
frame_callback,
|
|
319
|
+
every_n_frames=None,
|
|
320
|
+
verbose=False,
|
|
321
|
+
allow_empty_videos=False,
|
|
322
|
+
recursive=True):
|
|
323
|
+
"""
|
|
324
|
+
Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
|
|
325
|
+
all videos in [input_video_folder].
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
input_video_folder (str): video folder to process
|
|
329
|
+
frame_callback (function): callback to run on frames, should take an np.array and a string and
|
|
330
|
+
return a single value. callback should expect PIL-formatted (RGB) images.
|
|
331
|
+
every_n_frames (int, optional): sample every Nth frame starting from the first frame;
|
|
332
|
+
if this is None or 1, every frame is processed.
|
|
333
|
+
verbose (bool, optional): enable additional debug console output
|
|
334
|
+
allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
|
|
335
|
+
frames (by default, this is an error).
|
|
336
|
+
recursive (bool, optional): recurse into [input_video_folder]
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
dict: dict with keys 'video_filenames' (list of str), 'frame_rates' (list of floats),
|
|
340
|
+
'results' (list of list of dicts). 'video_filenames' will contain *relative* filenames.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
to_return = {'video_filenames':[],'frame_rates':[],'results':[]}
|
|
344
|
+
|
|
345
|
+
# Recursively enumerate video files
|
|
346
|
+
input_files_full_paths = find_videos(input_video_folder,
|
|
347
|
+
recursive=recursive,
|
|
348
|
+
convert_slashes=True,
|
|
349
|
+
return_relative_paths=False)
|
|
350
|
+
print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_video_folder))
|
|
351
|
+
|
|
352
|
+
if len(input_files_full_paths) == 0:
|
|
353
|
+
return to_return
|
|
354
|
+
|
|
355
|
+
# Process each video
|
|
356
|
+
|
|
357
|
+
# video_fn_abs = input_files_full_paths[0]
|
|
358
|
+
for video_fn_abs in tqdm(input_files_full_paths):
|
|
359
|
+
video_results = run_callback_on_frames(input_video_file=video_fn_abs,
|
|
360
|
+
frame_callback=frame_callback,
|
|
361
|
+
every_n_frames=every_n_frames,
|
|
362
|
+
verbose=verbose,
|
|
363
|
+
frames_to_process=None,
|
|
364
|
+
allow_empty_videos=allow_empty_videos)
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
dict: dict with keys 'frame_filenames' (list), 'frame_rate' (float), 'results' (list).
|
|
368
|
+
'frame_filenames' are synthetic filenames (e.g. frame000000.jpg); 'results' are
|
|
369
|
+
in the same format used in the 'images' array in the MD results format.
|
|
370
|
+
"""
|
|
371
|
+
video_filename_relative = os.path.relpath(video_fn_abs,input_video_folder)
|
|
372
|
+
video_filename_relative = video_filename_relative.replace('\\','/')
|
|
373
|
+
to_return['video_filenames'].append(video_filename_relative)
|
|
374
|
+
to_return['frame_rates'].append(video_results['frame_rate'])
|
|
375
|
+
for r in video_results['results']:
|
|
376
|
+
assert r['file'].startswith('frame')
|
|
377
|
+
r['file'] = video_filename_relative + '/' + r['file']
|
|
378
|
+
to_return['results'].append(video_results['results'])
|
|
379
|
+
|
|
380
|
+
# ...for each video
|
|
381
|
+
|
|
382
|
+
n_videos = len(input_files_full_paths)
|
|
383
|
+
assert len(to_return['video_filenames']) == n_videos
|
|
384
|
+
assert len(to_return['frame_rates']) == n_videos
|
|
385
|
+
assert len(to_return['results']) == n_videos
|
|
211
386
|
|
|
387
|
+
return to_return
|
|
212
388
|
|
|
389
|
+
# ...def run_callback_on_frames_for_folder(...)
|
|
390
|
+
|
|
391
|
+
|
|
213
392
|
def video_to_frames(input_video_file,
|
|
214
393
|
output_folder,
|
|
215
394
|
overwrite=True,
|
|
@@ -220,7 +399,7 @@ def video_to_frames(input_video_file,
|
|
|
220
399
|
frames_to_extract=None,
|
|
221
400
|
allow_empty_videos=False):
|
|
222
401
|
"""
|
|
223
|
-
Renders frames from [input_video_file] to
|
|
402
|
+
Renders frames from [input_video_file] to .jpg files in [output_folder].
|
|
224
403
|
|
|
225
404
|
With help from:
|
|
226
405
|
|
|
@@ -341,7 +520,7 @@ def video_to_frames(input_video_file,
|
|
|
341
520
|
# ...if we need to check whether to skip this video entirely
|
|
342
521
|
|
|
343
522
|
if verbose:
|
|
344
|
-
print('
|
|
523
|
+
print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,Fs))
|
|
345
524
|
|
|
346
525
|
frame_filenames = []
|
|
347
526
|
|
|
@@ -410,8 +589,8 @@ def video_to_frames(input_video_file,
|
|
|
410
589
|
|
|
411
590
|
# ...if we need to deal with resizing
|
|
412
591
|
|
|
413
|
-
|
|
414
|
-
frame_filename = os.path.join(output_folder,
|
|
592
|
+
frame_filename_relative = _frame_number_to_filename(frame_number)
|
|
593
|
+
frame_filename = os.path.join(output_folder,frame_filename_relative)
|
|
415
594
|
frame_filenames.append(frame_filename)
|
|
416
595
|
|
|
417
596
|
if overwrite == False and os.path.isfile(frame_filename):
|
|
@@ -441,9 +620,13 @@ def video_to_frames(input_video_file,
|
|
|
441
620
|
except Exception as e:
|
|
442
621
|
print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
|
|
443
622
|
|
|
623
|
+
# ...for each frame
|
|
624
|
+
|
|
444
625
|
if len(frame_filenames) == 0:
|
|
445
|
-
|
|
446
|
-
input_video_file))
|
|
626
|
+
if allow_empty_videos:
|
|
627
|
+
print('Warning: found no frames in file {}'.format(input_video_file))
|
|
628
|
+
else:
|
|
629
|
+
raise Exception('Error: found no frames in file {}'.format(input_video_file))
|
|
447
630
|
|
|
448
631
|
if verbose:
|
|
449
632
|
print('\nExtracted {} of {} frames for {}'.format(
|
|
@@ -457,7 +640,7 @@ def video_to_frames(input_video_file,
|
|
|
457
640
|
|
|
458
641
|
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
459
642
|
every_n_frames,overwrite,verbose,quality,max_width,
|
|
460
|
-
frames_to_extract):
|
|
643
|
+
frames_to_extract,allow_empty_videos):
|
|
461
644
|
"""
|
|
462
645
|
Internal function to call video_to_frames for a single video in the context of
|
|
463
646
|
video_folder_to_frames; makes sure the right output folder exists, then calls
|
|
@@ -474,10 +657,15 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
|
474
657
|
|
|
475
658
|
# Render frames
|
|
476
659
|
# input_video_file = input_fn_absolute; output_folder = output_folder_video
|
|
477
|
-
frame_filenames,fs = video_to_frames(input_fn_absolute,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
660
|
+
frame_filenames,fs = video_to_frames(input_fn_absolute,
|
|
661
|
+
output_folder_video,
|
|
662
|
+
overwrite=overwrite,
|
|
663
|
+
every_n_frames=every_n_frames,
|
|
664
|
+
verbose=verbose,
|
|
665
|
+
quality=quality,
|
|
666
|
+
max_width=max_width,
|
|
667
|
+
frames_to_extract=frames_to_extract,
|
|
668
|
+
allow_empty_videos=allow_empty_videos)
|
|
481
669
|
|
|
482
670
|
return frame_filenames,fs
|
|
483
671
|
|
|
@@ -487,7 +675,7 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
487
675
|
n_threads=1, every_n_frames=None,
|
|
488
676
|
verbose=False, parallelization_uses_threads=True,
|
|
489
677
|
quality=None, max_width=None,
|
|
490
|
-
frames_to_extract=None):
|
|
678
|
+
frames_to_extract=None, allow_empty_videos=False):
|
|
491
679
|
"""
|
|
492
680
|
For every video file in input_folder, creates a folder within output_folder_base, and
|
|
493
681
|
renders frame of that video to images in that folder.
|
|
@@ -545,7 +733,7 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
545
733
|
frame_filenames,fs = \
|
|
546
734
|
_video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
|
|
547
735
|
every_n_frames,overwrite,verbose,quality,max_width,
|
|
548
|
-
frames_to_extract)
|
|
736
|
+
frames_to_extract,allow_empty_videos)
|
|
549
737
|
frame_filenames_by_video.append(frame_filenames)
|
|
550
738
|
fs_by_video.append(fs)
|
|
551
739
|
else:
|
|
@@ -563,7 +751,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
563
751
|
verbose=verbose,
|
|
564
752
|
quality=quality,
|
|
565
753
|
max_width=max_width,
|
|
566
|
-
frames_to_extract=frames_to_extract
|
|
754
|
+
frames_to_extract=frames_to_extract,
|
|
755
|
+
allow_empty_videos=allow_empty_videos)
|
|
567
756
|
results = list(tqdm(pool.imap(
|
|
568
757
|
partial(process_video_with_options),input_files_relative_paths),
|
|
569
758
|
total=len(input_files_relative_paths)))
|
|
@@ -592,7 +781,8 @@ class FrameToVideoOptions:
|
|
|
592
781
|
self.non_video_behavior = 'error'
|
|
593
782
|
|
|
594
783
|
|
|
595
|
-
def frame_results_to_video_results(input_file,output_file,options=None
|
|
784
|
+
def frame_results_to_video_results(input_file,output_file,options=None,
|
|
785
|
+
video_filename_to_frame_rate=None):
|
|
596
786
|
"""
|
|
597
787
|
Given an MD results file produced at the *frame* level, corresponding to a directory
|
|
598
788
|
created with video_folder_to_frames, maps those frame-level results back to the
|
|
@@ -605,6 +795,8 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
605
795
|
output_file (str): the .json file to which we should write video-level results
|
|
606
796
|
options (FrameToVideoOptions, optional): parameters for converting frame-level results
|
|
607
797
|
to video-level results, see FrameToVideoOptions for details
|
|
798
|
+
video_filename_to_frame_rate (dict): maps (relative) video path names to frame rates,
|
|
799
|
+
used only to populate the output file
|
|
608
800
|
"""
|
|
609
801
|
|
|
610
802
|
if options is None:
|
|
@@ -693,6 +885,10 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
693
885
|
im_out['file'] = video_name
|
|
694
886
|
im_out['detections'] = canonical_detections
|
|
695
887
|
|
|
888
|
+
if (video_filename_to_frame_rate is not None) and \
|
|
889
|
+
(video_name in video_filename_to_frame_rate):
|
|
890
|
+
im_out['frame_rate'] = video_filename_to_frame_rate[video_name]
|
|
891
|
+
|
|
696
892
|
# 'max_detection_conf' is no longer included in output files by default
|
|
697
893
|
if False:
|
|
698
894
|
im_out['max_detection_conf'] = 0
|