megadetector 5.0.6__py3-none-any.whl → 5.0.8__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 (75) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +297 -202
  2. api/batch_processing/data_preparation/manage_video_batch.py +7 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
  5. api/batch_processing/postprocessing/compare_batch_results.py +111 -61
  6. api/batch_processing/postprocessing/convert_output_format.py +24 -6
  7. api/batch_processing/postprocessing/load_api_results.py +56 -72
  8. api/batch_processing/postprocessing/md_to_labelme.py +119 -51
  9. api/batch_processing/postprocessing/merge_detections.py +30 -5
  10. api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
  11. api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
  12. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -0
  13. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  14. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  15. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +224 -76
  16. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  17. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  18. classification/prepare_classification_script.py +191 -191
  19. data_management/cct_json_utils.py +7 -2
  20. data_management/coco_to_labelme.py +263 -0
  21. data_management/coco_to_yolo.py +72 -48
  22. data_management/databases/integrity_check_json_db.py +75 -64
  23. data_management/databases/subset_json_db.py +1 -1
  24. data_management/generate_crops_from_cct.py +1 -1
  25. data_management/get_image_sizes.py +44 -26
  26. data_management/importers/animl_results_to_md_results.py +3 -5
  27. data_management/importers/noaa_seals_2019.py +2 -2
  28. data_management/importers/zamba_results_to_md_results.py +2 -2
  29. data_management/labelme_to_coco.py +264 -127
  30. data_management/labelme_to_yolo.py +96 -53
  31. data_management/lila/create_lila_blank_set.py +557 -0
  32. data_management/lila/create_lila_test_set.py +2 -1
  33. data_management/lila/create_links_to_md_results_files.py +1 -1
  34. data_management/lila/download_lila_subset.py +138 -45
  35. data_management/lila/generate_lila_per_image_labels.py +23 -14
  36. data_management/lila/get_lila_annotation_counts.py +16 -10
  37. data_management/lila/lila_common.py +15 -42
  38. data_management/lila/test_lila_metadata_urls.py +116 -0
  39. data_management/read_exif.py +65 -16
  40. data_management/remap_coco_categories.py +84 -0
  41. data_management/resize_coco_dataset.py +14 -31
  42. data_management/wi_download_csv_to_coco.py +239 -0
  43. data_management/yolo_output_to_md_output.py +40 -13
  44. data_management/yolo_to_coco.py +313 -100
  45. detection/process_video.py +36 -14
  46. detection/pytorch_detector.py +1 -1
  47. detection/run_detector.py +73 -18
  48. detection/run_detector_batch.py +116 -27
  49. detection/run_inference_with_yolov5_val.py +135 -27
  50. detection/run_tiled_inference.py +153 -43
  51. detection/tf_detector.py +2 -1
  52. detection/video_utils.py +4 -2
  53. md_utils/ct_utils.py +101 -6
  54. md_utils/md_tests.py +264 -17
  55. md_utils/path_utils.py +326 -47
  56. md_utils/process_utils.py +26 -7
  57. md_utils/split_locations_into_train_val.py +215 -0
  58. md_utils/string_utils.py +10 -0
  59. md_utils/url_utils.py +66 -3
  60. md_utils/write_html_image_list.py +12 -2
  61. md_visualization/visualization_utils.py +380 -74
  62. md_visualization/visualize_db.py +41 -10
  63. md_visualization/visualize_detector_output.py +185 -104
  64. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
  65. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
  66. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
  67. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  68. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  69. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  70. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  71. taxonomy_mapping/species_lookup.py +33 -13
  72. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  73. md_visualization/visualize_megadb.py +0 -183
  74. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
  75. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,7 @@
2
2
  #
3
3
  # yolo_to_coco.py
4
4
  #
5
- # Converts a YOLO-formatted dataset to a COCO-formatted dataset.
6
- #
7
- # Currently supports only a single folder (i.e., no recursion). Treats images without
8
- # corresponding .txt files as empty.
5
+ # Converts a folder of YOLO-formatted annotation files to a COCO-formatted dataset.
9
6
  #
10
7
  ########
11
8
 
@@ -14,57 +11,241 @@
14
11
  import json
15
12
  import os
16
13
 
17
- from PIL import Image
14
+ from multiprocessing.pool import ThreadPool
15
+ from multiprocessing.pool import Pool
16
+ from functools import partial
17
+
18
18
  from tqdm import tqdm
19
19
 
20
20
  from md_utils.path_utils import find_images
