megadetector 5.0.12__py3-none-any.whl → 5.0.14__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 +178 -44
- 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 +548 -244
- megadetector/detection/pytorch_detector.py +33 -14
- megadetector/detection/run_detector.py +17 -5
- megadetector/detection/run_detector_batch.py +179 -65
- megadetector/detection/run_inference_with_yolov5_val.py +527 -357
- megadetector/detection/tf_detector.py +14 -3
- megadetector/detection/video_utils.py +284 -61
- 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/convert_output_format.py +12 -5
- 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 +380 -128
- megadetector/utils/path_utils.py +39 -6
- megadetector/utils/process_utils.py +13 -4
- megadetector/visualization/visualization_utils.py +7 -2
- megadetector/visualization/visualize_db.py +79 -77
- megadetector/visualization/visualize_detector_output.py +0 -1
- {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/LICENSE +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/METADATA +2 -2
- {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/RECORD +45 -43
- {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/top_level.txt +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/WHEEL +0 -0
|
@@ -12,8 +12,6 @@ writing them to disk. The upside, though, is that this approach allows you to r
|
|
|
12
12
|
detection elimination after running MegaDetector, and it allows allows more efficient re-use
|
|
13
13
|
of frames if you end up running MD more than once, or running multiple versions of MD.
|
|
14
14
|
|
|
15
|
-
TODO: optionally skip writing frames to disk, and process frames in memory.
|
|
16
|
-
|
|
17
15
|
"""
|
|
18
16
|
|
|
19
17
|
#%% Imports
|
|
@@ -25,16 +23,18 @@ import argparse
|
|
|
25
23
|
import itertools
|
|
26
24
|
import json
|
|
27
25
|
import shutil
|
|
26
|
+
import getpass
|
|
28
27
|
|
|
29
28
|
from uuid import uuid1
|
|
30
29
|
|
|
31
30
|
from megadetector.detection import run_detector_batch
|
|
32
31
|
from megadetector.visualization import visualize_detector_output
|
|
33
32
|
from megadetector.utils.ct_utils import args_to_object
|
|
34
|
-
from megadetector.utils.path_utils import insert_before_extension
|
|
33
|
+
from megadetector.utils.path_utils import insert_before_extension, clean_path
|
|
35
34
|
from megadetector.detection.video_utils import video_to_frames
|
|
36
35
|
from megadetector.detection.video_utils import frames_to_video
|
|
37
36
|
from megadetector.detection.video_utils import frame_results_to_video_results
|
|
37
|
+
from megadetector.detection.video_utils import _add_frame_numbers_to_results
|
|
38
38
|
from megadetector.detection.video_utils import video_folder_to_frames
|
|
39
39
|
from megadetector.detection.video_utils import default_fourcc
|
|
40
40
|
|
|
@@ -46,99 +46,259 @@ class ProcessVideoOptions:
|
|
|
46
46
|
Options controlling the behavior of process_video()
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
49
|
+
def __init__(self):
|
|
50
|
+
|
|
51
|
+
#: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
|
|
52
|
+
#:
|
|
53
|
+
#: Use the string "no_detection" to indicate that you only want to extract frames,
|
|
54
|
+
#: not run a model. If you do this, you almost definitely want to set
|
|
55
|
+
#: keep_extracted_frames to "True", otherwise everything in this module is a no-op.
|
|
56
|
+
#: I.e., there's no reason to extract frames, do nothing with them, then delete them.
|
|
57
|
+
self.model_file = 'MDV5A'
|
|
58
|
+
|
|
59
|
+
#: Video (of folder of videos) to process
|
|
60
|
+
self.input_video_file = ''
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
#: .json file to which we should write results
|
|
63
|
+
self.output_json_file = None
|
|
64
|
+
|
|
65
|
+
#: File to which we should write a video with boxes, only relevant if
|
|
66
|
+
#: render_output_video is True
|
|
67
|
+
self.output_video_file = None
|
|
68
|
+
|
|
69
|
+
#: Folder to use for extracted frames; will use a folder in system temp space
|
|
70
|
+
#: if this is None
|
|
71
|
+
self.frame_folder = None
|
|
72
|
+
|
|
73
|
+
#: Folder to use for rendered frames (if rendering output video); will use a folder
|
|
74
|
+
#: in system temp space if this is None
|
|
75
|
+
self.frame_rendering_folder = None
|
|
76
|
+
|
|
77
|
+
#: Should we render a video with detection boxes?
|
|
78
|
+
#:
|
|
79
|
+
#: Only supported when processing a single video, not a folder.
|
|
80
|
+
self.render_output_video = False
|
|
81
|
+
|
|
82
|
+
#: If we are rendering boxes to a new video, should we keep the temporary
|
|
83
|
+
#: rendered frames?
|
|
84
|
+
self.keep_rendered_frames = False
|
|
85
|
+
|
|
86
|
+
#: Should we keep the extracted frames?
|
|
87
|
+
self.keep_extracted_frames = False
|
|
88
|
+
|
|
89
|
+
#: Should we delete the entire folder the extracted frames are written to?
|
|
90
|
+
#:
|
|
91
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
92
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
93
|
+
self.force_extracted_frame_folder_deletion = False
|
|
94
|
+
|
|
95
|
+
#: Should we delete the entire folder the rendered frames are written to?
|
|
96
|
+
#:
|
|
97
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
98
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
99
|
+
self.force_rendered_frame_folder_deletion = False
|
|
100
|
+
|
|
101
|
+
#: If we've already run MegaDetector on this video or folder of videos, i.e. if we
|
|
102
|
+
#: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
|
|
103
|
+
self.reuse_results_if_available = False
|
|
104
|
+
|
|
105
|
+
#: If we've already split this video or folder of videos into frames, should we
|
|
106
|
+
#: we re-use those extracted frames? Defaults to reprocessing.
|
|
107
|
+
self.reuse_frames_if_available = False
|
|
108
|
+
|
|
109
|
+
#: If [input_video_file] is a folder, should we search for videos recursively?
|
|
110
|
+
self.recursive = False
|
|
111
|
+
|
|
112
|
+
#: Enable additional debug console output
|
|
113
|
+
self.verbose = False
|
|
114
|
+
|
|
115
|
+
#: fourcc code to use for writing videos; only relevant if render_output_video is True
|
|
116
|
+
self.fourcc = None
|
|
69
117
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
#: force a specific frame rate for output videos; only relevant if render_output_video
|
|
119
|
+
#: is True
|
|
120
|
+
self.rendering_fs = None
|
|
121
|
+
|
|
122
|
+
#: Confidence threshold to use for writing videos with boxes, only relevant if
|
|
123
|
+
#: if render_output_video is True. Defaults to choosing a reasonable threshold
|
|
124
|
+
#: based on the model version.
|
|
125
|
+
self.rendering_confidence_threshold = None
|
|
126
|
+
|
|
127
|
+
#: Detections below this threshold will not be included in the output file.
|
|
128
|
+
self.json_confidence_threshold = 0.005
|
|
129
|
+
|
|
130
|
+
#: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
|
|
131
|
+
#: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
|
|
132
|
+
#: typical value. Mutually exclusive with [frames_to_extract].
|
|
133
|
+
self.frame_sample = None
|
|
134
|
+
|
|
135
|
+
#: Extract a specific set of frames (list of ints, or a single int). Mutually exclusive with
|
|
136
|
+
#: [frame_sample].
|
|
137
|
+
self.frames_to_extract = None
|
|
138
|
+
|
|
139
|
+
#: Number of workers to use for parallelization; set to <= 1 to disable parallelization
|
|
140
|
+
self.n_cores = 1
|
|
74
141
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
142
|
+
#: For debugging only, stop processing after a certain number of frames.
|
|
143
|
+
self.debug_max_frames = -1
|
|
144
|
+
|
|
145
|
+
#: File containing non-standard categories, typically only used if you're running a non-MD
|
|
146
|
+
#: detector.
|
|
147
|
+
self.class_mapping_filename = None
|
|
148
|
+
|
|
149
|
+
#: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
|
|
150
|
+
self.quality = 90
|
|
151
|
+
|
|
152
|
+
#: Resize frames so they're at most this wide
|
|
153
|
+
self.max_width = None
|
|
154
|
+
|
|
155
|
+
#: Run the model at this image size (don't mess with this unless you know what you're
|
|
156
|
+
#: getting into)
|
|
157
|
+
self.image_size = None
|
|
158
|
+
|
|
159
|
+
#: Enable image augmentation
|
|
160
|
+
self.augment = False
|
|
78
161
|
|
|
79
|
-
|
|
80
|
-
|
|
162
|
+
# ...class ProcessVideoOptions
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
#%% Functions
|
|
166
|
+
|
|
167
|
+
def _select_temporary_output_folders(options):
|
|
168
|
+
"""
|
|
169
|
+
Choose folders in system temp space for writing temporary frames. Does not create folders,
|
|
170
|
+
just defines them.
|
|
171
|
+
"""
|
|
81
172
|
|
|
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
|
|
173
|
+
tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
|
|
87
174
|
|
|
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
|
|
175
|
+
# If we create a folder like "process_camera_trap_video" in the system temp dir, it may
|
|
176
|
+
# be the case that no one else can write to it, even to create user-specific subfolders.
|
|
177
|
+
# If we create a uuid-named folder in the system temp dir, we make a mess.
|
|
178
|
+
#
|
|
179
|
+
# Compromise with "process_camera_trap_video-[user]".
|
|
180
|
+
user_tempdir = tempdir + '-' + getpass.getuser()
|
|
97
181
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
# I don't know whether it's possible for a username to contain characters that are
|
|
183
|
+
# not valid filename characters, but just to be sure...
|
|
184
|
+
user_tempdir = clean_path(user_tempdir)
|
|
101
185
|
|
|
102
|
-
|
|
103
|
-
|
|
186
|
+
frame_output_folder = os.path.join(
|
|
187
|
+
user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
|
|
104
188
|
|
|
105
|
-
|
|
106
|
-
|
|
189
|
+
rendering_output_folder = os.path.join(
|
|
190
|
+
tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
|
|
191
|
+
|
|
192
|
+
temporary_folder_info = \
|
|
193
|
+
{
|
|
194
|
+
'temp_folder_base':user_tempdir,
|
|
195
|
+
'frame_output_folder':frame_output_folder,
|
|
196
|
+
'rendering_output_folder':rendering_output_folder
|
|
197
|
+
}
|
|
107
198
|
|
|
108
|
-
|
|
109
|
-
fourcc = None
|
|
199
|
+
return temporary_folder_info
|
|
110
200
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
201
|
+
# ...def _create_frame_output_folders(...)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
|
|
205
|
+
"""
|
|
206
|
+
If necessary, delete rendered frames and/or the entire rendering output folder.
|
|
207
|
+
"""
|
|
118
208
|
|
|
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
|
|
209
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
123
210
|
|
|
124
|
-
|
|
125
|
-
|
|
211
|
+
# (Optionally) delete the temporary directory we used for rendered detection images
|
|
212
|
+
if not options.keep_rendered_frames:
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
|
|
216
|
+
# If (a) we're supposed to delete the temporary rendering folder no
|
|
217
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
218
|
+
# whole tree
|
|
219
|
+
if options.force_rendered_frame_folder_deletion and \
|
|
220
|
+
(not caller_provided_rendering_output_folder):
|
|
221
|
+
|
|
222
|
+
if options.verbose:
|
|
223
|
+
print('Recursively deleting rendered frame folder {}'.format(
|
|
224
|
+
rendering_output_folder))
|
|
225
|
+
|
|
226
|
+
shutil.rmtree(rendering_output_folder)
|
|
227
|
+
|
|
228
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
229
|
+
else:
|
|
230
|
+
|
|
231
|
+
if options.force_rendered_frame_folder_deletion:
|
|
232
|
+
assert caller_provided_rendering_output_folder
|
|
233
|
+
print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
|
|
234
|
+
'user-provided folder, only removing frames')
|
|
235
|
+
|
|
236
|
+
for rendered_frame_fn in detected_frame_files:
|
|
237
|
+
os.remove(rendered_frame_fn)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
241
|
+
rendering_output_folder,str(e)))
|
|
242
|
+
pass
|
|
126
243
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
244
|
+
elif options.force_rendered_frame_folder_deletion:
|
|
245
|
+
|
|
246
|
+
print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
|
|
247
|
+
'specified, not deleting')
|
|
248
|
+
|
|
249
|
+
# ...def _clean_up_rendered_frames(...)
|
|
133
250
|
|
|
134
|
-
# ...class ProcessVideoOptions
|
|
135
251
|
|
|
252
|
+
def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
253
|
+
"""
|
|
254
|
+
If necessary, delete extracted frames and/or the entire temporary frame folder.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
258
|
+
|
|
259
|
+
if not options.keep_extracted_frames:
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
|
|
263
|
+
# If (a) we're supposed to delete the temporary frame folder no
|
|
264
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
265
|
+
# whole tree.
|
|
266
|
+
if options.force_extracted_frame_folder_deletion and \
|
|
267
|
+
(not caller_provided_frame_output_folder):
|
|
268
|
+
|
|
269
|
+
if options.verbose:
|
|
270
|
+
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
271
|
+
|
|
272
|
+
shutil.rmtree(frame_output_folder)
|
|
273
|
+
|
|
274
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
275
|
+
else:
|
|
276
|
+
|
|
277
|
+
if options.force_extracted_frame_folder_deletion:
|
|
278
|
+
assert caller_provided_frame_output_folder
|
|
279
|
+
print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
|
|
280
|
+
'user-provided folder, only removing frames')
|
|
281
|
+
|
|
282
|
+
for extracted_frame_fn in frame_filenames:
|
|
283
|
+
os.remove(extracted_frame_fn)
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
print('Warning: error removing extracted frames from folder {}:\n{}'.format(
|
|
287
|
+
frame_output_folder,str(e)))
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
elif options.force_extracted_frame_folder_deletion:
|
|
291
|
+
|
|
292
|
+
print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
|
|
293
|
+
'specified, not deleting')
|
|
294
|
+
|
|
295
|
+
# ...def _clean_up_extracted_frames
|
|
136
296
|
|
|
137
|
-
#%% Functions
|
|
138
297
|
|
|
139
298
|
def process_video(options):
|
|
140
299
|
"""
|
|
141
|
-
Process a single video through MD, optionally writing a new video with boxes
|
|
300
|
+
Process a single video through MD, optionally writing a new video with boxes.
|
|
301
|
+
Can also be used just to split a video into frames, without running a model.
|
|
142
302
|
|
|
143
303
|
Args:
|
|
144
304
|
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
@@ -154,40 +314,50 @@ def process_video(options):
|
|
|
154
314
|
if options.render_output_video and (options.output_video_file is None):
|
|
155
315
|
options.output_video_file = options.input_video_file + '.detections.mp4'
|
|
156
316
|
|
|
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
|
|
317
|
+
if options.model_file == 'no_detection' and not options.keep_extracted_frames:
|
|
318
|
+
print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
|
|
319
|
+
return
|
|
171
320
|
|
|
172
|
-
|
|
321
|
+
# Track whether frame and rendering folders were created by this script
|
|
322
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
323
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
324
|
+
|
|
325
|
+
# This does not create any folders, just defines temporary folder names in
|
|
326
|
+
# case we need them.
|
|
327
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
328
|
+
|
|
329
|
+
if (caller_provided_frame_output_folder):
|
|
173
330
|
frame_output_folder = options.frame_folder
|
|
174
331
|
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.
|
|
332
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
333
|
+
|
|
181
334
|
os.makedirs(frame_output_folder, exist_ok=True)
|
|
182
335
|
|
|
336
|
+
|
|
337
|
+
## Extract frames
|
|
338
|
+
|
|
183
339
|
frame_filenames, Fs = video_to_frames(
|
|
184
|
-
|
|
185
|
-
|
|
340
|
+
options.input_video_file,
|
|
341
|
+
frame_output_folder,
|
|
342
|
+
every_n_frames=options.frame_sample,
|
|
343
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
344
|
+
quality=options.quality,
|
|
345
|
+
max_width=options.max_width,
|
|
346
|
+
verbose=options.verbose,
|
|
347
|
+
frames_to_extract=options.frames_to_extract)
|
|
186
348
|
|
|
187
349
|
image_file_names = frame_filenames
|
|
188
350
|
if options.debug_max_frames > 0:
|
|
189
351
|
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
190
|
-
|
|
352
|
+
|
|
353
|
+
if options.model_file == 'no_detection':
|
|
354
|
+
assert options.keep_extracted_frames, \
|
|
355
|
+
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
## Run MegaDetector
|
|
360
|
+
|
|
191
361
|
if options.reuse_results_if_available and \
|
|
192
362
|
os.path.isfile(options.output_json_file):
|
|
193
363
|
print('Loading results from {}'.format(options.output_json_file))
|
|
@@ -195,12 +365,17 @@ def process_video(options):
|
|
|
195
365
|
results = json.load(f)
|
|
196
366
|
else:
|
|
197
367
|
results = run_detector_batch.load_and_run_detector_batch(
|
|
198
|
-
options.model_file,
|
|
368
|
+
options.model_file,
|
|
369
|
+
image_file_names,
|
|
199
370
|
confidence_threshold=options.json_confidence_threshold,
|
|
200
371
|
n_cores=options.n_cores,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
372
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
373
|
+
quiet=True,
|
|
374
|
+
augment=options.augment,
|
|
375
|
+
image_size=options.image_size)
|
|
376
|
+
|
|
377
|
+
_add_frame_numbers_to_results(results)
|
|
378
|
+
|
|
204
379
|
run_detector_batch.write_results_to_file(
|
|
205
380
|
results, options.output_json_file,
|
|
206
381
|
relative_path_base=frame_output_folder,
|
|
@@ -213,15 +388,11 @@ def process_video(options):
|
|
|
213
388
|
if options.render_output_video:
|
|
214
389
|
|
|
215
390
|
# Render detections to images
|
|
216
|
-
if
|
|
391
|
+
if (caller_provided_rendering_output_folder):
|
|
217
392
|
rendering_output_dir = options.frame_rendering_folder
|
|
218
393
|
else:
|
|
219
|
-
rendering_output_dir =
|
|
220
|
-
tempdir, os.path.basename(options.input_video_file) + '_detections')
|
|
394
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
221
395
|
|
|
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
396
|
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
226
397
|
|
|
227
398
|
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
@@ -231,55 +402,43 @@ def process_video(options):
|
|
|
231
402
|
confidence_threshold=options.rendering_confidence_threshold)
|
|
232
403
|
|
|
233
404
|
# Combine into a video
|
|
234
|
-
if options.
|
|
405
|
+
if options.rendering_fs is not None:
|
|
406
|
+
rendering_fs = options.rendering_fs
|
|
407
|
+
elif options.frame_sample is None:
|
|
235
408
|
rendering_fs = Fs
|
|
236
409
|
else:
|
|
410
|
+
# If the original video was 30fps and we sampled every 10th frame,
|
|
411
|
+
# render at 3fps
|
|
237
412
|
rendering_fs = Fs / options.frame_sample
|
|
238
413
|
|
|
239
|
-
print('Rendering
|
|
240
|
-
options.output_video_file,rendering_fs,Fs))
|
|
241
|
-
frames_to_video(detected_frame_files,
|
|
414
|
+
print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
|
|
415
|
+
len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
|
|
416
|
+
frames_to_video(detected_frame_files,
|
|
417
|
+
rendering_fs,
|
|
418
|
+
options.output_video_file,
|
|
419
|
+
codec_spec=options.fourcc)
|
|
242
420
|
|
|
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
|
|
421
|
+
# Possibly clean up rendered frames
|
|
422
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
255
423
|
|
|
256
424
|
# ...if we're rendering video
|
|
257
425
|
|
|
258
426
|
|
|
259
427
|
## (Optionally) delete the extracted frames
|
|
428
|
+
_clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
|
|
260
429
|
|
|
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
430
|
# ...process_video()
|
|
278
431
|
|
|
279
432
|
|
|
280
433
|
def process_video_folder(options):
|
|
281
434
|
"""
|
|
282
|
-
Process a folder of videos through MD
|
|
435
|
+
Process a folder of videos through MD. Can also be used just to split a folder of
|
|
436
|
+
videos into frames, without running a model.
|
|
437
|
+
|
|
438
|
+
When this function is used to run MD, two .json files will get written, one with
|
|
439
|
+
an entry for each *frame* (identical to what's created by process_video()), and
|
|
440
|
+
one with an entry for each *video* (which is more suitable for, e.g., reading into
|
|
441
|
+
Timelapse).
|
|
283
442
|
|
|
284
443
|
Args:
|
|
285
444
|
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
@@ -291,43 +450,51 @@ def process_video_folder(options):
|
|
|
291
450
|
assert os.path.isdir(options.input_video_file), \
|
|
292
451
|
'{} is not a folder'.format(options.input_video_file)
|
|
293
452
|
|
|
294
|
-
|
|
295
|
-
'
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
453
|
+
if options.model_file == 'no_detection' and not options.keep_extracted_frames:
|
|
454
|
+
print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
if options.model_file != 'no_detection':
|
|
458
|
+
assert options.output_json_file is not None, \
|
|
459
|
+
'When processing a folder, you must specify an output .json file'
|
|
460
|
+
assert options.output_json_file.endswith('.json')
|
|
461
|
+
video_json = options.output_json_file
|
|
462
|
+
frames_json = options.output_json_file.replace('.json','.frames.json')
|
|
463
|
+
os.makedirs(os.path.dirname(video_json),exist_ok=True)
|
|
464
|
+
|
|
465
|
+
# Track whether frame and rendering folders were created by this script
|
|
466
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
467
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
468
|
+
|
|
469
|
+
# This does not create any folders, just defines temporary folder names in
|
|
470
|
+
# case we need them.
|
|
471
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
301
472
|
|
|
302
473
|
|
|
303
474
|
## Split every video into frames
|
|
304
475
|
|
|
305
|
-
if
|
|
476
|
+
if caller_provided_frame_output_folder:
|
|
306
477
|
frame_output_folder = options.frame_folder
|
|
307
478
|
else:
|
|
308
|
-
|
|
309
|
-
os.makedirs(tempdir,exist_ok=True)
|
|
479
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
310
480
|
|
|
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
481
|
os.makedirs(frame_output_folder, exist_ok=True)
|
|
321
482
|
|
|
322
483
|
print('Extracting frames')
|
|
484
|
+
|
|
323
485
|
frame_filenames, Fs, video_filenames = \
|
|
324
486
|
video_folder_to_frames(input_folder=options.input_video_file,
|
|
325
487
|
output_folder_base=frame_output_folder,
|
|
326
488
|
recursive=options.recursive,
|
|
327
489
|
overwrite=(not options.reuse_frames_if_available),
|
|
328
|
-
n_threads=options.n_cores,
|
|
329
|
-
|
|
330
|
-
|
|
490
|
+
n_threads=options.n_cores,
|
|
491
|
+
every_n_frames=options.frame_sample,
|
|
492
|
+
verbose=options.verbose,
|
|
493
|
+
quality=options.quality,
|
|
494
|
+
max_width=options.max_width,
|
|
495
|
+
frames_to_extract=options.frames_to_extract)
|
|
496
|
+
|
|
497
|
+
print('Extracted frames for {} videos'.format(len(set(video_filenames))))
|
|
331
498
|
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
332
499
|
|
|
333
500
|
if len(image_file_names) == 0:
|
|
@@ -340,23 +507,33 @@ def process_video_folder(options):
|
|
|
340
507
|
|
|
341
508
|
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
342
509
|
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
510
|
+
|
|
511
|
+
if options.model_file == 'no_detection':
|
|
512
|
+
assert options.keep_extracted_frames, \
|
|
513
|
+
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
514
|
+
return
|
|
343
515
|
|
|
344
516
|
|
|
345
517
|
## Run MegaDetector on the extracted frames
|
|
346
518
|
|
|
347
519
|
if options.reuse_results_if_available and \
|
|
348
520
|
os.path.isfile(frames_json):
|
|
349
|
-
print('
|
|
521
|
+
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
350
522
|
results = None
|
|
351
523
|
else:
|
|
352
524
|
print('Running MegaDetector')
|
|
353
525
|
results = run_detector_batch.load_and_run_detector_batch(
|
|
354
|
-
options.model_file,
|
|
526
|
+
options.model_file,
|
|
527
|
+
image_file_names,
|
|
355
528
|
confidence_threshold=options.json_confidence_threshold,
|
|
356
529
|
n_cores=options.n_cores,
|
|
357
|
-
|
|
358
|
-
|
|
530
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
531
|
+
quiet=True,
|
|
532
|
+
augment=options.augment,
|
|
533
|
+
image_size=options.image_size)
|
|
359
534
|
|
|
535
|
+
_add_frame_numbers_to_results(results)
|
|
536
|
+
|
|
360
537
|
run_detector_batch.write_results_to_file(
|
|
361
538
|
results, frames_json,
|
|
362
539
|
relative_path_base=frame_output_folder,
|
|
@@ -373,26 +550,23 @@ def process_video_folder(options):
|
|
|
373
550
|
## (Optionally) render output videos
|
|
374
551
|
|
|
375
552
|
if options.render_output_video:
|
|
376
|
-
|
|
553
|
+
|
|
377
554
|
# Render detections to images
|
|
378
|
-
if
|
|
379
|
-
|
|
555
|
+
if (caller_provided_rendering_output_folder):
|
|
556
|
+
rendering_output_dir = options.frame_rendering_folder
|
|
380
557
|
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)
|
|
558
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
559
|
+
|
|
560
|
+
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
388
561
|
|
|
389
562
|
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
390
563
|
detector_output_path=frames_json,
|
|
391
|
-
out_dir=
|
|
564
|
+
out_dir=rendering_output_dir,
|
|
392
565
|
images_dir=frame_output_folder,
|
|
393
566
|
confidence_threshold=options.rendering_confidence_threshold,
|
|
394
567
|
preserve_path_structure=True,
|
|
395
568
|
output_image_width=-1)
|
|
569
|
+
detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
|
|
396
570
|
|
|
397
571
|
# Choose an output folder
|
|
398
572
|
output_folder_is_input_folder = False
|
|
@@ -418,13 +592,19 @@ def process_video_folder(options):
|
|
|
418
592
|
|
|
419
593
|
video_fs = Fs[i_video]
|
|
420
594
|
|
|
421
|
-
if options.
|
|
595
|
+
if options.rendering_fs is not None:
|
|
596
|
+
rendering_fs = options.rendering_fs
|
|
597
|
+
elif options.frame_sample is None:
|
|
422
598
|
rendering_fs = video_fs
|
|
423
599
|
else:
|
|
600
|
+
# If the original video was 30fps and we sampled every 10th frame,
|
|
601
|
+
# render at 3fps
|
|
424
602
|
rendering_fs = video_fs / options.frame_sample
|
|
425
603
|
|
|
426
604
|
input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
|
|
427
|
-
video_frame_output_folder = os.path.join(
|
|
605
|
+
video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
|
|
606
|
+
|
|
607
|
+
video_frame_output_folder = video_frame_output_folder.replace('\\','/')
|
|
428
608
|
assert os.path.isdir(video_frame_output_folder), \
|
|
429
609
|
'Could not find frame folder for video {}'.format(input_video_file_relative)
|
|
430
610
|
|
|
@@ -445,47 +625,37 @@ def process_video_folder(options):
|
|
|
445
625
|
# Create the output video
|
|
446
626
|
print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
|
|
447
627
|
input_video_file_relative,video_output_file,rendering_fs,video_fs))
|
|
448
|
-
frames_to_video(video_frame_files,
|
|
628
|
+
frames_to_video(video_frame_files,
|
|
629
|
+
rendering_fs,
|
|
630
|
+
video_output_file,
|
|
631
|
+
codec_spec=options.fourcc)
|
|
449
632
|
|
|
450
633
|
# ...for each video
|
|
451
634
|
|
|
452
635
|
# 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
|
|
636
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
464
637
|
|
|
465
638
|
# ...if we're rendering video
|
|
466
639
|
|
|
467
640
|
|
|
468
641
|
## (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
|
|
642
|
+
_clean_up_extracted_frames(options, frame_output_folder, image_file_names)
|
|
483
643
|
|
|
484
644
|
# ...process_video_folder()
|
|
485
645
|
|
|
486
646
|
|
|
487
647
|
def options_to_command(options):
|
|
648
|
+
"""
|
|
649
|
+
Convert a ProcessVideoOptions obejct to a corresponding command line.
|
|
488
650
|
|
|
651
|
+
Args:
|
|
652
|
+
options (ProcessVideoOptions): the options set to render as a command line
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
str: the command line coresponding to [options]
|
|
656
|
+
|
|
657
|
+
:meta private:
|
|
658
|
+
"""
|
|
489
659
|
cmd = 'python process_video.py'
|
|
490
660
|
cmd += ' "' + options.model_file + '"'
|
|
491
661
|
cmd += ' "' + options.input_video_file + '"'
|
|
@@ -518,12 +688,30 @@ def options_to_command(options):
|
|
|
518
688
|
cmd += ' --n_cores ' + str(options.n_cores)
|
|
519
689
|
if options.frame_sample is not None:
|
|
520
690
|
cmd += ' --frame_sample ' + str(options.frame_sample)
|
|
691
|
+
if options.frames_to_extract is not None:
|
|
692
|
+
cmd += ' --frames_to_extract '
|
|
693
|
+
if isinstance(options.frames_to_extract,int):
|
|
694
|
+
frames_to_extract = [options.frames_to_extract]
|
|
695
|
+
else:
|
|
696
|
+
frames_to_extract = options.frames_to_extract
|
|
697
|
+
for frame_number in frames_to_extract:
|
|
698
|
+
cmd += ' {}'.format(frame_number)
|
|
521
699
|
if options.debug_max_frames is not None:
|
|
522
700
|
cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
|
|
523
701
|
if options.class_mapping_filename is not None:
|
|
524
702
|
cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
|
|
525
703
|
if options.fourcc is not None:
|
|
526
704
|
cmd += ' --fourcc ' + options.fourcc
|
|
705
|
+
if options.quality is not None:
|
|
706
|
+
cmd += ' --quality ' + str(options.quality)
|
|
707
|
+
if options.max_width is not None:
|
|
708
|
+
cmd += ' --max_width ' + str(options.max_width)
|
|
709
|
+
if options.verbose:
|
|
710
|
+
cmd += ' --verbose'
|
|
711
|
+
if options.force_extracted_frame_folder_deletion:
|
|
712
|
+
cmd += ' --force_extracted_frame_folder_deletion'
|
|
713
|
+
if options.force_rendered_frame_folder_deletion:
|
|
714
|
+
cmd += ' --force_rendered_frame_folder_deletion'
|
|
527
715
|
|
|
528
716
|
return cmd
|
|
529
717
|
|
|
@@ -532,14 +720,19 @@ def options_to_command(options):
|
|
|
532
720
|
|
|
533
721
|
if False:
|
|
534
722
|
|
|
723
|
+
pass
|
|
724
|
+
|
|
535
725
|
#%% Process a folder of videos
|
|
536
726
|
|
|
537
727
|
model_file = 'MDV5A'
|
|
538
|
-
input_dir = r'
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
728
|
+
# input_dir = r'g:\temp\test-videos'
|
|
729
|
+
input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
730
|
+
output_base = r'g:\temp\video_test'
|
|
731
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
732
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
733
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
734
|
+
output_video_folder = os.path.join(output_base,'output_videos')
|
|
735
|
+
|
|
543
736
|
|
|
544
737
|
print('Processing folder {}'.format(input_dir))
|
|
545
738
|
|
|
@@ -547,50 +740,125 @@ if False:
|
|
|
547
740
|
options.model_file = model_file
|
|
548
741
|
options.input_video_file = input_dir
|
|
549
742
|
options.output_video_file = output_video_folder
|
|
550
|
-
options.frame_folder = frame_folder
|
|
551
743
|
options.output_json_file = output_json_file
|
|
552
|
-
options.
|
|
553
|
-
options.
|
|
744
|
+
options.recursive = True
|
|
745
|
+
options.reuse_frames_if_available = False
|
|
746
|
+
options.reuse_results_if_available = False
|
|
747
|
+
options.quality = 90
|
|
748
|
+
options.frame_sample = 10
|
|
749
|
+
options.max_width = 1280
|
|
750
|
+
options.n_cores = 4
|
|
751
|
+
options.verbose = True
|
|
752
|
+
options.render_output_video = True
|
|
753
|
+
options.frame_folder = frame_folder
|
|
754
|
+
options.frame_rendering_folder = rendering_folder
|
|
554
755
|
options.keep_extracted_frames = True
|
|
555
756
|
options.keep_rendered_frames = True
|
|
556
|
-
options.
|
|
557
|
-
options.
|
|
558
|
-
options.
|
|
559
|
-
# options.
|
|
560
|
-
# options.fourcc = 'mp4v'
|
|
757
|
+
options.force_extracted_frame_folder_deletion = False
|
|
758
|
+
options.force_rendered_frame_folder_deletion = False
|
|
759
|
+
options.fourcc = 'mp4v'
|
|
760
|
+
# options.rendering_confidence_threshold = 0.15
|
|
561
761
|
|
|
562
|
-
cmd = options_to_command(options)
|
|
563
|
-
|
|
762
|
+
cmd = options_to_command(options); print(cmd)
|
|
763
|
+
|
|
564
764
|
# import clipboard; clipboard.copy(cmd)
|
|
565
|
-
|
|
566
|
-
if False:
|
|
567
|
-
process_video_folder(options)
|
|
765
|
+
# process_video_folder(options)
|
|
568
766
|
|
|
569
767
|
|
|
570
768
|
#%% Process a single video
|
|
571
769
|
|
|
572
|
-
fn =
|
|
770
|
+
fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
|
|
771
|
+
assert os.path.isfile(fn)
|
|
573
772
|
model_file = 'MDV5A'
|
|
574
773
|
input_video_file = fn
|
|
575
|
-
|
|
576
|
-
|
|
774
|
+
|
|
775
|
+
output_base = r'g:\temp\video_test'
|
|
776
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
777
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
778
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
779
|
+
output_video_file = os.path.join(output_base,'output_video.mp4')
|
|
577
780
|
|
|
578
781
|
options = ProcessVideoOptions()
|
|
579
782
|
options.model_file = model_file
|
|
580
783
|
options.input_video_file = input_video_file
|
|
581
|
-
options.frame_folder = frame_folder
|
|
582
|
-
options.frame_rendering_folder = rendering_folder
|
|
583
784
|
options.render_output_video = True
|
|
584
|
-
options.output_video_file =
|
|
785
|
+
options.output_video_file = output_video_file
|
|
786
|
+
options.output_json_file = output_json_file
|
|
787
|
+
options.verbose = True
|
|
788
|
+
options.quality = 75
|
|
789
|
+
options.frame_sample = 10
|
|
790
|
+
options.max_width = 1600
|
|
791
|
+
options.frame_folder = frame_folder
|
|
792
|
+
options.frame_rendering_folder = rendering_folder
|
|
793
|
+
options.keep_extracted_frames = False
|
|
794
|
+
options.keep_rendered_frames = False
|
|
795
|
+
options.force_extracted_frame_folder_deletion = True
|
|
796
|
+
options.force_rendered_frame_folder_deletion = True
|
|
797
|
+
options.fourcc = 'mp4v'
|
|
798
|
+
# options.rendering_confidence_threshold = 0.15
|
|
799
|
+
|
|
800
|
+
cmd = options_to_command(options); print(cmd)
|
|
801
|
+
|
|
802
|
+
# import clipboard; clipboard.copy(cmd)
|
|
803
|
+
process_video(options)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
#%% Extract specific frames from a single video, no detection
|
|
807
|
+
|
|
808
|
+
fn = r'g:\temp\test-videos\person_and_dog\DSCF0064.AVI'
|
|
809
|
+
assert os.path.isfile(fn)
|
|
810
|
+
model_file = 'no_detection'
|
|
811
|
+
input_video_file = fn
|
|
812
|
+
|
|
813
|
+
output_base = r'g:\temp\video_test'
|
|
814
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
815
|
+
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
816
|
+
|
|
817
|
+
options = ProcessVideoOptions()
|
|
818
|
+
options.model_file = model_file
|
|
819
|
+
options.input_video_file = input_video_file
|
|
820
|
+
options.verbose = True
|
|
821
|
+
options.quality = 90
|
|
822
|
+
options.frame_sample = None
|
|
823
|
+
options.frames_to_extract = [0,100]
|
|
824
|
+
options.max_width = None
|
|
825
|
+
options.frame_folder = frame_folder
|
|
826
|
+
options.keep_extracted_frames = True
|
|
827
|
+
|
|
828
|
+
cmd = options_to_command(options); print(cmd)
|
|
585
829
|
|
|
586
|
-
cmd = options_to_command(options)
|
|
587
|
-
print(cmd)
|
|
588
830
|
# import clipboard; clipboard.copy(cmd)
|
|
831
|
+
process_video(options)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
#%% Extract specific frames from a folder, no detection
|
|
835
|
+
|
|
836
|
+
fn = r'g:\temp\test-videos\person_and_dog'
|
|
837
|
+
assert os.path.isdir(fn)
|
|
838
|
+
model_file = 'no_detection'
|
|
839
|
+
input_video_file = fn
|
|
589
840
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
841
|
+
output_base = r'g:\temp\video_test'
|
|
842
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
843
|
+
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
844
|
+
|
|
845
|
+
options = ProcessVideoOptions()
|
|
846
|
+
options.model_file = model_file
|
|
847
|
+
options.input_video_file = input_video_file
|
|
848
|
+
options.verbose = True
|
|
849
|
+
options.quality = 90
|
|
850
|
+
options.frame_sample = None
|
|
851
|
+
options.frames_to_extract = [0,100]
|
|
852
|
+
options.max_width = None
|
|
853
|
+
options.frame_folder = frame_folder
|
|
854
|
+
options.keep_extracted_frames = True
|
|
593
855
|
|
|
856
|
+
cmd = options_to_command(options); print(cmd)
|
|
857
|
+
|
|
858
|
+
# import clipboard; clipboard.copy(cmd)
|
|
859
|
+
process_video(options)
|
|
860
|
+
|
|
861
|
+
|
|
594
862
|
#%% Command-line driver
|
|
595
863
|
|
|
596
864
|
def main():
|
|
@@ -602,7 +870,8 @@ def main():
|
|
|
602
870
|
'producing a new video with detections annotated'))
|
|
603
871
|
|
|
604
872
|
parser.add_argument('model_file', type=str,
|
|
605
|
-
help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")'
|
|
873
|
+
help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A"), '\
|
|
874
|
+
'or the string "no_detection" to run just frame extraction')
|
|
606
875
|
|
|
607
876
|
parser.add_argument('input_video_file', type=str,
|
|
608
877
|
help='video file (or folder) to process')
|
|
@@ -640,7 +909,8 @@ def main():
|
|
|
640
909
|
help='enable video output rendering (not rendered by default)')
|
|
641
910
|
|
|
642
911
|
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(
|
|
912
|
+
help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
|
|
913
|
+
default_fourcc))
|
|
644
914
|
|
|
645
915
|
parser.add_argument('--keep_rendered_frames',
|
|
646
916
|
action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
|
|
@@ -658,24 +928,45 @@ def main():
|
|
|
658
928
|
'whether other files were present in the folder.')
|
|
659
929
|
|
|
660
930
|
parser.add_argument('--rendering_confidence_threshold', type=float,
|
|
661
|
-
default=None,
|
|
931
|
+
default=None,
|
|
932
|
+
help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
|
|
933
|
+
|
|
934
|
+
parser.add_argument('--rendering_fs', type=float,
|
|
935
|
+
default=None,
|
|
936
|
+
help='force a specific frame rate for output videos (only relevant when using '\
|
|
937
|
+
'--render_output_video) (defaults to the original frame rate)')
|
|
662
938
|
|
|
663
939
|
parser.add_argument('--json_confidence_threshold', type=float,
|
|
664
|
-
default=
|
|
940
|
+
default=default_options.json_confidence_threshold,
|
|
941
|
+
help="don't include boxes in the .json file with confidence "\
|
|
665
942
|
'below this threshold (default {})'.format(
|
|
666
943
|
default_options.json_confidence_threshold))
|
|
667
944
|
|
|
668
945
|
parser.add_argument('--n_cores', type=int,
|
|
669
|
-
default=
|
|
946
|
+
default=default_options.n_cores,
|
|
947
|
+
help='Number of cores to use for frame separation and detection. '\
|
|
670
948
|
'If using a GPU, this option will be respected for frame separation but '\
|
|
671
949
|
'ignored for detection. Only relevant to frame separation when processing '\
|
|
672
|
-
'a folder.')
|
|
950
|
+
'a folder. Default {}.'.format(default_options.n_cores))
|
|
673
951
|
|
|
674
952
|
parser.add_argument('--frame_sample', type=int,
|
|
675
953
|
default=None, help='process every Nth frame (defaults to every frame)')
|
|
676
954
|
|
|
955
|
+
parser.add_argument('--frames_to_extract', nargs='+', type=int,
|
|
956
|
+
default=None, help='extract specific frames (one or more ints)')
|
|
957
|
+
|
|
958
|
+
parser.add_argument('--quality', type=int,
|
|
959
|
+
default=default_options.quality,
|
|
960
|
+
help='JPEG quality for extracted frames (defaults to {})'.format(
|
|
961
|
+
default_options.quality))
|
|
962
|
+
|
|
963
|
+
parser.add_argument('--max_width', type=int,
|
|
964
|
+
default=default_options.max_width,
|
|
965
|
+
help='Resize frames larger than this before writing (defaults to {})'.format(
|
|
966
|
+
default_options.max_width))
|
|
967
|
+
|
|
677
968
|
parser.add_argument('--debug_max_frames', type=int,
|
|
678
|
-
default=-1, help='
|
|
969
|
+
default=-1, help='Trim to N frames for debugging (impacts model execution, '\
|
|
679
970
|
'not frame rendering)')
|
|
680
971
|
|
|
681
972
|
parser.add_argument('--class_mapping_filename',
|
|
@@ -685,6 +976,19 @@ def main():
|
|
|
685
976
|
'the addition of "1" to all category IDs, so your class mapping should start '\
|
|
686
977
|
'at zero.')
|
|
687
978
|
|
|
979
|
+
parser.add_argument('--verbose', action='store_true',
|
|
980
|
+
help='Enable additional debug output')
|
|
981
|
+
|
|
982
|
+
parser.add_argument('--image_size',
|
|
983
|
+
type=int,
|
|
984
|
+
default=None,
|
|
985
|
+
help=('Force image resizing to a specific integer size on the long '\
|
|
986
|
+
'axis (not recommended to change this)'))
|
|
987
|
+
|
|
988
|
+
parser.add_argument('--augment',
|
|
989
|
+
action='store_true',
|
|
990
|
+
help='Enable image augmentation')
|
|
991
|
+
|
|
688
992
|
if len(sys.argv[1:]) == 0:
|
|
689
993
|
parser.print_help()
|
|
690
994
|
parser.exit()
|