megadetector 5.0.5__py3-none-any.whl → 5.0.7__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 (132) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +302 -263
  2. api/batch_processing/data_preparation/manage_video_batch.py +81 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/categorize_detections_by_size.py +50 -19
  5. api/batch_processing/postprocessing/compare_batch_results.py +110 -60
  6. api/batch_processing/postprocessing/load_api_results.py +56 -70
  7. api/batch_processing/postprocessing/md_to_coco.py +1 -1
  8. api/batch_processing/postprocessing/md_to_labelme.py +2 -1
  9. api/batch_processing/postprocessing/postprocess_batch_results.py +240 -81
  10. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
  11. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  12. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  13. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +227 -75
  14. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  15. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  16. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +2 -2
  17. classification/prepare_classification_script.py +191 -191
  18. data_management/coco_to_yolo.py +68 -45
  19. data_management/databases/integrity_check_json_db.py +7 -5
  20. data_management/generate_crops_from_cct.py +3 -3
  21. data_management/get_image_sizes.py +8 -6
  22. data_management/importers/add_timestamps_to_icct.py +79 -0
  23. data_management/importers/animl_results_to_md_results.py +160 -0
  24. data_management/importers/auckland_doc_test_to_json.py +4 -4
  25. data_management/importers/auckland_doc_to_json.py +1 -1
  26. data_management/importers/awc_to_json.py +5 -5
  27. data_management/importers/bellevue_to_json.py +5 -5
  28. data_management/importers/carrizo_shrubfree_2018.py +5 -5
  29. data_management/importers/carrizo_trail_cam_2017.py +5 -5
  30. data_management/importers/cct_field_adjustments.py +2 -3
  31. data_management/importers/channel_islands_to_cct.py +4 -4
  32. data_management/importers/ena24_to_json.py +5 -5
  33. data_management/importers/helena_to_cct.py +10 -10
  34. data_management/importers/idaho-camera-traps.py +12 -12
  35. data_management/importers/idfg_iwildcam_lila_prep.py +8 -8
  36. data_management/importers/jb_csv_to_json.py +4 -4
  37. data_management/importers/missouri_to_json.py +1 -1
  38. data_management/importers/noaa_seals_2019.py +1 -1
  39. data_management/importers/pc_to_json.py +5 -5
  40. data_management/importers/prepare-noaa-fish-data-for-lila.py +4 -4
  41. data_management/importers/prepare_zsl_imerit.py +5 -5
  42. data_management/importers/rspb_to_json.py +4 -4
  43. data_management/importers/save_the_elephants_survey_A.py +5 -5
  44. data_management/importers/save_the_elephants_survey_B.py +6 -6
  45. data_management/importers/snapshot_safari_importer.py +9 -9
  46. data_management/importers/snapshot_serengeti_lila.py +9 -9
  47. data_management/importers/timelapse_csv_set_to_json.py +5 -7
  48. data_management/importers/ubc_to_json.py +4 -4
  49. data_management/importers/umn_to_json.py +4 -4
  50. data_management/importers/wellington_to_json.py +1 -1
  51. data_management/importers/wi_to_json.py +2 -2
  52. data_management/importers/zamba_results_to_md_results.py +181 -0
  53. data_management/labelme_to_coco.py +35 -7
  54. data_management/labelme_to_yolo.py +229 -0
  55. data_management/lila/add_locations_to_island_camera_traps.py +1 -1
  56. data_management/lila/add_locations_to_nacti.py +147 -0
  57. data_management/lila/create_lila_blank_set.py +474 -0
  58. data_management/lila/create_lila_test_set.py +2 -1
  59. data_management/lila/create_links_to_md_results_files.py +106 -0
  60. data_management/lila/download_lila_subset.py +46 -21
  61. data_management/lila/generate_lila_per_image_labels.py +23 -14
  62. data_management/lila/get_lila_annotation_counts.py +17 -11
  63. data_management/lila/lila_common.py +14 -11
  64. data_management/lila/test_lila_metadata_urls.py +116 -0
  65. data_management/ocr_tools.py +829 -0
  66. data_management/resize_coco_dataset.py +13 -11
  67. data_management/yolo_output_to_md_output.py +84 -12
  68. data_management/yolo_to_coco.py +38 -20
  69. detection/process_video.py +36 -14
  70. detection/pytorch_detector.py +23 -8
  71. detection/run_detector.py +76 -19
  72. detection/run_detector_batch.py +178 -63
  73. detection/run_inference_with_yolov5_val.py +326 -57
  74. detection/run_tiled_inference.py +153 -43
  75. detection/video_utils.py +34 -8
  76. md_utils/ct_utils.py +172 -1
  77. md_utils/md_tests.py +372 -51
  78. md_utils/path_utils.py +167 -39
  79. md_utils/process_utils.py +26 -7
  80. md_utils/split_locations_into_train_val.py +215 -0
  81. md_utils/string_utils.py +10 -0
  82. md_utils/url_utils.py +0 -2
  83. md_utils/write_html_image_list.py +9 -26
  84. md_visualization/plot_utils.py +12 -8
  85. md_visualization/visualization_utils.py +106 -7
  86. md_visualization/visualize_db.py +16 -8
  87. md_visualization/visualize_detector_output.py +208 -97
  88. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/METADATA +3 -6
  89. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/RECORD +98 -121
  90. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
  91. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  92. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  93. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  94. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  95. taxonomy_mapping/species_lookup.py +33 -13
  96. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  97. api/synchronous/api_core/yolov5/detect.py +0 -252
  98. api/synchronous/api_core/yolov5/export.py +0 -607
  99. api/synchronous/api_core/yolov5/hubconf.py +0 -146
  100. api/synchronous/api_core/yolov5/models/__init__.py +0 -0
  101. api/synchronous/api_core/yolov5/models/common.py +0 -738
  102. api/synchronous/api_core/yolov5/models/experimental.py +0 -104
  103. api/synchronous/api_core/yolov5/models/tf.py +0 -574
  104. api/synchronous/api_core/yolov5/models/yolo.py +0 -338
  105. api/synchronous/api_core/yolov5/train.py +0 -670
  106. api/synchronous/api_core/yolov5/utils/__init__.py +0 -36
  107. api/synchronous/api_core/yolov5/utils/activations.py +0 -103
  108. api/synchronous/api_core/yolov5/utils/augmentations.py +0 -284
  109. api/synchronous/api_core/yolov5/utils/autoanchor.py +0 -170
  110. api/synchronous/api_core/yolov5/utils/autobatch.py +0 -66
  111. api/synchronous/api_core/yolov5/utils/aws/__init__.py +0 -0
  112. api/synchronous/api_core/yolov5/utils/aws/resume.py +0 -40
  113. api/synchronous/api_core/yolov5/utils/benchmarks.py +0 -148
  114. api/synchronous/api_core/yolov5/utils/callbacks.py +0 -71
  115. api/synchronous/api_core/yolov5/utils/dataloaders.py +0 -1087
  116. api/synchronous/api_core/yolov5/utils/downloads.py +0 -178
  117. api/synchronous/api_core/yolov5/utils/flask_rest_api/example_request.py +0 -19
  118. api/synchronous/api_core/yolov5/utils/flask_rest_api/restapi.py +0 -46
  119. api/synchronous/api_core/yolov5/utils/general.py +0 -1018
  120. api/synchronous/api_core/yolov5/utils/loggers/__init__.py +0 -187
  121. api/synchronous/api_core/yolov5/utils/loggers/wandb/__init__.py +0 -0
  122. api/synchronous/api_core/yolov5/utils/loggers/wandb/log_dataset.py +0 -27
  123. api/synchronous/api_core/yolov5/utils/loggers/wandb/sweep.py +0 -41
  124. api/synchronous/api_core/yolov5/utils/loggers/wandb/wandb_utils.py +0 -577
  125. api/synchronous/api_core/yolov5/utils/loss.py +0 -234
  126. api/synchronous/api_core/yolov5/utils/metrics.py +0 -355
  127. api/synchronous/api_core/yolov5/utils/plots.py +0 -489
  128. api/synchronous/api_core/yolov5/utils/torch_utils.py +0 -314
  129. api/synchronous/api_core/yolov5/val.py +0 -394
  130. md_utils/matlab_porting_tools.py +0 -97
  131. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
  132. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
