megadetector 5.0.13__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.

@@ -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
@@ -98,7 +99,7 @@ def find_videos(dirname,
98
99
  return find_video_strings(files)
99
100
 
100
101
 
101
- #%% Function for rendering frames to video and vice-versa
102
+ #%% Functions for rendering frames to video and vice-versa
102
103
 
103
104
  # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
104
105
 
@@ -169,9 +170,55 @@ def _frame_number_to_filename(frame_number):
169
170
  return 'frame{:06d}.jpg'.format(frame_number)
170
171
 
171
172
 
172
- def video_to_frames(input_video_file, output_folder, overwrite=True,
173
- every_n_frames=None, verbose=False, quality=None,
174
- max_width=None):
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):
175
222
  """
176
223
  Renders frames from [input_video_file] to a .jpg in [output_folder].
177
224
 
@@ -184,11 +231,18 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
184
231
  output_folder (str): folder to put frame images in
185
232
  overwrite (bool, optional): whether to overwrite existing frame images
186
233
  every_n_frames (int, optional): sample every Nth frame starting from the first frame;
187
- 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.
188
236
  verbose (bool, optional): enable additional debug console output
189
237
  quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
190
238
  to the opencv default (typically 95).
191
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).
192
246
 
193
247
  Returns:
194
248
  tuple: length-2 tuple containing (list of frame filenames,frame rate)
@@ -196,6 +250,14 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
196
250
 
197
251
  assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
198
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
+
199
261
  vidcap = cv2.VideoCapture(input_video_file)
200
262
  n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
201
263
  Fs = vidcap.get(cv2.CAP_PROP_FPS)
@@ -211,9 +273,17 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
211
273
  for frame_number in range(0,n_frames):
212
274
 
213
275
  if every_n_frames is not None:
276
+ assert frames_to_extract is None, \
277
+ 'Internal error: frames_to_extract and every_n_frames are exclusive'
214
278
  if (frame_number % every_n_frames) != 0:
215
279
  continue
216
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
+
217
287
  frame_filename = _frame_number_to_filename(frame_number)
218
288
  frame_filename = os.path.join(output_folder,frame_filename)
219
289
  frame_filenames.append(frame_filename)
@@ -240,15 +310,23 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
240
310
  last_expected_frame_number = n_frames-1
241
311
  if every_n_frames is not None:
242
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):
243
316
 
317
+ pass
318
+
244
319
  # If no frames are missing, or only frames very close to the end of the video are "missing",
245
320
  # skip this video
246
- if (missing_frame_number is None) or \
321
+ elif (missing_frame_number is None) or \
247
322
  (allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
323
+
248
324
  if verbose:
249
325
  print('Skipping video {}, all output frames exist'.format(input_video_file))
250
326
  return frame_filenames,Fs
327
+
251
328
  else:
329
+
252
330
  # If we found some frames, but not all, print a message
253
331
  if verbose and found_existing_frame:
254
332
  print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
@@ -264,10 +342,14 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
264
342
 
265
343
  frame_filenames = []
266
344
 
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.
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
271
353
  imwrite_patched = False
272
354
  n_imwrite_parameters = None
273
355
 
@@ -299,6 +381,12 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
299
381
  if every_n_frames is not None:
300
382
  if frame_number % every_n_frames != 0:
301
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
302
390
 
303
391
  # Has resizing been requested?
304
392
  if max_width is not None:
@@ -350,6 +438,10 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
350
438
  except Exception as e:
351
439
  print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
352
440
 
441
+ if len(frame_filenames) == 0:
442
+ raise Exception('Error: found no frames in file {}'.format(
443
+ input_video_file))
444
+
353
445
  if verbose:
354
446
  print('\nExtracted {} of {} frames for {}'.format(
355
447
  len(frame_filenames),n_frames,input_video_file))
@@ -361,10 +453,12 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
361
453
 
362
454
 
363
455
  def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
364
- every_n_frames,overwrite,verbose,quality,max_width):
456
+ every_n_frames,overwrite,verbose,quality,max_width,
457
+ frames_to_extract):
365
458
  """
