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
|
@@ -125,8 +125,13 @@ class TFDetector:
|
|
|
125
125
|
return box_tensor_out, score_tensor_out, class_tensor_out
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def generate_detections_one_image(self,
|
|
129
|
-
|
|
128
|
+
def generate_detections_one_image(self,
|
|
129
|
+
image,
|
|
130
|
+
image_id,
|
|
131
|
+
detection_threshold,
|
|
132
|
+
image_size=None,
|
|
133
|
+
skip_image_resizing=False,
|
|
134
|
+
augment=False):
|
|
130
135
|
"""
|
|
131
136
|
Runs the detector on an image.
|
|
132
137
|
|
|
@@ -139,7 +144,9 @@ class TFDetector:
|
|
|
139
144
|
if (a) you're using a model other than MegaDetector or (b) you know what you're
|
|
140
145
|
doing
|
|
141
146
|
skip_image_resizing (bool, optional): whether to skip internal image resizing (and rely on external
|
|
142
|
-
resizing)
|
|
147
|
+
resizing). Not currently supported, but included here for compatibility with PTDetector.
|
|
148
|
+
augment (bool, optional): enable image augmentation. Not currently supported, but included
|
|
149
|
+
here for compatibility with PTDetector.
|
|
143
150
|
|
|
144
151
|
Returns:
|
|
145
152
|
dict: a dictionary with the following fields:
|
|
@@ -151,7 +158,11 @@ class TFDetector:
|
|
|
151
158
|
|
|
152
159
|
assert image_size is None, 'Image sizing not supported for TF detectors'
|
|
153
160
|
assert not skip_image_resizing, 'Image sizing not supported for TF detectors'
|
|
161
|
+
assert not augment, 'Image augmentation is not supported for TF detectors'
|
|
154
162
|
|
|
163
|
+
if detection_threshold is None:
|
|
164
|
+
detection_threshold = 0
|
|
165
|
+
|
|
155
166
|
result = { 'file': image_id }
|
|
156
167
|
|
|
157
168
|
try:
|
|
@@ -9,6 +9,7 @@ Utilities for splitting, rendering, and assembling videos.
|
|
|
9
9
|
#%% Constants, imports, environment
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
+
import re
|
|
12
13
|
import cv2
|
|
13
14
|
import glob
|
|
14
15
|
import json
|
|
@@ -18,6 +19,7 @@ from multiprocessing.pool import ThreadPool
|
|
|
18
19
|
from multiprocessing.pool import Pool
|
|
19
20
|
from tqdm import tqdm
|
|
20
21
|
from functools import partial
|
|
22
|
+
from inspect import signature
|
|
21
23
|
|
|
22
24
|
from megadetector.utils import path_utils
|
|
23
25
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
@@ -92,10 +94,12 @@ def find_videos(dirname,
|
|
|
92
94
|
if convert_slashes:
|
|
93
95
|
files = [fn.replace('\\', '/') for fn in files]
|
|
94
96
|
|
|
97
|
+
files = [fn for fn in files if os.path.isfile(fn)]
|
|
98
|
+
|
|
95
99
|
return find_video_strings(files)
|
|
96
100
|
|
|
97
101
|
|
|
98
|
-
#%%
|
|
102
|
+
#%% Functions for rendering frames to video and vice-versa
|
|
99
103
|
|
|
100
104
|
# http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
|
|
101
105
|
|
|
@@ -118,8 +122,11 @@ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
|
|
|
118
122
|
codec_spec = 'h264'
|
|
119
123
|
|
|
120
124
|
if len(images) == 0:
|
|
125
|
+
print('Warning: no frames to render')
|
|
121
126
|
return
|
|
122
127
|
|
|
128
|
+
os.makedirs(os.path.dirname(output_file_name),exist_ok=True)
|
|
129
|
+
|
|
123
130
|
# Determine the width and height from the first image
|
|
124
131
|
frame = cv2.imread(images[0])
|
|
125
132
|
cv2.imshow('video',frame)
|
|
@@ -163,8 +170,55 @@ def _frame_number_to_filename(frame_number):
|
|
|
163
170
|
return 'frame{:06d}.jpg'.format(frame_number)
|
|
164
171
|
|
|
165
172
|
|
|
166
|
-
def
|
|
167
|
-
|
|
173
|
+
def _filename_to_frame_number(filename):
|
|
174
|
+
"""
|
|
175
|
+
Extract the frame number from a filename that was created using
|
|
176
|
+
_frame_number_to_filename.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
filename (str): a filename created with _frame_number_to_filename.
|
|
180
|
+
Returns:
|
|
181
|
+
int: the frame number extracted from [filename]
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
filename = os.path.basename(filename)
|
|
185
|
+
match = re.search(r'frame(\d+)\.jpg', filename)
|
|
186
|
+
if match is None:
|
|
187
|
+
raise ValueError('{} does not appear to be a frame file'.format(filename))
|
|
188
|
+
frame_number = match.group(1)
|
|
189
|
+
try:
|
|
190
|
+
frame_number = int(frame_number)
|
|
191
|
+
except:
|
|
192
|
+
raise ValueError('Filename {} does contain a valid frame number'.format(filename))
|
|
193
|
+
|
|
194
|
+
return frame_number
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _add_frame_numbers_to_results(results):
|
|
198
|
+
"""
|
|
199
|
+
Given the 'images' list from a set of MD results that was generated on video frames,
|
|
200
|
+
add a 'frame_number' field to each image.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
results (list): list of image dicts
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# Add video-specific fields to the results
|
|
207
|
+
for im in results:
|
|
208
|
+
fn = im['file']
|
|
209
|
+
frame_number = _filename_to_frame_number(fn)
|
|
210
|
+
im['frame_number'] = frame_number
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def video_to_frames(input_video_file,
|
|
214
|
+
output_folder,
|
|
215
|
+
overwrite=True,
|
|
216
|
+
every_n_frames=None,
|
|
217
|
+
verbose=False,
|
|
218
|
+
quality=None,
|
|
219
|
+
max_width=None,
|
|
220
|
+
frames_to_extract=None,
|
|
221
|
+
allow_empty_videos=False):
|
|
168
222
|
"""
|
|
169
223
|
Renders frames from [input_video_file] to a .jpg in [output_folder].
|
|
170
224
|
|
|
@@ -177,8 +231,18 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
177
231
|
output_folder (str): folder to put frame images in
|
|
178
232
|
overwrite (bool, optional): whether to overwrite existing frame images
|
|
179
233
|
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
|
|
234
|
+
if this is None or 1, every frame is extracted. Mutually exclusive with
|
|
235
|
+
frames_to_extract.
|
|
181
236
|
verbose (bool, optional): enable additional debug console output
|
|
237
|
+
quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
|
|
238
|
+
to the opencv default (typically 95).
|
|
239
|
+
max_width (int, optional): resize frames to be no wider than [max_width]
|
|
240
|
+
frames_to_extract (list of int, optional): extract this specific set of frames;
|
|
241
|
+
mutually exclusive with every_n_frames. If all values are beyond the length
|
|
242
|
+
of the video, no frames are extracted. Can also be a single int, specifying
|
|
243
|
+
a single frame number.
|
|
244
|
+
allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
|
|
245
|
+
frames (by default, this is an error).
|
|
182
246
|
|
|
183
247
|
Returns:
|
|
184
248
|
tuple: length-2 tuple containing (list of frame filenames,frame rate)
|
|
@@ -186,6 +250,14 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
186
250
|
|
|
187
251
|
assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
|
|
188
252
|
|
|
253
|
+
if isinstance(frames_to_extract,int):
|
|
254
|
+
frames_to_extract = [frames_to_extract]
|
|
255
|
+
|
|
256
|
+
if (frames_to_extract is not None) and (every_n_frames is not None):
|
|
257
|
+
raise ValueError('frames_to_extract and every_n_frames are mutually exclusive')
|
|
258
|
+
|
|
259
|
+
os.makedirs(output_folder,exist_ok=True)
|
|
260
|
+
|
|
189
261
|
vidcap = cv2.VideoCapture(input_video_file)
|
|
190
262
|
n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
191
263
|
Fs = vidcap.get(cv2.CAP_PROP_FPS)
|
|
@@ -194,36 +266,74 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
194
266
|
if overwrite == False:
|
|
195
267
|
|
|
196
268
|
missing_frame_number = None
|
|
269
|
+
missing_frame_filename = None
|
|
197
270
|
frame_filenames = []
|
|
271
|
+
found_existing_frame = False
|
|
198
272
|
|
|
199
273
|
for frame_number in range(0,n_frames):
|
|
200
274
|
|
|
201
275
|
if every_n_frames is not None:
|
|
202
|
-
|
|
276
|
+
assert frames_to_extract is None, \
|
|
277
|
+
'Internal error: frames_to_extract and every_n_frames are exclusive'
|
|
278
|
+
if (frame_number % every_n_frames) != 0:
|
|
203
279
|
continue
|
|
204
280
|
|
|
281
|
+
if frames_to_extract is not None:
|
|
282
|
+
assert every_n_frames is None, \
|
|
283
|
+
'Internal error: frames_to_extract and every_n_frames are exclusive'
|
|
284
|
+
if frame_number not in frames_to_extract:
|
|
285
|
+
continue
|
|
286
|
+
|
|
205
287
|
frame_filename = _frame_number_to_filename(frame_number)
|
|
206
288
|
frame_filename = os.path.join(output_folder,frame_filename)
|
|
207
289
|
frame_filenames.append(frame_filename)
|
|
208
290
|
if os.path.isfile(frame_filename):
|
|
291
|
+
found_existing_frame = True
|
|
209
292
|
continue
|
|
210
293
|
else:
|
|
211
294
|
missing_frame_number = frame_number
|
|
295
|
+
missing_frame_filename = frame_filename
|
|
212
296
|
break
|
|
213
297
|
|
|
298
|
+
if verbose and missing_frame_number is not None:
|
|
299
|
+
print('Missing frame {} ({}) for video {}'.format(
|
|
300
|
+
missing_frame_number,
|
|
301
|
+
missing_frame_filename,
|
|
302
|
+
input_video_file))
|
|
303
|
+
|
|
214
304
|
# OpenCV seems to over-report the number of frames by 1 in some cases, or fails
|
|
215
305
|
# to read the last frame; either way, I'm allowing one missing frame.
|
|
216
306
|
allow_last_frame_missing = True
|
|
217
307
|
|
|
218
|
-
|
|
219
|
-
|
|
308
|
+
# This doesn't have to mean literally the last frame number, it just means that if
|
|
309
|
+
# we find this frame or later, we consider the video done
|
|
310
|
+
last_expected_frame_number = n_frames-1
|
|
311
|
+
if every_n_frames is not None:
|
|
312
|
+
last_expected_frame_number -= (every_n_frames*2)
|
|
313
|
+
|
|
314
|
+
# When specific frames are requested, if anything is missing, reprocess the video
|
|
315
|
+
if (frames_to_extract is not None) and (missing_frame_number is not None):
|
|
316
|
+
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
# If no frames are missing, or only frames very close to the end of the video are "missing",
|
|
320
|
+
# skip this video
|
|
321
|
+
elif (missing_frame_number is None) or \
|
|
322
|
+
(allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
|
|
323
|
+
|
|
220
324
|
if verbose:
|
|
221
325
|
print('Skipping video {}, all output frames exist'.format(input_video_file))
|
|
222
326
|
return frame_filenames,Fs
|
|
327
|
+
|
|
223
328
|
else:
|
|
224
|
-
|
|
225
|
-
#
|
|
226
|
-
|
|
329
|
+
|
|
330
|
+
# If we found some frames, but not all, print a message
|
|
331
|
+
if verbose and found_existing_frame:
|
|
332
|
+
print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
|
|
333
|
+
input_video_file,
|
|
334
|
+
missing_frame_number,
|
|
335
|
+
missing_frame_filename,
|
|
336
|
+
last_expected_frame_number))
|
|
227
337
|
|
|
228
338
|
# ...if we need to check whether to skip this video entirely
|
|
229
339
|
|
|
@@ -232,6 +342,32 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
232
342
|
|
|
233
343
|
frame_filenames = []
|
|
234
344
|
|
|
345
|
+
# YOLOv5 does some totally bananas monkey-patching of opencv, which causes
|
|
346
|
+
# problems if we try to supply a third parameter to imwrite (to specify JPEG
|
|
347
|
+
# quality). Detect this case, and ignore the quality parameter if it looks
|
|
348
|
+
# like imwrite has been messed with.
|
|
349
|
+
#
|
|
350
|
+
# See:
|
|
351
|
+
#
|
|
352
|
+
# https://github.com/ultralytics/yolov5/issues/7285
|
|
353
|
+
imwrite_patched = False
|
|
354
|
+
n_imwrite_parameters = None
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
# calling signature() on the native cv2.imwrite function will
|
|
358
|
+
# fail, so an exception here is a good thing. In fact I don't think
|
|
359
|
+
# there's a case where this *succeeds* and the number of parameters
|
|
360
|
+
# is wrong.
|
|
361
|
+
sig = signature(cv2.imwrite)
|
|
362
|
+
n_imwrite_parameters = len(sig.parameters)
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
if (n_imwrite_parameters is not None) and (n_imwrite_parameters < 3):
|
|
367
|
+
imwrite_patched = True
|
|
368
|
+
if verbose and (quality is not None):
|
|
369
|
+
print('Warning: quality value supplied, but YOLOv5 has mucked with cv2.imwrite, ignoring quality')
|
|
370
|
+
|
|
235
371
|
# for frame_number in tqdm(range(0,n_frames)):
|
|
236
372
|
for frame_number in range(0,n_frames):
|
|
237
373
|
|
|
@@ -245,7 +381,32 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
245
381
|
if every_n_frames is not None:
|
|
246
382
|
if frame_number % every_n_frames != 0:
|
|
247
383
|
continue
|
|
384
|
+
|
|
385
|
+
if frames_to_extract is not None:
|
|
386
|
+
if frame_number > max(frames_to_extract):
|
|
387
|
+
break
|
|
388
|
+
if frame_number not in frames_to_extract:
|
|
389
|
+
continue
|
|
248
390
|
|
|
391
|
+
# Has resizing been requested?
|
|
392
|
+
if max_width is not None:
|
|
393
|
+
|
|
394
|
+
# image.shape is h/w/dims
|
|
395
|
+
input_shape = image.shape
|
|
396
|
+
assert input_shape[2] == 3
|
|
397
|
+
input_width = input_shape[1]
|
|
398
|
+
|
|
399
|
+
# Is resizing necessary?
|
|
400
|
+
if input_width > max_width:
|
|
401
|
+
|
|
402
|
+
scale = max_width / input_width
|
|
403
|
+
assert scale <= 1.0
|
|
404
|
+
|
|
405
|
+
# INTER_AREA is recommended for size reduction
|
|
406
|
+
image = cv2.resize(image, (0,0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
|
|
407
|
+
|
|
408
|
+
# ...if we need to deal with resizing
|
|
409
|
+
|
|
249
410
|
frame_filename = _frame_number_to_filename(frame_number)
|
|
250
411
|
frame_filename = os.path.join(output_folder,frame_filename)
|
|
251
412
|
frame_filenames.append(frame_filename)
|
|
@@ -256,9 +417,18 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
256
417
|
else:
|
|
257
418
|
try:
|
|
258
419
|
if frame_filename.isascii():
|
|
259
|
-
|
|
420
|
+
|
|
421
|
+
if quality is None or imwrite_patched:
|
|
422
|
+
cv2.imwrite(os.path.normpath(frame_filename),image)
|
|
423
|
+
else:
|
|
424
|
+
cv2.imwrite(os.path.normpath(frame_filename),image,
|
|
425
|
+
[int(cv2.IMWRITE_JPEG_QUALITY), quality])
|
|
260
426
|
else:
|
|
261
|
-
|
|
427
|
+
if quality is None:
|
|
428
|
+
is_success, im_buf_arr = cv2.imencode('.jpg', image)
|
|
429
|
+
else:
|
|
430
|
+
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
|
431
|
+
is_success, im_buf_arr = cv2.imencode('.jpg', image, encode_param)
|
|
262
432
|
im_buf_arr.tofile(frame_filename)
|
|
263
433
|
assert os.path.isfile(frame_filename), \
|
|
264
434
|
'Output frame {} unavailable'.format(frame_filename)
|
|
@@ -268,8 +438,13 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
268
438
|
except Exception as e:
|
|
269
439
|
print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
|
|
270
440
|
|
|
441
|
+
if len(frame_filenames) == 0:
|
|
442
|
+
raise Exception('Error: found no frames in file {}'.format(
|
|
443
|
+
input_video_file))
|
|
444
|
+
|
|
271
445
|
if verbose:
|
|
272
|
-
print('\nExtracted {} of {} frames'.format(
|
|
446
|
+
print('\nExtracted {} of {} frames for {}'.format(
|
|
447
|
+
len(frame_filenames),n_frames,input_video_file))
|
|
273
448
|
|
|
274
449
|
vidcap.release()
|
|
275
450
|
return frame_filenames,Fs
|
|
@@ -277,10 +452,13 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
277
452
|
# ...def video_to_frames(...)
|
|
278
453
|
|
|
279
454
|
|
|
280
|
-
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
455
|
+
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
456
|
+
every_n_frames,overwrite,verbose,quality,max_width,
|
|
457
|
+
frames_to_extract):
|
|
281
458
|
"""
|
|
282
|
-
Internal function to call video_to_frames in the context of
|
|
283
|
-
makes sure the right output folder exists, then calls
|
|
459
|
+
Internal function to call video_to_frames for a single video in the context of
|
|
460
|
+
video_folder_to_frames; makes sure the right output folder exists, then calls
|
|
461
|
+
video_to_frames.
|
|
284
462
|
"""
|
|
285
463
|
|
|
286
464
|
input_fn_absolute = os.path.join(input_folder,relative_fn)
|
|
@@ -295,7 +473,8 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,ever
|
|
|
295
473
|
# input_video_file = input_fn_absolute; output_folder = output_folder_video
|
|
296
474
|
frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
|
|
297
475
|
overwrite=overwrite,every_n_frames=every_n_frames,
|
|
298
|
-
verbose=verbose
|
|
476
|
+
verbose=verbose,quality=quality,max_width=max_width,
|
|
477
|
+
frames_to_extract=frames_to_extract)
|
|
299
478
|
|
|
300
479
|
return frame_filenames,fs
|
|
301
480
|
|
|
@@ -303,7 +482,9 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,ever
|
|
|
303
482
|
def video_folder_to_frames(input_folder, output_folder_base,
|
|
304
483
|
recursive=True, overwrite=True,
|
|
305
484
|
n_threads=1, every_n_frames=None,
|
|
306
|
-
verbose=False, parallelization_uses_threads=True
|
|
485
|
+
verbose=False, parallelization_uses_threads=True,
|
|
486
|
+
quality=None, max_width=None,
|
|
487
|
+
frames_to_extract=None):
|
|
307
488
|
"""
|
|
308
489
|
For every video file in input_folder, creates a folder within output_folder_base, and
|
|
309
490
|
renders frame of that video to images in that folder.
|
|
@@ -317,11 +498,19 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
317
498
|
n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
|
|
318
499
|
parallelism
|
|
319
500
|
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
|
|
501
|
+
if this is None or 1, every frame is extracted. Mutually exclusive with
|
|
502
|
+
frames_to_extract.
|
|
321
503
|
verbose (bool, optional): enable additional debug console output
|
|
322
504
|
parallelization_uses_threads (bool, optional): whether to use threads (True) or
|
|
323
505
|
processes (False) for parallelization; ignored if n_threads <= 1
|
|
324
|
-
|
|
506
|
+
quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
|
|
507
|
+
to the opencv default (typically 95).
|
|
508
|
+
max_width (int, optional): resize frames to be no wider than [max_width]
|
|
509
|
+
frames_to_extract (list of int, optional): extract this specific set of frames from
|
|
510
|
+
each video; mutually exclusive with every_n_frames. If all values are beyond
|
|
511
|
+
the length of a video, no frames are extracted. Can also be a single int,
|
|
512
|
+
specifying a single frame number.
|
|
513
|
+
|
|
325
514
|
Returns:
|
|
326
515
|
tuple: a length-3 tuple containing:
|
|
327
516
|
- list of lists of frame filenames; the Nth list of frame filenames corresponds to
|
|
@@ -352,7 +541,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
352
541
|
|
|
353
542
|
frame_filenames,fs = \
|
|
354
543
|
_video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
|
|
355
|
-
every_n_frames,overwrite,verbose
|
|
544
|
+
every_n_frames,overwrite,verbose,quality,max_width,
|
|
545
|
+
frames_to_extract)
|
|
356
546
|
frame_filenames_by_video.append(frame_filenames)
|
|
357
547
|
fs_by_video.append(fs)
|
|
358
548
|
else:
|
|
@@ -367,7 +557,10 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
367
557
|
output_folder_base=output_folder_base,
|
|
368
558
|
every_n_frames=every_n_frames,
|
|
369
559
|
overwrite=overwrite,
|
|
370
|
-
verbose=verbose
|
|
560
|
+
verbose=verbose,
|
|
561
|
+
quality=quality,
|
|
562
|
+
max_width=max_width,
|
|
563
|
+
frames_to_extract=frames_to_extract)
|
|
371
564
|
results = list(tqdm(pool.imap(
|
|
372
565
|
partial(process_video_with_options),input_files_relative_paths),
|
|
373
566
|
total=len(input_files_relative_paths)))
|
|
@@ -385,15 +578,17 @@ class FrameToVideoOptions:
|
|
|
385
578
|
frame_results_to_video_results()
|
|
386
579
|
"""
|
|
387
580
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
581
|
+
def __init__(self):
|
|
582
|
+
|
|
583
|
+
#: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
|
|
584
|
+
#: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
|
|
585
|
+
self.nth_highest_confidence = 1
|
|
586
|
+
|
|
587
|
+
#: What to do if a file referred to in a .json results file appears not to be a
|
|
588
|
+
#: video; can be 'error' or 'skip_with_warning'
|
|
589
|
+
self.non_video_behavior = 'error'
|
|
396
590
|
|
|
591
|
+
|
|
397
592
|
def frame_results_to_video_results(input_file,output_file,options=None):
|
|
398
593
|
"""
|
|
399
594
|
Given an MD results file produced at the *frame* level, corresponding to a directory
|
|
@@ -419,16 +614,19 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
419
614
|
images = input_data['images']
|
|
420
615
|
detection_categories = input_data['detection_categories']
|
|
421
616
|
|
|
617
|
+
|
|
422
618
|
## Break into videos
|
|
423
619
|
|
|
424
|
-
|
|
620
|
+
video_to_frame_info = defaultdict(list)
|
|
425
621
|
|
|
426
622
|
# im = images[0]
|
|
427
623
|
for im in tqdm(images):
|
|
428
624
|
|
|
429
625
|
fn = im['file']
|
|
430
626
|
video_name = os.path.dirname(fn)
|
|
627
|
+
|
|
431
628
|
if not is_video_file(video_name):
|
|
629
|
+
|
|
432
630
|
if options.non_video_behavior == 'error':
|
|
433
631
|
raise ValueError('{} is not a video file'.format(video_name))
|
|
434
632
|
elif options.non_video_behavior == 'skip_with_warning':
|
|
@@ -437,25 +635,37 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
437
635
|
else:
|
|
438
636
|
raise ValueError('Unrecognized non-video handling behavior: {}'.format(
|
|
439
637
|
options.non_video_behavior))
|
|
440
|
-
|
|
638
|
+
|
|
639
|
+
# Attach video-specific fields to the output, specifically attach the frame
|
|
640
|
+
# number to both the video and each detection. Only the frame number for the
|
|
641
|
+
# canonical detection will end up in the video-level output file.
|
|
642
|
+
frame_number = _filename_to_frame_number(fn)
|
|
643
|
+
im['frame_number'] = frame_number
|
|
644
|
+
for detection in im['detections']:
|
|
645
|
+
detection['frame_number'] = frame_number
|
|
646
|
+
|
|
647
|
+
video_to_frame_info[video_name].append(im)
|
|
648
|
+
|
|
649
|
+
# ...for each frame referred to in the results file
|
|
441
650
|
|
|
442
651
|
print('Found {} unique videos in {} frame-level results'.format(
|
|
443
|
-
len(
|
|
652
|
+
len(video_to_frame_info),len(images)))
|
|
444
653
|
|
|
445
654
|
output_images = []
|
|
446
655
|
|
|
656
|
+
|
|
447
657
|
## For each video...
|
|
448
658
|
|
|
449
|
-
# video_name = list(
|
|
450
|
-
for video_name in tqdm(
|
|
659
|
+
# video_name = list(video_to_frame_info.keys())[0]
|
|
660
|
+
for video_name in tqdm(video_to_frame_info):
|
|
451
661
|
|
|
452
|
-
frames =
|
|
662
|
+
frames = video_to_frame_info[video_name]
|
|
453
663
|
|
|
454
664
|
all_detections_this_video = []
|
|
455
665
|
|
|
456
666
|
# frame = frames[0]
|
|
457
667
|
for frame in frames:
|
|
458
|
-
if frame['detections'] is not None:
|
|
668
|
+
if ('detections' in frame) and (frame['detections'] is not None):
|
|
459
669
|
all_detections_this_video.extend(frame['detections'])
|
|
460
670
|
|
|
461
671
|
# At most one detection for each category for the whole video
|
|
@@ -502,37 +712,60 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
502
712
|
# ...def frame_results_to_video_results(...)
|
|
503
713
|
|
|
504
714
|
|
|
505
|
-
#%% Test
|
|
715
|
+
#%% Test drivers
|
|
506
716
|
|
|
507
717
|
if False:
|
|
508
718
|
|
|
719
|
+
pass
|
|
720
|
+
|
|
509
721
|
#%% Constants
|
|
510
722
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
input_folder
|
|
514
|
-
|
|
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
|
-
|
|
723
|
+
input_folder = r'G:\temp\usu-long\data'
|
|
724
|
+
frame_folder_base = r'g:\temp\usu-long-single-frames'
|
|
725
|
+
assert os.path.isdir(input_folder)
|
|
726
|
+
|
|
522
727
|
|
|
523
728
|
#%% Split videos into frames
|
|
524
729
|
|
|
525
730
|
frame_filenames_by_video,fs_by_video,video_filenames = \
|
|
526
|
-
video_folder_to_frames(input_folder,
|
|
731
|
+
video_folder_to_frames(input_folder,
|
|
732
|
+
frame_folder_base,
|
|
733
|
+
recursive=True,
|
|
734
|
+
overwrite=True,
|
|
735
|
+
n_threads=10,
|
|
736
|
+
every_n_frames=None,
|
|
737
|
+
verbose=True,
|
|
738
|
+
parallelization_uses_threads=True,
|
|
739
|
+
quality=None,
|
|
740
|
+
max_width=None,
|
|
741
|
+
frames_to_extract=150)
|
|
742
|
+
|
|
527
743
|
|
|
744
|
+
#%% Constants for detection tests
|
|
745
|
+
|
|
746
|
+
detected_frame_folder_base = r'e:\video_test\detected_frames'
|
|
747
|
+
rendered_videos_folder_base = r'e:\video_test\rendered_videos'
|
|
748
|
+
os.makedirs(detected_frame_folder_base,exist_ok=True)
|
|
749
|
+
os.makedirs(rendered_videos_folder_base,exist_ok=True)
|
|
750
|
+
results_file = r'results.json'
|
|
751
|
+
confidence_threshold = 0.75
|
|
752
|
+
|
|
753
|
+
#%% Load detector output
|
|
528
754
|
|
|
755
|
+
with open(results_file,'r') as f:
|
|
756
|
+
detection_results = json.load(f)
|
|
757
|
+
detections = detection_results['images']
|
|
758
|
+
detector_label_map = detection_results['detection_categories']
|
|
759
|
+
for d in detections:
|
|
760
|
+
d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
|
|
761
|
+
|
|
762
|
+
|
|
529
763
|
#%% List image files, break into folders
|
|
530
764
|
|
|
531
765
|
frame_files = path_utils.find_images(frame_folder_base,True)
|
|
532
766
|
frame_files = [s.replace('\\','/') for s in frame_files]
|
|
533
767
|
print('Enumerated {} total frames'.format(len(frame_files)))
|
|
534
768
|
|
|
535
|
-
Fs = 30.01
|
|
536
769
|
# Find unique folders
|
|
537
770
|
folders = set()
|
|
538
771
|
# fn = frame_files[0]
|
|
@@ -542,16 +775,6 @@ if False:
|
|
|
542
775
|
print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
|
|
543
776
|
|
|
544
777
|
|
|
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
778
|
#%% Render detector frames
|
|
556
779
|
|
|
557
780
|
# folder = list(folders)[0]
|
|
@@ -22,20 +22,22 @@ class SizeCategorizationOptions:
|
|
|
22
22
|
Options used to parameterize categorize_detections_by_size().
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
def __init__(self):
|
|
26
|
+
|
|
27
|
+
#: Thresholds to use for separation, as a fraction of the image size.
|
|
28
|
+
#:
|
|
29
|
+
#: Should be sorted from smallest to largest.
|
|
30
|
+
self.size_thresholds = [0.95]
|
|
31
|
+
|
|
32
|
+
#: List of category numbers to use in separation; uses all categories if None
|
|
33
|
+
self.categories_to_separate = None
|
|
34
|
+
|
|
35
|
+
#: Dimension to use for thresholding; can be "size", "width", or "height"
|
|
36
|
+
self.measurement = 'size'
|
|
37
|
+
|
|
38
|
+
#: Categories to assign to thresholded ranges; should have the same length as
|
|
39
|
+
#: "size_thresholds".
|
|
40
|
+
self.size_category_names = ['large_detection']
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
#%% Main functions
|