megadetector 5.0.11__py3-none-any.whl → 5.0.12__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/api/__init__.py +0 -0
- megadetector/api/batch_processing/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
- megadetector/api/batch_processing/api_core/server.py +294 -0
- megadetector/api/batch_processing/api_core/server_api_config.py +98 -0
- megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
- megadetector/api/batch_processing/api_core/server_job_status_table.py +152 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +92 -0
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +126 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +266 -0
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +627 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +855 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +610 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +699 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +506 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +34 -0
- megadetector/data_management/camtrap_dp_to_coco.py +239 -0
- megadetector/data_management/cct_json_utils.py +395 -0
- megadetector/data_management/cct_to_md.py +176 -0
- megadetector/data_management/cct_to_wi.py +289 -0
- megadetector/data_management/coco_to_labelme.py +272 -0
- megadetector/data_management/coco_to_yolo.py +662 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
- megadetector/data_management/databases/integrity_check_json_db.py +477 -0
- megadetector/data_management/databases/subset_json_db.py +115 -0
- megadetector/data_management/generate_crops_from_cct.py +149 -0
- megadetector/data_management/get_image_sizes.py +189 -0
- megadetector/data_management/importers/add_nacti_sizes.py +52 -0
- megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
- megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
- megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
- megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
- megadetector/data_management/importers/awc_to_json.py +191 -0
- megadetector/data_management/importers/bellevue_to_json.py +273 -0
- megadetector/data_management/importers/cacophony-thermal-importer.py +796 -0
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
- megadetector/data_management/importers/cct_field_adjustments.py +58 -0
- megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
- megadetector/data_management/importers/ena24_to_json.py +276 -0
- megadetector/data_management/importers/filenames_to_json.py +386 -0
- megadetector/data_management/importers/helena_to_cct.py +283 -0
- megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
- megadetector/data_management/importers/jb_csv_to_json.py +150 -0
- megadetector/data_management/importers/mcgill_to_json.py +250 -0
- megadetector/data_management/importers/missouri_to_json.py +490 -0
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
- megadetector/data_management/importers/noaa_seals_2019.py +181 -0
- megadetector/data_management/importers/pc_to_json.py +365 -0
- megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
- megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
- megadetector/data_management/importers/rspb_to_json.py +356 -0
- megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
- megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
- megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
- megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
- megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
- megadetector/data_management/importers/sulross_get_exif.py +65 -0
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
- megadetector/data_management/importers/ubc_to_json.py +399 -0
- megadetector/data_management/importers/umn_to_json.py +507 -0
- megadetector/data_management/importers/wellington_to_json.py +263 -0
- megadetector/data_management/importers/wi_to_json.py +442 -0
- megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
- megadetector/data_management/labelme_to_coco.py +547 -0
- megadetector/data_management/labelme_to_yolo.py +272 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
- megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
- megadetector/data_management/lila/create_lila_blank_set.py +558 -0
- megadetector/data_management/lila/create_lila_test_set.py +152 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +178 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +300 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
- megadetector/data_management/ocr_tools.py +874 -0
- megadetector/data_management/read_exif.py +681 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +246 -0
- megadetector/data_management/yolo_output_to_md_output.py +441 -0
- megadetector/data_management/yolo_to_coco.py +676 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +114 -0
- megadetector/detection/process_video.py +702 -0
- megadetector/detection/pytorch_detector.py +341 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +917 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +189 -0
- megadetector/detection/video_utils.py +606 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +163 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +958 -0
- megadetector/postprocessing/convert_output_format.py +396 -0
- megadetector/postprocessing/load_api_results.py +195 -0
- megadetector/postprocessing/md_to_coco.py +310 -0
- megadetector/postprocessing/md_to_labelme.py +330 -0
- megadetector/postprocessing/merge_detections.py +401 -0
- megadetector/postprocessing/postprocess_batch_results.py +1902 -0
- megadetector/postprocessing/remap_detection_categories.py +170 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1631 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +696 -0
- megadetector/postprocessing/top_folders_to_bottom.py +223 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +590 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +219 -0
- megadetector/taxonomy_mapping/species_lookup.py +834 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/azure_utils.py +178 -0
- megadetector/utils/ct_utils.py +612 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +968 -0
- megadetector/utils/path_utils.py +1044 -0
- megadetector/utils/process_utils.py +157 -0
- megadetector/utils/sas_blob_utils.py +509 -0
- megadetector/utils/split_locations_into_train_val.py +228 -0
- megadetector/utils/string_utils.py +92 -0
- megadetector/utils/url_utils.py +323 -0
- megadetector/utils/write_html_image_list.py +225 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +293 -0
- megadetector/visualization/render_images_with_thumbnails.py +275 -0
- megadetector/visualization/visualization_utils.py +1536 -0
- megadetector/visualization/visualize_db.py +550 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
- megadetector-5.0.12.dist-info/RECORD +199 -0
- megadetector-5.0.12.dist-info/top_level.txt +1 -0
- megadetector-5.0.11.dist-info/RECORD +0 -5
- megadetector-5.0.11.dist-info/top_level.txt +0 -1
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
video_utils.py
|
|
4
|
+
|
|
5
|
+
Utilities for splitting, rendering, and assembling videos.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
#%% Constants, imports, environment
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import cv2
|
|
13
|
+
import glob
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from multiprocessing.pool import ThreadPool
|
|
18
|
+
from multiprocessing.pool import Pool
|
|
19
|
+
from tqdm import tqdm
|
|
20
|
+
from functools import partial
|
|
21
|
+
|
|
22
|
+
from megadetector.utils import path_utils
|
|
23
|
+
from megadetector.visualization import visualization_utils as vis_utils
|
|
24
|
+
|
|
25
|
+
default_fourcc = 'h264'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#%% Path utilities
|
|
29
|
+
|
|
30
|
+
VIDEO_EXTENSIONS = ('.mp4','.avi','.mpeg','.mpg')
|
|
31
|
+
|
|
32
|
+
def is_video_file(s,video_extensions=VIDEO_EXTENSIONS):
|
|
33
|
+
"""
|
|
34
|
+
Checks a file's extension against a set of known video file
|
|
35
|
+
extensions to determine whether it's a video file. Performs a
|
|
36
|
+
case-insensitive comparison.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
s (str): filename to check for probable video-ness
|
|
40
|
+
video_extensions (list, optional): list of video file extensions
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
bool: True if this looks like a video file, else False
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
ext = os.path.splitext(s)[1]
|
|
47
|
+
return ext.lower() in video_extensions
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_video_strings(strings):
|
|
51
|
+
"""
|
|
52
|
+
Given a list of strings that are potentially video file names, looks for
|
|
53
|
+
strings that actually look like video file names (based on extension).
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
strings (list): list of strings to check for video-ness
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
list: a subset of [strings] that looks like they are video filenames
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
return [s for s in strings if is_video_file(s.lower())]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_videos(dirname,
|
|
66
|
+
recursive=False,
|
|
67
|
+
convert_slashes=True,
|
|
68
|
+
return_relative_paths=False):
|
|
69
|
+
"""
|
|
70
|
+
Finds all files in a directory that look like video file names.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
dirname (str): folder to search for video files
|
|
74
|
+
recursive (bool, optional): whether to search [dirname] recursively
|
|
75
|
+
convert_slashes (bool, optional): forces forward slashes in the returned files,
|
|
76
|
+
otherwise uses the native path separator
|
|
77
|
+
return_relative_paths (bool, optional): forces the returned filenames to be
|
|
78
|
+
relative to [dirname], otherwise returns absolute paths
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A list of filenames within [dirname] that appear to be videos
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if recursive:
|
|
85
|
+
files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
|
|
86
|
+
else:
|
|
87
|
+
files = glob.glob(os.path.join(dirname, '*.*'))
|
|
88
|
+
|
|
89
|
+
if return_relative_paths:
|
|
90
|
+
files = [os.path.relpath(fn,dirname) for fn in files]
|
|
91
|
+
|
|
92
|
+
if convert_slashes:
|
|
93
|
+
files = [fn.replace('\\', '/') for fn in files]
|
|
94
|
+
|
|
95
|
+
return find_video_strings(files)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#%% Function for rendering frames to video and vice-versa
|
|
99
|
+
|
|
100
|
+
# http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
|
|
101
|
+
|
|
102
|
+
def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
|
|
103
|
+
"""
|
|
104
|
+
Given a list of image files and a sample rate, concatenates those images into
|
|
105
|
+
a video and writes to a new video file.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
images (list): a list of frame file names to concatenate into a video
|
|
109
|
+
Fs (float): the frame rate in fps
|
|
110
|
+
output_file_name (str): the output video file, no checking is performed to make
|
|
111
|
+
sure the extension is compatible with the codec
|
|
112
|
+
codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
|
|
113
|
+
and generally works on Windows, but when this fails (which is around 50% of the time
|
|
114
|
+
on Linux), mp4v is a good second choice
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
if codec_spec is None:
|
|
118
|
+
codec_spec = 'h264'
|
|
119
|
+
|
|
120
|
+
if len(images) == 0:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Determine the width and height from the first image
|
|
124
|
+
frame = cv2.imread(images[0])
|
|
125
|
+
cv2.imshow('video',frame)
|
|
126
|
+
height, width, channels = frame.shape
|
|
127
|
+
|
|
128
|
+
# Define the codec and create VideoWriter object
|
|
129
|
+
fourcc = cv2.VideoWriter_fourcc(*codec_spec)
|
|
130
|
+
out = cv2.VideoWriter(output_file_name, fourcc, Fs, (width, height))
|
|
131
|
+
|
|
132
|
+
for image in images:
|
|
133
|
+
frame = cv2.imread(image)
|
|
134
|
+
out.write(frame)
|
|
135
|
+
|
|
136
|
+
out.release()
|
|
137
|
+
cv2.destroyAllWindows()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_video_fs(input_video_file):
|
|
141
|
+
"""
|
|
142
|
+
Retrieves the frame rate of [input_video_file].
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
input_video_file (str): video file for which we want the frame rate
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
float: the frame rate of [input_video_file]
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
|
|
152
|
+
vidcap = cv2.VideoCapture(input_video_file)
|
|
153
|
+
Fs = vidcap.get(cv2.CAP_PROP_FPS)
|
|
154
|
+
vidcap.release()
|
|
155
|
+
return Fs
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _frame_number_to_filename(frame_number):
|
|
159
|
+
"""
|
|
160
|
+
Ensures that frame images are given consistent filenames.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
return 'frame{:06d}.jpg'.format(frame_number)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
167
|
+
every_n_frames=None, verbose=False):
|
|
168
|
+
"""
|
|
169
|
+
Renders frames from [input_video_file] to a .jpg in [output_folder].
|
|
170
|
+
|
|
171
|
+
With help from:
|
|
172
|
+
|
|
173
|
+
https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
input_video_file (str): video file to split into frames
|
|
177
|
+
output_folder (str): folder to put frame images in
|
|
178
|
+
overwrite (bool, optional): whether to overwrite existing frame images
|
|
179
|
+
every_n_frames (int, optional): sample every Nth frame starting from the first frame;
|
|
180
|
+
if this is None or 1, every frame is extracted
|
|
181
|
+
verbose (bool, optional): enable additional debug console output
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
tuple: length-2 tuple containing (list of frame filenames,frame rate)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
|
|
188
|
+
|
|
189
|
+
vidcap = cv2.VideoCapture(input_video_file)
|
|
190
|
+
n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
191
|
+
Fs = vidcap.get(cv2.CAP_PROP_FPS)
|
|
192
|
+
|
|
193
|
+
# If we're not over-writing, check whether all frame images already exist
|
|
194
|
+
if overwrite == False:
|
|
195
|
+
|
|
196
|
+
missing_frame_number = None
|
|
197
|
+
frame_filenames = []
|
|
198
|
+
|
|
199
|
+
for frame_number in range(0,n_frames):
|
|
200
|
+
|
|
201
|
+
if every_n_frames is not None:
|
|
202
|
+
if frame_number % every_n_frames != 0:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
frame_filename = _frame_number_to_filename(frame_number)
|
|
206
|
+
frame_filename = os.path.join(output_folder,frame_filename)
|
|
207
|
+
frame_filenames.append(frame_filename)
|
|
208
|
+
if os.path.isfile(frame_filename):
|
|
209
|
+
continue
|
|
210
|
+
else:
|
|
211
|
+
missing_frame_number = frame_number
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
# OpenCV seems to over-report the number of frames by 1 in some cases, or fails
|
|
215
|
+
# to read the last frame; either way, I'm allowing one missing frame.
|
|
216
|
+
allow_last_frame_missing = True
|
|
217
|
+
|
|
218
|
+
if missing_frame_number is None or \
|
|
219
|
+
(allow_last_frame_missing and (missing_frame_number == n_frames-1)):
|
|
220
|
+
if verbose:
|
|
221
|
+
print('Skipping video {}, all output frames exist'.format(input_video_file))
|
|
222
|
+
return frame_filenames,Fs
|
|
223
|
+
else:
|
|
224
|
+
pass
|
|
225
|
+
# print("Rendering video {}, couldn't find frame {}".format(
|
|
226
|
+
# input_video_file,missing_frame_number))
|
|
227
|
+
|
|
228
|
+
# ...if we need to check whether to skip this video entirely
|
|
229
|
+
|
|
230
|
+
if verbose:
|
|
231
|
+
print('Reading {} frames at {} Hz from {}'.format(n_frames,Fs,input_video_file))
|
|
232
|
+
|
|
233
|
+
frame_filenames = []
|
|
234
|
+
|
|
235
|
+
# for frame_number in tqdm(range(0,n_frames)):
|
|
236
|
+
for frame_number in range(0,n_frames):
|
|
237
|
+
|
|
238
|
+
success,image = vidcap.read()
|
|
239
|
+
if not success:
|
|
240
|
+
assert image is None
|
|
241
|
+
if verbose:
|
|
242
|
+
print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
if every_n_frames is not None:
|
|
246
|
+
if frame_number % every_n_frames != 0:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
frame_filename = _frame_number_to_filename(frame_number)
|
|
250
|
+
frame_filename = os.path.join(output_folder,frame_filename)
|
|
251
|
+
frame_filenames.append(frame_filename)
|
|
252
|
+
|
|
253
|
+
if overwrite == False and os.path.isfile(frame_filename):
|
|
254
|
+
# print('Skipping frame {}'.format(frame_filename))
|
|
255
|
+
pass
|
|
256
|
+
else:
|
|
257
|
+
try:
|
|
258
|
+
if frame_filename.isascii():
|
|
259
|
+
cv2.imwrite(os.path.normpath(frame_filename),image)
|
|
260
|
+
else:
|
|
261
|
+
is_success, im_buf_arr = cv2.imencode('.jpg', image)
|
|
262
|
+
im_buf_arr.tofile(frame_filename)
|
|
263
|
+
assert os.path.isfile(frame_filename), \
|
|
264
|
+
'Output frame {} unavailable'.format(frame_filename)
|
|
265
|
+
except KeyboardInterrupt:
|
|
266
|
+
vidcap.release()
|
|
267
|
+
raise
|
|
268
|
+
except Exception as e:
|
|
269
|
+
print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
|
|
270
|
+
|
|
271
|
+
if verbose:
|
|
272
|
+
print('\nExtracted {} of {} frames'.format(len(frame_filenames),n_frames))
|
|
273
|
+
|
|
274
|
+
vidcap.release()
|
|
275
|
+
return frame_filenames,Fs
|
|
276
|
+
|
|
277
|
+
# ...def video_to_frames(...)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,every_n_frames,overwrite,verbose):
|
|
281
|
+
"""
|
|
282
|
+
Internal function to call video_to_frames in the context of video_folder_to_frames;
|
|
283
|
+
makes sure the right output folder exists, then calls video_to_frames.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
input_fn_absolute = os.path.join(input_folder,relative_fn)
|
|
287
|
+
assert os.path.isfile(input_fn_absolute),\
|
|
288
|
+
'Could not find file {}'.format(input_fn_absolute)
|
|
289
|
+
|
|
290
|
+
# Create the target output folder
|
|
291
|
+
output_folder_video = os.path.join(output_folder_base,relative_fn)
|
|
292
|
+
os.makedirs(output_folder_video,exist_ok=True)
|
|
293
|
+
|
|
294
|
+
# Render frames
|
|
295
|
+
# input_video_file = input_fn_absolute; output_folder = output_folder_video
|
|
296
|
+
frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
|
|
297
|
+
overwrite=overwrite,every_n_frames=every_n_frames,
|
|
298
|
+
verbose=verbose)
|
|
299
|
+
|
|
300
|
+
return frame_filenames,fs
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def video_folder_to_frames(input_folder, output_folder_base,
|
|
304
|
+
recursive=True, overwrite=True,
|
|
305
|
+
n_threads=1, every_n_frames=None,
|
|
306
|
+
verbose=False, parallelization_uses_threads=True):
|
|
307
|
+
"""
|
|
308
|
+
For every video file in input_folder, creates a folder within output_folder_base, and
|
|
309
|
+
renders frame of that video to images in that folder.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
input_folder (str): folder to process
|
|
313
|
+
output_folder_base (str): root folder for output images; subfolders will be
|
|
314
|
+
created for each input video
|
|
315
|
+
recursive (bool, optional): whether to recursively process videos in [input_folder]
|
|
316
|
+
overwrite (bool, optional): whether to overwrite existing frame images
|
|
317
|
+
n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
|
|
318
|
+
parallelism
|
|
319
|
+
every_n_frames (int, optional): sample every Nth frame starting from the first frame;
|
|
320
|
+
if this is None or 1, every frame is extracted
|
|
321
|
+
verbose (bool, optional): enable additional debug console output
|
|
322
|
+
parallelization_uses_threads (bool, optional): whether to use threads (True) or
|
|
323
|
+
processes (False) for parallelization; ignored if n_threads <= 1
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
tuple: a length-3 tuple containing:
|
|
327
|
+
- list of lists of frame filenames; the Nth list of frame filenames corresponds to
|
|
328
|
+
the Nth video
|
|
329
|
+
- list of video frame rates; the Nth value corresponds to the Nth video
|
|
330
|
+
- list of video filenames
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
# Recursively enumerate video files
|
|
334
|
+
input_files_full_paths = find_videos(input_folder,recursive=recursive)
|
|
335
|
+
print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
|
|
336
|
+
if len(input_files_full_paths) == 0:
|
|
337
|
+
return [],[],[]
|
|
338
|
+
|
|
339
|
+
input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
|
|
340
|
+
input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
|
|
341
|
+
|
|
342
|
+
os.makedirs(output_folder_base,exist_ok=True)
|
|
343
|
+
|
|
344
|
+
frame_filenames_by_video = []
|
|
345
|
+
fs_by_video = []
|
|
346
|
+
|
|
347
|
+
if n_threads == 1:
|
|
348
|
+
# For each video
|
|
349
|
+
#
|
|
350
|
+
# input_fn_relative = input_files_relative_paths[0]
|
|
351
|
+
for input_fn_relative in tqdm(input_files_relative_paths):
|
|
352
|
+
|
|
353
|
+
frame_filenames,fs = \
|
|
354
|
+
_video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
|
|
355
|
+
every_n_frames,overwrite,verbose)
|
|
356
|
+
frame_filenames_by_video.append(frame_filenames)
|
|
357
|
+
fs_by_video.append(fs)
|
|
358
|
+
else:
|
|
359
|
+
if parallelization_uses_threads:
|
|
360
|
+
print('Starting a worker pool with {} threads'.format(n_threads))
|
|
361
|
+
pool = ThreadPool(n_threads)
|
|
362
|
+
else:
|
|
363
|
+
print('Starting a worker pool with {} processes'.format(n_threads))
|
|
364
|
+
pool = Pool(n_threads)
|
|
365
|
+
process_video_with_options = partial(_video_to_frames_for_folder,
|
|
366
|
+
input_folder=input_folder,
|
|
367
|
+
output_folder_base=output_folder_base,
|
|
368
|
+
every_n_frames=every_n_frames,
|
|
369
|
+
overwrite=overwrite,
|
|
370
|
+
verbose=verbose)
|
|
371
|
+
results = list(tqdm(pool.imap(
|
|
372
|
+
partial(process_video_with_options),input_files_relative_paths),
|
|
373
|
+
total=len(input_files_relative_paths)))
|
|
374
|
+
frame_filenames_by_video = [x[0] for x in results]
|
|
375
|
+
fs_by_video = [x[1] for x in results]
|
|
376
|
+
|
|
377
|
+
return frame_filenames_by_video,fs_by_video,input_files_full_paths
|
|
378
|
+
|
|
379
|
+
# ...def video_folder_to_frames(...)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class FrameToVideoOptions:
|
|
383
|
+
"""
|
|
384
|
+
Options controlling the conversion of frame-level results to video-level results via
|
|
385
|
+
frame_results_to_video_results()
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
#: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
|
|
389
|
+
#: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
|
|
390
|
+
nth_highest_confidence = 1
|
|
391
|
+
|
|
392
|
+
#: What to do if a file referred to in a .json results file appears not to be a
|
|
393
|
+
#: video; can be 'error' or 'skip_with_warning'
|
|
394
|
+
non_video_behavior = 'error'
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def frame_results_to_video_results(input_file,output_file,options=None):
|
|
398
|
+
"""
|
|
399
|
+
Given an MD results file produced at the *frame* level, corresponding to a directory
|
|
400
|
+
created with video_folder_to_frames, maps those frame-level results back to the
|
|
401
|
+
video level for use in Timelapse.
|
|
402
|
+
|
|
403
|
+
Preserves everything in the input .json file other than the images.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
input_file (str): the frame-level MD results file to convert to video-level results
|
|
407
|
+
output_file (str): the .json file to which we should write video-level results
|
|
408
|
+
options (FrameToVideoOptions, optional): parameters for converting frame-level results
|
|
409
|
+
to video-level results, see FrameToVideoOptions for details
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
if options is None:
|
|
413
|
+
options = FrameToVideoOptions()
|
|
414
|
+
|
|
415
|
+
# Load results
|
|
416
|
+
with open(input_file,'r') as f:
|
|
417
|
+
input_data = json.load(f)
|
|
418
|
+
|
|
419
|
+
images = input_data['images']
|
|
420
|
+
detection_categories = input_data['detection_categories']
|
|
421
|
+
|
|
422
|
+
## Break into videos
|
|
423
|
+
|
|
424
|
+
video_to_frames = defaultdict(list)
|
|
425
|
+
|
|
426
|
+
# im = images[0]
|
|
427
|
+
for im in tqdm(images):
|
|
428
|
+
|
|
429
|
+
fn = im['file']
|
|
430
|
+
video_name = os.path.dirname(fn)
|
|
431
|
+
if not is_video_file(video_name):
|
|
432
|
+
if options.non_video_behavior == 'error':
|
|
433
|
+
raise ValueError('{} is not a video file'.format(video_name))
|
|
434
|
+
elif options.non_video_behavior == 'skip_with_warning':
|
|
435
|
+
print('Warning: {} is not a video file'.format(video_name))
|
|
436
|
+
continue
|
|
437
|
+
else:
|
|
438
|
+
raise ValueError('Unrecognized non-video handling behavior: {}'.format(
|
|
439
|
+
options.non_video_behavior))
|
|
440
|
+
video_to_frames[video_name].append(im)
|
|
441
|
+
|
|
442
|
+
print('Found {} unique videos in {} frame-level results'.format(
|
|
443
|
+
len(video_to_frames),len(images)))
|
|
444
|
+
|
|
445
|
+
output_images = []
|
|
446
|
+
|
|
447
|
+
## For each video...
|
|
448
|
+
|
|
449
|
+
# video_name = list(video_to_frames.keys())[0]
|
|
450
|
+
for video_name in tqdm(video_to_frames):
|
|
451
|
+
|
|
452
|
+
frames = video_to_frames[video_name]
|
|
453
|
+
|
|
454
|
+
all_detections_this_video = []
|
|
455
|
+
|
|
456
|
+
# frame = frames[0]
|
|
457
|
+
for frame in frames:
|
|
458
|
+
if frame['detections'] is not None:
|
|
459
|
+
all_detections_this_video.extend(frame['detections'])
|
|
460
|
+
|
|
461
|
+
# At most one detection for each category for the whole video
|
|
462
|
+
canonical_detections = []
|
|
463
|
+
|
|
464
|
+
# category_id = list(detection_categories.keys())[0]
|
|
465
|
+
for category_id in detection_categories:
|
|
466
|
+
|
|
467
|
+
category_detections = [det for det in all_detections_this_video if \
|
|
468
|
+
det['category'] == category_id]
|
|
469
|
+
|
|
470
|
+
# Find the nth-highest-confidence video to choose a confidence value
|
|
471
|
+
if len(category_detections) >= options.nth_highest_confidence:
|
|
472
|
+
|
|
473
|
+
category_detections_by_confidence = sorted(category_detections,
|
|
474
|
+
key = lambda i: i['conf'],reverse=True)
|
|
475
|
+
canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
|
|
476
|
+
canonical_detections.append(canonical_detection)
|
|
477
|
+
|
|
478
|
+
# Prepare the output representation for this video
|
|
479
|
+
im_out = {}
|
|
480
|
+
im_out['file'] = video_name
|
|
481
|
+
im_out['detections'] = canonical_detections
|
|
482
|
+
|
|
483
|
+
# 'max_detection_conf' is no longer included in output files by default
|
|
484
|
+
if False:
|
|
485
|
+
im_out['max_detection_conf'] = 0
|
|
486
|
+
if len(canonical_detections) > 0:
|
|
487
|
+
confidences = [d['conf'] for d in canonical_detections]
|
|
488
|
+
im_out['max_detection_conf'] = max(confidences)
|
|
489
|
+
|
|
490
|
+
output_images.append(im_out)
|
|
491
|
+
|
|
492
|
+
# ...for each video
|
|
493
|
+
|
|
494
|
+
output_data = input_data
|
|
495
|
+
output_data['images'] = output_images
|
|
496
|
+
s = json.dumps(output_data,indent=1)
|
|
497
|
+
|
|
498
|
+
# Write the output file
|
|
499
|
+
with open(output_file,'w') as f:
|
|
500
|
+
f.write(s)
|
|
501
|
+
|
|
502
|
+
# ...def frame_results_to_video_results(...)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
#%% Test driver
|
|
506
|
+
|
|
507
|
+
if False:
|
|
508
|
+
|
|
509
|
+
#%% Constants
|
|
510
|
+
|
|
511
|
+
Fs = 30.01
|
|
512
|
+
confidence_threshold = 0.75
|
|
513
|
+
input_folder = 'z:\\'
|
|
514
|
+
frame_folder_base = r'e:\video_test\frames'
|
|
515
|
+
detected_frame_folder_base = r'e:\video_test\detected_frames'
|
|
516
|
+
rendered_videos_folder_base = r'e:\video_test\rendered_videos'
|
|
517
|
+
|
|
518
|
+
results_file = r'results.json'
|
|
519
|
+
os.makedirs(detected_frame_folder_base,exist_ok=True)
|
|
520
|
+
os.makedirs(rendered_videos_folder_base,exist_ok=True)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
#%% Split videos into frames
|
|
524
|
+
|
|
525
|
+
frame_filenames_by_video,fs_by_video,video_filenames = \
|
|
526
|
+
video_folder_to_frames(input_folder,frame_folder_base,recursive=True)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
#%% List image files, break into folders
|
|
530
|
+
|
|
531
|
+
frame_files = path_utils.find_images(frame_folder_base,True)
|
|
532
|
+
frame_files = [s.replace('\\','/') for s in frame_files]
|
|
533
|
+
print('Enumerated {} total frames'.format(len(frame_files)))
|
|
534
|
+
|
|
535
|
+
Fs = 30.01
|
|
536
|
+
# Find unique folders
|
|
537
|
+
folders = set()
|
|
538
|
+
# fn = frame_files[0]
|
|
539
|
+
for fn in frame_files:
|
|
540
|
+
folders.add(os.path.dirname(fn))
|
|
541
|
+
folders = [s.replace('\\','/') for s in folders]
|
|
542
|
+
print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
#%% Load detector output
|
|
546
|
+
|
|
547
|
+
with open(results_file,'r') as f:
|
|
548
|
+
detection_results = json.load(f)
|
|
549
|
+
detections = detection_results['images']
|
|
550
|
+
detector_label_map = detection_results['detection_categories']
|
|
551
|
+
for d in detections:
|
|
552
|
+
d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
#%% Render detector frames
|
|
556
|
+
|
|
557
|
+
# folder = list(folders)[0]
|
|
558
|
+
for folder in folders:
|
|
559
|
+
|
|
560
|
+
frame_files_this_folder = [fn for fn in frame_files if folder in fn]
|
|
561
|
+
folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
|
|
562
|
+
detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
|
|
563
|
+
print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
|
|
564
|
+
assert len(frame_files_this_folder) == len(detection_results_this_folder)
|
|
565
|
+
|
|
566
|
+
rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
|
|
567
|
+
os.makedirs(rendered_frame_output_folder,exist_ok=True)
|
|
568
|
+
|
|
569
|
+
# d = detection_results_this_folder[0]
|
|
570
|
+
for d in tqdm(detection_results_this_folder):
|
|
571
|
+
|
|
572
|
+
input_file = os.path.join(frame_folder_base,d['file'])
|
|
573
|
+
output_file = os.path.join(detected_frame_folder_base,d['file'])
|
|
574
|
+
os.makedirs(os.path.dirname(output_file),exist_ok=True)
|
|
575
|
+
vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
|
|
576
|
+
confidence_threshold)
|
|
577
|
+
|
|
578
|
+
# ...for each file in this folder
|
|
579
|
+
|
|
580
|
+
# ...for each folder
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
#%% Render output videos
|
|
584
|
+
|
|
585
|
+
# folder = list(folders)[0]
|
|
586
|
+
for folder in tqdm(folders):
|
|
587
|
+
|
|
588
|
+
folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
|
|
589
|
+
rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
|
|
590
|
+
assert os.path.isdir(rendered_detector_output_folder)
|
|
591
|
+
|
|
592
|
+
frame_files_relative = os.listdir(rendered_detector_output_folder)
|
|
593
|
+
frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
|
|
594
|
+
for s in frame_files_relative]
|
|
595
|
+
|
|
596
|
+
output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
|
|
597
|
+
os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
|
|
598
|
+
|
|
599
|
+
original_video_filename = output_video_filename.replace(
|
|
600
|
+
rendered_videos_folder_base,input_folder)
|
|
601
|
+
assert os.path.isfile(original_video_filename)
|
|
602
|
+
Fs = get_video_fs(original_video_filename)
|
|
603
|
+
|
|
604
|
+
frames_to_video(frame_files_absolute, Fs, output_video_filename)
|
|
605
|
+
|
|
606
|
+
# ...for each video
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
add_max_conf.py
|
|
4
|
+
|
|
5
|
+
The MD output format included a "max_detection_conf" field with each image
|
|
6
|
+
up to and including version 1.2; it was removed as of version 1.3 (it's
|
|
7
|
+
redundant with the individual detection confidence values).
|
|
8
|
+
|
|
9
|
+
Just in case someone took a dependency on that field, this script allows you
|
|
10
|
+
to add it back to an existing .json file.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
#%% Imports and constants
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import json
|
|
18
|
+
from megadetector.utils import ct_utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
#%% Main function
|
|
22
|
+
|
|
23
|
+
def add_max_conf(input_file,output_file):
|
|
24
|
+
|
|
25
|
+
assert os.path.isfile(input_file), "Can't find input file {}".format(input_file)
|
|
26
|
+
|
|
27
|
+
with open(input_file,'r') as f:
|
|
28
|
+
d = json.load(f)
|
|
29
|
+
|
|
30
|
+
for im in d['images']:
|
|
31
|
+
|
|
32
|
+
max_conf = ct_utils.get_max_conf(im)
|
|
33
|
+
|
|
34
|
+
if 'max_detection_conf' in im:
|
|
35
|
+
assert abs(max_conf - im['max_detection_conf']) < 0.00001
|
|
36
|
+
else:
|
|
37
|
+
im['max_detection_conf'] = max_conf
|
|
38
|
+
|
|
39
|
+
with open(output_file,'w') as f:
|
|
40
|
+
json.dump(d,f,indent=1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#%% Driver
|
|
44
|
+
|
|
45
|
+
import sys,argparse
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
|
|
49
|
+
parser = argparse.ArgumentParser()
|
|
50
|
+
parser.add_argument('input_file',type=str,
|
|
51
|
+
help='Input .json file')
|
|
52
|
+
parser.add_argument('output_file',type=str,
|
|
53
|
+
help='Output .json file')
|
|
54
|
+
|
|
55
|
+
if len(sys.argv[1:]) == 0:
|
|
56
|
+
parser.print_help()
|
|
57
|
+
parser.exit()
|
|
58
|
+
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
add_max_conf(args.input_file, args.output_file)
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
main()
|
|
64
|
+
|