366
- Internal function to call video_to_frames in the context of video_folder_to_frames;
367
- 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.
368
462
  """
369
463
 
370
464
  input_fn_absolute = os.path.join(input_folder,relative_fn)
@@ -379,7 +473,8 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
379
473
  # input_video_file = input_fn_absolute; output_folder = output_folder_video
380
474
  frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
381
475
  overwrite=overwrite,every_n_frames=every_n_frames,
382
- verbose=verbose,quality=quality,max_width=max_width)
476
+ verbose=verbose,quality=quality,max_width=max_width,
477
+ frames_to_extract=frames_to_extract)
383
478
 
384
479
  return frame_filenames,fs
385
480
 
@@ -388,7 +483,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
388
483
  recursive=True, overwrite=True,
389
484
  n_threads=1, every_n_frames=None,
390
485
  verbose=False, parallelization_uses_threads=True,
391
- quality=None, max_width=None):
486
+ quality=None, max_width=None,
487
+ frames_to_extract=None):
392
488
  """
393
489
  For every video file in input_folder, creates a folder within output_folder_base, and
394
490
  renders frame of that video to images in that folder.
@@ -402,13 +498,18 @@ def video_folder_to_frames(input_folder, output_folder_base,
402
498
  n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
403
499
  parallelism
404
500
  every_n_frames (int, optional): sample every Nth frame starting from the first frame;
405
- 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.
406
503
  verbose (bool, optional): enable additional debug console output
407
504
  parallelization_uses_threads (bool, optional): whether to use threads (True) or
408
505
  processes (False) for parallelization; ignored if n_threads <= 1
409
506
  quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
410
507
  to the opencv default (typically 95).
411
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.
412
513
 
413
514
  Returns:
414
515
  tuple: a length-3 tuple containing:
@@ -440,7 +541,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
440
541
 
441
542
  frame_filenames,fs = \
442
543
  _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
443
- every_n_frames,overwrite,verbose,quality,max_width)
544
+ every_n_frames,overwrite,verbose,quality,max_width,
545
+ frames_to_extract)
444
546
  frame_filenames_by_video.append(frame_filenames)
445
547
  fs_by_video.append(fs)
446
548
  else:
@@ -457,7 +559,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
457
559
  overwrite=overwrite,
458
560
  verbose=verbose,
459
561
  quality=quality,
460
- max_width=max_width)
562
+ max_width=max_width,
563
+ frames_to_extract=frames_to_extract)
461
564
  results = list(tqdm(pool.imap(
462
565
  partial(process_video_with_options),input_files_relative_paths),
463
566
  total=len(input_files_relative_paths)))
@@ -485,7 +588,7 @@ class FrameToVideoOptions:
485
588
  #: video; can be 'error' or 'skip_with_warning'
486
589
  self.non_video_behavior = 'error'
487
590
 
488
-
591
+
489
592
  def frame_results_to_video_results(input_file,output_file,options=None):
