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.

Files changed (40) hide show
  1. megadetector/api/batch_processing/api_core/server.py +1 -1
  2. megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
  3. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
  4. megadetector/api/batch_processing/api_core/server_utils.py +0 -4
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  6. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
  7. megadetector/classification/efficientnet/utils.py +0 -3
  8. megadetector/data_management/camtrap_dp_to_coco.py +0 -2
  9. megadetector/data_management/cct_json_utils.py +15 -6
  10. megadetector/data_management/coco_to_labelme.py +12 -1
  11. megadetector/data_management/databases/integrity_check_json_db.py +43 -27
  12. megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
  13. megadetector/data_management/ocr_tools.py +0 -4
  14. megadetector/data_management/read_exif.py +171 -43
  15. megadetector/data_management/rename_images.py +187 -0
  16. megadetector/data_management/wi_download_csv_to_coco.py +3 -2
  17. megadetector/data_management/yolo_output_to_md_output.py +7 -2
  18. megadetector/detection/process_video.py +360 -216
  19. megadetector/detection/pytorch_detector.py +17 -3
  20. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  21. megadetector/detection/tf_detector.py +3 -0
  22. megadetector/detection/video_utils.py +122 -30
  23. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  24. megadetector/postprocessing/classification_postprocessing.py +716 -0
  25. megadetector/postprocessing/compare_batch_results.py +101 -93
  26. megadetector/postprocessing/merge_detections.py +18 -7
  27. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  28. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  29. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  30. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  31. megadetector/utils/ct_utils.py +5 -4
  32. megadetector/utils/md_tests.py +311 -115
  33. megadetector/utils/path_utils.py +1 -0
  34. megadetector/utils/process_utils.py +6 -3
  35. megadetector/visualization/visualize_db.py +79 -77
  36. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  38. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/RECORD +40 -38
  39. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/top_level.txt +0 -0
  40. {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
- if missing_frame_number is None or \
219
- (allow_last_frame_missing and (missing_frame_number == n_frames-1)):
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
- pass
225
- # print("Rendering video {}, couldn't find frame {}".format(
226
- # input_video_file,missing_frame_number))
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
- cv2.imwrite(os.path.normpath(frame_filename),image)
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
- is_success, im_buf_arr = cv2.imencode('.jpg', image)
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(len(frame_filenames),n_frames))
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,every_n_frames,overwrite,verbose):
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
- #: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
389
- #: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
390
- nth_highest_confidence = 1
391
-
392
- #: What to do if a file referred to in a .json results file appears not to be a
393
- #: video; can be 'error' or 'skip_with_warning'
394
- non_video_behavior = 'error'
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
- video_to_frames = defaultdict(list)
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
- video_to_frames[video_name].append(im)
532
+ video_to_frame_info[video_name].append(im)
441
533
 
442
534
  print('Found {} unique videos in {} frame-level results'.format(
443
- len(video_to_frames),len(images)))
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(video_to_frames.keys())[0]
450
- for video_name in tqdm(video_to_frames):
541
+ # video_name = list(video_to_frame_info.keys())[0]
542
+ for video_name in tqdm(video_to_frame_info):
451
543
 
452
- frames = video_to_frames[video_name]
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
- #: Thresholds to use for separation, as a fraction of the image size.
26
- #:
27
- #: Should be sorted from smallest to largest.
28
- size_thresholds = [0.95]
29
-
30
- #: List of category numbers to use in separation; uses all categories if None
31
- categories_to_separate = None
32
-
33
- #: Dimension to use for thresholding; can be "size", "width", or "height"
34
- measurement = 'size'
35
-
36
- #: Categories to assign to thresholded ranges; should have the same length as
37
- #: "size_thresholds".
38
- size_category_names = ['large_detection']
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