megadetector 5.0.13__py3-none-any.whl → 5.0.15__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,17 @@ 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 quality is not None and quality < 0:
254
+ quality = None
255
+
256
+ if isinstance(frames_to_extract,int):
257
+ frames_to_extract = [frames_to_extract]
258
+
259
+ if (frames_to_extract is not None) and (every_n_frames is not None):
260
+ raise ValueError('frames_to_extract and every_n_frames are mutually exclusive')
261
+
262
+ os.makedirs(output_folder,exist_ok=True)
263
+
199
264
  vidcap = cv2.VideoCapture(input_video_file)
200
265
  n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
201
266
  Fs = vidcap.get(cv2.CAP_PROP_FPS)
@@ -211,9 +276,17 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
211
276
  for frame_number in range(0,n_frames):
212
277
 
213
278
  if every_n_frames is not None:
279
+ assert frames_to_extract is None, \
280
+ 'Internal error: frames_to_extract and every_n_frames are exclusive'
214
281
  if (frame_number % every_n_frames) != 0:
215
282
  continue
216
283
 
284
+ if frames_to_extract is not None:
285
+ assert every_n_frames is None, \
286
+ 'Internal error: frames_to_extract and every_n_frames are exclusive'
287
+ if frame_number not in frames_to_extract:
288
+ continue
289
+
217
290
  frame_filename = _frame_number_to_filename(frame_number)
218
291
  frame_filename = os.path.join(output_folder,frame_filename)
219
292
  frame_filenames.append(frame_filename)
@@ -240,15 +313,23 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
240
313
  last_expected_frame_number = n_frames-1
241
314
  if every_n_frames is not None:
242
315
  last_expected_frame_number -= (every_n_frames*2)
316
+
317
+ # When specific frames are requested, if anything is missing, reprocess the video
318
+ if (frames_to_extract is not None) and (missing_frame_number is not None):
243
319
 
320
+ pass
321
+
244
322
  # If no frames are missing, or only frames very close to the end of the video are "missing",
245
323
  # skip this video
246
- if (missing_frame_number is None) or \
324
+ elif (missing_frame_number is None) or \
247
325
  (allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
326
+
248
327
  if verbose:
249
328
  print('Skipping video {}, all output frames exist'.format(input_video_file))
250
329
  return frame_filenames,Fs
330
+
251
331
  else:
332
+
252
333
  # If we found some frames, but not all, print a message
253
334
  if verbose and found_existing_frame:
254
335
  print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
@@ -264,10 +345,14 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
264
345
 
265
346
  frame_filenames = []
266
347
 
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.
348
+ # YOLOv5 does some totally bananas monkey-patching of opencv, which causes
349
+ # problems if we try to supply a third parameter to imwrite (to specify JPEG
350
+ # quality). Detect this case, and ignore the quality parameter if it looks
351
+ # like imwrite has been messed with.
352
+ #
353
+ # See:
354
+ #
355
+ # https://github.com/ultralytics/yolov5/issues/7285
271
356
  imwrite_patched = False
272
357
  n_imwrite_parameters = None
273
358
 
@@ -299,6 +384,12 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
299
384
  if every_n_frames is not None:
300
385
  if frame_number % every_n_frames != 0:
301
386
  continue
387
+
388
+ if frames_to_extract is not None:
389
+ if frame_number > max(frames_to_extract):
390
+ break
391
+ if frame_number not in frames_to_extract:
392
+ continue
302
393
 
303
394
  # Has resizing been requested?
304
395
  if max_width is not None:
@@ -350,6 +441,10 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
350
441
  except Exception as e:
351
442
  print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
352
443
 
444
+ if len(frame_filenames) == 0:
445
+ raise Exception('Error: found no frames in file {}'.format(
446
+ input_video_file))
447
+
353
448
  if verbose:
354
449
  print('\nExtracted {} of {} frames for {}'.format(
355
450
  len(frame_filenames),n_frames,input_video_file))
@@ -361,10 +456,12 @@ def video_to_frames(input_video_file, output_folder, overwrite=True,
361
456
 
362
457
 
363
458
  def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
364
- every_n_frames,overwrite,verbose,quality,max_width):
459
+ every_n_frames,overwrite,verbose,quality,max_width,
460
+ frames_to_extract):
365
461
  """
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.
462
+ Internal function to call video_to_frames for a single video in the context of
463
+ video_folder_to_frames; makes sure the right output folder exists, then calls
464
+ video_to_frames.
368
465
  """