21
+ from md_utils.ct_utils import invert_dictionary
22
+ from md_visualization.visualization_utils import open_image
23
+ from data_management.yolo_output_to_md_output import read_classes_from_yolo_dataset_file
24
+
25
+
26
+ #%% Support functions
27
+
28
+ def filename_to_image_id(fn):
29
+ return fn.replace(' ','_')
30
+
31
+ def _process_image(fn_abs,input_folder,category_id_to_name):
32
+ """
33
+ Internal support function for processing one image's labels.
34
+ """
35
+
36
+ # Create the image object for this image
37
+ fn_relative = os.path.relpath(fn_abs,input_folder)
38
+ image_id = filename_to_image_id(fn_relative)
39
+
40
+ # This is done in a separate loop now
41
+ #
42
+ # assert image_id not in image_ids, \
43
+ # 'Oops, you have hit a very esoteric case where you have the same filename ' + \
44
+ # 'with both spaces and underscores, this is not currently handled.'
45
+ # image_ids.add(image_id)
46
+
47
+ im = {}
48
+ im['file_name'] = fn_relative
49
+ im['id'] = image_id
50
+
51
+ annotations_this_image = []
52
+
53
+ try:
54
+ pil_im = open_image(fn_abs)
55
+ im_width, im_height = pil_im.size
56
+ im['width'] = im_width
57
+ im['height'] = im_height
58
+ im['error'] = None
59
+ except Exception as e:
60
+ print('Warning: error reading {}:\n{}'.format(fn_relative,str(e)))
61
+ im['width'] = -1
62
+ im['height'] = -1
63
+ im['error'] = str(e)
64
+ return (im,annotations_this_image)
65
+
66
+ # Is there an annotation file for this image?
67
+ annotation_file = os.path.splitext(fn_abs)[0] + '.txt'
68
+ if not os.path.isfile(annotation_file):
69
+ annotation_file = os.path.splitext(fn_abs)[0] + '.TXT'
70
+
71
+ if os.path.isfile(annotation_file):
72
+
73
+ with open(annotation_file,'r') as f:
74
+ lines = f.readlines()
75
+ lines = [s.strip() for s in lines]
76
+
77
+ # s = lines[0]
78
+ annotation_number = 0
79
+
80
+ for s in lines:
81
+
82
+ if len(s.strip()) == 0:
83
+ continue
84
+
85
+ tokens = s.split()
86
+ assert len(tokens) == 5
87
+ category_id = int(tokens[0])
88
+ assert category_id in category_id_to_name, \
89
+ 'Unrecognized category ID {} in annotation file {}'.format(
90
+ category_id,annotation_file)
91
+ ann = {}
92
+ ann['id'] = im['id'] + '_' + str(annotation_number)
93
+ ann['image_id'] = im['id']
94
+ ann['category_id'] = category_id
95
+ ann['sequence_level_annotation'] = False
96
+
97
+ # COCO: [x_min, y_min, width, height] in absolute coordinates
98
+ # YOLO: [class, x_center, y_center, width, height] in normalized coordinates
99
+
100
+ yolo_bbox = [float(x) for x in tokens[1:]]
101
+
102
+ normalized_x_center = yolo_bbox[0]
103
+ normalized_y_center = yolo_bbox[1]
104
+ normalized_width = yolo_bbox[2]
105
+ normalized_height = yolo_bbox[3]
106
+
107
+ absolute_x_center = normalized_x_center * im_width
108
+ absolute_y_center = normalized_y_center * im_height
109
+ absolute_width = normalized_width * im_width
110
+ absolute_height = normalized_height * im_height
111
+ absolute_x_min = absolute_x_center - absolute_width / 2
112
+ absolute_y_min = absolute_y_center - absolute_height / 2
113
+
114
+ coco_bbox = [absolute_x_min, absolute_y_min, absolute_width, absolute_height]
115
+
116
+ ann['bbox'] = coco_bbox
117
+ annotation_number += 1
118
+
119
+ annotations_this_image.append(ann)
120
+
121
+ # ...for each annotation
122
+
123
+ # ...if this image has annotations
124
+
125
+ return (im,annotations_this_image)
126
+
127
+ # ...def _process_image(...)
128
+
21
129
 
22
130
 
23
131
  #%% Main conversion function
24
132
 
