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.
- api/batch_processing/data_preparation/manage_local_batch.py +297 -202
- api/batch_processing/data_preparation/manage_video_batch.py +7 -2
- api/batch_processing/postprocessing/add_max_conf.py +1 -0
- api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
- api/batch_processing/postprocessing/compare_batch_results.py +111 -61
- api/batch_processing/postprocessing/convert_output_format.py +24 -6
- api/batch_processing/postprocessing/load_api_results.py +56 -72
- api/batch_processing/postprocessing/md_to_labelme.py +119 -51
- api/batch_processing/postprocessing/merge_detections.py +30 -5
- api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
- api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -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 +224 -76
- api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
- api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
- classification/prepare_classification_script.py +191 -191
- data_management/cct_json_utils.py +7 -2
- data_management/coco_to_labelme.py +263 -0
- data_management/coco_to_yolo.py +72 -48
- data_management/databases/integrity_check_json_db.py +75 -64
- data_management/databases/subset_json_db.py +1 -1
- data_management/generate_crops_from_cct.py +1 -1
- data_management/get_image_sizes.py +44 -26
- data_management/importers/animl_results_to_md_results.py +3 -5
- data_management/importers/noaa_seals_2019.py +2 -2
- data_management/importers/zamba_results_to_md_results.py +2 -2
- data_management/labelme_to_coco.py +264 -127
- data_management/labelme_to_yolo.py +96 -53
- data_management/lila/create_lila_blank_set.py +557 -0
- data_management/lila/create_lila_test_set.py +2 -1
- data_management/lila/create_links_to_md_results_files.py +1 -1
- data_management/lila/download_lila_subset.py +138 -45
- data_management/lila/generate_lila_per_image_labels.py +23 -14
- data_management/lila/get_lila_annotation_counts.py +16 -10
- data_management/lila/lila_common.py +15 -42
- data_management/lila/test_lila_metadata_urls.py +116 -0
- data_management/read_exif.py +65 -16
- data_management/remap_coco_categories.py +84 -0
- data_management/resize_coco_dataset.py +14 -31
- data_management/wi_download_csv_to_coco.py +239 -0
- data_management/yolo_output_to_md_output.py +40 -13
- data_management/yolo_to_coco.py +313 -100
- detection/process_video.py +36 -14
- detection/pytorch_detector.py +1 -1
- detection/run_detector.py +73 -18
- detection/run_detector_batch.py +116 -27
- detection/run_inference_with_yolov5_val.py +135 -27
- detection/run_tiled_inference.py +153 -43
- detection/tf_detector.py +2 -1
- detection/video_utils.py +4 -2
- md_utils/ct_utils.py +101 -6
- md_utils/md_tests.py +264 -17
- md_utils/path_utils.py +326 -47
- 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 +66 -3
- md_utils/write_html_image_list.py +12 -2
- md_visualization/visualization_utils.py +380 -74
- md_visualization/visualize_db.py +41 -10
- md_visualization/visualize_detector_output.py +185 -104
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.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
- md_visualization/visualize_megadb.py +0 -183
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
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
|
|
|
@@ -190,18 +212,9 @@ def get_detector_version_from_filename(detector_filename):
|
|
|
190
212
|
"v4.1.0", "v5a.0.0", and "v5b.0.0", respectively.
|
|
191
213
|
"""
|
|
192
214
|
|
|
193
|
-
fn = os.path.basename(detector_filename)
|
|
194
|
-
known_model_versions = {'v2':'v2.0.0',
|
|
195
|
-
'v3':'v3.0.0',
|
|
196
|
-
'v4.1':'v4.1.0',
|
|
197
|
-
'v5a.0.0':'v5a.0.0',
|
|
198
|
-
'v5b.0.0':'v5b.0.0',
|
|
199
|
-
'MDV5A':'v5a.0.0',
|
|
200
|
-
'MDV5B':'v5b.0.0',
|
|
201
|
-
'MDV4':'v4.1.0',
|
|
202
|
-
'MDV3':'v3.0.0'}
|
|
215
|
+
fn = os.path.basename(detector_filename).lower()
|
|
203
216
|
matches = []
|
|
204
|
-
for s in
|
|
217
|
+
for s in model_string_to_model_version.keys():
|
|
205
218
|
if s in fn:
|
|
206
219
|
matches.append(s)
|
|
207
220
|
if len(matches) == 0:
|
|
@@ -211,9 +224,51 @@ def get_detector_version_from_filename(detector_filename):
|
|
|
211
224
|
print('Warning: multiple MegaDetector versions for model file {}'.format(detector_filename))
|
|
212
225
|
return 'multiple'
|
|
213
226
|
else:
|
|
214
|
-
return
|
|
227
|
+
return model_string_to_model_version[matches[0]]
|
|
215
228
|
|
|
216
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
|
+
|
|
217
272
|
def get_typical_confidence_threshold_from_results(results):
|
|
218
273
|
"""
|
|
219
274
|
Given the .json data loaded from a MD results file, determine a typical confidence
|
detection/run_detector_batch.py
CHANGED
|
@@ -245,7 +245,8 @@ def process_images(im_files, detector, confidence_threshold, use_image_queue=Fal
|
|
|
245
245
|
quiet=False, image_size=None, checkpoint_queue=None, include_image_size=False,
|
|
246
246
|
include_image_timestamp=False, include_exif_data=False):
|
|
247
247
|
"""
|
|
248
|
-
Runs MegaDetector over a list of image files.
|
|
248
|
+
Runs MegaDetector over a list of image files. As of 3/2024, this entry point is used when the
|
|
249
|
+
image queue is enabled, but not in the standard inference path (which loops over process_image()).
|
|
249
250
|
|
|
250
251
|
Args
|
|
251
252
|
- im_files: list of str, paths to image files
|
|
@@ -269,7 +270,7 @@ def process_images(im_files, detector, confidence_threshold, use_image_queue=Fal
|
|
|
269
270
|
include_image_size=include_image_size,
|
|
270
271
|
include_image_timestamp=include_image_timestamp,
|
|
271
272
|
include_exif_data=include_exif_data)
|
|
272
|
-
else:
|
|
273
|
+
else:
|
|
273
274
|
results = []
|
|
274
275
|
for im_file in im_files:
|
|
275
276
|
result = process_image(im_file, detector, confidence_threshold,
|
|
@@ -662,7 +663,7 @@ def get_image_datetime(image):
|
|
|
662
663
|
|
|
663
664
|
def write_results_to_file(results, output_file, relative_path_base=None,
|
|
664
665
|
detector_file=None, info=None, include_max_conf=False,
|
|
665
|
-
custom_metadata=None):
|
|
666
|
+
custom_metadata=None, force_forward_slashes=True):
|
|
666
667
|
"""
|
|
667
668
|
Writes list of detection results to JSON output file. Format matches:
|
|
668
669
|
|
|
@@ -692,6 +693,14 @@ def write_results_to_file(results, output_file, relative_path_base=None,
|
|
|
692
693
|
results_relative.append(r_relative)
|
|
693
694
|
results = results_relative
|
|
694
695
|
|
|
696
|
+
if force_forward_slashes:
|
|
697
|
+
results_converted = []
|
|
698
|
+
for r in results:
|
|
699
|
+
r_converted = copy.copy(r)
|
|
700
|
+
r_converted['file'] = r_converted['file'].replace('\\','/')
|
|
701
|
+
results_converted.append(r_converted)
|
|
702
|
+
results = results_converted
|
|
703
|
+
|
|
695
704
|
# The typical case: we need to build the 'info' struct
|
|
696
705
|
if info is None:
|
|
697
706
|
|
|
@@ -751,17 +760,75 @@ if False:
|
|
|
751
760
|
|
|
752
761
|
#%%
|
|
753
762
|
|
|
763
|
+
model_file = 'MDV5A'
|
|
764
|
+
image_dir = r'g:\camera_traps\camera_trap_images'
|
|
765
|
+
output_file = r'g:\temp\md-test.json'
|
|
766
|
+
|
|
767
|
+
recursive = True
|
|
768
|
+
output_relative_filenames = True
|
|
769
|
+
include_max_conf = False
|
|
770
|
+
quiet = True
|
|
771
|
+
image_size = None
|
|
772
|
+
use_image_queue = False
|
|
773
|
+
confidence_threshold = 0.0001
|
|
774
|
+
checkpoint_frequency = 5
|
|
754
775
|
checkpoint_path = None
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
checkpoint_frequency = -1
|
|
758
|
-
results = None
|
|
776
|
+
resume_from_checkpoint = 'auto'
|
|
777
|
+
allow_checkpoint_overwrite = False
|
|
759
778
|
ncores = 1
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
779
|
+
class_mapping_filename = None
|
|
780
|
+
include_image_size = True
|
|
781
|
+
include_image_timestamp = True
|
|
782
|
+
include_exif_data = True
|
|
783
|
+
overwrite_handling = None
|
|
784
|
+
|
|
785
|
+
# Generate a command line
|
|
786
|
+
cmd = 'python run_detector_batch.py "{}" "{}" "{}"'.format(
|
|
787
|
+
model_file,image_dir,output_file)
|
|
788
|
+
|
|
789
|
+
if recursive:
|
|
790
|
+
cmd += ' --recursive'
|
|
791
|
+
if output_relative_filenames:
|
|
792
|
+
cmd += ' --output_relative_filenames'
|
|
793
|
+
if include_max_conf:
|
|
794
|
+
cmd += ' --include_max_conf'
|
|
795
|
+
if quiet:
|
|
796
|
+
cmd += ' --quiet'
|
|
797
|
+
if image_size is not None:
|
|
798
|
+
cmd += ' --image_size {}'.format(image_size)
|
|
799
|
+
if use_image_queue:
|
|
800
|
+
cmd += ' --use_image_queue'
|
|
801
|
+
if confidence_threshold is not None:
|
|
802
|
+
cmd += ' --threshold {}'.format(confidence_threshold)
|
|
803
|
+
if checkpoint_frequency is not None:
|
|
804
|
+
cmd += ' --checkpoint_frequency {}'.format(checkpoint_frequency)
|
|
805
|
+
if checkpoint_path is not None:
|
|
806
|
+
cmd += ' --checkpoint_path "{}"'.format(checkpoint_path)
|
|
807
|
+
if resume_from_checkpoint is not None:
|
|
808
|
+
cmd += ' --resume_from_checkpoint "{}"'.format(resume_from_checkpoint)
|
|
809
|
+
if allow_checkpoint_overwrite:
|
|
810
|
+
cmd += ' --allow_checkpoint_overwrite'
|
|
811
|
+
if ncores is not None:
|
|
812
|
+
cmd += ' --ncores {}'.format(ncores)
|
|
813
|
+
if class_mapping_filename is not None:
|
|
814
|
+
cmd += ' --class_mapping_filename "{}"'.format(class_mapping_filename)
|
|
815
|
+
if include_image_size:
|
|
816
|
+
cmd += ' --include_image_size'
|
|
817
|
+
if include_image_timestamp:
|
|
818
|
+
cmd += ' --include_image_timestamp'
|
|
819
|
+
if include_exif_data:
|
|
820
|
+
cmd += ' --include_exif_data'
|
|
821
|
+
if overwrite_handling is not None:
|
|
822
|
+
cmd += ' --overwrite_handling {}'.format(overwrite_handling)
|
|
823
|
+
|
|
824
|
+
print(cmd)
|
|
825
|
+
import clipboard; clipboard.copy(cmd)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
#%% Run inference interactively
|
|
829
|
+
|
|
764
830
|
image_file_names = path_utils.find_images(image_dir, recursive=False)
|
|
831
|
+
results = None
|
|
765
832
|
|
|
766
833
|
start_time = time.time()
|
|
767
834
|
|
|
@@ -840,12 +907,15 @@ def main():
|
|
|
840
907
|
'--checkpoint_path',
|
|
841
908
|
type=str,
|
|
842
909
|
default=None,
|
|
843
|
-
help='File name to which checkpoints will be written if checkpoint_frequency is > 0'
|
|
910
|
+
help='File name to which checkpoints will be written if checkpoint_frequency is > 0, ' + \
|
|
911
|
+
'defaults to md_checkpoint_[date].json in the same folder as the output file')
|
|
844
912
|
parser.add_argument(
|
|
845
913
|
'--resume_from_checkpoint',
|
|
846
914
|
type=str,
|
|
847
915
|
default=None,
|
|
848
|
-
help='Path to a JSON checkpoint file to resume from'
|
|
916
|
+
help='Path to a JSON checkpoint file to resume from, or "auto" to ' + \
|
|
917
|
+
'find the most recent checkpoint in the same folder as the output file. "auto" uses' + \
|
|
918
|
+
'checkpoint_path (rather than searching the output folder) if checkpoint_path is specified.')
|
|
849
919
|
parser.add_argument(
|
|
850
920
|
'--allow_checkpoint_overwrite',
|
|
851
921
|
action='store_true',
|
|
@@ -897,7 +967,7 @@ def main():
|
|
|
897
967
|
|
|
898
968
|
assert os.path.exists(args.detector_file), \
|
|
899
969
|
'detector file {} does not exist'.format(args.detector_file)
|
|
900
|
-
assert 0.0
|
|
970
|
+
assert 0.0 <= args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
|
|
901
971
|
assert args.output_file.endswith('.json'), 'output_file specified needs to end with .json'
|
|
902
972
|
if args.checkpoint_frequency != -1:
|
|
903
973
|
assert args.checkpoint_frequency > 0, 'Checkpoint_frequency needs to be > 0 or == -1'
|
|
@@ -919,19 +989,42 @@ def main():
|
|
|
919
989
|
else:
|
|
920
990
|
raise ValueError('Illegal overwrite handling string {}'.format(args.overwrite_handling))
|
|
921
991
|
|
|
992
|
+
output_dir = os.path.dirname(args.output_file)
|
|
993
|
+
|
|
994
|
+
if len(output_dir) > 0:
|
|
995
|
+
os.makedirs(output_dir,exist_ok=True)
|
|
996
|
+
|
|
997
|
+
assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
|
|
998
|
+
|
|
922
999
|
if args.class_mapping_filename is not None:
|
|
923
1000
|
load_custom_class_mapping(args.class_mapping_filename)
|
|
924
|
-
|
|
1001
|
+
|
|
925
1002
|
# Load the checkpoint if available
|
|
926
1003
|
#
|
|
927
1004
|
# Relative file names are only output at the end; all file paths in the checkpoint are
|
|
928
|
-
# still
|
|
1005
|
+
# still absolute paths.
|
|
929
1006
|
if args.resume_from_checkpoint is not None:
|
|
930
|
-
|
|
1007
|
+
if args.resume_from_checkpoint == 'auto':
|
|
1008
|
+
checkpoint_files = os.listdir(output_dir)
|
|
1009
|
+
checkpoint_files = [fn for fn in checkpoint_files if \
|
|
1010
|
+
(fn.startswith('md_checkpoint') and fn.endswith('.json'))]
|
|
1011
|
+
if len(checkpoint_files) == 0:
|
|
1012
|
+
raise ValueError('resume_from_checkpoint set to "auto", but no checkpoints found in {}'.format(
|
|
1013
|
+
output_dir))
|
|
1014
|
+
else:
|
|
1015
|
+
if len(checkpoint_files) > 1:
|
|
1016
|
+
print('Warning: found {} checkpoints in {}, using the latest'.format(
|
|
1017
|
+
len(checkpoint_files),output_dir))
|
|
1018
|
+
checkpoint_files = sorted(checkpoint_files)
|
|
1019
|
+
checkpoint_file_relative = checkpoint_files[-1]
|
|
1020
|
+
checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
|
|
1021
|
+
else:
|
|
1022
|
+
checkpoint_file = args.resume_from_checkpoint
|
|
1023
|
+
assert os.path.exists(checkpoint_file), \
|
|
931
1024
|
'File at resume_from_checkpoint specified does not exist'
|
|
932
|
-
with open(
|
|
1025
|
+
with open(checkpoint_file) as f:
|
|
933
1026
|
print('Loading previous results from checkpoint file {}'.format(
|
|
934
|
-
|
|
1027
|
+
checkpoint_file))
|
|
935
1028
|
saved = json.load(f)
|
|
936
1029
|
assert 'images' in saved, \
|
|
937
1030
|
'The checkpoint file does not have the correct fields; cannot be restored'
|
|
@@ -982,13 +1075,6 @@ def main():
|
|
|
982
1075
|
assert os.path.exists(image_file_names[0]), \
|
|
983
1076
|
'The first image to be processed does not exist at {}'.format(image_file_names[0])
|
|
984
1077
|
|
|
985
|
-
output_dir = os.path.dirname(args.output_file)
|
|
986
|
-
|
|
987
|
-
if len(output_dir) > 0:
|
|
988
|
-
os.makedirs(output_dir,exist_ok=True)
|
|
989
|
-
|
|
990
|
-
assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
|
|
991
|
-
|
|
992
1078
|
# Test that we can write to the output_file's dir if checkpointing requested
|
|
993
1079
|
if args.checkpoint_frequency != -1:
|
|
994
1080
|
|
|
@@ -996,7 +1082,7 @@ def main():
|
|
|
996
1082
|
checkpoint_path = args.checkpoint_path
|
|
997
1083
|
else:
|
|
998
1084
|
checkpoint_path = os.path.join(output_dir,
|
|
999
|
-
'
|
|
1085
|
+
'md_checkpoint_{}.json'.format(
|
|
1000
1086
|
datetime.utcnow().strftime("%Y%m%d%H%M%S")))
|
|
1001
1087
|
|
|
1002
1088
|
# Don't overwrite existing checkpoint files, this is a sure-fire way to eventually
|
|
@@ -1023,6 +1109,9 @@ def main():
|
|
|
1023
1109
|
|
|
1024
1110
|
else:
|
|
1025
1111
|
|
|
1112
|
+
if args.checkpoint_path is not None:
|
|
1113
|
+
print('Warning: checkpointing disabled because checkpoint_frequency is -1, ' + \
|
|
1114
|
+
'but a checkpoint path was specified')
|
|
1026
1115
|
checkpoint_path = None
|
|
1027
1116
|
|
|
1028
1117
|
start_time = time.time()
|
|
@@ -49,6 +49,7 @@ from tqdm import tqdm
|
|
|
49
49
|
|
|
50
50
|
from md_utils import path_utils
|
|
51
51
|
from md_utils import process_utils
|
|
52
|
+
from md_utils import string_utils
|
|
52
53
|
from data_management import yolo_output_to_md_output
|
|
53
54
|
from detection.run_detector import try_download_known_detector
|
|
54
55
|
|
|
@@ -68,17 +69,20 @@ class YoloInferenceOptions:
|
|
|
68
69
|
|
|
69
70
|
## Optional ##
|
|
70
71
|
|
|
71
|
-
# Required for YOLOv5
|
|
72
|
+
# Required for older YOLOv5 inference, not for newer ulytralytics inference
|
|
72
73
|
yolo_working_folder = None
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
# Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
|
|
76
|
+
# "the yolov5 repo" and "the ultralytics repo" (typically YOLOv8).
|
|
77
|
+
model_type = 'yolov5'
|
|
75
78
|
|
|
76
79
|
image_size = default_image_size_with_augmentation
|
|
77
80
|
conf_thres = '0.001'
|
|
78
81
|
batch_size = 1
|
|
79
82
|
device_string = '0'
|
|
80
83
|
augment = True
|
|
81
|
-
|
|
84
|
+
half_precision_enabled = None
|
|
85
|
+
|
|
82
86
|
symlink_folder = None
|
|
83
87
|
use_symlinks = True
|
|
84
88
|
|
|
@@ -97,16 +101,30 @@ class YoloInferenceOptions:
|
|
|
97
101
|
overwrite_handling = 'skip'
|
|
98
102
|
|
|
99
103
|
preview_yolo_command_only = False
|
|
104
|
+
|
|
105
|
+
treat_copy_failures_as_warnings = False
|
|
106
|
+
|
|
107
|
+
save_yolo_debug_output = False
|
|
108
|
+
|
|
109
|
+
recursive = True
|
|
100
110
|
|
|
101
111
|
|
|
102
112
|
#%% Main function
|
|
103
113
|
|
|
104
114
|
def run_inference_with_yolo_val(options):
|
|
105
115
|
|
|
106
|
-
##%%
|
|
116
|
+
##%% Input and path handling
|
|
117
|
+
|
|
118
|
+
if options.model_type == 'yolov8':
|
|
119
|
+
|
|
120
|
+
print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
|
|
121
|
+
options.model_type = 'ultralytics'
|
|
122
|
+
|
|
123
|
+
if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
|
|
124
|
+
print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"... did you mean to use --model_type yolov8?" ***\n\n')
|
|
107
125
|
|
|
108
126
|
if options.yolo_working_folder is None:
|
|
109
|
-
assert options.model_type == '
|
|
127
|
+
assert options.model_type == 'ultralytics', \
|
|
110
128
|
'A working folder is required to run YOLOv5 val.py'
|
|
111
129
|
else:
|
|
112
130
|
assert os.path.isdir(options.yolo_working_folder), \
|
|
@@ -115,6 +133,11 @@ def run_inference_with_yolo_val(options):
|
|
|
115
133
|
assert os.path.isdir(options.input_folder) or os.path.isfile(options.input_folder), \
|
|
116
134
|
'Could not find input {}'.format(options.input_folder)
|
|
117
135
|
|
|
136
|
+
if options.half_precision_enabled is not None:
|
|
137
|
+
assert options.half_precision_enabled in (0,1), \
|
|
138
|
+
'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
|
|
139
|
+
options.half_precision_enabled)
|
|
140
|
+
|
|
118
141
|
# If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
|
|
119
142
|
model_filename = try_download_known_detector(options.model_filename)
|
|
120
143
|
|
|
@@ -182,7 +205,7 @@ def run_inference_with_yolo_val(options):
|
|
|
182
205
|
##%% Enumerate images
|
|
183
206
|
|
|
184
207
|
if os.path.isdir(options.input_folder):
|
|
185
|
-
image_files_absolute = path_utils.find_images(options.input_folder,recursive=
|
|
208
|
+
image_files_absolute = path_utils.find_images(options.input_folder,recursive=options.recursive)
|
|
186
209
|
else:
|
|
187
210
|
assert os.path.isfile(options.input_folder)
|
|
188
211
|
with open(options.input_folder,'r') as f:
|
|
@@ -218,10 +241,20 @@ def run_inference_with_yolo_val(options):
|
|
|
218
241
|
else:
|
|
219
242
|
shutil.copyfile(image_fn,symlink_full_path)
|
|
220
243
|
except Exception as e:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
error_string = str(e)
|
|
245
|
+
image_id_to_error[image_id] = error_string
|
|
246
|
+
# Always break if the user is trying to create symlinks on Windows without
|
|
247
|
+
# permission, 100% of images will always fail in this case.
|
|
248
|
+
if ('a required privilege is not held by the client' in error_string.lower()) or \
|
|
249
|
+
(not options.treat_copy_failures_as_warnings):
|
|
250
|
+
print('\nError copying/creating link for input file {}: {}'.format(
|
|
251
|
+
image_fn,error_string))
|
|
252
|
+
|
|
253
|
+
raise
|
|
254
|
+
else:
|
|
255
|
+
print('Warning: error copying/creating link for input file {}: {}'.format(
|
|
256
|
+
image_fn,error_string))
|
|
257
|
+
continue
|
|
225
258
|
|
|
226
259
|
# ...for each image
|
|
227
260
|
|
|
@@ -270,17 +303,34 @@ def run_inference_with_yolo_val(options):
|
|
|
270
303
|
if options.augment:
|
|
271
304
|
cmd += ' --augment'
|
|
272
305
|
|
|
273
|
-
|
|
306
|
+
# --half is a store_true argument for YOLOv5's val.py
|
|
307
|
+
if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
|
|
308
|
+
cmd += ' --half'
|
|
274
309
|
|
|
310
|
+
# Sometimes useful for debugging
|
|
311
|
+
# cmd += ' --save_conf --save_txt'
|
|
312
|
+
|
|
313
|
+
elif options.model_type == 'ultralytics':
|
|
314
|
+
|
|
275
315
|
if options.augment:
|
|
276
316
|
augment_string = 'augment'
|
|
277
317
|
else:
|
|
278
318
|
augment_string = ''
|
|
279
319
|
|
|
280
|
-
cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}"'
|
|
281
|
-
augment_string,model_filename,image_size_string,options.batch_size,
|
|
282
|
-
|
|
283
|
-
cmd += '
|
|
320
|
+
cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
|
|
321
|
+
format(augment_string,model_filename,image_size_string,options.batch_size,
|
|
322
|
+
yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
|
|
323
|
+
cmd += ' save_json exist_ok'
|
|
324
|
+
|
|
325
|
+
if (options.half_precision_enabled is not None):
|
|
326
|
+
if options.half_precision_enabled == 1:
|
|
327
|
+
cmd += ' --half=True'
|
|
328
|
+
else:
|
|
329
|
+
assert options.half_precision_enabled == 0
|
|
330
|
+
cmd += ' --half=False'
|
|
331
|
+
|
|
332
|
+
# Sometimes useful for debugging
|
|
333
|
+
# cmd += ' save_conf save_txt'
|
|
284
334
|
|
|
285
335
|
else:
|
|
286
336
|
|
|
@@ -293,38 +343,84 @@ def run_inference_with_yolo_val(options):
|
|
|
293
343
|
|
|
294
344
|
if options.yolo_working_folder is not None:
|
|
295
345
|
current_dir = os.getcwd()
|
|
296
|
-
os.chdir(options.yolo_working_folder)
|
|
346
|
+
os.chdir(options.yolo_working_folder)
|
|
347
|
+
|
|
297
348
|
print('Running YOLO inference command:\n{}\n'.format(cmd))
|
|
298
349
|
|
|
299
350
|
if options.preview_yolo_command_only:
|
|
351
|
+
|
|
300
352
|
if options.remove_symlink_folder:
|
|
301
353
|
try:
|
|
354
|
+
print('Removing YOLO symlink folder {}'.format(symlink_folder))
|
|
302
355
|
shutil.rmtree(symlink_folder)
|
|
303
356
|
except Exception:
|
|
304
357
|
print('Warning: error removing symlink folder {}'.format(symlink_folder))
|
|
305
358
|
pass
|
|
306
359
|
if options.remove_yolo_results_folder:
|
|
307
360
|
try:
|
|
361
|
+
print('Removing YOLO results folder {}'.format(yolo_results_folder))
|
|
308
362
|
shutil.rmtree(yolo_results_folder)
|
|
309
363
|
except Exception:
|
|
310
364
|
print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
|
|
311
365
|
pass
|
|
312
366
|
|
|
313
367
|
sys.exit()
|
|
314
|
-
|
|
315
|
-
execution_result = process_utils.execute_and_print(cmd)
|
|
368
|
+
|
|
369
|
+
execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
|
|
316
370
|
assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
|
|
317
371
|
yolo_console_output = execution_result['output']
|
|
372
|
+
|
|
373
|
+
if options.save_yolo_debug_output:
|
|
374
|
+
with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w') as f:
|
|
375
|
+
for s in yolo_console_output:
|
|
376
|
+
f.write(s + '\n')
|
|
377
|
+
with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
|
|
378
|
+
json.dump(image_id_to_file,f,indent=1)
|
|
379
|
+
with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
|
|
380
|
+
json.dump(image_id_to_error,f,indent=1)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
|
|
384
|
+
yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
|
|
318
385
|
|
|
386
|
+
# Find errors that occurred during the initial corruption check; these will not be included in the
|
|
387
|
+
# output. Errors that occur during inference will be handled separately.
|
|
319
388
|
yolo_read_failures = []
|
|
389
|
+
|
|
320
390
|
for line in yolo_console_output:
|
|
391
|
+
# Lines look like:
|
|
392
|
+
#
|
|
393
|
+
# For ultralytics val:
|
|
394
|
+
#
|
|
395
|
+
# val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'
|
|
396
|
+
# line = "val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'"
|
|
397
|
+
#
|
|
398
|
+
# For yolov5 val.py:
|
|
399
|
+
#
|
|
400
|
+
# test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'
|
|
401
|
+
# line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
|
|
321
402
|
if 'cannot identify image file' in line:
|
|
322
403
|
tokens = line.split('cannot identify image file')
|
|
323
404
|
image_name = tokens[-1].strip()
|
|
324
405
|
assert image_name[0] == "'" and image_name [-1] == "'"
|
|
325
406
|
image_name = image_name[1:-1]
|
|
326
407
|
yolo_read_failures.append(image_name)
|
|
327
|
-
|
|
408
|
+
elif 'ignoring corrupt image/label' in line:
|
|
409
|
+
assert 'WARNING' in line
|
|
410
|
+
if '⚠️' in line:
|
|
411
|
+
assert line.startswith('val'), \
|
|
412
|
+
'Unrecognized line in YOLO output: {}'.format(line)
|
|
413
|
+
tokens = line.split('ignoring corrupt image/label')
|
|
414
|
+
image_name = tokens[0].split('⚠️')[-1].strip()
|
|
415
|
+
else:
|
|
416
|
+
assert line.startswith('test'), \
|
|
417
|
+
'Unrecognized line in YOLO output: {}'.format(line)
|
|
418
|
+
tokens = line.split('ignoring corrupt image/label')
|
|
419
|
+
image_name = tokens[0].split('WARNING:')[-1].strip()
|
|
420
|
+
assert image_name.endswith(':')
|
|
421
|
+
image_name = image_name[0:-1]
|
|
422
|
+
yolo_read_failures.append(image_name)
|
|
423
|
+
|
|
328
424
|
# image_file = yolo_read_failures[0]
|
|
329
425
|
for image_file in yolo_read_failures:
|
|
330
426
|
image_id = os.path.splitext(os.path.basename(image_file))[0]
|
|
@@ -338,7 +434,7 @@ def run_inference_with_yolo_val(options):
|
|
|
338
434
|
|
|
339
435
|
##%% Convert results to MD format
|
|
340
436
|
|
|
341
|
-
json_files = glob.glob(yolo_results_folder+ '/yolo_results/*.json')
|
|
437
|
+
json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
|
|
342
438
|
assert len(json_files) == 1
|
|
343
439
|
yolo_json_file = json_files[0]
|
|
344
440
|
|
|
@@ -390,7 +486,7 @@ def run_inference_with_yolo_val(options):
|
|
|
390
486
|
|
|
391
487
|
#%% Command-line driver
|
|
392
488
|
|
|
393
|
-
import argparse
|
|
489
|
+
import argparse
|
|
394
490
|
from md_utils.ct_utils import args_to_object
|
|
395
491
|
|
|
396
492
|
def main():
|
|
@@ -422,9 +518,12 @@ def main():
|
|
|
422
518
|
parser.add_argument(
|
|
423
519
|
'--batch_size', default=options.batch_size, type=int,
|
|
424
520
|
help='inference batch size (default {})'.format(options.batch_size))
|
|
521
|
+
parser.add_argument(
|
|
522
|
+
'--half_precision_enabled', default=None, type=int,
|
|
523
|
+
help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably full for YOLOv8 and half for YOLOv5')
|
|
425
524
|
parser.add_argument(
|
|
426
525
|
'--device_string', default=options.device_string, type=str,
|
|
427
|
-
help='CUDA device specifier,
|
|
526
|
+
help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(options.device_string))
|
|
428
527
|
parser.add_argument(
|
|
429
528
|
'--overwrite_handling', default=options.overwrite_handling, type=str,
|
|
430
529
|
help='action to take if the output file exists (skip, error, overwrite) (default {})'.format(
|
|
@@ -435,7 +534,7 @@ def main():
|
|
|
435
534
|
'(otherwise defaults to MD categories)')
|
|
436
535
|
parser.add_argument(
|
|
437
536
|
'--model_type', default=options.model_type, type=str,
|
|
438
|
-
help='Model type (yolov5 or yolov8) (default {})'.format(options.model_type))
|
|
537
|
+
help='Model type ("yolov5" or "ultralytics" ("yolov8" behaves the same as "ultralytics")) (default {})'.format(options.model_type))
|
|
439
538
|
|
|
440
539
|
parser.add_argument(
|
|
441
540
|
'--symlink_folder', type=str,
|
|
@@ -452,6 +551,13 @@ def main():
|
|
|
452
551
|
parser.add_argument(
|
|
453
552
|
'--no_remove_yolo_results_folder', action='store_true',
|
|
454
553
|
help='don\'t remove the temporary folder full of YOLO intermediate files')
|
|
554
|
+
parser.add_argument(
|
|
555
|
+
'--save_yolo_debug_output', action='store_true',
|
|
556
|
+
help='write yolo console output to a text file in the results folder, along with additional debug files')
|
|
557
|
+
|
|
558
|
+
parser.add_argument(
|
|
559
|
+
'--nonrecursive', action='store_true',
|
|
560
|
+
help='Disable recursive folder processing')
|
|
455
561
|
|
|
456
562
|
parser.add_argument(
|
|
457
563
|
'--preview_yolo_command_only', action='store_true',
|
|
@@ -474,14 +580,15 @@ def main():
|
|
|
474
580
|
|
|
475
581
|
# If the caller hasn't specified an image size, choose one based on whether augmentation
|
|
476
582
|
# is enabled.
|
|
477
|
-
if args.image_size is None:
|
|
478
|
-
assert
|
|
479
|
-
|
|
583
|
+
if args.image_size is None:
|
|
584
|
+
assert args.augment_enabled in (0,1), \
|
|
585
|
+
'Illegal augment_enabled value {}'.format(args.augment_enabled)
|
|
586
|
+
if args.augment_enabled == 1:
|
|
480
587
|
args.image_size = default_image_size_with_augmentation
|
|
481
588
|
else:
|
|
482
589
|
args.image_size = default_image_size_with_no_augmentation
|
|
483
590
|
augment_enabled_string = 'enabled'
|
|
484
|
-
if not
|
|
591
|
+
if not args.augment_enabled:
|
|
485
592
|
augment_enabled_string = 'disabled'
|
|
486
593
|
print('Augmentation is {}, using default image size {}'.format(
|
|
487
594
|
augment_enabled_string,args.image_size))
|
|
@@ -491,6 +598,7 @@ def main():
|
|
|
491
598
|
if args.yolo_dataset_file is not None:
|
|
492
599
|
options.yolo_category_id_to_name = args.yolo_dataset_file
|
|
493
600
|
|
|
601
|
+
options.recursive = (not options.nonrecursive)
|
|
494
602
|
options.remove_symlink_folder = (not options.no_remove_symlink_folder)
|
|
495
603
|
options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
|
|
496
604
|
options.use_symlinks = (not options.no_use_symlinks)
|