369
466
 
370
467
  input_fn_absolute = os.path.join(input_folder,relative_fn)
@@ -379,7 +476,8 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
379
476
  # input_video_file = input_fn_absolute; output_folder = output_folder_video
380
477
  frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
381
478
  overwrite=overwrite,every_n_frames=every_n_frames,
382
- verbose=verbose,quality=quality,max_width=max_width)
479
+ verbose=verbose,quality=quality,max_width=max_width,
480
+ frames_to_extract=frames_to_extract)
383
481
 
384
482
  return frame_filenames,fs
385
483
 
@@ -388,7 +486,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
388
486
  recursive=True, overwrite=True,
389
487
  n_threads=1, every_n_frames=None,
390
488
  verbose=False, parallelization_uses_threads=True,
391
- quality=None, max_width=None):
489
+ quality=None, max_width=None,
490
+ frames_to_extract=None):
392
491
  """
393
492
  For every video file in input_folder, creates a folder within output_folder_base, and
394
493
  renders frame of that video to images in that folder.
@@ -402,13 +501,18 @@ def video_folder_to_frames(input_folder, output_folder_base,
402
501
  n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
403
502
  parallelism
404
503
  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
504
+ if this is None or 1, every frame is extracted. Mutually exclusive with
505
+ frames_to_extract.
406
506
  verbose (bool, optional): enable additional debug console output
407
507
  parallelization_uses_threads (bool, optional): whether to use threads (True) or
408
508
  processes (False) for parallelization; ignored if n_threads <= 1
409
509
  quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
410
510
  to the opencv default (typically 95).
411
511
  max_width (int, optional): resize frames to be no wider than [max_width]
512
+ frames_to_extract (list of int, optional): extract this specific set of frames from
513
+ each video; mutually exclusive with every_n_frames. If all values are beyond
514
+ the length of a video, no frames are extracted. Can also be a single int,
515
+ specifying a single frame number.
412
516
 
413
517
  Returns:
414
518
  tuple: a length-3 tuple containing:
@@ -440,7 +544,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
440
544
 
441
545
  frame_filenames,fs = \
442
546
  _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
443
- every_n_frames,overwrite,verbose,quality,max_width)
547
+ every_n_frames,overwrite,verbose,quality,max_width,
548
+ frames_to_extract)
444
549
  frame_filenames_by_video.append(frame_filenames)
445
550
  fs_by_video.append(fs)
446
551
  else:
@@ -457,7 +562,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
457
562
  overwrite=overwrite,
458
563
  verbose=verbose,
459
564
  quality=quality,
460
- max_width=max_width)
565
+ max_width=max_width,
566
+ frames_to_extract=frames_to_extract)
461
567
  results = list(tqdm(pool.imap(
462
568
  partial(process_video_with_options),input_files_relative_paths),
463
569
  total=len(input_files_relative_paths)))
@@ -485,7 +591,7 @@ class FrameToVideoOptions:
485
591
  #: video; can be 'error' or 'skip_with_warning'
486
592
  self.non_video_behavior = 'error'
487
593
 
488
-
594
+
489
595
  def frame_results_to_video_results(input_file,output_file,options=None):
490
596
  """
491
597
  Given an MD results file produced at the *frame* level, corresponding to a directory
@@ -511,6 +617,7 @@ def frame_results_to_video_results(input_file,output_file,options=None):
511
617
  images = input_data['images']
512
618
  detection_categories = input_data['detection_categories']
513
619
 
620
+
514
621
  ## Break into videos
515
622
 
516
623
  video_to_frame_info = defaultdict(list)
@@ -520,7 +627,9 @@ def frame_results_to_video_results(input_file,output_file,options=None):
520
627
 