25
- def yolo_to_coco(input_folder,class_name_file,output_file=None):
133
+ def yolo_to_coco(input_folder,
134
+ class_name_file,
135
+ output_file=None,
136
+ empty_image_handling='no_annotations',
137
+ empty_image_category_name='empty',
138
+ error_image_handling='no_annotations',
139
+ allow_images_without_label_files=True,
140
+ n_workers=1,
141
+ pool_type='thread',
142
+ recursive=True,
143
+ exclude_string=None,
144
+ include_string=None):
26
145
  """
27
146
  Convert the YOLO-formatted data in [input_folder] to a COCO-formatted dictionary,
28
- reading class names from the flat list [class_name_file]. Optionally writes the output
29
- dataset to [output_file].
147
+ reading class names from [class_name_file], which can be a flat list with a .txt
148
+ extension or a YOLO dataset.yml file. Optionally writes the output dataset to [output_file].
149
+
150
+ empty_image_handling can be:
151
+
152
+ * 'no_annotations': include the image in the image list, with no annotations
153
+
154
+ * 'empty_annotations': include the image in the image list, and add an annotation without
155
+ any bounding boxes, using a category called [empty_image_category_name].
156
+
157
+ * 'skip': don't include the image in the image list
158
+
159
+ * 'error': there shouldn't be any empty images
160
+
161
+ error_image_handling can be:
162
+
163
+ * 'skip': don't include the image at all
164
+
165
+ * 'no_annotations': include with no annotations
166
+
167
+ All images will be assigned an "error" value, usually None.
168
+
169
+ Returns a COCO-formatted dictionary.
30
170
  """
31
171
 
32
- # Validate input
172
+ ## Validate input
33
173
 
34
174
  assert os.path.isdir(input_folder)
35
175
  assert os.path.isfile(class_name_file)
36
176
 
177
+ assert empty_image_handling in \
178
+ ('no_annotations','empty_annotations','skip','error'), \
179
+ 'Unrecognized empty image handling spec: {}'.format(empty_image_handling)
180
+
181
+
182
+ ## Read class names
37
183
 
38
- # Class names
39
-
40
- with open(class_name_file,'r') as f:
41
- lines = f.readlines()
42
- assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
43
- lines = [s.strip() for s in lines]
44
- assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
184
+ ext = os.path.splitext(class_name_file)[1][1:]
185
+ assert ext in ('yml','txt','yaml','data'), 'Unrecognized class name file type {}'.format(
186
+ class_name_file)
45
187
 
46
- # Blank lines should only appear at the end
47
- b_found_blank = False
48
- for s in lines:
49
- if len(s) == 0:
50
- b_found_blank = True
51
- elif b_found_blank:
52
- raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
53
- class_name_file))
54
-
55
- category_id_to_name = {}
188
+ if ext in ('txt','data'):
56
189
 
57
- for i_category_id,category_name in enumerate(lines):
58
- assert len(category_name) > 0
59
- category_id_to_name[i_category_id] = category_name
190
+ with open(class_name_file,'r') as f:
191
+ lines = f.readlines()
192
+ assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
193
+ class_names = [s.strip() for s in lines]
194
+ assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
60
195
 
196
+ # Blank lines should only appear at the end
197
+ b_found_blank = False
198
+ for s in lines:
199
+ if len(s) == 0:
200
+ b_found_blank = True
201
+ elif b_found_blank:
202
+ raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
203
+ class_name_file))
204
+
205
+ category_id_to_name = {}
206
+ for i_category_id,category_name in enumerate(class_names):
207
+ assert len(category_name) > 0
208
+ category_id_to_name[i_category_id] = category_name
209
+
210
+ else:
61
211
 
62
- # Enumerate images
212
+ assert ext in ('yml','yaml')
213
+ category_id_to_name = read_classes_from_yolo_dataset_file(class_name_file)
63
214
 
64
- image_files = find_images(input_folder,recursive=False)
215
+ # Find or create the empty image category, if necessary
216
+ empty_category_id = None
217
+
218
+ if (empty_image_handling == 'empty_annotations'):
219
+ category_name_to_id = invert_dictionary(category_id_to_name)
220
+ if empty_image_category_name in category_name_to_id:
221
+ empty_category_id = category_name_to_id[empty_image_category_name]
222
+ print('Using existing empty image category with name {}, ID {}'.format(
223
+ empty_image_category_name,empty_category_id))
224
+ else:
225
+ empty_category_id = len(category_id_to_name)
226
+ print('Adding an empty category with name {}, ID {}'.format(
227
+ empty_image_category_name,empty_category_id))
228
+ category_id_to_name[empty_category_id] = empty_image_category_name
229
+
230
+
231
+ ## Enumerate images
232
+
233
+ print('Enumerating images...')
234
+
235
+ image_files_abs = find_images(input_folder,recursive=recursive,convert_slashes=True)
65
236
 
