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.

Files changed (45) 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 +178 -44
  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 +548 -244
  19. megadetector/detection/pytorch_detector.py +33 -14
  20. megadetector/detection/run_detector.py +17 -5
  21. megadetector/detection/run_detector_batch.py +179 -65
  22. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  23. megadetector/detection/tf_detector.py +14 -3
  24. megadetector/detection/video_utils.py +284 -61
  25. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  26. megadetector/postprocessing/classification_postprocessing.py +716 -0
  27. megadetector/postprocessing/compare_batch_results.py +101 -93
  28. megadetector/postprocessing/convert_output_format.py +12 -5
  29. megadetector/postprocessing/merge_detections.py +18 -7
  30. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  31. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  32. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  33. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  34. megadetector/utils/ct_utils.py +5 -4
  35. megadetector/utils/md_tests.py +380 -128
  36. megadetector/utils/path_utils.py +39 -6
  37. megadetector/utils/process_utils.py +13 -4
  38. megadetector/visualization/visualization_utils.py +7 -2
  39. megadetector/visualization/visualize_db.py +79 -77
  40. megadetector/visualization/visualize_detector_output.py +0 -1
  41. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/LICENSE +0 -0
  42. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/METADATA +2 -2
  43. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/RECORD +45 -43
  44. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/top_level.txt +0 -0
  45. {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, image, image_id, detection_threshold, image_size=None,
129
- skip_image_resizing=False):
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)... not currently supported, but included here for compatibility with PTDetector.
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
- #%% Function for rendering frames to video and vice-versa
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 video_to_frames(input_video_file, output_folder, overwrite=True,
167
- every_n_frames=None, verbose=False):
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
- if frame_number % every_n_frames != 0:
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
- if missing_frame_number is None or \
219
- (allow_last_frame_missing and (missing_frame_number == n_frames-1)):
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
- pass
225
- # print("Rendering video {}, couldn't find frame {}".format(
226
- # input_video_file,missing_frame_number))
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
- cv2.imwrite(os.path.normpath(frame_filename),image)
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
- is_success, im_buf_arr = cv2.imencode('.jpg', image)
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(len(frame_filenames),n_frames))
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,every_n_frames,overwrite,verbose):
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 video_folder_to_frames;
283
- makes sure the right output folder exists, then calls video_to_frames.
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
- #: 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'
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
- video_to_frames = defaultdict(list)
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
- video_to_frames[video_name].append(im)
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(video_to_frames),len(images)))
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(video_to_frames.keys())[0]
450
- for video_name in tqdm(video_to_frames):
659
+ # video_name = list(video_to_frame_info.keys())[0]
660
+ for video_name in tqdm(video_to_frame_info):
451
661
 
452
- frames = video_to_frames[video_name]
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 driver
715
+ #%% Test drivers
506
716
 
507
717
  if False:
508
718
 
719
+ pass
720
+
509
721
  #%% Constants
510
722
 
511
- Fs = 30.01
512
- confidence_threshold = 0.75
513
- input_folder = 'z:\\'
514
- frame_folder_base = r'e:\video_test\frames'
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,frame_folder_base,recursive=True)
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
- #: 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