megadetector 5.0.27__py3-none-any.whl → 5.0.29__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.
- megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
- megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
- megadetector/classification/aggregate_classifier_probs.py +3 -3
- megadetector/classification/analyze_failed_images.py +5 -5
- megadetector/classification/cache_batchapi_outputs.py +5 -5
- megadetector/classification/create_classification_dataset.py +11 -12
- megadetector/classification/crop_detections.py +10 -10
- megadetector/classification/csv_to_json.py +8 -8
- megadetector/classification/detect_and_crop.py +13 -15
- megadetector/classification/evaluate_model.py +7 -7
- megadetector/classification/identify_mislabeled_candidates.py +6 -6
- megadetector/classification/json_to_azcopy_list.py +1 -1
- megadetector/classification/json_validator.py +29 -32
- megadetector/classification/map_classification_categories.py +9 -9
- megadetector/classification/merge_classification_detection_output.py +12 -9
- megadetector/classification/prepare_classification_script.py +19 -19
- megadetector/classification/prepare_classification_script_mc.py +23 -23
- megadetector/classification/run_classifier.py +4 -4
- megadetector/classification/save_mislabeled.py +6 -6
- megadetector/classification/train_classifier.py +1 -1
- megadetector/classification/train_classifier_tf.py +9 -9
- megadetector/classification/train_utils.py +10 -10
- megadetector/data_management/annotations/annotation_constants.py +1 -1
- megadetector/data_management/camtrap_dp_to_coco.py +45 -45
- megadetector/data_management/cct_json_utils.py +101 -101
- megadetector/data_management/cct_to_md.py +49 -49
- megadetector/data_management/cct_to_wi.py +33 -33
- megadetector/data_management/coco_to_labelme.py +75 -75
- megadetector/data_management/coco_to_yolo.py +189 -189
- megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
- megadetector/data_management/databases/integrity_check_json_db.py +202 -188
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +38 -38
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +130 -124
- megadetector/data_management/labelme_to_yolo.py +78 -72
- megadetector/data_management/lila/create_lila_blank_set.py +81 -83
- megadetector/data_management/lila/create_lila_test_set.py +32 -31
- megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
- megadetector/data_management/lila/download_lila_subset.py +21 -24
- megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
- megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +70 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
- megadetector/data_management/mewc_to_md.py +339 -340
- megadetector/data_management/ocr_tools.py +258 -252
- megadetector/data_management/read_exif.py +232 -223
- megadetector/data_management/remap_coco_categories.py +26 -26
- megadetector/data_management/remove_exif.py +31 -20
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +41 -41
- megadetector/data_management/speciesnet_to_md.py +41 -41
- megadetector/data_management/wi_download_csv_to_coco.py +55 -55
- megadetector/data_management/yolo_output_to_md_output.py +117 -120
- megadetector/data_management/yolo_to_coco.py +195 -188
- megadetector/detection/change_detection.py +831 -0
- megadetector/detection/process_video.py +341 -338
- megadetector/detection/pytorch_detector.py +308 -266
- megadetector/detection/run_detector.py +186 -166
- megadetector/detection/run_detector_batch.py +366 -364
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +312 -253
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +291 -283
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +808 -311
- megadetector/postprocessing/combine_batch_outputs.py +20 -21
- megadetector/postprocessing/compare_batch_results.py +528 -517
- megadetector/postprocessing/convert_output_format.py +97 -97
- megadetector/postprocessing/create_crop_folder.py +220 -147
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -0
- megadetector/postprocessing/load_api_results.py +25 -22
- megadetector/postprocessing/md_to_coco.py +129 -98
- megadetector/postprocessing/md_to_labelme.py +89 -83
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +87 -114
- megadetector/postprocessing/postprocess_batch_results.py +319 -302
- megadetector/postprocessing/remap_detection_categories.py +36 -36
- megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
- megadetector/postprocessing/separate_detections_into_folders.py +226 -211
- megadetector/postprocessing/subset_json_detector_output.py +265 -262
- megadetector/postprocessing/top_folders_to_bottom.py +45 -45
- megadetector/postprocessing/validate_batch_results.py +70 -70
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -69
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +33 -33
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
- megadetector/taxonomy_mapping/taxonomy_graph.py +11 -11
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1019 -200
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +541 -518
- megadetector/utils/path_utils.py +1511 -406
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +73 -60
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2868
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +21 -21
- megadetector/visualization/render_images_with_thumbnails.py +37 -73
- megadetector/visualization/visualization_utils.py +424 -404
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +126 -98
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- megadetector/data_management/importers/add_nacti_sizes.py +0 -52
- megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
- megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
- megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
- megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
- megadetector/data_management/importers/awc_to_json.py +0 -191
- megadetector/data_management/importers/bellevue_to_json.py +0 -272
- megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
- megadetector/data_management/importers/cct_field_adjustments.py +0 -58
- megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
- megadetector/data_management/importers/ena24_to_json.py +0 -276
- megadetector/data_management/importers/filenames_to_json.py +0 -386
- megadetector/data_management/importers/helena_to_cct.py +0 -283
- megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
- megadetector/data_management/importers/jb_csv_to_json.py +0 -150
- megadetector/data_management/importers/mcgill_to_json.py +0 -250
- megadetector/data_management/importers/missouri_to_json.py +0 -490
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
- megadetector/data_management/importers/noaa_seals_2019.py +0 -181
- megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
- megadetector/data_management/importers/pc_to_json.py +0 -365
- megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
- megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
- megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
- megadetector/data_management/importers/rspb_to_json.py +0 -356
- megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
- megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
- megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
- megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
- megadetector/data_management/importers/sulross_get_exif.py +0 -65
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
- megadetector/data_management/importers/ubc_to_json.py +0 -399
- megadetector/data_management/importers/umn_to_json.py +0 -507
- megadetector/data_management/importers/wellington_to_json.py +0 -263
- megadetector/data_management/importers/wi_to_json.py +0 -442
- megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
- megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
- megadetector-5.0.27.dist-info/RECORD +0 -208
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
|
@@ -5,30 +5,30 @@ run_inference_with_yolov5_val.py
|
|
|
5
5
|
Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
|
|
6
6
|
val.py, converting the output to the standard MD format. The reasons this script exists,
|
|
7
7
|
as an alternative to the standard run_detector_batch.py are:
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
* This script provides access to YOLO's test-time augmentation tools.
|
|
10
10
|
* This script serves a reference implementation: by any reasonable definition, YOLOv5's
|
|
11
|
-
val.py produces the "correct" result for any image, since it matches what was used in
|
|
11
|
+
val.py produces the "correct" result for any image, since it matches what was used in
|
|
12
12
|
training.
|
|
13
|
-
* This script works for any Ultralytics detection model, including YOLOv8 models
|
|
13
|
+
* This script works for any Ultralytics detection model, including YOLOv8 models
|
|
14
14
|
|
|
15
|
-
YOLOv5's val.py uses each file's base name as a unique identifier, which doesn't work
|
|
15
|
+
YOLOv5's val.py uses each file's base name as a unique identifier, which doesn't work
|
|
16
16
|
when you have typical camera trap images like:
|
|
17
17
|
|
|
18
18
|
* a/b/c/RECONYX0001.JPG
|
|
19
19
|
* d/e/f/RECONYX0001.JPG
|
|
20
20
|
|
|
21
|
-
...both of which would just be "RECONYX0001.JPG". So this script jumps through a bunch of
|
|
22
|
-
hoops to put a symlinks in a flat folder, run YOLOv5 on that folder, and map the results back
|
|
21
|
+
...both of which would just be "RECONYX0001.JPG". So this script jumps through a bunch of
|
|
22
|
+
hoops to put a symlinks in a flat folder, run YOLOv5 on that folder, and map the results back
|
|
23
23
|
to the real files.
|
|
24
24
|
|
|
25
25
|
If you are running a YOLOv5 model, this script currently requires the caller to supply the path
|
|
26
|
-
where a working YOLOv5 install lives, and assumes that the current conda environment is all set up for
|
|
26
|
+
where a working YOLOv5 install lives, and assumes that the current conda environment is all set up for
|
|
27
27
|
YOLOv5. If you are running a YOLOv8 model, the folder doesn't matter, but it assumes that ultralytics
|
|
28
28
|
tools are available in the current environment.
|
|
29
29
|
|
|
30
|
-
By default, this script uses symlinks to format the input images in a way that YOLO's
|
|
31
|
-
val.py likes, as per above. This requires admin privileges on Windows... actually technically this
|
|
30
|
+
By default, this script uses symlinks to format the input images in a way that YOLO's
|
|
31
|
+
val.py likes, as per above. This requires admin privileges on Windows... actually technically this
|
|
32
32
|
only requires permissions to create symbolic links, but I've never seen a case where someone has
|
|
33
33
|
that permission and *doesn't* have admin privileges. If you are running this script on
|
|
34
34
|
Windows and you don't have admin privileges, use --no_use_symlinks, which will make copies of images,
|
|
@@ -46,14 +46,17 @@ import tempfile
|
|
|
46
46
|
import shutil
|
|
47
47
|
import json
|
|
48
48
|
import copy
|
|
49
|
+
import argparse
|
|
49
50
|
|
|
50
51
|
from tqdm import tqdm
|
|
51
52
|
|
|
52
53
|
from megadetector.utils import path_utils
|
|
53
54
|
from megadetector.utils import process_utils
|
|
54
55
|
from megadetector.utils import string_utils
|
|
56
|
+
from megadetector.utils.ct_utils import args_to_object
|
|
55
57
|
|
|
56
58
|
from megadetector.utils.ct_utils import is_iterable, split_list_into_fixed_size_chunks
|
|
59
|
+
from megadetector.utils import ct_utils
|
|
57
60
|
from megadetector.utils.path_utils import path_is_abs
|
|
58
61
|
from megadetector.data_management import yolo_output_to_md_output
|
|
59
62
|
from megadetector.detection.run_detector import try_download_known_detector
|
|
@@ -67,129 +70,129 @@ default_image_size_with_no_augmentation = 1280
|
|
|
67
70
|
|
|
68
71
|
class YoloInferenceOptions:
|
|
69
72
|
"""
|
|
70
|
-
Parameters that control the behavior of run_inference_with_yolov5_val(), including
|
|
73
|
+
Parameters that control the behavior of run_inference_with_yolov5_val(), including
|
|
71
74
|
the input/output filenames.
|
|
72
75
|
"""
|
|
73
|
-
|
|
76
|
+
|
|
74
77
|
def __init__(self):
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
## Required-ish ##
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
#: Folder of images to process (can be None if image_filename_list contains absolute paths)
|
|
79
82
|
self.input_folder = None
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
#: If this is None, [input_folder] can't be None, we'll process all images in [input_folder].
|
|
82
|
-
#:
|
|
83
|
-
#: If this is not None, and [input_folder] is not None, this should be a list of relative image
|
|
84
|
-
#: paths within [input_folder] to process, or a .txt or .json file containing a list of
|
|
85
|
+
#:
|
|
86
|
+
#: If this is not None, and [input_folder] is not None, this should be a list of relative image
|
|
87
|
+
#: paths within [input_folder] to process, or a .txt or .json file containing a list of
|
|
85
88
|
#: relative image paths.
|
|
86
89
|
#:
|
|
87
90
|
#: If this is not None, and [input_folder] is None, this should be a list of absolute image
|
|
88
|
-
#: paths, or a .txt or .json file containing a list of absolute image paths.
|
|
91
|
+
#: paths, or a .txt or .json file containing a list of absolute image paths.
|
|
89
92
|
self.image_filename_list = None
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
#: Model filename (ending in .pt), or a well-known model name (e.g. "MDV5A")
|
|
92
95
|
self.model_filename = None
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
#: .json output file, in MD results format
|
|
95
98
|
self.output_file = None
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
|
|
100
|
+
|
|
98
101
|
## Optional ##
|
|
99
|
-
|
|
102
|
+
|
|
100
103
|
#: Required for older YOLOv5 inference, not for newer ulytralytics/YOLOv8 inference
|
|
101
104
|
self.yolo_working_folder = None
|
|
102
|
-
|
|
105
|
+
|
|
103
106
|
#: Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
|
|
104
107
|
#: "the yolov5 repo" and "the ultralytics repo".
|
|
105
|
-
self.model_type = 'yolov5'
|
|
106
|
-
|
|
108
|
+
self.model_type = 'yolov5'
|
|
109
|
+
|
|
107
110
|
#: Image size to use; this is a single int, which in ultralytics's terminology means
|
|
108
111
|
#: "scale the long side of the image to this size, and preserve aspect ratio".
|
|
109
112
|
#:
|
|
110
113
|
#: If None, will choose based on whether augmentation is enabled.
|
|
111
114
|
self.image_size = None
|
|
112
|
-
|
|
115
|
+
|
|
113
116
|
#: Detections below this threshold will not be included in the output file
|
|
114
117
|
self.conf_thres = '0.001'
|
|
115
|
-
|
|
118
|
+
|
|
116
119
|
#: Batch size... has no impact on results, but may create memory issues if you set
|
|
117
120
|
#: this to large values
|
|
118
121
|
self.batch_size = 1
|
|
119
|
-
|
|
122
|
+
|
|
120
123
|
#: Device string: typically '0' for GPU 0, '1' for GPU 1, etc., or 'cpu'
|
|
121
124
|
self.device_string = '0'
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
#: Should we enable test-time augmentation?
|
|
124
127
|
self.augment = False
|
|
125
|
-
|
|
128
|
+
|
|
126
129
|
#: Should we enable half-precision inference?
|
|
127
130
|
self.half_precision_enabled = None
|
|
128
|
-
|
|
129
|
-
#: Where should we stash the temporary symlinks (or copies) used to give unique identifiers to image
|
|
131
|
+
|
|
132
|
+
#: Where should we stash the temporary symlinks (or copies) used to give unique identifiers to image
|
|
130
133
|
# files?
|
|
131
134
|
#:
|
|
132
135
|
#: If this is None, we'll create a folder in system temp space.
|
|
133
136
|
self.symlink_folder = None
|
|
134
|
-
|
|
137
|
+
|
|
135
138
|
#: Should we use symlinks to give unique identifiers to image files (vs. copies)?
|
|
136
139
|
self.use_symlinks = True
|
|
137
|
-
|
|
140
|
+
|
|
138
141
|
#: How should we guarantee that YOLO IDs (base filenames) are unique? Choices are:
|
|
139
142
|
#:
|
|
140
143
|
#: * 'verify': assume image IDs are unique, but verify and error if they're not
|
|
141
144
|
#: * 'links': create symlinks (or copies, depending on use_symlinks) to enforce uniqueness
|
|
142
145
|
#: * 'auto': check whether IDs are unique, create links if necessary
|
|
143
146
|
self.unique_id_strategy = 'links'
|
|
144
|
-
|
|
147
|
+
|
|
145
148
|
#: Temporary folder to stash intermediate YOLO results.
|
|
146
149
|
#:
|
|
147
|
-
#: If this is None, we'll create a folder in system temp space.
|
|
150
|
+
#: If this is None, we'll create a folder in system temp space.
|
|
148
151
|
self.yolo_results_folder = None
|
|
149
|
-
|
|
152
|
+
|
|
150
153
|
#: Should we remove the symlink folder when we're done?
|
|
151
154
|
self.remove_symlink_folder = True
|
|
152
|
-
|
|
155
|
+
|
|
153
156
|
#: Should we remove the intermediate results folder when we're done?
|
|
154
157
|
self.remove_yolo_results_folder = True
|
|
155
|
-
|
|
158
|
+
|
|
156
159
|
#: These are deliberately offset from the standard MD categories; YOLOv5
|
|
157
160
|
#: needs categories IDs to start at 0.
|
|
158
161
|
#:
|
|
159
162
|
#: This can also be a string that points to a YOLO dataset.yaml file.
|
|
160
163
|
self.yolo_category_id_to_name = {0:'animal',1:'person',2:'vehicle'}
|
|
161
|
-
|
|
164
|
+
|
|
162
165
|
#: What should we do if the output file already exists?
|
|
163
166
|
#:
|
|
164
167
|
#: Can be 'error', 'skip', or 'overwrite'.
|
|
165
168
|
self.overwrite_handling = 'skip'
|
|
166
|
-
|
|
169
|
+
|
|
167
170
|
#: If True, we'll do a dry run that lets you preview the YOLO val command, without
|
|
168
171
|
#: actually running it.
|
|
169
172
|
self.preview_yolo_command_only = False
|
|
170
|
-
|
|
173
|
+
|
|
171
174
|
#: By default, if any errors occur while we're copying images or creating symlinks, it's
|
|
172
175
|
#: game over. If this is True, those errors become warnings, and we plow ahead.
|
|
173
176
|
self.treat_copy_failures_as_warnings = False
|
|
174
|
-
|
|
177
|
+
|
|
175
178
|
#: Save YOLO console output
|
|
176
179
|
self.save_yolo_debug_output = False
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
#: Whether to search for images recursively within [input_folder]
|
|
179
182
|
#:
|
|
180
183
|
#: Ignored if a list of files is provided.
|
|
181
184
|
self.recursive = True
|
|
182
|
-
|
|
185
|
+
|
|
183
186
|
#: Maximum number of images to run in a single chunk
|
|
184
187
|
self.checkpoint_frequency = None
|
|
185
|
-
|
|
186
|
-
#: By default, if we're creating symlinks to images, we append a unique job ID to the
|
|
188
|
+
|
|
189
|
+
#: By default, if we're creating symlinks to images, we append a unique job ID to the
|
|
187
190
|
#: symlink folder. If the caller is 100% sure that the symlink folder can be re-used
|
|
188
191
|
#: across calls, this can be set to False.
|
|
189
192
|
self.append_job_id_to_symlink_folder = True
|
|
190
|
-
|
|
193
|
+
|
|
191
194
|
# ...def __init__()
|
|
192
|
-
|
|
195
|
+
|
|
193
196
|
# ...YoloInferenceOptions()
|
|
194
197
|
|
|
195
198
|
|
|
@@ -201,13 +204,13 @@ def _clean_up_temporary_folders(options,
|
|
|
201
204
|
"""
|
|
202
205
|
Remove temporary symlink/results folders, unless the caller requested that we leave them in place.
|
|
203
206
|
"""
|
|
204
|
-
|
|
207
|
+
|
|
205
208
|
if options.remove_symlink_folder:
|
|
206
209
|
shutil.rmtree(symlink_folder)
|
|
207
210
|
elif symlink_folder_is_temp_folder:
|
|
208
211
|
print('Warning: using temporary symlink folder {}, but not removing it'.format(
|
|
209
212
|
symlink_folder))
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
if options.remove_yolo_results_folder:
|
|
212
215
|
shutil.rmtree(yolo_results_folder)
|
|
213
216
|
elif yolo_folder_is_temp_folder:
|
|
@@ -219,44 +222,44 @@ def get_stats_for_category(filename,category='all'):
|
|
|
219
222
|
"""
|
|
220
223
|
Retrieve statistics for a category from the YOLO console output
|
|
221
224
|
stored in [filenam].
|
|
222
|
-
|
|
225
|
+
|
|
223
226
|
Args:
|
|
224
227
|
filename (str): a text file containing console output from a YOLO val run
|
|
225
228
|
category (optional, str): a category name
|
|
226
|
-
|
|
229
|
+
|
|
227
230
|
Returns:
|
|
228
231
|
dict: a dict with fields n_images, n_labels, P, R, mAP50, and mAP50-95
|
|
229
232
|
"""
|
|
230
|
-
|
|
233
|
+
|
|
231
234
|
with open(filename,'r',encoding='utf-8') as f:
|
|
232
235
|
lines = f.readlines()
|
|
233
|
-
|
|
236
|
+
|
|
234
237
|
# This is just a hedge to make sure there isn't some YOLO version floating
|
|
235
238
|
# around that used different IoU thresholds in the console output.
|
|
236
239
|
found_map50 = False
|
|
237
240
|
found_map5095 = False
|
|
238
|
-
|
|
241
|
+
|
|
239
242
|
for line in lines:
|
|
240
|
-
|
|
243
|
+
|
|
241
244
|
s = line.strip()
|
|
242
|
-
|
|
245
|
+
|
|
243
246
|
if ' map50 ' in s.lower() or ' map@.5 ' in s.lower():
|
|
244
247
|
found_map50 = True
|
|
245
248
|
if 'map50-95' in s.lower() or 'map@.5:.95' in s.lower():
|
|
246
249
|
found_map5095 = True
|
|
247
|
-
|
|
250
|
+
|
|
248
251
|
if not s.startswith(category):
|
|
249
252
|
continue
|
|
250
|
-
|
|
253
|
+
|
|
251
254
|
tokens = s.split(' ')
|
|
252
255
|
tokens_filtered = list(filter(None,tokens))
|
|
253
|
-
|
|
256
|
+
|
|
254
257
|
if len(tokens_filtered) != 7:
|
|
255
258
|
continue
|
|
256
|
-
|
|
259
|
+
|
|
257
260
|
assert found_map50 and found_map5095, \
|
|
258
261
|
'Parsing error in YOLO console output file {}'.format(filename)
|
|
259
|
-
|
|
262
|
+
|
|
260
263
|
to_return = {}
|
|
261
264
|
to_return['category'] = category
|
|
262
265
|
assert category == tokens_filtered[0]
|
|
@@ -267,68 +270,70 @@ def get_stats_for_category(filename,category='all'):
|
|
|
267
270
|
to_return['mAP50'] = float(tokens_filtered[5])
|
|
268
271
|
to_return['mAP50-95'] = float(tokens_filtered[6])
|
|
269
272
|
return to_return
|
|
270
|
-
|
|
273
|
+
|
|
271
274
|
# ...for each line
|
|
272
|
-
|
|
275
|
+
|
|
273
276
|
return None
|
|
274
277
|
|
|
275
|
-
|
|
278
|
+
|
|
276
279
|
#%% Main function
|
|
277
280
|
|
|
278
281
|
def run_inference_with_yolo_val(options):
|
|
279
282
|
"""
|
|
280
283
|
Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
|
|
281
284
|
val.py, converting the output to the standard MD format.
|
|
282
|
-
|
|
283
|
-
Args:
|
|
285
|
+
|
|
286
|
+
Args:
|
|
284
287
|
options (YoloInferenceOptions): all the parameters used to control this process,
|
|
285
|
-
including filenames; see YoloInferenceOptions for details
|
|
288
|
+
including filenames; see YoloInferenceOptions for details
|
|
286
289
|
"""
|
|
287
|
-
|
|
290
|
+
|
|
288
291
|
##%% Input and path handling
|
|
289
|
-
|
|
292
|
+
|
|
290
293
|
default_options = YoloInferenceOptions()
|
|
291
|
-
|
|
294
|
+
|
|
292
295
|
for k in options.__dict__.keys():
|
|
293
296
|
if k not in default_options.__dict__:
|
|
294
297
|
# Print warnings about unexpected variables, except for things like
|
|
295
298
|
# "no_append_job_id_to_symlink_folder", which just negate existing objects
|
|
296
299
|
if not k.startswith('no_'):
|
|
297
300
|
print('Warning: unexpected variable {} in options object'.format(k))
|
|
298
|
-
|
|
301
|
+
|
|
299
302
|
if options.model_type == 'yolov8':
|
|
300
|
-
|
|
301
|
-
print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model
|
|
303
|
+
|
|
304
|
+
print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model ' + \
|
|
305
|
+
'type string for YOLOv8 models')
|
|
302
306
|
options.model_type = 'ultralytics'
|
|
303
|
-
|
|
307
|
+
|
|
304
308
|
if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
|
|
305
|
-
print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"...
|
|
306
|
-
|
|
309
|
+
print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"... ' + \
|
|
310
|
+
'did you mean to use --model_type yolov8?" ***\n\n')
|
|
311
|
+
|
|
307
312
|
if options.yolo_working_folder is None:
|
|
308
313
|
assert options.model_type == 'ultralytics', \
|
|
309
314
|
'A working folder is required to run YOLOv5 val.py'
|
|
310
315
|
else:
|
|
311
316
|
assert os.path.isdir(options.yolo_working_folder), \
|
|
312
317
|
'Could not find working folder {}'.format(options.yolo_working_folder)
|
|
313
|
-
|
|
318
|
+
|
|
314
319
|
if options.half_precision_enabled is not None:
|
|
315
320
|
assert options.half_precision_enabled in (0,1), \
|
|
316
321
|
'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
|
|
317
322
|
options.half_precision_enabled)
|
|
318
|
-
|
|
323
|
+
|
|
319
324
|
# If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
|
|
320
325
|
model_filename = try_download_known_detector(options.model_filename)
|
|
321
|
-
|
|
326
|
+
|
|
322
327
|
assert os.path.isfile(model_filename), \
|
|
323
328
|
'Could not find model file {}'.format(model_filename)
|
|
324
|
-
|
|
329
|
+
|
|
325
330
|
assert (options.input_folder is not None) or (options.image_filename_list is not None), \
|
|
326
331
|
'You must specify a folder and/or a file list'
|
|
327
|
-
|
|
332
|
+
|
|
328
333
|
if options.input_folder is not None:
|
|
329
334
|
assert os.path.isdir(options.input_folder), 'Could not find input folder {}'.format(
|
|
330
335
|
options.input_folder)
|
|
331
|
-
|
|
336
|
+
|
|
332
337
|
if os.path.exists(options.output_file):
|
|
333
338
|
if options.overwrite_handling == 'skip':
|
|
334
339
|
print('Warning: output file {} exists, skipping'.format(options.output_file))
|
|
@@ -339,17 +344,17 @@ def run_inference_with_yolo_val(options):
|
|
|
339
344
|
raise ValueError('Output file {} exists'.format(options.output_file))
|
|
340
345
|
else:
|
|
341
346
|
raise ValueError('Unknown output handling method {}'.format(options.overwrite_handling))
|
|
342
|
-
|
|
347
|
+
|
|
343
348
|
os.makedirs(os.path.dirname(options.output_file),exist_ok=True)
|
|
344
|
-
|
|
349
|
+
|
|
345
350
|
if options.input_folder is not None:
|
|
346
351
|
options.input_folder = options.input_folder.replace('\\','/')
|
|
347
|
-
|
|
348
|
-
|
|
352
|
+
|
|
353
|
+
|
|
349
354
|
##%% Other input handling
|
|
350
|
-
|
|
355
|
+
|
|
351
356
|
if isinstance(options.yolo_category_id_to_name,str):
|
|
352
|
-
|
|
357
|
+
|
|
353
358
|
assert os.path.isfile(options.yolo_category_id_to_name)
|
|
354
359
|
yolo_dataset_file = options.yolo_category_id_to_name
|
|
355
360
|
options.yolo_category_id_to_name = \
|
|
@@ -360,9 +365,9 @@ def run_inference_with_yolo_val(options):
|
|
|
360
365
|
temporary_folder = None
|
|
361
366
|
symlink_folder_is_temp_folder = False
|
|
362
367
|
yolo_folder_is_temp_folder = False
|
|
363
|
-
|
|
368
|
+
|
|
364
369
|
job_id = str(uuid.uuid1())
|
|
365
|
-
|
|
370
|
+
|
|
366
371
|
def get_job_temporary_folder(tf):
|
|
367
372
|
if tf is not None:
|
|
368
373
|
return tf
|
|
@@ -370,39 +375,39 @@ def run_inference_with_yolo_val(options):
|
|
|
370
375
|
tf = os.path.join(tempdir_base,'md_to_yolo','md_to_yolo_' + job_id)
|
|
371
376
|
os.makedirs(tf,exist_ok=True)
|
|
372
377
|
return tf
|
|
373
|
-
|
|
378
|
+
|
|
374
379
|
symlink_folder = options.symlink_folder
|
|
375
380
|
yolo_results_folder = options.yolo_results_folder
|
|
376
|
-
|
|
381
|
+
|
|
377
382
|
if symlink_folder is None:
|
|
378
383
|
temporary_folder = get_job_temporary_folder(temporary_folder)
|
|
379
384
|
symlink_folder = os.path.join(temporary_folder,'symlinks')
|
|
380
385
|
symlink_folder_is_temp_folder = True
|
|
381
|
-
|
|
386
|
+
|
|
382
387
|
if yolo_results_folder is None:
|
|
383
388
|
temporary_folder = get_job_temporary_folder(temporary_folder)
|
|
384
389
|
yolo_results_folder = os.path.join(temporary_folder,'yolo_results')
|
|
385
390
|
yolo_folder_is_temp_folder = True
|
|
386
|
-
|
|
391
|
+
|
|
387
392
|
if options.append_job_id_to_symlink_folder:
|
|
388
|
-
# Attach a GUID to the symlink folder, regardless of whether we created it
|
|
393
|
+
# Attach a GUID to the symlink folder, regardless of whether we created it
|
|
389
394
|
symlink_folder_inner = os.path.join(symlink_folder,job_id)
|
|
390
395
|
else:
|
|
391
396
|
print('Re-using existing symlink folder {}'.format(symlink_folder))
|
|
392
397
|
symlink_folder_inner = symlink_folder
|
|
393
|
-
|
|
398
|
+
|
|
394
399
|
os.makedirs(symlink_folder_inner,exist_ok=True)
|
|
395
400
|
os.makedirs(yolo_results_folder,exist_ok=True)
|
|
396
|
-
|
|
401
|
+
|
|
397
402
|
|
|
398
403
|
##%% Enumerate images
|
|
399
|
-
|
|
404
|
+
|
|
400
405
|
image_files_relative = None
|
|
401
406
|
image_files_absolute = None
|
|
402
|
-
|
|
407
|
+
|
|
403
408
|
# If the caller just provided a folder, not a list of files...
|
|
404
409
|
if options.image_filename_list is None:
|
|
405
|
-
|
|
410
|
+
|
|
406
411
|
assert options.input_folder is not None and os.path.isdir(options.input_folder), \
|
|
407
412
|
'Could not find input folder {}'.format(options.input_folder)
|
|
408
413
|
image_files_relative = path_utils.find_images(options.input_folder,
|
|
@@ -411,18 +416,18 @@ def run_inference_with_yolo_val(options):
|
|
|
411
416
|
convert_slashes=True)
|
|
412
417
|
image_files_absolute = [os.path.join(options.input_folder,fn) for \
|
|
413
418
|
fn in image_files_relative]
|
|
414
|
-
|
|
419
|
+
|
|
415
420
|
else:
|
|
416
|
-
|
|
417
|
-
# If the caller provided a list of image files (rather than a filename pointing
|
|
421
|
+
|
|
422
|
+
# If the caller provided a list of image files (rather than a filename pointing
|
|
418
423
|
# to a list of image files)...
|
|
419
424
|
if is_iterable(options.image_filename_list) and not isinstance(options.image_filename_list,str):
|
|
420
|
-
|
|
425
|
+
|
|
421
426
|
image_files_relative = options.image_filename_list
|
|
422
|
-
|
|
427
|
+
|
|
423
428
|
# If the caller provided a filename pointing to a list of image files...
|
|
424
429
|
else:
|
|
425
|
-
|
|
430
|
+
|
|
426
431
|
assert isinstance(options.image_filename_list,str), \
|
|
427
432
|
'Unrecognized image filename list object type: {}'.format(options.image_filename_list)
|
|
428
433
|
assert os.path.isfile(options.image_filename_list), \
|
|
@@ -439,152 +444,152 @@ def run_inference_with_yolo_val(options):
|
|
|
439
444
|
with open(options.image_filename_list,'r') as f:
|
|
440
445
|
image_files_relative = f.readlines()
|
|
441
446
|
image_files_relative = [s.strip() for s in image_files_relative]
|
|
442
|
-
|
|
447
|
+
|
|
443
448
|
# ...whether the image filename list was supplied as list vs. a filename
|
|
444
|
-
|
|
449
|
+
|
|
445
450
|
if options.input_folder is None:
|
|
446
|
-
|
|
451
|
+
|
|
447
452
|
image_files_absolute = image_files_relative
|
|
448
|
-
|
|
453
|
+
|
|
449
454
|
else:
|
|
450
|
-
|
|
455
|
+
|
|
451
456
|
# The list should be relative filenames
|
|
452
457
|
for fn in image_files_relative:
|
|
453
458
|
assert not path_is_abs(fn), \
|
|
454
459
|
'When providing a folder and a list, paths in the list should be relative'
|
|
455
|
-
|
|
460
|
+
|
|
456
461
|
image_files_absolute = \
|
|
457
462
|
[os.path.join(options.input_folder,fn) for fn in image_files_relative]
|
|
458
|
-
|
|
463
|
+
|
|
459
464
|
for fn in image_files_absolute:
|
|
460
465
|
assert os.path.isfile(fn), 'Could not find image file {}'.format(fn)
|
|
461
|
-
|
|
466
|
+
|
|
462
467
|
# ...whether the caller supplied a list of filenames
|
|
463
|
-
|
|
468
|
+
|
|
464
469
|
image_files_absolute = [fn.replace('\\','/') for fn in image_files_absolute]
|
|
465
|
-
|
|
470
|
+
|
|
466
471
|
del image_files_relative
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
|
|
473
|
+
|
|
469
474
|
##%% Recurse if necessary to handle checkpoints
|
|
470
|
-
|
|
475
|
+
|
|
471
476
|
if options.checkpoint_frequency is not None and options.checkpoint_frequency > 0:
|
|
472
|
-
|
|
477
|
+
|
|
473
478
|
chunks = split_list_into_fixed_size_chunks(image_files_absolute,options.checkpoint_frequency)
|
|
474
|
-
|
|
479
|
+
|
|
475
480
|
chunk_output_files = []
|
|
476
|
-
|
|
481
|
+
|
|
477
482
|
# i_chunk = 0; chunk_files_abs = chunks[i_chunk]
|
|
478
483
|
for i_chunk,chunk_files_abs in enumerate(chunks):
|
|
479
|
-
|
|
484
|
+
|
|
480
485
|
print('Processing {} images from chunk {} of {}'.format(
|
|
481
486
|
len(chunk_files_abs),i_chunk,len(chunks)))
|
|
482
|
-
|
|
487
|
+
|
|
483
488
|
chunk_options = copy.deepcopy(options)
|
|
484
|
-
|
|
489
|
+
|
|
485
490
|
# Run each chunk without checkpointing
|
|
486
491
|
chunk_options.checkpoint_frequency = None
|
|
487
|
-
|
|
492
|
+
|
|
488
493
|
if options.input_folder is not None:
|
|
489
494
|
chunk_files_relative = \
|
|
490
495
|
[os.path.relpath(fn,options.input_folder) for fn in chunk_files_abs]
|
|
491
496
|
chunk_options.image_filename_list = chunk_files_relative
|
|
492
497
|
else:
|
|
493
498
|
chunk_options.image_filename_list = chunk_files_abs
|
|
494
|
-
|
|
499
|
+
|
|
495
500
|
chunk_options.image_filename_list = \
|
|
496
501
|
[fn.replace('\\','/') for fn in chunk_options.image_filename_list]
|
|
497
|
-
|
|
502
|
+
|
|
498
503
|
chunk_string = 'chunk_{}'.format(str(i_chunk).zfill(5))
|
|
499
504
|
chunk_options.yolo_results_folder = yolo_results_folder + '_' + chunk_string
|
|
500
505
|
chunk_options.symlink_folder = symlink_folder + '_' + chunk_string
|
|
501
|
-
|
|
506
|
+
|
|
502
507
|
# Put the output file in the parent job's scratch folder
|
|
503
508
|
chunk_output_file = os.path.join(yolo_results_folder,chunk_string + '_results_md_format.json')
|
|
504
509
|
chunk_output_files.append(chunk_output_file)
|
|
505
510
|
chunk_options.output_file = chunk_output_file
|
|
506
|
-
|
|
511
|
+
|
|
507
512
|
if os.path.isfile(chunk_output_file):
|
|
508
|
-
|
|
513
|
+
|
|
509
514
|
print('Chunk output file {} exists, checking completeness'.format(chunk_output_file))
|
|
510
|
-
|
|
515
|
+
|
|
511
516
|
with open(chunk_output_file,'r') as f:
|
|
512
517
|
chunk_results = json.load(f)
|
|
513
|
-
images_in_this_chunk_results_file = [im['file'] for im in chunk_results['images']]
|
|
518
|
+
images_in_this_chunk_results_file = [im['file'] for im in chunk_results['images']]
|
|
514
519
|
assert len(images_in_this_chunk_results_file) == len(chunk_options.image_filename_list), \
|
|
515
|
-
'Expected {} images in
|
|
516
|
-
|
|
517
|
-
|
|
520
|
+
f'Expected {len(chunk_options.image_filename_list)} images in ' + \
|
|
521
|
+
f'chunk results file {chunk_output_file}, found {len(images_in_this_chunk_results_file)}, ' + \
|
|
522
|
+
'possibly this is left over from a previous job?'
|
|
518
523
|
for fn in images_in_this_chunk_results_file:
|
|
519
524
|
assert fn in chunk_options.image_filename_list, \
|
|
520
|
-
'Unexpected image {} in chunk results file {},
|
|
521
|
-
|
|
522
|
-
|
|
525
|
+
f'Unexpected image {fn} in chunk results file {chunk_output_file}, ' + \
|
|
526
|
+
'possibly this is left over from a previous job?'
|
|
527
|
+
|
|
523
528
|
print('Chunk output file {} exists and is complete, skipping this chunk'.format(
|
|
524
529
|
chunk_output_file))
|
|
525
|
-
|
|
530
|
+
|
|
526
531
|
# ...if the outptut file exists
|
|
527
|
-
|
|
532
|
+
|
|
528
533
|
else:
|
|
529
|
-
|
|
534
|
+
|
|
530
535
|
run_inference_with_yolo_val(chunk_options)
|
|
531
|
-
|
|
536
|
+
|
|
532
537
|
# ...if we do/don't have to run this chunk
|
|
533
|
-
|
|
538
|
+
|
|
534
539
|
assert os.path.isfile(chunk_options.output_file)
|
|
535
|
-
|
|
540
|
+
|
|
536
541
|
# ...for each chunk
|
|
537
|
-
|
|
542
|
+
|
|
538
543
|
# Merge
|
|
539
544
|
_ = combine_batch_output_files(input_files=chunk_output_files,
|
|
540
545
|
output_file=options.output_file,
|
|
541
546
|
require_uniqueness=True,
|
|
542
547
|
verbose=True)
|
|
543
|
-
|
|
548
|
+
|
|
544
549
|
# Validate
|
|
545
550
|
with open(options.output_file,'r') as f:
|
|
546
551
|
combined_results = json.load(f)
|
|
547
552
|
assert len(combined_results['images']) == len(image_files_absolute), \
|
|
548
553
|
'Expected {} images in merged output file, found {}'.format(
|
|
549
554
|
len(image_files_absolute),len(combined_results['images']))
|
|
550
|
-
|
|
555
|
+
|
|
551
556
|
# Clean up
|
|
552
557
|
_clean_up_temporary_folders(options,
|
|
553
558
|
symlink_folder,yolo_results_folder,
|
|
554
559
|
symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
|
|
555
|
-
|
|
560
|
+
|
|
556
561
|
return
|
|
557
|
-
|
|
562
|
+
|
|
558
563
|
# ...if we need to make recursive calls for file chunks
|
|
559
|
-
|
|
560
|
-
|
|
564
|
+
|
|
565
|
+
|
|
561
566
|
##%% Create symlinks (or copy images) to give a unique ID to each image
|
|
562
|
-
|
|
567
|
+
|
|
563
568
|
# Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
|
|
564
569
|
# to the *original full path* for each image (not the symlink path).
|
|
565
|
-
image_id_to_file = {}
|
|
566
|
-
|
|
570
|
+
image_id_to_file = {}
|
|
571
|
+
|
|
567
572
|
# Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
|
|
568
573
|
# to errors, including errors that happen before we run the model at all (e.g. file access errors).
|
|
569
574
|
image_id_to_error = {}
|
|
570
|
-
|
|
575
|
+
|
|
571
576
|
create_links = True
|
|
572
|
-
|
|
577
|
+
|
|
573
578
|
if options.unique_id_strategy == 'links':
|
|
574
|
-
|
|
579
|
+
|
|
575
580
|
create_links = True
|
|
576
|
-
|
|
581
|
+
|
|
577
582
|
else:
|
|
578
|
-
|
|
583
|
+
|
|
579
584
|
assert options.unique_id_strategy in ('auto','verify'), \
|
|
580
585
|
'Unknown unique ID strategy {}'.format(options.unique_id_strategy)
|
|
581
|
-
|
|
586
|
+
|
|
582
587
|
image_ids_are_unique = True
|
|
583
|
-
|
|
584
|
-
for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
|
|
585
|
-
|
|
588
|
+
|
|
589
|
+
for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
|
|
590
|
+
|
|
586
591
|
image_id = os.path.splitext(os.path.basename(image_fn))[0]
|
|
587
|
-
|
|
592
|
+
|
|
588
593
|
# Is this image ID unique?
|
|
589
594
|
if image_id in image_id_to_file:
|
|
590
595
|
if options.unique_id_strategy == 'verify':
|
|
@@ -596,20 +601,20 @@ def run_inference_with_yolo_val(options):
|
|
|
596
601
|
image_ids_are_unique = False
|
|
597
602
|
image_id_to_file = {}
|
|
598
603
|
break
|
|
599
|
-
|
|
604
|
+
|
|
600
605
|
image_id_to_file[image_id] = image_fn
|
|
601
|
-
|
|
606
|
+
|
|
602
607
|
# ...for each image
|
|
603
|
-
|
|
608
|
+
|
|
604
609
|
if image_ids_are_unique:
|
|
605
|
-
|
|
610
|
+
|
|
606
611
|
print('"{}" specified for image uniqueness and images are unique, skipping links'.format(
|
|
607
612
|
options.unique_id_strategy))
|
|
608
613
|
assert len(image_id_to_file) == len(image_files_absolute)
|
|
609
614
|
create_links = False
|
|
610
|
-
|
|
615
|
+
|
|
611
616
|
else:
|
|
612
|
-
|
|
617
|
+
|
|
613
618
|
assert options.unique_id_strategy == 'auto'
|
|
614
619
|
create_links = True
|
|
615
620
|
link_type = 'copies'
|
|
@@ -617,31 +622,31 @@ def run_inference_with_yolo_val(options):
|
|
|
617
622
|
link_type = 'links'
|
|
618
623
|
print('"auto" specified for image uniqueness and images are not unique, defaulting to {}'.format(
|
|
619
624
|
link_type))
|
|
620
|
-
|
|
625
|
+
|
|
621
626
|
# ...which unique ID strategy?
|
|
622
|
-
|
|
627
|
+
|
|
623
628
|
if create_links:
|
|
624
|
-
|
|
629
|
+
|
|
625
630
|
if options.use_symlinks:
|
|
626
631
|
print('Creating {} symlinks in {}'.format(len(image_files_absolute),symlink_folder_inner))
|
|
627
632
|
else:
|
|
628
633
|
print('Symlinks disabled, copying {} images to {}'.format(len(image_files_absolute),symlink_folder_inner))
|
|
629
|
-
|
|
634
|
+
|
|
630
635
|
link_full_paths = []
|
|
631
|
-
|
|
636
|
+
|
|
632
637
|
# i_image = 0; image_fn = image_files_absolute[i_image]
|
|
633
638
|
for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
|
|
634
|
-
|
|
639
|
+
|
|
635
640
|
ext = os.path.splitext(image_fn)[1]
|
|
636
641
|
image_fn_without_extension = os.path.splitext(image_fn)[0]
|
|
637
|
-
|
|
642
|
+
|
|
638
643
|
# YOLO .json output identifies images by the base filename without the extension
|
|
639
644
|
image_id = str(i_image).zfill(10)
|
|
640
645
|
image_id_to_file[image_id] = image_fn
|
|
641
646
|
symlink_name = image_id + ext
|
|
642
647
|
symlink_full_path = os.path.join(symlink_folder_inner,symlink_name)
|
|
643
648
|
link_full_paths.append(symlink_full_path)
|
|
644
|
-
|
|
649
|
+
|
|
645
650
|
# If annotation files exist, link those too; only useful if we're reading the computed
|
|
646
651
|
# mAP value, but it doesn't hurt.
|
|
647
652
|
annotation_fn = image_fn_without_extension + '.txt'
|
|
@@ -649,10 +654,10 @@ def run_inference_with_yolo_val(options):
|
|
|
649
654
|
if os.path.isfile(annotation_fn):
|
|
650
655
|
annotation_file_exists = True
|
|
651
656
|
annotation_symlink_name = image_id + '.txt'
|
|
652
|
-
annotation_symlink_full_path = os.path.join(symlink_folder_inner,annotation_symlink_name)
|
|
653
|
-
|
|
657
|
+
annotation_symlink_full_path = os.path.join(symlink_folder_inner,annotation_symlink_name)
|
|
658
|
+
|
|
654
659
|
try:
|
|
655
|
-
|
|
660
|
+
|
|
656
661
|
if options.use_symlinks:
|
|
657
662
|
path_utils.safe_create_link(image_fn,symlink_full_path)
|
|
658
663
|
if annotation_file_exists:
|
|
@@ -661,74 +666,74 @@ def run_inference_with_yolo_val(options):
|
|
|
661
666
|
shutil.copyfile(image_fn,symlink_full_path)
|
|
662
667
|
if annotation_file_exists:
|
|
663
668
|
shutil.copyfile(annotation_fn,annotation_symlink_full_path)
|
|
664
|
-
|
|
669
|
+
|
|
665
670
|
except Exception as e:
|
|
666
|
-
|
|
671
|
+
|
|
667
672
|
error_string = str(e)
|
|
668
673
|
image_id_to_error[image_id] = error_string
|
|
669
|
-
|
|
674
|
+
|
|
670
675
|
# Always break if the user is trying to create symlinks on Windows without
|
|
671
676
|
# permission, 100% of images will always fail in this case.
|
|
672
677
|
if ('a required privilege is not held by the client' in error_string.lower()) or \
|
|
673
678
|
(not options.treat_copy_failures_as_warnings):
|
|
674
|
-
|
|
679
|
+
|
|
675
680
|
print('\nError copying/creating link for input file {}: {}'.format(
|
|
676
681
|
image_fn,error_string))
|
|
677
|
-
|
|
682
|
+
|
|
678
683
|
raise
|
|
679
|
-
|
|
684
|
+
|
|
680
685
|
else:
|
|
681
|
-
|
|
686
|
+
|
|
682
687
|
print('Warning: error copying/creating link for input file {}: {}'.format(
|
|
683
688
|
image_fn,error_string))
|
|
684
689
|
continue
|
|
685
|
-
|
|
690
|
+
|
|
686
691
|
# ...except
|
|
687
|
-
|
|
692
|
+
|
|
688
693
|
# ...for each image
|
|
689
|
-
|
|
694
|
+
|
|
690
695
|
# ...if we need to create links/copies
|
|
691
696
|
|
|
692
|
-
|
|
697
|
+
|
|
693
698
|
##%% Create the dataset file if necessary
|
|
694
|
-
|
|
699
|
+
|
|
695
700
|
# This may have been passed in as a string, but at this point, we should have
|
|
696
701
|
# loaded the dataset file.
|
|
697
702
|
assert isinstance(options.yolo_category_id_to_name,dict)
|
|
698
|
-
|
|
703
|
+
|
|
699
704
|
# Category IDs need to be continuous integers starting at 0
|
|
700
705
|
category_ids = sorted(list(options.yolo_category_id_to_name.keys()))
|
|
701
706
|
assert category_ids[0] == 0
|
|
702
707
|
assert len(category_ids) == 1 + category_ids[-1]
|
|
703
|
-
|
|
708
|
+
|
|
704
709
|
yolo_dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
|
|
705
|
-
yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
|
|
706
|
-
|
|
710
|
+
yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
|
|
711
|
+
|
|
707
712
|
with open(yolo_image_list_file,'w') as f:
|
|
708
|
-
|
|
713
|
+
|
|
709
714
|
if create_links:
|
|
710
715
|
image_files_to_write = link_full_paths
|
|
711
716
|
else:
|
|
712
717
|
image_files_to_write = image_files_absolute
|
|
713
|
-
|
|
718
|
+
|
|
714
719
|
for fn_abs in image_files_to_write:
|
|
715
|
-
# At least in YOLOv5 val (need to verify for YOLOv8 val), filenames in this
|
|
720
|
+
# At least in YOLOv5 val (need to verify for YOLOv8 val), filenames in this
|
|
716
721
|
# text file are treated as relative to the text file itself if they start with
|
|
717
722
|
# "./", otherwise they're treated as absolute paths. Since we don't want to put this
|
|
718
723
|
# text file in the image folder, we'll use absolute paths.
|
|
719
724
|
# fn_relative = os.path.relpath(fn_abs,options.input_folder)
|
|
720
725
|
# f.write(fn_relative + '\n')
|
|
721
726
|
f.write(fn_abs + '\n')
|
|
722
|
-
|
|
727
|
+
|
|
723
728
|
if create_links:
|
|
724
729
|
inference_folder = symlink_folder_inner
|
|
725
730
|
else:
|
|
726
731
|
# This doesn't matter, but it has to be a valid path
|
|
727
732
|
inference_folder = options.yolo_results_folder
|
|
728
|
-
|
|
733
|
+
|
|
729
734
|
with open(yolo_dataset_file,'w') as f:
|
|
730
|
-
|
|
731
|
-
f.write('path: {}\n'.format(inference_folder))
|
|
735
|
+
|
|
736
|
+
f.write('path: {}\n'.format(inference_folder))
|
|
732
737
|
# These need to be valid paths, even if you're not using them, and "." is always safe
|
|
733
738
|
f.write('train: .\n')
|
|
734
739
|
f.write('val: .\n')
|
|
@@ -744,7 +749,7 @@ def run_inference_with_yolo_val(options):
|
|
|
744
749
|
|
|
745
750
|
|
|
746
751
|
##%% Prepare Python command or YOLO CLI command
|
|
747
|
-
|
|
752
|
+
|
|
748
753
|
if options.image_size is None:
|
|
749
754
|
if options.augment:
|
|
750
755
|
image_size = default_image_size_with_augmentation
|
|
@@ -752,70 +757,70 @@ def run_inference_with_yolo_val(options):
|
|
|
752
757
|
image_size = default_image_size_with_no_augmentation
|
|
753
758
|
else:
|
|
754
759
|
image_size = options.image_size
|
|
755
|
-
|
|
760
|
+
|
|
756
761
|
image_size_string = str(round(image_size))
|
|
757
|
-
|
|
762
|
+
|
|
758
763
|
if options.model_type == 'yolov5':
|
|
759
|
-
|
|
764
|
+
|
|
760
765
|
cmd = 'python val.py --task test --data "{}"'.format(yolo_dataset_file)
|
|
761
766
|
cmd += ' --weights "{}"'.format(model_filename)
|
|
762
767
|
cmd += ' --batch-size {} --imgsz {} --conf-thres {}'.format(
|
|
763
768
|
options.batch_size,image_size_string,options.conf_thres)
|
|
764
769
|
cmd += ' --device "{}" --save-json'.format(options.device_string)
|
|
765
770
|
cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
|
|
766
|
-
|
|
771
|
+
|
|
767
772
|
# This is the NMS IoU threshold
|
|
768
773
|
# cmd += ' --iou-thres 0.6'
|
|
769
|
-
|
|
774
|
+
|
|
770
775
|
if options.augment:
|
|
771
776
|
cmd += ' --augment'
|
|
772
|
-
|
|
777
|
+
|
|
773
778
|
# --half is a store_true argument for YOLOv5's val.py
|
|
774
779
|
if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
|
|
775
780
|
cmd += ' --half'
|
|
776
|
-
|
|
781
|
+
|
|
777
782
|
# Sometimes useful for debugging
|
|
778
783
|
# cmd += ' --save_conf --save_txt'
|
|
779
|
-
|
|
784
|
+
|
|
780
785
|
elif options.model_type == 'ultralytics':
|
|
781
|
-
|
|
786
|
+
|
|
782
787
|
if options.augment:
|
|
783
788
|
augment_string = 'augment'
|
|
784
789
|
else:
|
|
785
790
|
augment_string = ''
|
|
786
|
-
|
|
791
|
+
|
|
787
792
|
cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
|
|
788
793
|
format(augment_string,model_filename,image_size_string,options.batch_size,
|
|
789
794
|
yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
|
|
790
795
|
cmd += ' save_json exist_ok'
|
|
791
|
-
|
|
796
|
+
|
|
792
797
|
if (options.half_precision_enabled is not None):
|
|
793
798
|
if options.half_precision_enabled == 1:
|
|
794
799
|
cmd += ' --half=True'
|
|
795
800
|
else:
|
|
796
801
|
assert options.half_precision_enabled == 0
|
|
797
802
|
cmd += ' --half=False'
|
|
798
|
-
|
|
803
|
+
|
|
799
804
|
# Sometimes useful for debugging
|
|
800
805
|
# cmd += ' save_conf save_txt'
|
|
801
|
-
|
|
806
|
+
|
|
802
807
|
else:
|
|
803
|
-
|
|
808
|
+
|
|
804
809
|
raise ValueError('Unrecognized model type {}'.format(options.model_type))
|
|
805
|
-
|
|
810
|
+
|
|
806
811
|
# print(cmd); import clipboard; clipboard.copy(cmd)
|
|
807
812
|
|
|
808
|
-
|
|
813
|
+
|
|
809
814
|
##%% Run YOLO command
|
|
810
|
-
|
|
815
|
+
|
|
811
816
|
if options.yolo_working_folder is not None:
|
|
812
817
|
current_dir = os.getcwd()
|
|
813
818
|
os.chdir(options.yolo_working_folder)
|
|
814
819
|
|
|
815
820
|
print('Running YOLO inference command:\n{}\n'.format(cmd))
|
|
816
|
-
|
|
821
|
+
|
|
817
822
|
if options.preview_yolo_command_only:
|
|
818
|
-
|
|
823
|
+
|
|
819
824
|
if options.remove_symlink_folder:
|
|
820
825
|
try:
|
|
821
826
|
print('Removing YOLO symlink folder {}'.format(symlink_folder))
|
|
@@ -830,34 +835,32 @@ def run_inference_with_yolo_val(options):
|
|
|
830
835
|
except Exception:
|
|
831
836
|
print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
|
|
832
837
|
pass
|
|
833
|
-
|
|
838
|
+
|
|
834
839
|
# sys.exit()
|
|
835
840
|
return
|
|
836
|
-
|
|
841
|
+
|
|
837
842
|
execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
|
|
838
843
|
assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
|
|
839
844
|
yolo_console_output = execution_result['output']
|
|
840
|
-
|
|
845
|
+
|
|
841
846
|
if options.save_yolo_debug_output:
|
|
842
|
-
|
|
847
|
+
|
|
843
848
|
with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w',encoding='utf-8') as f:
|
|
844
849
|
for s in yolo_console_output:
|
|
845
850
|
f.write(s + '\n')
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
851
|
+
ct_utils.write_json(os.path.join(yolo_results_folder,'image_id_to_file.json'), image_id_to_file)
|
|
852
|
+
ct_utils.write_json(os.path.join(yolo_results_folder,'image_id_to_error.json'), image_id_to_error)
|
|
853
|
+
|
|
854
|
+
|
|
852
855
|
# YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
|
|
853
856
|
yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
|
|
854
|
-
|
|
857
|
+
|
|
855
858
|
# Find errors that occurred during the initial corruption check; these will not be included in the
|
|
856
859
|
# output. Errors that occur during inference will be handled separately.
|
|
857
860
|
yolo_read_failures = []
|
|
858
|
-
|
|
861
|
+
|
|
859
862
|
for line in yolo_console_output:
|
|
860
|
-
|
|
863
|
+
|
|
861
864
|
#
|
|
862
865
|
# Lines indicating read failures look like:
|
|
863
866
|
#
|
|
@@ -869,30 +872,30 @@ def run_inference_with_yolo_val(options):
|
|
|
869
872
|
#
|
|
870
873
|
# line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
|
|
871
874
|
#
|
|
872
|
-
# In both cases, when we are using symlinks, the first filename is the symlink name, the
|
|
875
|
+
# In both cases, when we are using symlinks, the first filename is the symlink name, the
|
|
873
876
|
# second filename is the target, e.g.:
|
|
874
|
-
#
|
|
877
|
+
#
|
|
875
878
|
# line = "test: WARNING: /tmp/md_to_yolo/md_to_yolo_xyz/symlinks/xyz/0000000004.jpg: ignoring corrupt image/label: cannot identify image file '/tmp/md-tests/md-test-images/corrupt-images/real-file.jpg'"
|
|
876
879
|
#
|
|
877
880
|
# Windows example:
|
|
878
881
|
#
|
|
879
882
|
# line = "test: WARNING: g:\\temp\\md-test-images\\corrupt-images\\irfanview-can-still-read-me-caltech_camera_traps_5a0e37cc-23d2-11e8-a6a3-ec086b02610b.jpg: ignoring corrupt image/label: cannot identify image file 'g:\\\\temp\\\\md-test-images\\\\corrupt-images\\\\irfanview-can-still-read-me-caltech_camera_traps_5a0e37cc-23d2-11e8-a6a3-ec086b02610b.jpg'"
|
|
880
883
|
#
|
|
881
|
-
|
|
884
|
+
|
|
882
885
|
line = line.replace('⚠️',':')
|
|
883
886
|
if 'ignoring corrupt image/label' in line:
|
|
884
|
-
|
|
887
|
+
|
|
885
888
|
line_tokens = line.split('ignoring corrupt image/label')
|
|
886
889
|
assert len(line_tokens) == 2
|
|
887
|
-
|
|
890
|
+
|
|
888
891
|
tokens = line_tokens[0].split(':') # ,maxsplit=3)
|
|
889
892
|
tokens = [s.strip() for s in tokens]
|
|
890
|
-
|
|
893
|
+
|
|
891
894
|
# ['test', ' WARNING', ' a/b/c/d.jpg', ' ']
|
|
892
895
|
assert len(tokens[-1]) == 0
|
|
893
896
|
tokens = tokens[:-1]
|
|
894
897
|
assert 'warning' in tokens[1].lower()
|
|
895
|
-
|
|
898
|
+
|
|
896
899
|
if len(tokens) == 3:
|
|
897
900
|
image_name = tokens[2].strip()
|
|
898
901
|
else:
|
|
@@ -900,28 +903,28 @@ def run_inference_with_yolo_val(options):
|
|
|
900
903
|
assert len(tokens) == 4
|
|
901
904
|
assert len(tokens[2]) == 1
|
|
902
905
|
image_name = ':'.join(tokens[2:4])
|
|
903
|
-
|
|
906
|
+
|
|
904
907
|
yolo_read_failures.append(image_name)
|
|
905
|
-
|
|
908
|
+
|
|
906
909
|
# ...if this line indicated a corrupt image
|
|
907
|
-
|
|
910
|
+
|
|
908
911
|
# ...for each line in the console output
|
|
909
|
-
|
|
912
|
+
|
|
910
913
|
# image_file = yolo_read_failures[0]
|
|
911
914
|
for image_file in yolo_read_failures:
|
|
912
915
|
image_id = os.path.splitext(os.path.basename(image_file))[0]
|
|
913
916
|
assert image_id in image_id_to_file, 'Unexpected image ID {}'.format(image_id)
|
|
914
917
|
if image_id not in image_id_to_error:
|
|
915
918
|
image_id_to_error[image_id] = 'YOLO read failure'
|
|
916
|
-
|
|
919
|
+
|
|
917
920
|
if options.yolo_working_folder is not None:
|
|
918
921
|
os.chdir(current_dir)
|
|
919
|
-
|
|
920
|
-
|
|
922
|
+
|
|
923
|
+
|
|
921
924
|
##%% Convert results to MD format
|
|
922
|
-
|
|
925
|
+
|
|
923
926
|
json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
|
|
924
|
-
assert len(json_files) == 1
|
|
927
|
+
assert len(json_files) == 1
|
|
925
928
|
yolo_json_file = json_files[0]
|
|
926
929
|
|
|
927
930
|
# Map YOLO image IDs to paths
|
|
@@ -939,14 +942,14 @@ def run_inference_with_yolo_val(options):
|
|
|
939
942
|
# as the base path in this case.
|
|
940
943
|
relative_path = fn
|
|
941
944
|
image_id_to_relative_path[image_id] = relative_path
|
|
942
|
-
|
|
945
|
+
|
|
943
946
|
# Are we working with a base folder?
|
|
944
947
|
if options.input_folder is not None:
|
|
945
948
|
assert os.path.isdir(options.input_folder)
|
|
946
949
|
image_base = options.input_folder
|
|
947
950
|
else:
|
|
948
951
|
image_base = '/'
|
|
949
|
-
|
|
952
|
+
|
|
950
953
|
yolo_output_to_md_output.yolo_json_output_to_md_output(
|
|
951
954
|
yolo_json_file=yolo_json_file,
|
|
952
955
|
image_folder=image_base,
|
|
@@ -958,34 +961,32 @@ def run_inference_with_yolo_val(options):
|
|
|
958
961
|
|
|
959
962
|
|
|
960
963
|
##%% Clean up
|
|
961
|
-
|
|
964
|
+
|
|
962
965
|
_clean_up_temporary_folders(options,
|
|
963
966
|
symlink_folder,yolo_results_folder,
|
|
964
967
|
symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
|
|
965
|
-
|
|
968
|
+
|
|
966
969
|
# ...def run_inference_with_yolo_val()
|
|
967
970
|
|
|
968
971
|
|
|
969
972
|
#%% Command-line driver
|
|
970
973
|
|
|
971
|
-
|
|
972
|
-
from megadetector.utils.ct_utils import args_to_object
|
|
974
|
+
def main(): # noqa
|
|
973
975
|
|
|
974
|
-
def main():
|
|
975
|
-
|
|
976
976
|
options = YoloInferenceOptions()
|
|
977
|
-
|
|
977
|
+
|
|
978
978
|
parser = argparse.ArgumentParser()
|
|
979
979
|
parser.add_argument(
|
|
980
980
|
'model_filename',type=str,
|
|
981
981
|
help='model file name')
|
|
982
982
|
parser.add_argument(
|
|
983
983
|
'input_folder',type=str,
|
|
984
|
-
help='folder on which to recursively run the model, or a .json or .txt file
|
|
984
|
+
help='folder on which to recursively run the model, or a .json or .txt file ' + \
|
|
985
|
+
'containing a list of absolute image paths')
|
|
985
986
|
parser.add_argument(
|
|
986
987
|
'output_file',type=str,
|
|
987
988
|
help='.json file where output will be written')
|
|
988
|
-
|
|
989
|
+
|
|
989
990
|
parser.add_argument(
|
|
990
991
|
'--image_filename_list',type=str,default=None,
|
|
991
992
|
help='.json or .txt file containing a list of relative image filenames within [input_folder]')
|
|
@@ -1005,10 +1006,12 @@ def main():
|
|
|
1005
1006
|
help='inference batch size (default {})'.format(options.batch_size))
|
|
1006
1007
|
parser.add_argument(
|
|
1007
1008
|
'--half_precision_enabled', default=None, type=int,
|
|
1008
|
-
help='use half-precision-inference (1 or 0) (default is the underlying model\'s default,
|
|
1009
|
+
help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, ' + \
|
|
1010
|
+
'probably full for YOLOv8 and half for YOLOv5')
|
|
1009
1011
|
parser.add_argument(
|
|
1010
1012
|
'--device_string', default=options.device_string, type=str,
|
|
1011
|
-
help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for
|
|
1013
|
+
help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for ' + \
|
|
1014
|
+
'M1/M2 devices, or "cpu" (default {})'.format(
|
|
1012
1015
|
options.device_string))
|
|
1013
1016
|
parser.add_argument(
|
|
1014
1017
|
'--overwrite_handling', default=options.overwrite_handling, type=str,
|
|
@@ -1048,36 +1051,36 @@ def main():
|
|
|
1048
1051
|
parser.add_argument(
|
|
1049
1052
|
'--checkpoint_frequency', default=options.checkpoint_frequency, type=int,
|
|
1050
1053
|
help='break the job into chunks with no more than this many images (default {})'.format(
|
|
1051
|
-
options.checkpoint_frequency))
|
|
1054
|
+
options.checkpoint_frequency))
|
|
1052
1055
|
parser.add_argument(
|
|
1053
1056
|
'--no_append_job_id_to_symlink_folder', action='store_true',
|
|
1054
1057
|
help="don't append a unique job ID to the symlink folder name")
|
|
1055
1058
|
parser.add_argument(
|
|
1056
1059
|
'--nonrecursive', action='store_true',
|
|
1057
1060
|
help='disable recursive folder processing')
|
|
1058
|
-
|
|
1061
|
+
|
|
1059
1062
|
parser.add_argument(
|
|
1060
1063
|
'--preview_yolo_command_only', action='store_true',
|
|
1061
1064
|
help='don\'t run inference, just preview the YOLO inference command (still creates symlinks)')
|
|
1062
|
-
|
|
1065
|
+
|
|
1063
1066
|
if options.augment:
|
|
1064
1067
|
default_augment_enabled = 1
|
|
1065
1068
|
else:
|
|
1066
1069
|
default_augment_enabled = 0
|
|
1067
|
-
|
|
1070
|
+
|
|
1068
1071
|
parser.add_argument(
|
|
1069
1072
|
'--augment_enabled', default=default_augment_enabled, type=int,
|
|
1070
1073
|
help='enable/disable augmentation (default {})'.format(default_augment_enabled))
|
|
1071
|
-
|
|
1074
|
+
|
|
1072
1075
|
if len(sys.argv[1:]) == 0:
|
|
1073
1076
|
parser.print_help()
|
|
1074
1077
|
parser.exit()
|
|
1075
1078
|
|
|
1076
1079
|
args = parser.parse_args()
|
|
1077
|
-
|
|
1080
|
+
|
|
1078
1081
|
# If the caller hasn't specified an image size, choose one based on whether augmentation
|
|
1079
1082
|
# is enabled.
|
|
1080
|
-
if args.image_size is None:
|
|
1083
|
+
if args.image_size is None:
|
|
1081
1084
|
assert args.augment_enabled in (0,1), \
|
|
1082
1085
|
'Illegal augment_enabled value {}'.format(args.augment_enabled)
|
|
1083
1086
|
if args.augment_enabled == 1:
|
|
@@ -1089,38 +1092,38 @@ def main():
|
|
|
1089
1092
|
augment_enabled_string = 'disabled'
|
|
1090
1093
|
print('Augmentation is {}, using default image size {}'.format(
|
|
1091
1094
|
augment_enabled_string,args.image_size))
|
|
1092
|
-
|
|
1095
|
+
|
|
1093
1096
|
args_to_object(args, options)
|
|
1094
|
-
|
|
1097
|
+
|
|
1095
1098
|
if args.yolo_dataset_file is not None:
|
|
1096
1099
|
options.yolo_category_id_to_name = args.yolo_dataset_file
|
|
1097
|
-
|
|
1098
|
-
# The function convention is that input_folder should be None when we want to use a list of
|
|
1099
|
-
# absolute paths, but the CLI convention is that the required argument is always valid, whether
|
|
1100
|
+
|
|
1101
|
+
# The function convention is that input_folder should be None when we want to use a list of
|
|
1102
|
+
# absolute paths, but the CLI convention is that the required argument is always valid, whether
|
|
1100
1103
|
# it's a folder or a list of absolute paths.
|
|
1101
1104
|
if os.path.isfile(options.input_folder):
|
|
1102
1105
|
assert options.image_filename_list is None, \
|
|
1103
1106
|
'image_filename_list should not be specified when input_folder is a file'
|
|
1104
1107
|
options.image_filename_list = options.input_folder
|
|
1105
|
-
options.input_folder = None
|
|
1106
|
-
|
|
1108
|
+
options.input_folder = None
|
|
1109
|
+
|
|
1107
1110
|
options.recursive = (not options.nonrecursive)
|
|
1108
1111
|
options.append_job_id_to_symlink_folder = (not options.no_append_job_id_to_symlink_folder)
|
|
1109
1112
|
options.remove_symlink_folder = (not options.no_remove_symlink_folder)
|
|
1110
1113
|
options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
|
|
1111
1114
|
options.use_symlinks = (not options.no_use_symlinks)
|
|
1112
|
-
options.augment = (options.augment_enabled > 0)
|
|
1113
|
-
|
|
1115
|
+
options.augment = (options.augment_enabled > 0)
|
|
1116
|
+
|
|
1114
1117
|
del options.nonrecursive
|
|
1115
1118
|
del options.no_remove_symlink_folder
|
|
1116
1119
|
del options.no_remove_yolo_results_folder
|
|
1117
1120
|
del options.no_use_symlinks
|
|
1118
1121
|
del options.augment_enabled
|
|
1119
1122
|
del options.yolo_dataset_file
|
|
1120
|
-
|
|
1123
|
+
|
|
1121
1124
|
print(options.__dict__)
|
|
1122
|
-
|
|
1123
|
-
run_inference_with_yolo_val(options)
|
|
1125
|
+
|
|
1126
|
+
run_inference_with_yolo_val(options)
|
|
1124
1127
|
|
|
1125
1128
|
if __name__ == '__main__':
|
|
1126
1129
|
main()
|
|
@@ -1133,7 +1136,7 @@ if False:
|
|
|
1133
1136
|
|
|
1134
1137
|
#%% Debugging
|
|
1135
1138
|
|
|
1136
|
-
input_folder = r'g:\temp\md-test-images'
|
|
1139
|
+
input_folder = r'g:\temp\md-test-images'
|
|
1137
1140
|
model_filename = 'MDV5A'
|
|
1138
1141
|
output_folder = r'g:\temp\yolo-test-out'
|
|
1139
1142
|
yolo_working_folder = r'c:\git\yolov5-md'
|
|
@@ -1142,16 +1145,16 @@ if False:
|
|
|
1142
1145
|
symlink_folder = os.path.join(output_folder,'symlinks')
|
|
1143
1146
|
yolo_results_folder = os.path.join(output_folder,'yolo_results')
|
|
1144
1147
|
model_name = os.path.splitext(os.path.basename(model_filename))[0]
|
|
1145
|
-
|
|
1148
|
+
|
|
1146
1149
|
output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
|
|
1147
1150
|
job_name,model_name))
|
|
1148
|
-
|
|
1151
|
+
|
|
1149
1152
|
options = YoloInferenceOptions()
|
|
1150
|
-
|
|
1153
|
+
|
|
1151
1154
|
options.yolo_working_folder = yolo_working_folder
|
|
1152
1155
|
options.input_folder = input_folder
|
|
1153
1156
|
options.output_file = output_file
|
|
1154
|
-
|
|
1157
|
+
|
|
1155
1158
|
options.yolo_category_id_to_name = dataset_file
|
|
1156
1159
|
options.augment = False
|
|
1157
1160
|
options.conf_thres = '0.001'
|
|
@@ -1164,18 +1167,18 @@ if False:
|
|
|
1164
1167
|
options.image_size = round(1280 * 1.3)
|
|
1165
1168
|
else:
|
|
1166
1169
|
options.image_size = 1280
|
|
1167
|
-
|
|
1170
|
+
|
|
1168
1171
|
options.model_filename = model_filename
|
|
1169
|
-
|
|
1170
|
-
options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
|
|
1172
|
+
|
|
1173
|
+
options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
|
|
1171
1174
|
options.symlink_folder = symlink_folder # os.path.join(output_folder,'symlinks')
|
|
1172
1175
|
options.use_symlinks = False
|
|
1173
|
-
|
|
1176
|
+
|
|
1174
1177
|
options.remove_symlink_folder = True
|
|
1175
1178
|
options.remove_yolo_results_folder = True
|
|
1176
|
-
|
|
1179
|
+
|
|
1177
1180
|
options.checkpoint_frequency = None
|
|
1178
|
-
|
|
1181
|
+
|
|
1179
1182
|
cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
|
|
1180
1183
|
f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
|
|
1181
1184
|
f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
|
|
@@ -1183,10 +1186,10 @@ if False:
|
|
|
1183
1186
|
f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
|
|
1184
1187
|
f' --yolo_dataset_file {options.yolo_category_id_to_name} ' + \
|
|
1185
1188
|
f' --unique_id_strategy {options.unique_id_strategy} --overwrite_handling {options.overwrite_handling}'
|
|
1186
|
-
|
|
1189
|
+
|
|
1187
1190
|
if not options.remove_symlink_folder:
|
|
1188
1191
|
cmd += ' --no_remove_symlink_folder'
|
|
1189
|
-
if not options.remove_yolo_results_folder:
|
|
1192
|
+
if not options.remove_yolo_results_folder:
|
|
1190
1193
|
cmd += ' --no_remove_yolo_results_folder'
|
|
1191
1194
|
if options.checkpoint_frequency is not None:
|
|
1192
1195
|
cmd += f' --checkpoint_frequency {options.checkpoint_frequency}'
|
|
@@ -1194,7 +1197,7 @@ if False:
|
|
|
1194
1197
|
cmd += ' --no_use_symlinks'
|
|
1195
1198
|
if not options.augment:
|
|
1196
1199
|
cmd += ' --augment_enabled 0'
|
|
1197
|
-
|
|
1200
|
+
|
|
1198
1201
|
print(cmd)
|
|
1199
1202
|
execute_in_python = False
|
|
1200
1203
|
if execute_in_python:
|
|
@@ -1205,47 +1208,47 @@ if False:
|
|
|
1205
1208
|
|
|
1206
1209
|
|
|
1207
1210
|
#%% Run inference on a folder
|
|
1208
|
-
|
|
1211
|
+
|
|
1209
1212
|
input_folder = r'g:\temp\tegu-val-mini'.replace('\\','/')
|
|
1210
1213
|
model_filename = r'g:\temp\usgs-tegus-yolov5x-231003-b8-img1280-e3002-best.pt'
|
|
1211
1214
|
output_folder = r'g:\temp\tegu-scratch'
|
|
1212
1215
|
yolo_working_folder = r'c:\git\yolov5-tegus'
|
|
1213
1216
|
dataset_file = r'g:\temp\dataset.yaml'
|
|
1214
|
-
|
|
1217
|
+
|
|
1215
1218
|
# This only impacts the output file name, it's not passed to the inference function
|
|
1216
1219
|
job_name = 'yolo-inference-test'
|
|
1217
|
-
|
|
1220
|
+
|
|
1218
1221
|
model_name = os.path.splitext(os.path.basename(model_filename))[0]
|
|
1219
|
-
|
|
1222
|
+
|
|
1220
1223
|
symlink_folder = os.path.join(output_folder,'symlinks')
|
|
1221
1224
|
yolo_results_folder = os.path.join(output_folder,'yolo_results')
|
|
1222
|
-
|
|
1225
|
+
|
|
1223
1226
|
output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
|
|
1224
1227
|
job_name,model_name))
|
|
1225
|
-
|
|
1228
|
+
|
|
1226
1229
|
options = YoloInferenceOptions()
|
|
1227
|
-
|
|
1230
|
+
|
|
1228
1231
|
options.yolo_working_folder = yolo_working_folder
|
|
1229
1232
|
options.input_folder = input_folder
|
|
1230
1233
|
options.output_file = output_file
|
|
1231
|
-
|
|
1232
|
-
pass_image_filename_list = False
|
|
1234
|
+
|
|
1235
|
+
pass_image_filename_list = False
|
|
1233
1236
|
pass_relative_paths = True
|
|
1234
|
-
|
|
1237
|
+
|
|
1235
1238
|
if pass_image_filename_list:
|
|
1236
1239
|
if pass_relative_paths:
|
|
1237
1240
|
options.image_filename_list = [
|
|
1238
1241
|
r"val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(05) 18AUG17 - 05SEP17 FTC AEG#MFDC1949_000065.JPG",
|
|
1239
1242
|
r"val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(04) 27JUL17 - 18AUG17 FTC AEG#MFDC1902_000064.JPG"
|
|
1240
|
-
]
|
|
1243
|
+
]
|
|
1241
1244
|
else:
|
|
1242
1245
|
options.image_filename_list = [
|
|
1243
1246
|
r"g:/temp/tegu-val-mini/val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(05) 18AUG17 - 05SEP17 FTC AEG#MFDC1949_000065.JPG",
|
|
1244
1247
|
r"g:/temp/tegu-val-mini/val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(04) 27JUL17 - 18AUG17 FTC AEG#MFDC1902_000064.JPG"
|
|
1245
1248
|
]
|
|
1246
1249
|
else:
|
|
1247
|
-
options.image_filename_list = None
|
|
1248
|
-
|
|
1250
|
+
options.image_filename_list = None
|
|
1251
|
+
|
|
1249
1252
|
options.yolo_category_id_to_name = dataset_file
|
|
1250
1253
|
options.augment = False
|
|
1251
1254
|
options.conf_thres = '0.001'
|
|
@@ -1258,18 +1261,18 @@ if False:
|
|
|
1258
1261
|
options.image_size = round(1280 * 1.3)
|
|
1259
1262
|
else:
|
|
1260
1263
|
options.image_size = 1280
|
|
1261
|
-
|
|
1264
|
+
|
|
1262
1265
|
options.model_filename = model_filename
|
|
1263
|
-
|
|
1264
|
-
options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
|
|
1266
|
+
|
|
1267
|
+
options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
|
|
1265
1268
|
options.symlink_folder = symlink_folder # os.path.join(output_folder,'symlinks')
|
|
1266
1269
|
options.use_symlinks = False
|
|
1267
|
-
|
|
1270
|
+
|
|
1268
1271
|
options.remove_symlink_folder = True
|
|
1269
1272
|
options.remove_yolo_results_folder = True
|
|
1270
|
-
|
|
1273
|
+
|
|
1271
1274
|
options.checkpoint_frequency = 5
|
|
1272
|
-
|
|
1275
|
+
|
|
1273
1276
|
cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
|
|
1274
1277
|
f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
|
|
1275
1278
|
f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
|
|
@@ -1277,10 +1280,10 @@ if False:
|
|
|
1277
1280
|
f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
|
|
1278
1281
|
f' --yolo_dataset_file {options.yolo_category_id_to_name} ' + \
|
|
1279
1282
|
f' --unique_id_strategy {options.unique_id_strategy} --overwrite_handling {options.overwrite_handling}'
|
|
1280
|
-
|
|
1283
|
+
|
|
1281
1284
|
if not options.remove_symlink_folder:
|
|
1282
1285
|
cmd += ' --no_remove_symlink_folder'
|
|
1283
|
-
if not options.remove_yolo_results_folder:
|
|
1286
|
+
if not options.remove_yolo_results_folder:
|
|
1284
1287
|
cmd += ' --no_remove_yolo_results_folder'
|
|
1285
1288
|
if options.checkpoint_frequency is not None:
|
|
1286
1289
|
cmd += f' --checkpoint_frequency {options.checkpoint_frequency}'
|
|
@@ -1288,7 +1291,7 @@ if False:
|
|
|
1288
1291
|
cmd += ' --no_use_symlinks'
|
|
1289
1292
|
if not options.augment:
|
|
1290
1293
|
cmd += ' --augment_enabled 0'
|
|
1291
|
-
|
|
1294
|
+
|
|
1292
1295
|
print(cmd)
|
|
1293
1296
|
execute_in_python = False
|
|
1294
1297
|
if execute_in_python:
|