megadetector 5.0.12__py3-none-any.whl → 5.0.13__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/batch_processing/api_core/server.py +1 -1
- megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
- megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
- megadetector/api/batch_processing/api_core/server_utils.py +0 -4
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
- megadetector/classification/efficientnet/utils.py +0 -3
- megadetector/data_management/camtrap_dp_to_coco.py +0 -2
- megadetector/data_management/cct_json_utils.py +15 -6
- megadetector/data_management/coco_to_labelme.py +12 -1
- megadetector/data_management/databases/integrity_check_json_db.py +43 -27
- megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
- megadetector/data_management/ocr_tools.py +0 -4
- megadetector/data_management/read_exif.py +171 -43
- megadetector/data_management/rename_images.py +187 -0
- megadetector/data_management/wi_download_csv_to_coco.py +3 -2
- megadetector/data_management/yolo_output_to_md_output.py +7 -2
- megadetector/detection/process_video.py +360 -216
- megadetector/detection/pytorch_detector.py +17 -3
- megadetector/detection/run_inference_with_yolov5_val.py +527 -357
- megadetector/detection/tf_detector.py +3 -0
- megadetector/detection/video_utils.py +122 -30
- megadetector/postprocessing/categorize_detections_by_size.py +16 -14
- megadetector/postprocessing/classification_postprocessing.py +716 -0
- megadetector/postprocessing/compare_batch_results.py +101 -93
- megadetector/postprocessing/merge_detections.py +18 -7
- megadetector/postprocessing/postprocess_batch_results.py +133 -127
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
- megadetector/postprocessing/subset_json_detector_output.py +66 -62
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
- megadetector/utils/ct_utils.py +5 -4
- megadetector/utils/md_tests.py +311 -115
- megadetector/utils/path_utils.py +1 -0
- megadetector/utils/process_utils.py +6 -3
- megadetector/visualization/visualize_db.py +79 -77
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/RECORD +40 -38
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/top_level.txt +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
|
@@ -25,13 +25,14 @@ import argparse
|
|
|
25
25
|
import itertools
|
|
26
26
|
import json
|
|
27
27
|
import shutil
|
|
28
|
+
import getpass
|
|
28
29
|
|
|
29
30
|
from uuid import uuid1
|
|
30
31
|
|
|
31
32
|
from megadetector.detection import run_detector_batch
|
|
32
33
|
from megadetector.visualization import visualize_detector_output
|
|
33
34
|
from megadetector.utils.ct_utils import args_to_object
|
|
34
|
-
from megadetector.utils.path_utils import insert_before_extension
|
|
35
|
+
from megadetector.utils.path_utils import insert_before_extension, clean_path
|
|
35
36
|
from megadetector.detection.video_utils import video_to_frames
|
|
36
37
|
from megadetector.detection.video_utils import frames_to_video
|
|
37
38
|
from megadetector.detection.video_utils import frame_results_to_video_results
|
|
@@ -46,95 +47,234 @@ class ProcessVideoOptions:
|
|
|
46
47
|
Options controlling the behavior of process_video()
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
output_json_file = None
|
|
57
|
-
|
|
58
|
-
#: File to which we should write a video with boxes, only relevant if
|
|
59
|
-
#: render_output_video is True
|
|
60
|
-
output_video_file = None
|
|
61
|
-
|
|
62
|
-
#: Folder to use for extracted frames; will use a folder in system temp space
|
|
63
|
-
#: if this is None
|
|
64
|
-
frame_folder = None
|
|
50
|
+
def __init__(self):
|
|
51
|
+
|
|
52
|
+
#: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
|
|
53
|
+
self.model_file = 'MDV5A'
|
|
54
|
+
|
|
55
|
+
#: Video (of folder of videos) to process
|
|
56
|
+
self.input_video_file = ''
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
#: .json file to which we should write results
|
|
59
|
+
self.output_json_file = None
|
|
60
|
+
|
|
61
|
+
#: File to which we should write a video with boxes, only relevant if
|
|
62
|
+
#: render_output_video is True
|
|
63
|
+
self.output_video_file = None
|
|
64
|
+
|
|
65
|
+
#: Folder to use for extracted frames; will use a folder in system temp space
|
|
66
|
+
#: if this is None
|
|
67
|
+
self.frame_folder = None
|
|
68
|
+
|
|
69
|
+
# Folder to use for rendered frames (if rendering output video); will use a folder
|
|
70
|
+
#: in system temp space if this is None
|
|
71
|
+
self.frame_rendering_folder = None
|
|
72
|
+
|
|
73
|
+
#: Should we render a video with detection boxes?
|
|
74
|
+
#:
|
|
75
|
+
#: Only supported when processing a single video, not a folder.
|
|
76
|
+
self.render_output_video = False
|
|
77
|
+
|
|
78
|
+
#: If we are rendering boxes to a new video, should we keep the temporary
|
|
79
|
+
#: rendered frames?
|
|
80
|
+
self.keep_rendered_frames = False
|
|
81
|
+
|
|
82
|
+
#: Should we keep the extracted frames?
|
|
83
|
+
self.keep_extracted_frames = False
|
|
84
|
+
|
|
85
|
+
#: Should we delete the entire folder the extracted frames are written to?
|
|
86
|
+
#:
|
|
87
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
88
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
89
|
+
self.force_extracted_frame_folder_deletion = False
|
|
90
|
+
|
|
91
|
+
#: Should we delete the entire folder the rendered frames are written to?
|
|
92
|
+
#:
|
|
93
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
94
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
95
|
+
self.force_rendered_frame_folder_deletion = False
|
|
96
|
+
|
|
97
|
+
#: If we've already run MegaDetector on this video or folder of videos, i.e. if we
|
|
98
|
+
#: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
|
|
99
|
+
self.reuse_results_if_available = False
|
|
100
|
+
|
|
101
|
+
#: If we've already split this video or folder of videos into frames, should we
|
|
102
|
+
#: we re-use those extracted frames? Defaults to reprocessing.
|
|
103
|
+
self.reuse_frames_if_available = False
|
|
104
|
+
|
|
105
|
+
#: If [input_video_file] is a folder, should we search for videos recursively?
|
|
106
|
+
self.recursive = False
|
|
107
|
+
|
|
108
|
+
#: Enable additional debug console output
|
|
109
|
+
self.verbose = False
|
|
110
|
+
|
|
111
|
+
#: fourcc code to use for writing videos; only relevant if render_output_video is True
|
|
112
|
+
self.fourcc = None
|
|
69
113
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
#: Confidence threshold to use for writing videos with boxes, only relevant if
|
|
115
|
+
#: if render_output_video is True. Defaults to choosing a reasonable threshold
|
|
116
|
+
#: based on the model version.
|
|
117
|
+
self.rendering_confidence_threshold = None
|
|
118
|
+
|
|
119
|
+
#: Detections below this threshold will not be included in the output file.
|
|
120
|
+
self.json_confidence_threshold = 0.005
|
|
121
|
+
|
|
122
|
+
#: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
|
|
123
|
+
#: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
|
|
124
|
+
#: typical value.
|
|
125
|
+
self.frame_sample = None
|
|
126
|
+
|
|
127
|
+
#: Number of workers to use for parallelization; set to <= 1 to disable parallelization
|
|
128
|
+
self.n_cores = 1
|
|
74
129
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
#: For debugging only, stop processing after a certain number of frames.
|
|
131
|
+
self.debug_max_frames = -1
|
|
132
|
+
|
|
133
|
+
#: File containing non-standard categories, typically only used if you're running a non-MD
|
|
134
|
+
#: detector.
|
|
135
|
+
self.class_mapping_filename = None
|
|
136
|
+
|
|
137
|
+
#: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
|
|
138
|
+
self.quality = 90
|
|
139
|
+
|
|
140
|
+
#: Resize frames so they're at most this wide
|
|
141
|
+
self.max_width = 1600
|
|
78
142
|
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
# ...class ProcessVideoOptions
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
#%% Functions
|
|
147
|
+
|
|
148
|
+
def _select_temporary_output_folders(options):
|
|
149
|
+
"""
|
|
150
|
+
Choose folders in system temp space for writing temporary frames. Does not create folders,
|
|
151
|
+
just defines them.
|
|
152
|
+
"""
|
|
81
153
|
|
|
82
|
-
|
|
83
|
-
#:
|
|
84
|
-
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
85
|
-
#: for no reason other than being paranoid about deleting folders.
|
|
86
|
-
force_extracted_frame_folder_deletion = False
|
|
154
|
+
tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
|
|
87
155
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#: If we've already run MegaDetector on this video or folder of videos, i.e. if we
|
|
95
|
-
#: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
|
|
96
|
-
reuse_results_if_available = False
|
|
156
|
+
# If we create a folder like "process_camera_trap_video" in the system temp dir, it may
|
|
157
|
+
# be the case that no one else can write to it, even to create user-specific subfolders.
|
|
158
|
+
# If we create a uuid-named folder in the system temp dir, we make a mess.
|
|
159
|
+
#
|
|
160
|
+
# Compromise with "process_camera_trap_video-[user]".
|
|
161
|
+
user_tempdir = tempdir + '-' + getpass.getuser()
|
|
97
162
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
# I don't know whether it's possible for a username to contain characters that are
|
|
164
|
+
# not valid filename characters, but just to be sure...
|
|
165
|
+
user_tempdir = clean_path(user_tempdir)
|
|
101
166
|
|
|
102
|
-
|
|
103
|
-
|
|
167
|
+
frame_output_folder = os.path.join(
|
|
168
|
+
user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
|
|
104
169
|
|
|
105
|
-
|
|
106
|
-
|
|
170
|
+
rendering_output_folder = os.path.join(
|
|
171
|
+
tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
|
|
172
|
+
|
|
173
|
+
temporary_folder_info = \
|
|
174
|
+
{
|
|
175
|
+
'temp_folder_base':user_tempdir,
|
|
176
|
+
'frame_output_folder':frame_output_folder,
|
|
177
|
+
'rendering_output_folder':rendering_output_folder
|
|
178
|
+
}
|
|
107
179
|
|
|
108
|
-
|
|
109
|
-
fourcc = None
|
|
180
|
+
return temporary_folder_info
|
|
110
181
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
182
|
+
# ...def _create_frame_output_folders(...)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
|
|
186
|
+
"""
|
|
187
|
+
If necessary, delete rendered frames and/or the entire rendering output folder.
|
|
188
|
+
"""
|
|
118
189
|
|
|
119
|
-
|
|
120
|
-
#: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
|
|
121
|
-
#: typical value.
|
|
122
|
-
frame_sample = None
|
|
190
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
123
191
|
|
|
124
|
-
|
|
125
|
-
|
|
192
|
+
# (Optionally) delete the temporary directory we used for rendered detection images
|
|
193
|
+
if not options.keep_rendered_frames:
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
|
|
197
|
+
# If (a) we're supposed to delete the temporary rendering folder no
|
|
198
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
199
|
+
# whole tree
|
|
200
|
+
if options.force_rendered_frame_folder_deletion and \
|
|
201
|
+
(not caller_provided_rendering_output_folder):
|
|
202
|
+
|
|
203
|
+
if options.verbose:
|
|
204
|
+
print('Recursively deleting rendered frame folder {}'.format(
|
|
205
|
+
rendering_output_folder))
|
|
206
|
+
|
|
207
|
+
shutil.rmtree(rendering_output_folder)
|
|
208
|
+
|
|
209
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
210
|
+
else:
|
|
211
|
+
|
|
212
|
+
if options.force_rendered_frame_folder_deletion:
|
|
213
|
+
assert caller_provided_rendering_output_folder
|
|
214
|
+
print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
|
|
215
|
+
'user-provided folder, only removing frames')
|
|
216
|
+
|
|
217
|
+
for rendered_frame_fn in detected_frame_files:
|
|
218
|
+
os.remove(rendered_frame_fn)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
222
|
+
rendering_output_folder,str(e)))
|
|
223
|
+
pass
|
|
126
224
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
225
|
+
elif options.force_rendered_frame_folder_deletion:
|
|
226
|
+
|
|
227
|
+
print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
|
|
228
|
+
'specified, not deleting')
|
|
229
|
+
|
|
230
|
+
# ...def _clean_up_rendered_frames(...)
|
|
133
231
|
|
|
134
|
-
# ...class ProcessVideoOptions
|
|
135
232
|
|
|
233
|
+
def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
234
|
+
"""
|
|
235
|
+
If necessary, delete extracted frames and/or the entire temporary frame folder.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
239
|
+
|
|
240
|
+
if not options.keep_extracted_frames:
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
|
|
244
|
+
# If (a) we're supposed to delete the temporary frame folder no
|
|
245
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
246
|
+
# whole tree.
|
|
247
|
+
if options.force_extracted_frame_folder_deletion and \
|
|
248
|
+
(not caller_provided_frame_output_folder):
|
|
249
|
+
|
|
250
|
+
if options.verbose:
|
|
251
|
+
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
252
|
+
|
|
253
|
+
shutil.rmtree(frame_output_folder)
|
|
254
|
+
|
|
255
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
256
|
+
else:
|
|
257
|
+
|
|
258
|
+
if options.force_extracted_frame_folder_deletion:
|
|
259
|
+
assert caller_provided_frame_output_folder
|
|
260
|
+
print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
|
|
261
|
+
'user-provided folder, only removing frames')
|
|
262
|
+
|
|
263
|
+
for extracted_frame_fn in frame_filenames:
|
|
264
|
+
os.remove(extracted_frame_fn)
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print('Warning: error removing extracted frames from folder {}:\n{}'.format(
|
|
268
|
+
frame_output_folder,str(e)))
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
elif options.force_extracted_frame_folder_deletion:
|
|
272
|
+
|
|
273
|
+
print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
|
|
274
|
+
'specified, not deleting')
|
|
275
|
+
|
|
276
|
+
# ...def _clean_up_extracted_frames
|
|
136
277
|
|
|
137
|
-
#%% Functions
|
|
138
278
|
|
|
139
279
|
def process_video(options):
|
|
140
280
|
"""
|
|
@@ -154,35 +294,25 @@ def process_video(options):
|
|
|
154
294
|
if options.render_output_video and (options.output_video_file is None):
|
|
155
295
|
options.output_video_file = options.input_video_file + '.detections.mp4'
|
|
156
296
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# TODO:
|
|
161
|
-
#
|
|
162
|
-
# This is a lazy fix to an issue... if multiple users run this script, the
|
|
163
|
-
# "process_camera_trap_video" folder is owned by the first person who creates it, and others
|
|
164
|
-
# can't write to it. I could create uniquely-named folders, but I philosophically prefer
|
|
165
|
-
# to put all the individual UUID-named folders within a larger folder, so as to be a
|
|
166
|
-
# good tempdir citizen. So, the lazy fix is to make this world-writable.
|
|
167
|
-
try:
|
|
168
|
-
os.chmod(tempdir,0o777)
|
|
169
|
-
except Exception:
|
|
170
|
-
pass
|
|
297
|
+
# Track whether frame and rendering folders were created by this script
|
|
298
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
299
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
171
300
|
|
|
172
|
-
|
|
301
|
+
# This does not create any folders, just defines temporary folder names in
|
|
302
|
+
# case we need them.
|
|
303
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
304
|
+
|
|
305
|
+
if (caller_provided_frame_output_folder):
|
|
173
306
|
frame_output_folder = options.frame_folder
|
|
174
307
|
else:
|
|
175
|
-
frame_output_folder =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# TODO: keep track of whether we created this folder, delete if we're deleting the extracted
|
|
179
|
-
# frames and we created the folder, and the output files aren't in the same folder. For now,
|
|
180
|
-
# we're just deleting the extracted frames and leaving the empty folder around in this case.
|
|
308
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
309
|
+
|
|
181
310
|
os.makedirs(frame_output_folder, exist_ok=True)
|
|
182
311
|
|
|
183
312
|
frame_filenames, Fs = video_to_frames(
|
|
184
313
|
options.input_video_file, frame_output_folder,
|
|
185
|
-
every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available)
|
|
314
|
+
every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available),
|
|
315
|
+
quality=options.quality, max_width=options.max_width, verbose=options.verbose)
|
|
186
316
|
|
|
187
317
|
image_file_names = frame_filenames
|
|
188
318
|
if options.debug_max_frames > 0:
|
|
@@ -198,8 +328,8 @@ def process_video(options):
|
|
|
198
328
|
options.model_file, image_file_names,
|
|
199
329
|
confidence_threshold=options.json_confidence_threshold,
|
|
200
330
|
n_cores=options.n_cores,
|
|
201
|
-
|
|
202
|
-
|
|
331
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
332
|
+
quiet=True)
|
|
203
333
|
|
|
204
334
|
run_detector_batch.write_results_to_file(
|
|
205
335
|
results, options.output_json_file,
|
|
@@ -213,15 +343,11 @@ def process_video(options):
|
|
|
213
343
|
if options.render_output_video:
|
|
214
344
|
|
|
215
345
|
# Render detections to images
|
|
216
|
-
if
|
|
346
|
+
if (caller_provided_rendering_output_folder):
|
|
217
347
|
rendering_output_dir = options.frame_rendering_folder
|
|
218
348
|
else:
|
|
219
|
-
rendering_output_dir =
|
|
220
|
-
tempdir, os.path.basename(options.input_video_file) + '_detections')
|
|
349
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
221
350
|
|
|
222
|
-
# TODO: keep track of whether we created this folder, delete if we're deleting the rendered
|
|
223
|
-
# frames and we created the folder, and the output files aren't in the same folder. For now,
|
|
224
|
-
# we're just deleting the rendered frames and leaving the empty folder around in this case.
|
|
225
351
|
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
226
352
|
|
|
227
353
|
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
@@ -236,44 +362,20 @@ def process_video(options):
|
|
|
236
362
|
else:
|
|
237
363
|
rendering_fs = Fs / options.frame_sample
|
|
238
364
|
|
|
239
|
-
print('Rendering
|
|
240
|
-
options.output_video_file,rendering_fs,Fs))
|
|
241
|
-
frames_to_video(detected_frame_files, rendering_fs, options.output_video_file,
|
|
365
|
+
print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
|
|
366
|
+
len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
|
|
367
|
+
frames_to_video(detected_frame_files, rendering_fs, options.output_video_file,
|
|
368
|
+
codec_spec=options.fourcc)
|
|
242
369
|
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
if options.force_rendered_frame_folder_deletion:
|
|
247
|
-
shutil.rmtree(rendering_output_dir)
|
|
248
|
-
else:
|
|
249
|
-
for rendered_frame_fn in detected_frame_files:
|
|
250
|
-
os.remove(rendered_frame_fn)
|
|
251
|
-
except Exception as e:
|
|
252
|
-
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
253
|
-
rendering_output_dir,str(e)))
|
|
254
|
-
pass
|
|
370
|
+
# Possibly clean up rendered frames
|
|
371
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
255
372
|
|
|
256
373
|
# ...if we're rendering video
|
|
257
374
|
|
|
258
375
|
|
|
259
376
|
## (Optionally) delete the extracted frames
|
|
377
|
+
_clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
|
|
260
378
|
|
|
261
|
-
if not options.keep_extracted_frames:
|
|
262
|
-
|
|
263
|
-
try:
|
|
264
|
-
if options.force_extracted_frame_folder_deletion:
|
|
265
|
-
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
266
|
-
shutil.rmtree(frame_output_folder)
|
|
267
|
-
else:
|
|
268
|
-
for extracted_frame_fn in frame_filenames:
|
|
269
|
-
os.remove(extracted_frame_fn)
|
|
270
|
-
except Exception as e:
|
|
271
|
-
print('Warning: error removing extracted frames from folder {}:\n{}'.format(
|
|
272
|
-
frame_output_folder,str(e)))
|
|
273
|
-
pass
|
|
274
|
-
|
|
275
|
-
return results
|
|
276
|
-
|
|
277
379
|
# ...process_video()
|
|
278
380
|
|
|
279
381
|
|
|
@@ -299,24 +401,22 @@ def process_video_folder(options):
|
|
|
299
401
|
frames_json = options.output_json_file.replace('.json','.frames.json')
|
|
300
402
|
os.makedirs(os.path.dirname(video_json),exist_ok=True)
|
|
301
403
|
|
|
404
|
+
# Track whether frame and rendering folders were created by this script
|
|
405
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
406
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
407
|
+
|
|
408
|
+
# This does not create any folders, just defines temporary folder names in
|
|
409
|
+
# case we need them.
|
|
410
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
411
|
+
|
|
302
412
|
|
|
303
413
|
## Split every video into frames
|
|
304
414
|
|
|
305
|
-
if
|
|
415
|
+
if caller_provided_frame_output_folder:
|
|
306
416
|
frame_output_folder = options.frame_folder
|
|
307
417
|
else:
|
|
308
|
-
|
|
309
|
-
os.makedirs(tempdir,exist_ok=True)
|
|
418
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
310
419
|
|
|
311
|
-
# TODO: see above; this is a lazy fix to a permissions issue
|
|
312
|
-
try:
|
|
313
|
-
os.chmod(tempdir,0o777)
|
|
314
|
-
except Exception:
|
|
315
|
-
pass
|
|
316
|
-
|
|
317
|
-
frame_output_folder = os.path.join(
|
|
318
|
-
tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
|
|
319
|
-
|
|
320
420
|
os.makedirs(frame_output_folder, exist_ok=True)
|
|
321
421
|
|
|
322
422
|
print('Extracting frames')
|
|
@@ -325,8 +425,11 @@ def process_video_folder(options):
|
|
|
325
425
|
output_folder_base=frame_output_folder,
|
|
326
426
|
recursive=options.recursive,
|
|
327
427
|
overwrite=(not options.reuse_frames_if_available),
|
|
328
|
-
n_threads=options.n_cores,
|
|
329
|
-
|
|
428
|
+
n_threads=options.n_cores,
|
|
429
|
+
every_n_frames=options.frame_sample,
|
|
430
|
+
verbose=options.verbose,
|
|
431
|
+
quality=options.quality,
|
|
432
|
+
max_width=options.max_width)
|
|
330
433
|
|
|
331
434
|
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
332
435
|
|
|
@@ -346,7 +449,7 @@ def process_video_folder(options):
|
|
|
346
449
|
|
|
347
450
|
if options.reuse_results_if_available and \
|
|
348
451
|
os.path.isfile(frames_json):
|
|
349
|
-
print('
|
|
452
|
+
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
350
453
|
results = None
|
|
351
454
|
else:
|
|
352
455
|
print('Running MegaDetector')
|
|
@@ -354,8 +457,8 @@ def process_video_folder(options):
|
|
|
354
457
|
options.model_file, image_file_names,
|
|
355
458
|
confidence_threshold=options.json_confidence_threshold,
|
|
356
459
|
n_cores=options.n_cores,
|
|
357
|
-
|
|
358
|
-
|
|
460
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
461
|
+
quiet=True)
|
|
359
462
|
|
|
360
463
|
run_detector_batch.write_results_to_file(
|
|
361
464
|
results, frames_json,
|
|
@@ -373,26 +476,23 @@ def process_video_folder(options):
|
|
|
373
476
|
## (Optionally) render output videos
|
|
374
477
|
|
|
375
478
|
if options.render_output_video:
|
|
376
|
-
|
|
479
|
+
|
|
377
480
|
# Render detections to images
|
|
378
|
-
if
|
|
379
|
-
|
|
481
|
+
if (caller_provided_rendering_output_folder):
|
|
482
|
+
rendering_output_dir = options.frame_rendering_folder
|
|
380
483
|
else:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# TODO: keep track of whether we created this folder, delete if we're deleting the rendered
|
|
385
|
-
# frames and we created the folder, and the output files aren't in the same folder. For now,
|
|
386
|
-
# we're just deleting the rendered frames and leaving the empty folder around in this case.
|
|
387
|
-
os.makedirs(frame_rendering_output_dir,exist_ok=True)
|
|
484
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
485
|
+
|
|
486
|
+
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
388
487
|
|
|
389
488
|
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
390
489
|
detector_output_path=frames_json,
|
|
391
|
-
out_dir=
|
|
490
|
+
out_dir=rendering_output_dir,
|
|
392
491
|
images_dir=frame_output_folder,
|
|
393
492
|
confidence_threshold=options.rendering_confidence_threshold,
|
|
394
493
|
preserve_path_structure=True,
|
|
395
494
|
output_image_width=-1)
|
|
495
|
+
detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
|
|
396
496
|
|
|
397
497
|
# Choose an output folder
|
|
398
498
|
output_folder_is_input_folder = False
|
|
@@ -424,7 +524,9 @@ def process_video_folder(options):
|
|
|
424
524
|
rendering_fs = video_fs / options.frame_sample
|
|
425
525
|
|
|
426
526
|
input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
|
|
427
|
-
video_frame_output_folder = os.path.join(
|
|
527
|
+
video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
|
|
528
|
+
|
|
529
|
+
video_frame_output_folder = video_frame_output_folder.replace('\\','/')
|
|
428
530
|
assert os.path.isdir(video_frame_output_folder), \
|
|
429
531
|
'Could not find frame folder for video {}'.format(input_video_file_relative)
|
|
430
532
|
|
|
@@ -450,42 +552,29 @@ def process_video_folder(options):
|
|
|
450
552
|
# ...for each video
|
|
451
553
|
|
|
452
554
|
# Possibly clean up rendered frames
|
|
453
|
-
|
|
454
|
-
try:
|
|
455
|
-
if options.force_rendered_frame_folder_deletion:
|
|
456
|
-
shutil.rmtree(frame_rendering_output_dir)
|
|
457
|
-
else:
|
|
458
|
-
for rendered_frame_fn in detected_frame_files:
|
|
459
|
-
os.remove(rendered_frame_fn)
|
|
460
|
-
except Exception as e:
|
|
461
|
-
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
462
|
-
frame_rendering_output_dir,str(e)))
|
|
463
|
-
pass
|
|
555
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
464
556
|
|
|
465
557
|
# ...if we're rendering video
|
|
466
558
|
|
|
467
559
|
|
|
468
560
|
## (Optionally) delete the extracted frames
|
|
469
|
-
|
|
470
|
-
if not options.keep_extracted_frames:
|
|
471
|
-
try:
|
|
472
|
-
print('Deleting frame cache')
|
|
473
|
-
if options.force_extracted_frame_folder_deletion:
|
|
474
|
-
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
475
|
-
shutil.rmtree(frame_output_folder)
|
|
476
|
-
else:
|
|
477
|
-
for frame_fn in image_file_names:
|
|
478
|
-
os.remove(frame_fn)
|
|
479
|
-
except Exception as e:
|
|
480
|
-
print('Warning: error deleting frames from folder {}:\n{}'.format(
|
|
481
|
-
frame_output_folder,str(e)))
|
|
482
|
-
pass
|
|
561
|
+
_clean_up_extracted_frames(options, frame_output_folder, image_file_names)
|
|
483
562
|
|
|
484
563
|
# ...process_video_folder()
|
|
485
564
|
|
|
486
565
|
|
|
487
566
|
def options_to_command(options):
|
|
567
|
+
"""
|
|
568
|
+
Convert a ProcessVideoOptions obejct to a corresponding command line.
|
|
488
569
|
|
|
570
|
+
Args:
|
|
571
|
+
options (ProcessVideoOptions): the options set to render as a command line
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
str: the command line coresponding to [options]
|
|
575
|
+
|
|
576
|
+
:meta private:
|
|
577
|
+
"""
|
|
489
578
|
cmd = 'python process_video.py'
|
|
490
579
|
cmd += ' "' + options.model_file + '"'
|
|
491
580
|
cmd += ' "' + options.input_video_file + '"'
|
|
@@ -524,6 +613,16 @@ def options_to_command(options):
|
|
|
524
613
|
cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
|
|
525
614
|
if options.fourcc is not None:
|
|
526
615
|
cmd += ' --fourcc ' + options.fourcc
|
|
616
|
+
if options.quality is not None:
|
|
617
|
+
cmd += ' --quality ' + str(options.quality)
|
|
618
|
+
if options.max_width is not None:
|
|
619
|
+
cmd += ' --max_width ' + str(options.max_width)
|
|
620
|
+
if options.verbose:
|
|
621
|
+
cmd += ' --verbose'
|
|
622
|
+
if options.force_extracted_frame_folder_deletion:
|
|
623
|
+
cmd += ' --force_extracted_frame_folder_deletion'
|
|
624
|
+
if options.force_rendered_frame_folder_deletion:
|
|
625
|
+
cmd += ' --force_rendered_frame_folder_deletion'
|
|
527
626
|
|
|
528
627
|
return cmd
|
|
529
628
|
|
|
@@ -535,11 +634,12 @@ if False:
|
|
|
535
634
|
#%% Process a folder of videos
|
|
536
635
|
|
|
537
636
|
model_file = 'MDV5A'
|
|
538
|
-
input_dir = r'
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
637
|
+
input_dir = r'g:\temp\test-videos'
|
|
638
|
+
output_base = r'g:\temp\video_test'
|
|
639
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
640
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
641
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
642
|
+
output_video_folder = os.path.join(output_base,'output_videos')
|
|
543
643
|
|
|
544
644
|
print('Processing folder {}'.format(input_dir))
|
|
545
645
|
|
|
@@ -547,21 +647,31 @@ if False:
|
|
|
547
647
|
options.model_file = model_file
|
|
548
648
|
options.input_video_file = input_dir
|
|
549
649
|
options.output_video_file = output_video_folder
|
|
550
|
-
options.frame_folder = frame_folder
|
|
551
650
|
options.output_json_file = output_json_file
|
|
552
|
-
options.frame_rendering_folder = rendering_folder
|
|
553
|
-
options.render_output_video = True
|
|
554
|
-
options.keep_extracted_frames = True
|
|
555
|
-
options.keep_rendered_frames = True
|
|
556
651
|
options.recursive = True
|
|
557
|
-
options.reuse_frames_if_available =
|
|
558
|
-
options.reuse_results_if_available =
|
|
652
|
+
options.reuse_frames_if_available = False
|
|
653
|
+
options.reuse_results_if_available = False
|
|
654
|
+
options.quality = 90
|
|
655
|
+
options.frame_sample = 10
|
|
656
|
+
options.max_width = 1280
|
|
657
|
+
options.n_cores = 5
|
|
658
|
+
options.verbose = True
|
|
659
|
+
options.render_output_video = True
|
|
660
|
+
|
|
661
|
+
options.frame_folder = None # frame_folder
|
|
662
|
+
options.frame_rendering_folder = None # rendering_folder
|
|
663
|
+
|
|
664
|
+
options.keep_extracted_frames = False
|
|
665
|
+
options.keep_rendered_frames = False
|
|
666
|
+
options.force_extracted_frame_folder_deletion = True
|
|
667
|
+
options.force_rendered_frame_folder_deletion = True
|
|
668
|
+
|
|
559
669
|
# options.confidence_threshold = 0.15
|
|
560
|
-
|
|
670
|
+
options.fourcc = 'mp4v'
|
|
561
671
|
|
|
562
|
-
cmd = options_to_command(options)
|
|
563
|
-
|
|
564
|
-
|
|
672
|
+
cmd = options_to_command(options); print(cmd)
|
|
673
|
+
|
|
674
|
+
import clipboard; clipboard.copy(cmd)
|
|
565
675
|
|
|
566
676
|
if False:
|
|
567
677
|
process_video_folder(options)
|
|
@@ -569,23 +679,42 @@ if False:
|
|
|
569
679
|
|
|
570
680
|
#%% Process a single video
|
|
571
681
|
|
|
572
|
-
fn =
|
|
682
|
+
fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
|
|
573
683
|
model_file = 'MDV5A'
|
|
574
684
|
input_video_file = fn
|
|
575
|
-
|
|
576
|
-
|
|
685
|
+
|
|
686
|
+
output_base = r'g:\temp\video_test'
|
|
687
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
688
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
689
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
690
|
+
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
577
691
|
|
|
578
692
|
options = ProcessVideoOptions()
|
|
579
693
|
options.model_file = model_file
|
|
580
694
|
options.input_video_file = input_video_file
|
|
581
|
-
options.frame_folder = frame_folder
|
|
582
|
-
options.frame_rendering_folder = rendering_folder
|
|
583
695
|
options.render_output_video = True
|
|
584
|
-
options.output_video_file =
|
|
696
|
+
options.output_video_file = output_video_file
|
|
697
|
+
|
|
698
|
+
options.verbose = True
|
|
699
|
+
|
|
700
|
+
options.quality = 75
|
|
701
|
+
options.frame_sample = None # 10
|
|
702
|
+
options.max_width = 600
|
|
703
|
+
|
|
704
|
+
options.frame_folder = None # frame_folder
|
|
705
|
+
options.frame_rendering_folder = None # rendering_folder
|
|
585
706
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
707
|
+
options.keep_extracted_frames = False
|
|
708
|
+
options.keep_rendered_frames = False
|
|
709
|
+
options.force_extracted_frame_folder_deletion = True
|
|
710
|
+
options.force_rendered_frame_folder_deletion = True
|
|
711
|
+
|
|
712
|
+
# options.confidence_threshold = 0.15
|
|
713
|
+
options.fourcc = 'mp4v'
|
|
714
|
+
|
|
715
|
+
cmd = options_to_command(options); print(cmd)
|
|
716
|
+
|
|
717
|
+
import clipboard; clipboard.copy(cmd)
|
|
589
718
|
|
|
590
719
|
if False:
|
|
591
720
|
process_video(options)
|
|
@@ -640,7 +769,8 @@ def main():
|
|
|
640
769
|
help='enable video output rendering (not rendered by default)')
|
|
641
770
|
|
|
642
771
|
parser.add_argument('--fourcc', default=default_fourcc,
|
|
643
|
-
help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
|
|
772
|
+
help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
|
|
773
|
+
default_fourcc))
|
|
644
774
|
|
|
645
775
|
parser.add_argument('--keep_rendered_frames',
|
|
646
776
|
action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
|
|
@@ -666,7 +796,7 @@ def main():
|
|
|
666
796
|
default_options.json_confidence_threshold))
|
|
667
797
|
|
|
668
798
|
parser.add_argument('--n_cores', type=int,
|
|
669
|
-
default=1, help='
|
|
799
|
+
default=1, help='Number of cores to use for frame separation and detection. '\
|
|
670
800
|
'If using a GPU, this option will be respected for frame separation but '\
|
|
671
801
|
'ignored for detection. Only relevant to frame separation when processing '\
|
|
672
802
|
'a folder.')
|
|
@@ -674,8 +804,18 @@ def main():
|
|
|
674
804
|
parser.add_argument('--frame_sample', type=int,
|
|
675
805
|
default=None, help='process every Nth frame (defaults to every frame)')
|
|
676
806
|
|
|
807
|
+
parser.add_argument('--quality', type=int,
|
|
808
|
+
default=default_options.quality,
|
|
809
|
+
help='JPEG quality for extracted frames (defaults to {})'.format(
|
|
810
|
+
default_options.quality))
|
|
811
|
+
|
|
812
|
+
parser.add_argument('--max_width', type=int,
|
|
813
|
+
default=default_options.max_width,
|
|
814
|
+
help='Resize frames larger than this before writing (defaults to {})'.format(
|
|
815
|
+
default_options.max_width))
|
|
816
|
+
|
|
677
817
|
parser.add_argument('--debug_max_frames', type=int,
|
|
678
|
-
default=-1, help='
|
|
818
|
+
default=-1, help='Trim to N frames for debugging (impacts model execution, '\
|
|
679
819
|
'not frame rendering)')
|
|
680
820
|
|
|
681
821
|
parser.add_argument('--class_mapping_filename',
|
|
@@ -685,6 +825,10 @@ def main():
|
|
|
685
825
|
'the addition of "1" to all category IDs, so your class mapping should start '\
|
|
686
826
|
'at zero.')
|
|
687
827
|
|
|
828
|
+
parser.add_argument('--verbose', action='store_true',
|
|
829
|
+
help='Enable additional debug output')
|
|
830
|
+
|
|
831
|
+
|
|
688
832
|
if len(sys.argv[1:]) == 0:
|
|
689
833
|
parser.print_help()
|
|
690
834
|
parser.exit()
|