66
- images = []
67
- annotations = []
237
+ n_files_original = len(image_files_abs)
238
+
239
+ # Optionally include/exclude images matching specific strings
240
+ if exclude_string is not None:
241
+ image_files_abs = [fn for fn in image_files_abs if exclude_string not in fn]
242
+ if include_string is not None:
243
+ image_files_abs = [fn for fn in image_files_abs if include_string in fn]
244
+
245
+ if len(image_files_abs) != n_files_original or exclude_string is not None or include_string is not None:
246
+ n_excluded = n_files_original - len(image_files_abs)
247
+ print('Excluded {} of {} images based on filenames'.format(n_excluded,n_files_original))
248
+
68
249
  categories = []
69
250
 
70
251
  for category_id in category_id_to_name:
@@ -74,79 +255,111 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
74
255
  info['version'] = '1.0'
75
256
  info['description'] = 'Converted from YOLO format'
76
257
 
77
- # fn = image_files[0]
78
- for fn in tqdm(image_files):
79
-
80
- im = Image.open(fn)
81
- im_width, im_height = im.size
82
-
83
- # Create the image object for this image
84
- im = {}
85
- fn_relative = os.path.relpath(fn,input_folder)
86
- im['file_name'] = fn_relative
87
- im['id'] = fn_relative.replace(' ','_')
88
- im['location'] = 'unknown'
89
- images.append(im)
90
-
91
- # Is there an annotation file for this image?
92
- annotation_file = os.path.splitext(fn)[0] + '.txt'
93
- if not os.path.isfile(annotation_file):
94
- annotation_file = os.path.splitext(fn)[0] + '.TXT'
95
- if not os.path.isfile(annotation_file):
96
- # This is an image with no annotations, currently don't do anything special
97
- # here
98
- pass
258
+ image_ids = set()
259
+
260
+
261
+ ## If we're expected to have labels for every image, check before we process all the images
262
+
263
+ if not allow_images_without_label_files:
264
+ print('Verifying that label files exist')
265
+ for image_file_abs in tqdm(image_files_abs):
266
+ label_file_abs = os.path.splitext(image_file_abs)[0] + '.txt'
267
+ assert os.path.isfile(label_file_abs), \
268
+ 'No annotation file for {}'.format(image_file_abs)
269
+
270
+
271
+ ## Initial loop to make sure image IDs will be unique
272
+
273
+ print('Validating image IDs...')
274
+
275
+ for fn_abs in tqdm(image_files_abs):
276
+
277
+ fn_relative = os.path.relpath(fn_abs,input_folder)
278
+ image_id = filename_to_image_id(fn_relative)
279
+ assert image_id not in image_ids, \
280
+ 'Oops, you have hit a very esoteric case where you have the same filename ' + \
281
+ 'with both spaces and underscores, this is not currently handled.'
282
+ image_ids.add(image_id)
283
+
284
+
285
+ ## Main loop to process labels
286
+
287
+ print('Processing labels...')
288
+
289
+ if n_workers <= 1:
290
+
291
+ image_results = []
292
+ for fn_abs in tqdm(image_files_abs):
293
+ image_results.append(_process_image(fn_abs,input_folder,category_id_to_name))
294
+
295
+ else:
296
+
297
+ assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
298
+
299
+ if pool_type == 'thread':
300
+ pool = ThreadPool(n_workers)
99
301
  else:
100
- with open(annotation_file,'r') as f:
101
- lines = f.readlines()
102
- lines = [s.strip() for s in lines]
103
-
104
- # s = lines[0]
105
- annotation_number = 0
106
- for s in lines:
107
- if len(s.strip()) == 0:
108
- continue
109
- tokens = s.split()
110
- assert len(tokens) == 5
111
- category_id = int(tokens[0])
112
- assert category_id in category_id_to_name, \
113
- 'Unrecognized category ID {} in annotation file {}'.format(
114
- category_id,annotation_file)
115
- ann = {}
116
- ann['id'] = im['id'] + '_' + str(annotation_number)
117
- ann['image_id'] = im['id']
118
- ann['category_id'] = category_id
119
- ann['sequence_level_annotation'] = False
120
-
121
- # COCO: [x_min, y_min, width, height] in absolute coordinates
122
- # YOLO: [class, x_center, y_center, width, height] in normalized coordinates
123
-
124
- yolo_bbox = [float(x) for x in tokens[1:]]
125
-
126
- normalized_x_center = yolo_bbox[0]
127
- normalized_y_center = yolo_bbox[1]
128
- normalized_width = yolo_bbox[2]
129
- normalized_height = yolo_bbox[3]
130
-
131
- absolute_x_center = normalized_x_center * im_width
132
- absolute_y_center = normalized_y_center * im_height
133
- absolute_width = normalized_width * im_width
134
- absolute_height = normalized_height * im_height
135
- absolute_x_min = absolute_x_center - absolute_width / 2
136
- absolute_y_min = absolute_y_center - absolute_height / 2
137
-
138
- coco_bbox = [absolute_x_min, absolute_y_min, absolute_width, absolute_height]
302
+ pool = Pool(n_workers)
303
+
304
+ print('Starting a {} pool of {} workers'.format(pool_type,n_workers))
305
+
306
+ p = partial(_process_image,input_folder=input_folder,
307
+ category_id_to_name=category_id_to_name)
308
+ image_results = list(tqdm(pool.imap(p, image_files_abs),
309
+ total=len(image_files_abs)))
139
310
 
140
- ann['bbox'] = coco_bbox
141
- annotation_number += 1
311
+
312
+ assert len(image_results) == len(image_files_abs)
313
+
314
+
315
+ ## Re-assembly of results into a COCO dict
316
+
317
+ print('Assembling labels...')
318
+
319
+ images = []
320
+ annotations = []
321
+
322
+ for image_result in tqdm(image_results):
323
+
324
+ im = image_result[0]
325
+ annotations_this_image = image_result[1]
326
+
327
+ # If we have annotations for this image
328
+ if len(annotations_this_image) > 0:
329
+ assert im['error'] is None
330
+ images.append(im)
331
+ for ann in annotations_this_image:
332
+ annotations.append(ann)
142
333
 
143
- annotations.append(ann)
334
+ # If this image failed to read
335
+ elif im['error'] is not None:
336
+
337
+ if error_image_handling == 'skip':
338
+ pass
339
+ elif error_image_handling == 'no_annotations':
340
+ images.append(im)
144
341
 
145
- # ...for each annotation
342
+ # If this image read successfully, but there are no annotations
343
+ else:
146
344
 
147
- # ...if this image has annotations
345
+ if empty_image_handling == 'skip':
346
+ pass
347
+ elif empty_image_handling == 'no_annotations':
348
+ images.append(im)
349
+ elif empty_image_handling == 'empty_annotations':
350
+ assert empty_category_id is not None
351
+ ann = {}
352
+ ann['id'] = im['id'] + '_0'
353
+ ann['image_id'] = im['id']
354
+ ann['category_id'] = empty_category_id
355
+ ann['sequence_level_annotation'] = False
356
+ # This would also be a reasonable thing to do, but it's not the convention
357
+ # we're adopting.
358
+ # ann['bbox'] = [0,0,0,0]
359
+ annotations.append(ann)
360
+ images.append(im)
148
361
 
149
- # ...for each image
362
+ # ...for each image result
150
363
 
151
364
  print('Read {} annotations for {} images'.format(len(annotations),
152
365
  len(images)))
@@ -26,12 +26,17 @@ from detection.video_utils import frame_results_to_video_results
26
26
  from detection.video_utils import video_folder_to_frames
27
27
  from uuid import uuid1
28
28
 
29
+ from detection.video_utils import default_fourcc
30
+
29
31
 
30
32
  #%% Options classes
31
33
 
32
34
  class ProcessVideoOptions:
33
35
 
34
- model_file = ''
36
+ # Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
37
+ model_file = 'MDV5A'
38
+
39
+ # Can be a file or a folder
35
40
  input_video_file = ''
36
41
 
37
42
  output_json_file = None
@@ -72,9 +77,10 @@ class ProcessVideoOptions:
72
77
 
73
78
  recursive = False
74
79
  verbose = False
80
+
75
81
  fourcc = None
76
82
 
77
- rendering_confidence_threshold = 0.15
83
+ rendering_confidence_threshold = None
78
84
  json_confidence_threshold = 0.005
79
85
  frame_sample = None
80
86
 
@@ -175,8 +181,14 @@ def process_video(options):
175
181
  confidence_threshold=options.rendering_confidence_threshold)
176
182
 
177
183
  # Combine into a video