490
593
  """
491
594
  Given an MD results file produced at the *frame* level, corresponding to a directory
@@ -511,6 +614,7 @@ def frame_results_to_video_results(input_file,output_file,options=None):
511
614
  images = input_data['images']
512
615
  detection_categories = input_data['detection_categories']
513
616
 
617
+
514
618
  ## Break into videos
515
619
 
516
620
  video_to_frame_info = defaultdict(list)
@@ -520,7 +624,9 @@ def frame_results_to_video_results(input_file,output_file,options=None):
520
624
 
521
625
  fn = im['file']
522
626
  video_name = os.path.dirname(fn)
627
+
523
628
  if not is_video_file(video_name):
629
+
524
630
  if options.non_video_behavior == 'error':
525
631
  raise ValueError('{} is not a video file'.format(video_name))
526
632
  elif options.non_video_behavior == 'skip_with_warning':
@@ -529,13 +635,25 @@ def frame_results_to_video_results(input_file,output_file,options=None):
529
635
  else:
530
636
  raise ValueError('Unrecognized non-video handling behavior: {}'.format(
531
637
  options.non_video_behavior))
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
+
532
647
  video_to_frame_info[video_name].append(im)
533
648
 
649
+ # ...for each frame referred to in the results file
650
+
534
651
  print('Found {} unique videos in {} frame-level results'.format(
535
652
  len(video_to_frame_info),len(images)))
536
653
 
537
654
  output_images = []
538
655
 
656
+
539
657
  ## For each video...
540
658
 
541
659
  # video_name = list(video_to_frame_info.keys())[0]
@@ -594,37 +712,60 @@ def frame_results_to_video_results(input_file,output_file,options=None):
594
712
  # ...def frame_results_to_video_results(...)
595
713
 
596
714
 
597
- #%% Test driver
715
+ #%% Test drivers
598
716
 
599
717
  if False:
600
718
 
719
+ pass
720
+
601
721
  #%% Constants
602
722
 
603
- Fs = 30.01
604
- confidence_threshold = 0.75
605
- input_folder = 'z:\\'
606
- frame_folder_base = r'e:\video_test\frames'
607
- detected_frame_folder_base = r'e:\video_test\detected_frames'
608
- rendered_videos_folder_base = r'e:\video_test\rendered_videos'
609
-
610
- results_file = r'results.json'
611
- os.makedirs(detected_frame_folder_base,exist_ok=True)
612
- os.makedirs(rendered_videos_folder_base,exist_ok=True)
613
-
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
+
614
727
 
615
728
  #%% Split videos into frames
616
729
 
617
730
  frame_filenames_by_video,fs_by_video,video_filenames = \
618
- 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
+
619
743
 
744
+ #%% Constants for detection tests
620
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
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
+
621
763
  #%% List image files, break into folders
622
764
 
623
765
  frame_files = path_utils.find_images(frame_folder_base,True)
624
766
  frame_files = [s.replace('\\','/') for s in frame_files]
625
767
  print('Enumerated {} total frames'.format(len(frame_files)))
626
768
 
627
- Fs = 30.01
628
769
  # Find unique folders
629
770
  folders = set()
630
771
  # fn = frame_files[0]
@@ -634,16 +775,6 @@ if False:
634
775
  print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
635
776
 
636
777
 
637
- #%% Load detector output
638
-
639
- with open(results_file,'r') as f:
640
- detection_results = json.load(f)
641
- detections = detection_results['images']
642
- detector_label_map = detection_results['detection_categories']
643
- for d in detections:
644
- d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
645
-
646
-
647
778
  #%% Render detector frames
648
779
 
649
780
  # folder = list(folders)[0]
@@ -30,8 +30,11 @@ CONF_DIGITS = 3
30
30
 
31
31
  #%% Conversion functions
32
32
 
33
- def convert_json_to_csv(input_path,output_path=None,min_confidence=None,
34
- omit_bounding_boxes=False,output_encoding=None,
33
+ def convert_json_to_csv(input_path,
34
+ output_path=None,
35
+ min_confidence=None,
36
+ omit_bounding_boxes=False,
37
+ output_encoding=None,
35
38
  overwrite=True):
36
39
  """
37
40
  Converts a MD results .json file to a totally non-standard .csv format.
@@ -76,9 +79,9 @@ def convert_json_to_csv(input_path,output_path=None,min_confidence=None,
76
79
  # n_non_empty_detection_categories = len(annotation_constants.annotation_bbox_categories) - 1
77
80
  n_non_empty_detection_categories = annotation_constants.NUM_DETECTOR_CATEGORIES
78
81
  detection_category_column_names = []
79
- assert annotation_constants.detector_bbox_categories[0] == 'empty'
82
+ assert annotation_constants.detector_bbox_category_id_to_name[0] == 'empty'
80
83
  for cat_id in range(1,n_non_empty_detection_categories+1):
81
- cat_name = annotation_constants.detector_bbox_categories[cat_id]
84
+ cat_name = annotation_constants.detector_bbox_category_id_to_name[cat_id]
82
85
  detection_category_column_names.append('max_conf_' + cat_name)
83
86
 
84
87
  n_classification_categories = 0
@@ -370,6 +373,8 @@ def main():
370
373
  parser.add_argument('--output_path',type=str,default=None,
371
374
  help='Output filename ending in .json or .csv (defaults to ' + \
372
375
  'input file, with .json/.csv replaced by .csv/.json)')
376
+ parser.add_argument('--omit_bounding_boxes',action='store_true',
377
+ help='Output bounding box text from .csv output (large and usually not useful)')
373
378
 
374
379
  if len(sys.argv[1:]) == 0:
375
380
  parser.print_help()
@@ -386,9 +391,11 @@ def main():
386
391
  raise ValueError('Illegal input file extension')
387
392
 
388
393
  if args.input_path.endswith('.csv') and args.output_path.endswith('.json'):
394
+ assert not args.omit_bounding_boxes, \
395
+ '--omit_bounding_boxes does not apply to csv --> json conversion'
389
396
  convert_csv_to_json(args.input_path,args.output_path)
390
397
  elif args.input_path.endswith('.json') and args.output_path.endswith('.csv'):
391
- convert_json_to_csv(args.input_path,args.output_path)
398
+ convert_json_to_csv(args.input_path,args.output_path,omit_bounding_boxes=args.omit_bounding_boxes)
392
399
  else:
393
400
  raise ValueError('Illegal format combination')
394
401