521
628
  fn = im['file']
522
629
  video_name = os.path.dirname(fn)
630
+
523
631
  if not is_video_file(video_name):
632
+
524
633
  if options.non_video_behavior == 'error':
525
634
  raise ValueError('{} is not a video file'.format(video_name))
526
635
  elif options.non_video_behavior == 'skip_with_warning':
@@ -529,13 +638,25 @@ def frame_results_to_video_results(input_file,output_file,options=None):
529
638
  else:
530
639
  raise ValueError('Unrecognized non-video handling behavior: {}'.format(
531
640
  options.non_video_behavior))
641
+
642
+ # Attach video-specific fields to the output, specifically attach the frame
643
+ # number to both the video and each detection. Only the frame number for the
644
+ # canonical detection will end up in the video-level output file.
645
+ frame_number = _filename_to_frame_number(fn)
646
+ im['frame_number'] = frame_number
647
+ for detection in im['detections']:
648
+ detection['frame_number'] = frame_number
649
+
532
650
  video_to_frame_info[video_name].append(im)
533
651
 
652
+ # ...for each frame referred to in the results file
653
+
534
654
  print('Found {} unique videos in {} frame-level results'.format(
535
655
  len(video_to_frame_info),len(images)))
536
656
 
537
657
  output_images = []
538
658
 
659
+
539
660
  ## For each video...
540
661
 
541
662
  # video_name = list(video_to_frame_info.keys())[0]
@@ -594,37 +715,60 @@ def frame_results_to_video_results(input_file,output_file,options=None):
594
715
  # ...def frame_results_to_video_results(...)
595
716
 
596
717
 
597
- #%% Test driver
718
+ #%% Test drivers
598
719
 
599
720
  if False:
600
721
 
722
+ pass
723
+
601
724
  #%% Constants
602
725
 
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
-
726
+ input_folder = r'G:\temp\usu-long\data'
727
+ frame_folder_base = r'g:\temp\usu-long-single-frames'
728
+ assert os.path.isdir(input_folder)
729
+
614
730
 
615
731
  #%% Split videos into frames
616
732
 
617
733
  frame_filenames_by_video,fs_by_video,video_filenames = \
618
- video_folder_to_frames(input_folder,frame_folder_base,recursive=True)
734
+ video_folder_to_frames(input_folder,
735
+ frame_folder_base,
736
+ recursive=True,
737
+ overwrite=True,
738
+ n_threads=10,
739
+ every_n_frames=None,
740
+ verbose=True,
741
+ parallelization_uses_threads=True,
742
+ quality=None,
743
+ max_width=None,
744
+ frames_to_extract=150)
745
+
619
746
 
747
+ #%% Constants for detection tests
620
748
 
749
+ detected_frame_folder_base = r'e:\video_test\detected_frames'
750
+ rendered_videos_folder_base = r'e:\video_test\rendered_videos'
751
+ os.makedirs(detected_frame_folder_base,exist_ok=True)
752
+ os.makedirs(rendered_videos_folder_base,exist_ok=True)
753
+ results_file = r'results.json'
754
+ confidence_threshold = 0.75
755
+
756
+ #%% Load detector output
757
+
758
+ with open(results_file,'r') as f:
759
+ detection_results = json.load(f)
760
+ detections = detection_results['images']
761
+ detector_label_map = detection_results['detection_categories']
762
+ for d in detections:
763
+ d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
764
+
765
+
621
766
  #%% List image files, break into folders
622
767
 
623
768
  frame_files = path_utils.find_images(frame_folder_base,True)
624
769
  frame_files = [s.replace('\\','/') for s in frame_files]
625
770
  print('Enumerated {} total frames'.format(len(frame_files)))
626
771
 
627
- Fs = 30.01
628
772
  # Find unique folders
629
773
  folders = set()
630
774
  # fn = frame_files[0]
@@ -634,16 +778,6 @@ if False:
634
778
  print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
635
779
 
636
780
 
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
781
  #%% Render detector frames
648
782
 
649
783
  # 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