178
- print('Rendering video to {} at {} fps'.format(options.output_video_file,Fs))
179
- frames_to_video(detected_frame_files, Fs, options.output_video_file, codec_spec=options.fourcc)
184
+ if options.frame_sample is None:
185
+ rendering_fs = Fs
186
+ else:
187
+ rendering_fs = Fs / options.frame_sample
188
+
189
+ print('Rendering video to {} at {} fps (original video {} fps)'.format(
190
+ options.output_video_file,rendering_fs,Fs))
191
+ frames_to_video(detected_frame_files, rendering_fs, options.output_video_file, codec_spec=options.fourcc)
180
192
 
181
193
  # Delete the temporary directory we used for detection images
182
194
  if not options.keep_rendered_frames:
@@ -344,11 +356,19 @@ def process_video_folder(options):
344
356
  output_video_folder = options.input_video_file
345
357
 
346
358
  # For each video
359
+ #
360
+ # TODO: parallelize this loop
361
+ #
347
362
  # i_video=0; input_video_file_abs = video_filenames[i_video]
348
363
  for i_video,input_video_file_abs in enumerate(video_filenames):
349
364
 
350
365
  video_fs = Fs[i_video]
351
366
 
367
+ if options.frame_sample is None:
368
+ rendering_fs = video_fs
369
+ else:
370
+ rendering_fs = video_fs / options.frame_sample
371
+
352
372
  input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
353
373
  video_frame_output_folder = os.path.join(frame_rendering_output_dir,input_video_file_relative)
354
374
  assert os.path.isdir(video_frame_output_folder), \
@@ -371,11 +391,10 @@ def process_video_folder(options):
371
391
  os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
372
392
 
373
393
  # Create the output video
374
- print('Rendering detections for video {} to {} at {} fps'.format(input_video_file_relative,
375
- video_output_file,video_fs))
376
- frames_to_video(video_frame_files, video_fs, video_output_file, codec_spec=options.fourcc)
377
-
378
-
394
+ print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
395
+ input_video_file_relative,video_output_file,rendering_fs,video_fs))
396
+ frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
397
+
379
398
  # ...for each video
380
399
 
381
400
  # Possibly clean up rendered frames
@@ -525,12 +544,14 @@ if False:
525
544
 
526
545
  def main():
527
546
 
547
+ default_options = ProcessVideoOptions()
548
+
528
549
  parser = argparse.ArgumentParser(description=(
529
550
  'Run MegaDetector on each frame in a video (or every Nth frame), optionally '\
530
551
  'producing a new video with detections annotated'))
531
552
 
532
553
  parser.add_argument('model_file', type=str,
533
- help='MegaDetector model file')
554
+ help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
534
555
 
535
556
  parser.add_argument('input_video_file', type=str,
536
557
  help='video file (or folder) to process')
@@ -567,8 +588,8 @@ def main():
567
588
  parser.add_argument('--render_output_video', action='store_true',
568
589
  help='enable video output rendering (not rendered by default)')
569
590
 
570
- parser.add_argument('--fourcc', default=None,
571
- help='fourcc code to use for video encoding, only used if render_output_video is True')
591
+ parser.add_argument('--fourcc', default=default_fourcc,
592
+ help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(default_fourcc))
572
593
 
573
594
  parser.add_argument('--keep_rendered_frames',
574
595
  action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
@@ -586,11 +607,12 @@ def main():
586
607
  'whether other files were present in the folder.')
587
608
 
588
609
  parser.add_argument('--rendering_confidence_threshold', type=float,
589
- default=0.8, help="don't render boxes with confidence below this threshold")
610
+ default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
590
611
 
591
612
  parser.add_argument('--json_confidence_threshold', type=float,
592
613
  default=0.0, help="don't include boxes in the .json file with confidence "\
593
- 'below this threshold')
614
+ 'below this threshold (default {})'.format(
615
+ default_options.json_confidence_threshold))
594
616
 
595
617
  parser.add_argument('--n_cores', type=int,
596
618
  default=1, help='number of cores to use for frame separation and detection. '\
@@ -234,7 +234,7 @@ class PTDetector:
234
234
  if self.device == 'mps':
235
235
  # As of v1.13.0.dev20220824, nms is not implemented for MPS.
236
236
  #
237
- # Send predication back to the CPU to fix.
237
+ # Send prediction back to the CPU to fix.
238
238
  pred = non_max_suppression(prediction=pred.cpu(), conf_thres=detection_threshold)
239
239
  else:
240
240
  pred = non_max_suppression(prediction=pred, conf_thres=detection_threshold)