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
|
@@ -152,6 +152,9 @@ class TFDetector:
|
|
|
152
152
|
assert image_size is None, 'Image sizing not supported for TF detectors'
|
|
153
153
|
assert not skip_image_resizing, 'Image sizing not supported for TF detectors'
|
|
154
154
|
|
|
155
|
+
if detection_threshold is None:
|
|
156
|
+
detection_threshold = 0
|
|
157
|
+
|
|
155
158
|
result = { 'file': image_id }
|
|
156
159
|
|
|
157
160
|
try:
|
|
@@ -18,6 +18,7 @@ from multiprocessing.pool import ThreadPool
|
|
|
18
18
|
from multiprocessing.pool import Pool
|
|
19
19
|
from tqdm import tqdm
|
|
20
20
|
from functools import partial
|
|
21
|
+
from inspect import signature
|
|
21
22
|
|
|
22
23
|
from megadetector.utils import path_utils
|
|
23
24
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
@@ -92,6 +93,8 @@ def find_videos(dirname,
|
|
|
92
93
|
if convert_slashes:
|
|
93
94
|
files = [fn.replace('\\', '/') for fn in files]
|
|
94
95
|
|
|
96
|
+
files = [fn for fn in files if os.path.isfile(fn)]
|
|
97
|
+
|
|
95
98
|
return find_video_strings(files)
|
|
96
99
|
|
|
97
100
|
|
|
@@ -118,8 +121,11 @@ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
|
|
|
118
121
|
codec_spec = 'h264'
|
|
119
122
|
|
|
120
123
|
if len(images) == 0:
|
|
124
|
+
print('Warning: no frames to render')
|
|
121
125
|
return
|
|
122
126
|
|
|
127
|
+
os.makedirs(os.path.dirname(output_file_name),exist_ok=True)
|
|
128
|
+
|
|
123
129
|
# Determine the width and height from the first image
|
|
124
130
|
frame = cv2.imread(images[0])
|
|
125
131
|
cv2.imshow('video',frame)
|
|
@@ -164,7 +170,8 @@ def _frame_number_to_filename(frame_number):
|
|
|
164
170
|
|
|
165
171
|
|
|
166
172
|
def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
167
|
-
every_n_frames=None, verbose=False
|
|
173
|
+
every_n_frames=None, verbose=False, quality=None,
|
|
174
|
+
max_width=None):
|
|
168
175
|
"""
|
|
169
176
|
Renders frames from [input_video_file] to a .jpg in [output_folder].
|
|
170
177
|
|
|
@@ -179,6 +186,9 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
179
186
|
every_n_frames (int, optional): sample every Nth frame starting from the first frame;
|
|
180
187
|
if this is None or 1, every frame is extracted
|
|
181
188
|
verbose (bool, optional): enable additional debug console output
|
|
189
|
+
quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
|
|
190
|
+
to the opencv default (typically 95).
|
|
191
|
+
max_width (int, optional): resize frames to be no wider than [max_width]
|
|
182
192
|
|
|
183
193
|
Returns:
|
|
184
194
|
tuple: length-2 tuple containing (list of frame filenames,frame rate)
|
|
@@ -194,36 +204,58 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
194
204
|
if overwrite == False:
|
|
195
205
|
|
|
196
206
|
missing_frame_number = None
|
|
207
|
+
missing_frame_filename = None
|
|
197
208
|
frame_filenames = []
|
|
209
|
+
found_existing_frame = False
|
|
198
210
|
|
|
199
211
|
for frame_number in range(0,n_frames):
|
|
200
212
|
|
|
201
213
|
if every_n_frames is not None:
|
|
202
|
-
if frame_number % every_n_frames != 0:
|
|
214
|
+
if (frame_number % every_n_frames) != 0:
|
|
203
215
|
continue
|
|
204
216
|
|
|
205
217
|
frame_filename = _frame_number_to_filename(frame_number)
|
|
206
218
|
frame_filename = os.path.join(output_folder,frame_filename)
|
|
207
219
|
frame_filenames.append(frame_filename)
|
|
208
220
|
if os.path.isfile(frame_filename):
|
|
221
|
+
found_existing_frame = True
|
|
209
222
|
continue
|
|
210
223
|
else:
|
|
211
224
|
missing_frame_number = frame_number
|
|
225
|
+
missing_frame_filename = frame_filename
|
|
212
226
|
break
|
|
213
227
|
|
|
228
|
+
if verbose and missing_frame_number is not None:
|
|
229
|
+
print('Missing frame {} ({}) for video {}'.format(
|
|
230
|
+
missing_frame_number,
|
|
231
|
+
missing_frame_filename,
|
|
232
|
+
input_video_file))
|
|
233
|
+
|
|
214
234
|
# OpenCV seems to over-report the number of frames by 1 in some cases, or fails
|
|
215
235
|
# to read the last frame; either way, I'm allowing one missing frame.
|
|
216
236
|
allow_last_frame_missing = True
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
# This doesn't have to mean literally the last frame number, it just means that if
|
|
239
|
+
# we find this frame or later, we consider the video done
|
|
240
|
+
last_expected_frame_number = n_frames-1
|
|
241
|
+
if every_n_frames is not None:
|
|
242
|
+
last_expected_frame_number -= (every_n_frames*2)
|
|
243
|
+
|
|
244
|
+
# If no frames are missing, or only frames very close to the end of the video are "missing",
|
|
245
|
+
# skip this video
|
|
246
|
+
if (missing_frame_number is None) or \
|
|
247
|
+
(allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
|
|
220
248
|
if verbose:
|
|
221
249
|
print('Skipping video {}, all output frames exist'.format(input_video_file))
|
|
222
250
|
return frame_filenames,Fs
|
|
223
251
|
else:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
# If we found some frames, but not all, print a message
|
|
253
|
+
if verbose and found_existing_frame:
|
|
254
|
+
print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
|
|
255
|
+
input_video_file,
|
|
256
|
+
missing_frame_number,
|
|
257
|
+
missing_frame_filename,
|
|
258
|
+
last_expected_frame_number))
|
|
227
259
|
|
|
228
260
|
# ...if we need to check whether to skip this video entirely
|
|
229
261
|
|
|
@@ -232,6 +264,28 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
232
264
|
|
|
233
265
|
frame_filenames = []
|
|
234
266
|
|
|
267
|
+
# YOLOv5 does some totally bananas monkey-patching of opencv,
|
|
268
|
+
# which causes problems if we try to supply a third parameter to
|
|
269
|
+
# imwrite (to specify JPEG quality). Detect this case, and ignore the quality
|
|
270
|
+
# parameter if it looks like imwrite has been messed with.
|
|
271
|
+
imwrite_patched = False
|
|
272
|
+
n_imwrite_parameters = None
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
# calling signature() on the native cv2.imwrite function will
|
|
276
|
+
# fail, so an exception here is a good thing. In fact I don't think
|
|
277
|
+
# there's a case where this *succeeds* and the number of parameters
|
|
278
|
+
# is wrong.
|
|
279
|
+
sig = signature(cv2.imwrite)
|
|
280
|
+
n_imwrite_parameters = len(sig.parameters)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
if (n_imwrite_parameters is not None) and (n_imwrite_parameters < 3):
|
|
285
|
+
imwrite_patched = True
|
|
286
|
+
if verbose and (quality is not None):
|
|
287
|
+
print('Warning: quality value supplied, but YOLOv5 has mucked with cv2.imwrite, ignoring quality')
|
|
288
|
+
|
|
235
289
|
# for frame_number in tqdm(range(0,n_frames)):
|
|
236
290
|
for frame_number in range(0,n_frames):
|
|
237
291
|
|
|
@@ -246,6 +300,25 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
246
300
|
if frame_number % every_n_frames != 0:
|
|
247
301
|
continue
|
|
248
302
|
|
|
303
|
+
# Has resizing been requested?
|
|
304
|
+
if max_width is not None:
|
|
305
|
+
|
|
306
|
+
# image.shape is h/w/dims
|
|
307
|
+
input_shape = image.shape
|
|
308
|
+
assert input_shape[2] == 3
|
|
309
|
+
input_width = input_shape[1]
|
|
310
|
+
|
|
311
|
+
# Is resizing necessary?
|
|
312
|
+
if input_width > max_width:
|
|
313
|
+
|
|
314
|
+
scale = max_width / input_width
|
|
315
|
+
assert scale <= 1.0
|
|
316
|
+
|
|
317
|
+
# INTER_AREA is recommended for size reduction
|
|
318
|
+
image = cv2.resize(image, (0,0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
|
|
319
|
+
|
|
320
|
+
# ...if we need to deal with resizing
|
|
321
|
+
|
|
249
322
|
frame_filename = _frame_number_to_filename(frame_number)
|
|
250
323
|
frame_filename = os.path.join(output_folder,frame_filename)
|
|
251
324
|
frame_filenames.append(frame_filename)
|
|
@@ -256,9 +329,18 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
256
329
|
else:
|
|
257
330
|
try:
|
|
258
331
|
if frame_filename.isascii():
|
|
259
|
-
|
|
332
|
+
|
|
333
|
+
if quality is None or imwrite_patched:
|
|
334
|
+
cv2.imwrite(os.path.normpath(frame_filename),image)
|
|
335
|
+
else:
|
|
336
|
+
cv2.imwrite(os.path.normpath(frame_filename),image,
|
|
337
|
+
[int(cv2.IMWRITE_JPEG_QUALITY), quality])
|
|
260
338
|
else:
|
|
261
|
-
|
|
339
|
+
if quality is None:
|
|
340
|
+
is_success, im_buf_arr = cv2.imencode('.jpg', image)
|
|
341
|
+
else:
|
|
342
|
+
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
|
343
|
+
is_success, im_buf_arr = cv2.imencode('.jpg', image, encode_param)
|
|
262
344
|
im_buf_arr.tofile(frame_filename)
|
|
263
345
|
assert os.path.isfile(frame_filename), \
|
|
264
346
|
'Output frame {} unavailable'.format(frame_filename)
|
|
@@ -269,7 +351,8 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
269
351
|
print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
|
|
270
352
|
|
|
271
353
|
if verbose:
|
|
272
|
-
print('\nExtracted {} of {} frames'.format(
|
|
354
|
+
print('\nExtracted {} of {} frames for {}'.format(
|
|
355
|
+
len(frame_filenames),n_frames,input_video_file))
|
|
273
356
|
|
|
274
357
|
vidcap.release()
|
|
275
358
|
return frame_filenames,Fs
|
|
@@ -277,7 +360,8 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
|
|
|
277
360
|
# ...def video_to_frames(...)
|
|
278
361
|
|
|
279
362
|
|
|
280
|
-
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
363
|
+
def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
364
|
+
every_n_frames,overwrite,verbose,quality,max_width):
|
|
281
365
|
"""
|
|
282
366
|
Internal function to call video_to_frames in the context of video_folder_to_frames;
|
|
283
367
|
makes sure the right output folder exists, then calls video_to_frames.
|
|
@@ -295,7 +379,7 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,ever
|
|
|
295
379
|
# input_video_file = input_fn_absolute; output_folder = output_folder_video
|
|
296
380
|
frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
|
|
297
381
|
overwrite=overwrite,every_n_frames=every_n_frames,
|
|
298
|
-
verbose=verbose)
|
|
382
|
+
verbose=verbose,quality=quality,max_width=max_width)
|
|
299
383
|
|
|
300
384
|
return frame_filenames,fs
|
|
301
385
|
|
|
@@ -303,7 +387,8 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,ever
|
|
|
303
387
|
def video_folder_to_frames(input_folder, output_folder_base,
|
|
304
388
|
recursive=True, overwrite=True,
|
|
305
389
|
n_threads=1, every_n_frames=None,
|
|
306
|
-
verbose=False, parallelization_uses_threads=True
|
|
390
|
+
verbose=False, parallelization_uses_threads=True,
|
|
391
|
+
quality=None, max_width=None):
|
|
307
392
|
"""
|
|
308
393
|
For every video file in input_folder, creates a folder within output_folder_base, and
|
|
309
394
|
renders frame of that video to images in that folder.
|
|
@@ -321,7 +406,10 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
321
406
|
verbose (bool, optional): enable additional debug console output
|
|
322
407
|
parallelization_uses_threads (bool, optional): whether to use threads (True) or
|
|
323
408
|
processes (False) for parallelization; ignored if n_threads <= 1
|
|
324
|
-
|
|
409
|
+
quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
|
|
410
|
+
to the opencv default (typically 95).
|
|
411
|
+
max_width (int, optional): resize frames to be no wider than [max_width]
|
|
412
|
+
|
|
325
413
|
Returns:
|
|
326
414
|
tuple: a length-3 tuple containing:
|
|
327
415
|
- list of lists of frame filenames; the Nth list of frame filenames corresponds to
|
|
@@ -352,7 +440,7 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
352
440
|
|
|
353
441
|
frame_filenames,fs = \
|
|
354
442
|
_video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
|
|
355
|
-
every_n_frames,overwrite,verbose)
|
|
443
|
+
every_n_frames,overwrite,verbose,quality,max_width)
|
|
356
444
|
frame_filenames_by_video.append(frame_filenames)
|
|
357
445
|
fs_by_video.append(fs)
|
|
358
446
|
else:
|
|
@@ -367,7 +455,9 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
367
455
|
output_folder_base=output_folder_base,
|
|
368
456
|
every_n_frames=every_n_frames,
|
|
369
457
|
overwrite=overwrite,
|
|
370
|
-
verbose=verbose
|
|
458
|
+
verbose=verbose,
|
|
459
|
+
quality=quality,
|
|
460
|
+
max_width=max_width)
|
|
371
461
|
results = list(tqdm(pool.imap(
|
|
372
462
|
partial(process_video_with_options),input_files_relative_paths),
|
|
373
463
|
total=len(input_files_relative_paths)))
|
|
@@ -385,13 +475,15 @@ class FrameToVideoOptions:
|
|
|
385
475
|
frame_results_to_video_results()
|
|
386
476
|
"""
|
|
387
477
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
478
|
+
def __init__(self):
|
|
479
|
+
|
|
480
|
+
#: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
|
|
481
|
+
#: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
|
|
482
|
+
self.nth_highest_confidence = 1
|
|
483
|
+
|
|
484
|
+
#: What to do if a file referred to in a .json results file appears not to be a
|
|
485
|
+
#: video; can be 'error' or 'skip_with_warning'
|
|
486
|
+
self.non_video_behavior = 'error'
|
|
395
487
|
|
|
396
488
|
|
|
397
489
|
def frame_results_to_video_results(input_file,output_file,options=None):
|
|
@@ -421,7 +513,7 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
421
513
|
|
|
422
514
|
## Break into videos
|
|
423
515
|
|
|
424
|
-
|
|
516
|
+
video_to_frame_info = defaultdict(list)
|
|
425
517
|
|
|
426
518
|
# im = images[0]
|
|
427
519
|
for im in tqdm(images):
|
|
@@ -437,25 +529,25 @@ def frame_results_to_video_results(input_file,output_file,options=None):
|
|
|
437
529
|
else:
|
|
438
530
|
raise ValueError('Unrecognized non-video handling behavior: {}'.format(
|
|
439
531
|
options.non_video_behavior))
|
|
440
|
-
|
|
532
|
+
video_to_frame_info[video_name].append(im)
|
|
441
533
|
|
|
442
534
|
print('Found {} unique videos in {} frame-level results'.format(
|
|
443
|
-
len(
|
|
535
|
+
len(video_to_frame_info),len(images)))
|
|
444
536
|
|
|
445
537
|
output_images = []
|
|
446
538
|
|
|
447
539
|
## For each video...
|
|
448
540
|
|
|
449
|
-
# video_name = list(
|
|
450
|
-
for video_name in tqdm(
|
|
541
|
+
# video_name = list(video_to_frame_info.keys())[0]
|
|
542
|
+
for video_name in tqdm(video_to_frame_info):
|
|
451
543
|
|
|
452
|
-
frames =
|
|
544
|
+
frames = video_to_frame_info[video_name]
|
|
453
545
|
|
|
454
546
|
all_detections_this_video = []
|
|
455
547
|
|
|
456
548
|
# frame = frames[0]
|
|
457
549
|
for frame in frames:
|
|
458
|
-
if frame['detections'] is not None:
|
|
550
|
+
if ('detections' in frame) and (frame['detections'] is not None):
|
|
459
551
|
all_detections_this_video.extend(frame['detections'])
|
|
460
552
|
|
|
461
553
|
# At most one detection for each category for the whole video
|
|
@@ -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
|