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.
- api/batch_processing/data_preparation/manage_local_batch.py +302 -263
- api/batch_processing/data_preparation/manage_video_batch.py +81 -2
- api/batch_processing/postprocessing/add_max_conf.py +1 -0
- api/batch_processing/postprocessing/categorize_detections_by_size.py +50 -19
- api/batch_processing/postprocessing/compare_batch_results.py +110 -60
- api/batch_processing/postprocessing/load_api_results.py +56 -70
- api/batch_processing/postprocessing/md_to_coco.py +1 -1
- api/batch_processing/postprocessing/md_to_labelme.py +2 -1
- api/batch_processing/postprocessing/postprocess_batch_results.py +240 -81
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
- api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
- api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +227 -75
- api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
- api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
- api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +2 -2
- classification/prepare_classification_script.py +191 -191
- data_management/coco_to_yolo.py +68 -45
- data_management/databases/integrity_check_json_db.py +7 -5
- data_management/generate_crops_from_cct.py +3 -3
- data_management/get_image_sizes.py +8 -6
- data_management/importers/add_timestamps_to_icct.py +79 -0
- data_management/importers/animl_results_to_md_results.py +160 -0
- data_management/importers/auckland_doc_test_to_json.py +4 -4
- data_management/importers/auckland_doc_to_json.py +1 -1
- data_management/importers/awc_to_json.py +5 -5
- data_management/importers/bellevue_to_json.py +5 -5
- data_management/importers/carrizo_shrubfree_2018.py +5 -5
- data_management/importers/carrizo_trail_cam_2017.py +5 -5
- data_management/importers/cct_field_adjustments.py +2 -3
- data_management/importers/channel_islands_to_cct.py +4 -4
- data_management/importers/ena24_to_json.py +5 -5
- data_management/importers/helena_to_cct.py +10 -10
- data_management/importers/idaho-camera-traps.py +12 -12
- data_management/importers/idfg_iwildcam_lila_prep.py +8 -8
- data_management/importers/jb_csv_to_json.py +4 -4
- data_management/importers/missouri_to_json.py +1 -1
- data_management/importers/noaa_seals_2019.py +1 -1
- data_management/importers/pc_to_json.py +5 -5
- data_management/importers/prepare-noaa-fish-data-for-lila.py +4 -4
- data_management/importers/prepare_zsl_imerit.py +5 -5
- data_management/importers/rspb_to_json.py +4 -4
- data_management/importers/save_the_elephants_survey_A.py +5 -5
- data_management/importers/save_the_elephants_survey_B.py +6 -6
- data_management/importers/snapshot_safari_importer.py +9 -9
- data_management/importers/snapshot_serengeti_lila.py +9 -9
- data_management/importers/timelapse_csv_set_to_json.py +5 -7
- data_management/importers/ubc_to_json.py +4 -4
- data_management/importers/umn_to_json.py +4 -4
- data_management/importers/wellington_to_json.py +1 -1
- data_management/importers/wi_to_json.py +2 -2
- data_management/importers/zamba_results_to_md_results.py +181 -0
- data_management/labelme_to_coco.py +35 -7
- data_management/labelme_to_yolo.py +229 -0
- data_management/lila/add_locations_to_island_camera_traps.py +1 -1
- data_management/lila/add_locations_to_nacti.py +147 -0
- data_management/lila/create_lila_blank_set.py +474 -0
- data_management/lila/create_lila_test_set.py +2 -1
- data_management/lila/create_links_to_md_results_files.py +106 -0
- data_management/lila/download_lila_subset.py +46 -21
- data_management/lila/generate_lila_per_image_labels.py +23 -14
- data_management/lila/get_lila_annotation_counts.py +17 -11
- data_management/lila/lila_common.py +14 -11
- data_management/lila/test_lila_metadata_urls.py +116 -0
- data_management/ocr_tools.py +829 -0
- data_management/resize_coco_dataset.py +13 -11
- data_management/yolo_output_to_md_output.py +84 -12
- data_management/yolo_to_coco.py +38 -20
- detection/process_video.py +36 -14
- detection/pytorch_detector.py +23 -8
- detection/run_detector.py +76 -19
- detection/run_detector_batch.py +178 -63
- detection/run_inference_with_yolov5_val.py +326 -57
- detection/run_tiled_inference.py +153 -43
- detection/video_utils.py +34 -8
- md_utils/ct_utils.py +172 -1
- md_utils/md_tests.py +372 -51
- md_utils/path_utils.py +167 -39
- md_utils/process_utils.py +26 -7
- md_utils/split_locations_into_train_val.py +215 -0
- md_utils/string_utils.py +10 -0
- md_utils/url_utils.py +0 -2
- md_utils/write_html_image_list.py +9 -26
- md_visualization/plot_utils.py +12 -8
- md_visualization/visualization_utils.py +106 -7
- md_visualization/visualize_db.py +16 -8
- md_visualization/visualize_detector_output.py +208 -97
- {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/METADATA +3 -6
- {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/RECORD +98 -121
- {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
- taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
- taxonomy_mapping/map_new_lila_datasets.py +43 -39
- taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
- taxonomy_mapping/preview_lila_taxonomy.py +27 -27
- taxonomy_mapping/species_lookup.py +33 -13
- taxonomy_mapping/taxonomy_csv_checker.py +7 -5
- api/synchronous/api_core/yolov5/detect.py +0 -252
- api/synchronous/api_core/yolov5/export.py +0 -607
- api/synchronous/api_core/yolov5/hubconf.py +0 -146
- api/synchronous/api_core/yolov5/models/__init__.py +0 -0
- api/synchronous/api_core/yolov5/models/common.py +0 -738
- api/synchronous/api_core/yolov5/models/experimental.py +0 -104
- api/synchronous/api_core/yolov5/models/tf.py +0 -574
- api/synchronous/api_core/yolov5/models/yolo.py +0 -338
- api/synchronous/api_core/yolov5/train.py +0 -670
- api/synchronous/api_core/yolov5/utils/__init__.py +0 -36
- api/synchronous/api_core/yolov5/utils/activations.py +0 -103
- api/synchronous/api_core/yolov5/utils/augmentations.py +0 -284
- api/synchronous/api_core/yolov5/utils/autoanchor.py +0 -170
- api/synchronous/api_core/yolov5/utils/autobatch.py +0 -66
- api/synchronous/api_core/yolov5/utils/aws/__init__.py +0 -0
- api/synchronous/api_core/yolov5/utils/aws/resume.py +0 -40
- api/synchronous/api_core/yolov5/utils/benchmarks.py +0 -148
- api/synchronous/api_core/yolov5/utils/callbacks.py +0 -71
- api/synchronous/api_core/yolov5/utils/dataloaders.py +0 -1087
- api/synchronous/api_core/yolov5/utils/downloads.py +0 -178
- api/synchronous/api_core/yolov5/utils/flask_rest_api/example_request.py +0 -19
- api/synchronous/api_core/yolov5/utils/flask_rest_api/restapi.py +0 -46
- api/synchronous/api_core/yolov5/utils/general.py +0 -1018
- api/synchronous/api_core/yolov5/utils/loggers/__init__.py +0 -187
- api/synchronous/api_core/yolov5/utils/loggers/wandb/__init__.py +0 -0
- api/synchronous/api_core/yolov5/utils/loggers/wandb/log_dataset.py +0 -27
- api/synchronous/api_core/yolov5/utils/loggers/wandb/sweep.py +0 -41
- api/synchronous/api_core/yolov5/utils/loggers/wandb/wandb_utils.py +0 -577
- api/synchronous/api_core/yolov5/utils/loss.py +0 -234
- api/synchronous/api_core/yolov5/utils/metrics.py +0 -355
- api/synchronous/api_core/yolov5/utils/plots.py +0 -489
- api/synchronous/api_core/yolov5/utils/torch_utils.py +0 -314
- api/synchronous/api_core/yolov5/val.py +0 -394
- md_utils/matlab_porting_tools.py +0 -97
- {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
- {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
|
-
#
|
|
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
|
-
|
|
158
|
-
|
|
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.
|
|
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
|
-
|
|
73
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
|
414
|
+
# ...def yolo_txt_output_to_md_output(...)
|
|
343
415
|
|
|
344
416
|
|
|
345
417
|
#%% Interactive driver
|
data_management/yolo_to_coco.py
CHANGED
|
@@ -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
|
-
#
|
|
41
|
+
# Read class names
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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)
|
detection/process_video.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
179
|
-
|
|
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(
|
|
375
|
-
|
|
376
|
-
frames_to_video(video_frame_files,
|
|
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=
|
|
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=
|
|
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. '\
|
detection/pytorch_detector.py
CHANGED
|
@@ -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
|
|
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
|
-
# *
|
|
23
|
+
# * Supported but non-default (used for PyPI packaging):
|
|
24
24
|
#
|
|
25
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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 =
|
|
299
|
-
im_file =
|
|
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
|
|
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
|
|
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
|