@@ -49,6 +49,8 @@ def resize_coco_dataset(input_folder,input_filename,
49
49
  of the way there, due to what appears to be a slight bias inherent to MD. If a box extends
50
50
  within [right_edge_quantization_threshold] (a small number, from 0 to 1, but probably around
51
51
  0.02) of the right edge of the image, it will be extended to the far right edge.
52
+
53
+ Returns the COCO database with resized images.
52
54
  """
53
55
 
54
56
  # Read input data
@@ -62,7 +64,9 @@ def resize_coco_dataset(input_folder,input_filename,
62
64
 
63
65
  # For each image
64
66
 
65
- # im = d['images'][1]
67
+ # TODO: this is trivially parallelizable
68
+ #
69
+ # im = d['images'][0]
66
70
  for im in tqdm(d['images']):
67
71
 
68
72
  input_fn_relative = im['file_name']
@@ -143,6 +147,8 @@ def resize_coco_dataset(input_folder,input_filename,
143
147
  with open(output_filename,'w') as f:
144
148
  json.dump(d,f,indent=1)
145
149
 
150
+ return d
151
+
146
152
  # ...def resize_coco_dataset(...)
147
153
 
148
154
 
@@ -153,17 +159,13 @@ if False:
153
159
  pass
154
160
 
155
161
  #%% Test resizing
156
-
157
- # input_filename = os.path.expanduser('~/tmp/labelme_to_coco_test.json')
158
- # input_folder = os.path.expanduser('~/data/labelme-json-test')
159
- # target_size = (600,-1)
160
-
161
- input_folder = os.path.expanduser('~/data/usgs-kissel-training')
162
- input_filename = os.path.expanduser('~/data/usgs-tegus.json')
162
+
163
+ input_folder = os.path.expanduser('~/data/usgs-tegus/usgs-kissel-training')
164
+ input_filename = os.path.expanduser('~/data/usgs-tegus/usgs-kissel-training.json')
163
165
  target_size = (1600,-1)
164
166
 
165
- output_filename = insert_before_extension(input_filename,'resized')
166
- output_folder = input_folder + '-resized'
167
+ output_filename = insert_before_extension(input_filename,'resized-test')
168
+ output_folder = input_folder + '-resized-test'
167
169
 
168
170
  correct_size_image_handling = 'rewrite'
169
171
 
@@ -184,7 +186,7 @@ if False:
184
186
  options.viz_size = (900, -1)
185
187
  options.num_to_visualize = 5000
186
188
 
187
- html_file,_ = visualize_db.process_images(output_filename,
189
+ html_file,_ = visualize_db.visualize_db(output_filename,
188
190
  os.path.expanduser('~/tmp/resize_coco_preview'),
189
191
  output_folder,options)
190
192
 
@@ -42,8 +42,9 @@
42
42
  #%% Imports and constants
43
43
 
44
44
  import json
45
- import os
46
45
  import csv
46
+ import os
47
+ import re
47
48
 
48
49
  from collections import defaultdict
49
50
  from tqdm import tqdm
@@ -58,6 +59,42 @@ from detection.run_detector import CONF_DIGITS, COORD_DIGITS
58
59
 
59
60
  #%% Support functions
60
61
 
62
+ def read_classes_from_yolo_dataset_file(fn):
63
+ """
64
+ Read a dictionary mapping integer class IDs to class names from a YOLOv5/YOLOv8
65
+ dataset.yaml file or a .json file. A .json file should contain a dictionary mapping
66
+ integer category IDs to string category names.
67
+ """
68
+
69
+ if fn.endswith('.yml') or fn.endswith('.yaml'):
70
+
71
+ with open(fn,'r') as f:
72
+ lines = f.readlines()
73
+
74
+ category_id_to_name = {}
75
+ pat = '\d+:.+'
76
+ for s in lines:
77
+ if re.search(pat,s) is not None:
78
+ tokens = s.split(':')
79
+ assert len(tokens) == 2, 'Invalid token in category file {}'.format(fn)
80
+ category_id_to_name[int(tokens[0].strip())] = tokens[1].strip()
81
+
82
+ elif fn.endswith('.json'):
83
+
84
+ with open(fn,'r') as f:
85
+ d_in = json.load(f)
86
+ category_id_to_name = {}
87
+ for k in d_in.keys():
88
+ category_id_to_name[int(k)] = d_in[k]
89
+
90
+ else:
91
+
92
+ raise ValueError('Unrecognized category file type: {}'.format(fn))
93
+
94
+ assert len(category_id_to_name) > 0, 'Failed to read class mappings from {}'.format(fn)
95
+ return category_id_to_name
96
+
97
+
61
98
  def yolo_json_output_to_md_output(yolo_json_file, image_folder,
62
99
  output_file, yolo_category_id_to_name,
63
100
  detector_name='unknown',
@@ -68,23 +105,32 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
68
105
  """
69
106
  Convert a YOLOv5 .json file to MD .json format.
70
107
 
71
- Args
72
- - yolo_json_file: the .json file to convert from YOLOv5 format to MD output format
73
- - image_folder: the .json file contains relative path names, this is the path base
108
+ Args:
109
+
110
+ - yolo_json_file: the .json file to convert from YOLOv5 format to MD output format.
111
+
112
+ - image_folder: the .json file contains relative path names, this is the path base.
113
+
74
114
  - yolo_category_id_to_name: the .json file contains only numeric identifiers for
75
115
  categories, but we want names and numbers for the output format; this is a
76
- dict mapping numbers to names
77
- - detector_name: a string put in the output file, not otherwise used here
116
+ dict mapping numbers to names. Can also be a YOLOv5 dataset.yaml file.
117
+
118
+ - detector_name: a string that gets put in the output file, not otherwise used within
119
+ this function.
120
+
78
121
  - image_id_to_relative_path: YOLOv5 .json uses only basenames (e.g. abc1234.JPG);
79
122
  by default these will be appended to the input path to create pathnames, so if you
80
123
  have a flat folder, this is fine. If you want to map base names to relative paths, use
81
124
  this dict.
125
+
82
126
  - offset_yolo_class_ids: YOLOv5 class IDs always start at zero; if you want to make the
83
- output classes start at 1, set offset_yolo_class_ids
127
+ output classes start at 1, set offset_yolo_class_ids to True.
128
+
84
129
  - truncate_to_standard_md_precision: YOLOv5 .json includes lots of (not-super-meaningful)
85
130
  precision, set this to truncate to COORD_DIGITS and CONF_DIGITS.
86
- - image_id_to_error: if you want to include image IDs in the output file because you couldn't
87
- prepare the input file in the first place, include them here.
131
+
132
+ - image_id_to_error: if you want to include image IDs in the output file for which you couldn't
133
+ prepare the input file in the first place due to errors, include them here.
88
134
  """
89
135
 
90
136
  assert os.path.isfile(yolo_json_file), \
@@ -95,7 +141,14 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
95
141
  if image_id_to_error is None:
96
142
  image_id_to_error = {}
97
143
 
98
- print('Converting {} to MD format'.format(yolo_json_file))
144
+ print('Converting {} to MD format and writing results to {}'.format(
145
+ yolo_json_file,output_file))
146
+
147
+ if isinstance(yolo_category_id_to_name,str):
148
+ assert os.path.isfile(yolo_category_id_to_name), \
149
+ 'YOLO category mapping specified as a string, but file does not exist: {}'.format(
150
+ yolo_category_id_to_name)
151
+ yolo_category_id_to_name = read_classes_from_yolo_dataset_file(yolo_category_id_to_name)
99
152
 
100
153
  if image_id_to_relative_path is None:
101
154
 
@@ -158,6 +211,16 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
158
211
 
159
212
  # ...if image IDs are formatted as integers in YOLO output
160
213
 
214
+ # In a modified version of val.py, we use negative category IDs to indicate an error
215
+ # that happened during inference (typically truncated images with valid headers,
216
+ # so corruption was not detected during val.py's initial corruption check pass.
217
+ for det in detections:
218
+ if det['category_id'] < 0:
219
+ assert 'error' in det, 'Negative category ID present with no error string'
220
+ error_string = det['error']
221
+ print('Caught inference-time failure {} for image {}'.format(error_string,det['image_id']))
222
+ image_id_to_error[det['image_id']] = error_string
223
+
161
224
  output_images = []
162
225
 
163
226
  # image_file_relative = image_files_relative[10]
@@ -238,7 +301,7 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
238
301
  d['images'] = output_images
239
302
  d['info'] = {'format_version':1.3,'detector':detector_name}
240
303
  d['detection_categories'] = {}
241
-
304
+
242
305
  for cat_id in yolo_category_id_to_name:
243
306
  yolo_cat_id = int(cat_id)
244
307
  if offset_yolo_class_ids:
@@ -248,9 +311,18 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
248
311
  with open(output_file,'w') as f:
249
312
  json.dump(d,f,indent=1)
250
313
 
314
+ # ...def yolo_json_output_to_md_output(...)
251
315
 
316
+
252
317
  def yolo_txt_output_to_md_output(input_results_folder, image_folder,
253
318
  output_file, detector_tag=None):
319
+ """
320
+ Converts a folder of YOLO-outptu .txt files to MD .json format.
321
+
322
+ Less finished than the .json conversion function; this .txt conversion assumes
323
+ a hard-coded mapping representing the standard MD categories (in MD indexing,
324
+ 1/2/3=animal/person/vehicle; in YOLO indexing, 0/1/2=animal/person/vehicle).
325
+ """
254
326
 
255
327
  assert os.path.isdir(input_results_folder)
256
328
  assert os.path.isdir(image_folder)
@@ -339,7 +411,7 @@ def yolo_txt_output_to_md_output(input_results_folder, image_folder,
339
411
  with open(output_file,'w') as f:
340
412
  json.dump(output_content,f,indent=1)
341
413
 
342
- # ...def yolo_output_to_md_output()
414
+ # ...def yolo_txt_output_to_md_output(...)
343
415
 
344
416
 
345
417
  #%% Interactive driver
@@ -18,11 +18,19 @@ from PIL import Image
18
18
  from tqdm import tqdm
19
19
 
20
20
  from md_utils.path_utils import find_images
21
+ from data_management.yolo_output_to_md_output import read_classes_from_yolo_dataset_file
21
22
 
22
23
 
23
24
  #%% Main conversion function
24
25
 
25
26
  def yolo_to_coco(input_folder,class_name_file,output_file=None):
27
+ """
28
+ Convert the YOLO-formatted data in [input_folder] to a COCO-formatted dictionary,
29
+ reading class names from [class_name_file], which can be a flat list with a .txt
30
+ extension or a YOLO dataset.yml file. Optionally writes the output dataset to [output_file].
31
+
32
+ Returns a COCO-formatted dictionary.
33
+ """
26
34
 
27
35
  # Validate input
28
36
 
@@ -30,29 +38,39 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
30
38
  assert os.path.isfile(class_name_file)
31
39
 
32
40
 
33
- # Class names
41
+ # Read class names
34
42
 
35
- with open(class_name_file,'r') as f:
36
- lines = f.readlines()
37
- assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
38
- lines = [s.strip() for s in lines]
39
- assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
43
+ ext = os.path.splitext(class_name_file)[1][1:]
44
+ assert ext in ('yml','txt','yaml'), 'Unrecognized class name file type {}'.format(
45
+ class_name_file)
40
46
 
41
- # Blank lines should only appear at the end
42
- b_found_blank = False
43
- for s in lines:
44
- if len(s) == 0:
45
- b_found_blank = True
46
- elif b_found_blank:
47
- raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
48
- class_name_file))
49
-
50
- category_id_to_name = {}
47
+ if ext == 'txt':
51
48
 
52
- for i_category_id,category_name in enumerate(lines):
53
- assert len(category_name) > 0
54
- category_id_to_name[i_category_id] = category_name
49
+ with open(class_name_file,'r') as f:
50
+ lines = f.readlines()
51
+ assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
52
+ class_names = [s.strip() for s in lines]
53
+ assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
55
54
 
55
+ # Blank lines should only appear at the end
56
+ b_found_blank = False
57
+ for s in lines:
58
+ if len(s) == 0:
59
+ b_found_blank = True
60
+ elif b_found_blank:
61
+ raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
62
+ class_name_file))
63
+
64
+ category_id_to_name = {}
65
+ for i_category_id,category_name in enumerate(class_names):
66
+ assert len(category_name) > 0
67
+ category_id_to_name[i_category_id] = category_name
68
+
69
+ else:
70
+
71
+ assert ext in ('yml','yaml')
72
+ category_id_to_name = read_classes_from_yolo_dataset_file(class_name_file)
73
+
56
74
 
57
75
  # Enumerate images
58
76
 
@@ -209,7 +227,7 @@ if False:
209
227
  viz_options.parallelize_rendering = True
210
228
  viz_options.include_filename_links = True
211
229
 
212
- html_output_file, _ = visualize_db.process_images(db_path=output_file,
230
+ html_output_file, _ = visualize_db.visualize_db(db_path=output_file,
213
231
  output_dir=preview_folder,
214
232
  image_base_dir=input_folder,
215
233
  options=viz_options)
@@ -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. '\
@@ -17,17 +17,31 @@ from md_utils import ct_utils
17
17
 
18
18
  # We support a few ways of accessing the YOLOv5 dependencies:
19
19
  #
20
- # * The standard configuration as of 9.2023 expects that the YOLOv5 repo is checked
20
+ # * The standard configuration as of 2023.09 expects that the YOLOv5 repo is checked
21
21
  # out and on the PYTHONPATH (import utils)
22
22
  #
23
- # * Experimental: pip install ultralytics (doesn't totally work yet)
23
+ # * Supported but non-default (used for PyPI packaging):
24
24
  #
25
- # * Experimental but works so far: pip install yolov5
25
+ # pip install ultralytics-yolov5
26
+ #
27
+ # * Works, but not supported:
28
+ #
29
+ # pip install yolov5
30
+ #
31
+ # * Unfinished:
32
+ #
33
+ # pip install ultralytics
34
+ #
35
+ # If try_ultralytics_import is True, we'll try to import all YOLOv5 dependencies from
36
+ # ultralytics.utils and ultralytics.data. But as of 2023.11, this results in a "No
37
+ # module named 'models'" error when running MDv5, and there's no upside to this approach
38
+ # compared to using either of the YOLOv5 PyPI packages, so... punting on this for now.
26
39
 
27
40
  utils_imported = False
28
41
  try_yolov5_import = True
29
42
 
30
- # This still encounters some namespace issues
43
+ # See above; this should remain as "False" unless we update the MegaDetector .pt file
44
+ # to use more recent YOLOv5 namespace conventions.
31
45
  try_ultralytics_import = False
32
46
 
33
47
  # First try importing from the yolov5 package
@@ -77,7 +91,7 @@ if not utils_imported:
77
91
  except ImportError:
78
92
  from utils.general import scale_boxes as scale_coords
79
93
  utils_imported = True
80
- print('Imported YOLOv5 from PYTHONPATH')
94
+ print('Imported YOLOv5 as utils.*')
81
95
  except ModuleNotFoundError:
82
96
  raise ModuleNotFoundError('Could not import YOLOv5 functions.')
83
97
 
@@ -220,7 +234,7 @@ class PTDetector:
220
234
  if self.device == 'mps':
221
235
  # As of v1.13.0.dev20220824, nms is not implemented for MPS.
222
236
  #
223
- # Send predication back to the CPU to fix.
237
+ # Send prediction back to the CPU to fix.
224
238
  pred = non_max_suppression(prediction=pred.cpu(), conf_thres=detection_threshold)
225
239
  else:
226
240
  pred = non_max_suppression(prediction=pred, conf_thres=detection_threshold)
@@ -295,10 +309,11 @@ if __name__ == '__main__':
295
309
  import md_visualization.visualization_utils as vis_utils
296
310
  import os
297
311
 
298
- model_file = os.path.expanduser('~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt')
299
- im_file = r"G:\temp\coyote\DSCF0043.JPG"
312
+ model_file = 'MDV5A'
313
+ im_file = os.path.expanduser('~/git/MegaDetector/images/nacti.jpg')
300
314
 
301
315
  detector = PTDetector(model_file)
302
316
  image = vis_utils.load_image(im_file)
303
317
 
304
318
  res = detector.generate_detections_one_image(image, im_file, detection_threshold=0.00001)
319
+ print(res)
detection/run_detector.py CHANGED
@@ -10,12 +10,7 @@
10
10
  # This script is not a good way to process lots of images (tens of thousands,
11
11
  # say). It does not facilitate checkpointing the results so if it crashes you
12
12
  # would have to start from scratch. If you want to run a detector (e.g., ours)
13
- # on lots of images, you should check out:
14
- #
15
- # 1) run_detector_batch.py (for local execution)
16
- #
17
- # 2) https://github.com/agentmorris/MegaDetector/tree/master/api/batch_processing
18
- # (for running large jobs on Azure ML)
13
+ # on lots of images, you should check out run_detector_batch.py.
19
14
  #
20
15
  # To run this script, we recommend you set up a conda virtual environment
21
16
  # following instructions in the Installation section on the main README, using
@@ -136,6 +131,33 @@ downloadable_models = {
136
131
  'MDV5B':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5b.0.0.pt'
137
132
  }
138
133
 
134
+ model_string_to_model_version = {
135
+ 'v2':'v2.0.0',
136
+ 'v3':'v3.0.0',
137
+ 'v4.1':'v4.1.0',
138
+ 'v5a.0.0':'v5a.0.0',
139
+ 'v5b.0.0':'v5b.0.0',
140
+ 'mdv5a':'v5a.0.0',
141
+ 'mdv5b':'v5b.0.0',
142
+ 'mdv4':'v4.1.0',
143
+ 'mdv3':'v3.0.0'
144
+ }
145
+
146
+ # Approximate inference speeds (in images per second) for MDv5 based on
147
+ # benchmarks, only used for reporting very coarse expectations about inference time.
148
+ device_token_to_mdv5_inference_speed = {
149
+ '4090':17.6,
150
+ '3090':11.4,
151
+ '3080':9.5,
152
+ '3050':4.2,
153
+ 'P2000':2.1,
154
+ # These are written this way because they're MDv4 benchmarks, and MDv5
155
+ # is around 3.5x faster than MDv4.
156
+ 'V100':2.79*3.5,
157
+ '2080':2.3*3.5,
158
+ '2060':1.6*3.5
159
+ }
160
+
139
161
 
140
162
  #%% Utility functions
141
163
 
@@ -164,7 +186,9 @@ def get_detector_metadata_from_version_string(detector_version):
164
186
  if detector_version not in DETECTOR_METADATA:
165
187
  print('Warning: no metadata for unknown detector version {}'.format(detector_version))
166
188
  default_detector_metadata = {
167
- 'megadetector_version':'unknown'
189
+ 'megadetector_version':'unknown',
190
+ 'typical_detection_threshold':0.5,
191
+ 'conservative_detection_threshold':0.25
168
192
  }
169
193
  return default_detector_metadata
170
194
  else:
@@ -188,18 +212,9 @@ def get_detector_version_from_filename(detector_filename):
188
212
  "v4.1.0", "v5a.0.0", and "v5b.0.0", respectively.
189
213
  """
190
214
 
191
- fn = os.path.basename(detector_filename)
192
- known_model_versions = {'v2':'v2.0.0',
193
- 'v3':'v3.0.0',
194
- 'v4.1':'v4.1.0',
195
- 'v5a.0.0':'v5a.0.0',
196
- 'v5b.0.0':'v5b.0.0',
197
- 'MDV5A':'v5a.0.0',
198
- 'MDV5B':'v5b.0.0',
199
- 'MDV4':'v4.1.0',
200
- 'MDV3':'v3.0.0'}
215
+ fn = os.path.basename(detector_filename).lower()
201
216
  matches = []
202
- for s in known_model_versions.keys():
217
+ for s in model_string_to_model_version.keys():
203
218
  if s in fn:
204
219
  matches.append(s)
205
220
  if len(matches) == 0:
@@ -209,9 +224,51 @@ def get_detector_version_from_filename(detector_filename):
209
224
  print('Warning: multiple MegaDetector versions for model file {}'.format(detector_filename))
210
225
  return 'multiple'
211
226
  else:
212
- return known_model_versions[matches[0]]
227
+ return model_string_to_model_version[matches[0]]
213
228
 
214
229
 
230
+ def estimate_md_images_per_second(model_file, device_name=None):
231
+ """
232
+ Estimate how fast MegaDetector will run based on benchmarks. Defaults to querying
233
+ the current device. Returns None if no data is available for the current card/model.
234
+ Estimates only available for a small handful of GPUs.
235
+ """
236
+
237
+ if device_name is None:
238
+ try:
239
+ import torch
240
+ device_name = torch.cuda.get_device_name()
241
+ except Exception as e:
242
+ print('Error querying device name: {}'.format(e))
243
+ return None
244
+
245
+ model_file = model_file.lower().strip()
246
+ if model_file in model_string_to_model_version.values():
247
+ model_version = model_file
248
+ else:
249
+ model_version = get_detector_version_from_filename(model_file)
250
+ if model_version not in model_string_to_model_version.values():
251
+ print('Error determining model version for model file {}'.format(model_file))
252
+ return None
253
+
254
+ mdv5_inference_speed = None
255
+ for device_token in device_token_to_mdv5_inference_speed.keys():
256
+ if device_token in device_name:
257
+ mdv5_inference_speed = device_token_to_mdv5_inference_speed[device_token]
258
+ break
259
+
260
+ if mdv5_inference_speed is None:
261
+ print('No speed estimate available for {}'.format(device_name))
262
+
263
+ if 'v5' in model_version:
264
+ return mdv5_inference_speed
265
+ elif 'v2' in model_version or 'v3' in model_version or 'v4' in model_version:
266
+ return mdv5_inference_speed / 3.5
267
+ else:
268
+ print('Could not estimate inference speed for model file {}'.format(model_file))
269
+ return None
270
+
271
+
215
272
  def get_typical_confidence_threshold_from_results(results):
216
273
  """
217
274
  Given the .json data loaded from a MD results file, determine a typical confidence