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
|
@@ -44,55 +44,55 @@ class TopFoldersToBottomOptions:
|
|
|
44
44
|
"""
|
|
45
45
|
Options used to parameterize top_folders_to_bottom()
|
|
46
46
|
"""
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
def __init__(self,input_folder,output_folder,copy=True,n_threads=1):
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
#: Whether to copy (True) vs. move (False) false when re-organizing
|
|
51
51
|
self.copy = copy
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
#: Number of worker threads to use, or <1 to disable parallelization
|
|
54
54
|
self.n_threads = n_threads
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
#: Input folder
|
|
57
57
|
self.input_folder = input_folder
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
#: Output folder
|
|
60
60
|
self.output_folder = output_folder
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
#: If this is False and an output file exists, throw an error
|
|
63
63
|
self.overwrite = False
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
|
|
66
66
|
#%% Main functions
|
|
67
67
|
|
|
68
68
|
def _process_file(relative_filename,options,execute=True):
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
assert ('/' in relative_filename) and \
|
|
71
71
|
('\\' not in relative_filename) and \
|
|
72
72
|
(not path_is_abs(relative_filename))
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
# Find top-level folder
|
|
75
75
|
tokens = relative_filename.split('/')
|
|
76
76
|
topmost_folder = tokens.pop(0)
|
|
77
77
|
tokens.insert(len(tokens)-1,topmost_folder)
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
# Find file/folder names
|
|
80
80
|
output_relative_path = '/'.join(tokens)
|
|
81
81
|
output_relative_folder = '/'.join(tokens[0:-1])
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
output_absolute_folder = os.path.join(options.output_folder,output_relative_folder)
|
|
84
84
|
output_absolute_path = os.path.join(options.output_folder,output_relative_path)
|
|
85
85
|
|
|
86
86
|
if execute:
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
os.makedirs(output_absolute_folder,exist_ok=True)
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
input_absolute_path = os.path.join(options.input_folder,relative_filename)
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
if not options.overwrite:
|
|
93
93
|
assert not os.path.isfile(output_absolute_path), \
|
|
94
94
|
'Error: output file {} exists'.format(output_absolute_path)
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
# Move or copy
|
|
97
97
|
if options.copy:
|
|
98
98
|
shutil.copy(input_absolute_path, output_absolute_path)
|
|
@@ -100,7 +100,7 @@ def _process_file(relative_filename,options,execute=True):
|
|
|
100
100
|
shutil.move(input_absolute_path, output_absolute_path)
|
|
101
101
|
|
|
102
102
|
return output_absolute_path
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
# ...def _process_file()
|
|
105
105
|
|
|
106
106
|
|
|
@@ -131,93 +131,93 @@ def top_folders_to_bottom(options):
|
|
|
131
131
|
|
|
132
132
|
"""
|
|
133
133
|
os.makedirs(options.output_folder,exist_ok=True)
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
# Enumerate input folder
|
|
136
136
|
print('Enumerating files...')
|
|
137
137
|
files = list(Path(options.input_folder).rglob('*'))
|
|
138
138
|
files = [p for p in files if not p.is_dir()]
|
|
139
139
|
files = [str(s) for s in files]
|
|
140
140
|
print('Enumerated {} files'.format(len(files)))
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
# Convert absolute paths to relative paths
|
|
143
143
|
relative_files = [os.path.relpath(s,options.input_folder) for s in files]
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
# Standardize delimiters
|
|
146
146
|
relative_files = [s.replace('\\','/') for s in relative_files]
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
base_files = [s for s in relative_files if '/' not in s]
|
|
149
149
|
if len(base_files) > 0:
|
|
150
150
|
print('Warning: ignoring {} files in the base folder'.format(len(base_files)))
|
|
151
151
|
relative_files = [s for s in relative_files if '/' in s]
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
# Make sure each input file maps to a unique output file
|
|
154
154
|
absolute_output_files = [_process_file(s, options, execute=False) for s in relative_files]
|
|
155
155
|
assert len(absolute_output_files) == len(set(absolute_output_files)),\
|
|
156
156
|
"Error: input filenames don't map to unique output filenames"
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
# relative_filename = relative_files[0]
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
# Loop
|
|
161
161
|
if options.n_threads <= 1:
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
for relative_filename in tqdm(relative_files):
|
|
164
164
|
_process_file(relative_filename,options)
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
else:
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
print('Starting a pool with {} threads'.format(options.n_threads))
|
|
169
169
|
pool = ThreadPool(options.n_threads)
|
|
170
170
|
process_file_with_options = partial(_process_file, options=options)
|
|
171
171
|
_ = list(tqdm(pool.imap(process_file_with_options, relative_files), total=len(relative_files)))
|
|
172
172
|
|
|
173
|
-
# ...def top_folders_to_bottom()
|
|
174
|
-
|
|
173
|
+
# ...def top_folders_to_bottom()
|
|
174
|
+
|
|
175
175
|
|
|
176
176
|
#%% Interactive driver
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
if False:
|
|
179
179
|
|
|
180
180
|
pass
|
|
181
181
|
|
|
182
182
|
#%%
|
|
183
|
-
|
|
183
|
+
|
|
184
184
|
input_folder = r"G:\temp\output"
|
|
185
|
-
output_folder = r"G:\temp\output-inverted"
|
|
185
|
+
output_folder = r"G:\temp\output-inverted"
|
|
186
186
|
options = TopFoldersToBottomOptions(input_folder,output_folder,copy=True,n_threads=10)
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
#%%
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
top_folders_to_bottom(options)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
#%% Command-line driver
|
|
193
|
+
#%% Command-line driver
|
|
194
194
|
|
|
195
195
|
# python top_folders_to_bottom.py "g:\temp\separated_images" "g:\temp\separated_images_inverted" --n_threads 100
|
|
196
196
|
|
|
197
|
-
def main():
|
|
198
|
-
|
|
197
|
+
def main(): # noqa
|
|
198
|
+
|
|
199
199
|
parser = argparse.ArgumentParser()
|
|
200
200
|
parser.add_argument('input_folder', type=str, help='Input image folder')
|
|
201
201
|
parser.add_argument('output_folder', type=str, help='Output image folder')
|
|
202
202
|
|
|
203
|
-
parser.add_argument('--copy', action='store_true',
|
|
203
|
+
parser.add_argument('--copy', action='store_true',
|
|
204
204
|
help='Copy images, instead of moving (moving is the default)')
|
|
205
|
-
parser.add_argument('--overwrite', action='store_true',
|
|
205
|
+
parser.add_argument('--overwrite', action='store_true',
|
|
206
206
|
help='Allow image overwrite (default=False)')
|
|
207
207
|
parser.add_argument('--n_threads', type=int, default=1,
|
|
208
208
|
help='Number of threads to use for parallel operation (default=1)')
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
if len(sys.argv[1:])==0:
|
|
211
211
|
parser.print_help()
|
|
212
212
|
parser.exit()
|
|
213
|
-
|
|
214
|
-
args = parser.parse_args()
|
|
215
|
-
|
|
213
|
+
|
|
214
|
+
args = parser.parse_args()
|
|
215
|
+
|
|
216
216
|
# Convert to an options object
|
|
217
217
|
options = TopFoldersToBottomOptions(
|
|
218
218
|
args.input_folder,args.output_folder,copy=args.copy,n_threads=args.n_threads)
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
top_folders_to_bottom(options)
|
|
221
|
-
|
|
222
|
-
if __name__ == '__main__':
|
|
221
|
+
|
|
222
|
+
if __name__ == '__main__':
|
|
223
223
|
main()
|
|
@@ -41,27 +41,27 @@ class ValidateBatchResultsOptions:
|
|
|
41
41
|
"""
|
|
42
42
|
Options controlling the behavior of validate_bach_results()
|
|
43
43
|
"""
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
def __init__(self):
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
#: Should we verify that images exist? If this is True, and the .json
|
|
48
48
|
#: file contains relative paths, relative_path_base needs to be specified.
|
|
49
49
|
self.check_image_existence = False
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
#: If check_image_existence is True, where do the images live?
|
|
52
52
|
#:
|
|
53
53
|
#: If None, assumes absolute paths.
|
|
54
54
|
self.relative_path_base = None
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
#: Should we return the loaded data, or just the validation results?
|
|
57
57
|
self.return_data = False
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
#: Enable additional debug output
|
|
60
60
|
self.verbose = False
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
#: Should we raise errors immediately (vs. just catching and reporting)?
|
|
63
63
|
self.raise_errors = False
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
# ...class ValidateBatchResultsOptions
|
|
66
66
|
|
|
67
67
|
|
|
@@ -70,67 +70,67 @@ class ValidateBatchResultsOptions:
|
|
|
70
70
|
def validate_batch_results(json_filename,options=None):
|
|
71
71
|
"""
|
|
72
72
|
Verify that [json_filename] is a valid MD output file. Currently errors on invalid files.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
75
|
json_filename (str): the filename to validate
|
|
76
|
-
options (ValidateBatchResultsOptions, optional): all the parameters used to control this
|
|
76
|
+
options (ValidateBatchResultsOptions, optional): all the parameters used to control this
|
|
77
77
|
process, see ValidateBatchResultsOptions for details
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
Returns:
|
|
80
80
|
dict: a dict with a field called "validation_results", which is itself a dict. The reason
|
|
81
|
-
it's a dict inside a dict is that if return_data is True, the outer dict also contains all
|
|
81
|
+
it's a dict inside a dict is that if return_data is True, the outer dict also contains all
|
|
82
82
|
the loaded data. The "validation_results" dict contains fields called "errors", "warnings",
|
|
83
83
|
and "filename". "errors" and "warnings" are lists of strings, although "errors" will never
|
|
84
84
|
be longer than N=1, since validation fails at the first error.
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
"""
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
if options is None:
|
|
89
89
|
options = ValidateBatchResultsOptions()
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
if options.verbose:
|
|
92
92
|
print('Loading results from {}'.format(json_filename))
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
with open(json_filename,'r') as f:
|
|
95
95
|
d = json.load(f)
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
validation_results = {}
|
|
98
98
|
validation_results['filename'] = json_filename
|
|
99
99
|
validation_results['warnings'] = []
|
|
100
100
|
validation_results['errors'] = []
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
if not isinstance(d,dict):
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
validation_results['errors'].append('Input data is not a dict')
|
|
105
105
|
to_return = {}
|
|
106
106
|
to_return['validation_results'] = validation_results
|
|
107
107
|
return to_return
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
try:
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
## Info validation
|
|
112
|
-
|
|
113
|
-
if
|
|
112
|
+
|
|
113
|
+
if 'info' not in d:
|
|
114
114
|
raise ValueError('Input does not contain info field')
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
info = d['info']
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
if not isinstance(info,dict):
|
|
119
119
|
raise ValueError('Input contains invalid info field')
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
if 'format_version' not in info :
|
|
122
122
|
raise ValueError('Input does not specify format version')
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
format_version = float(info['format_version'])
|
|
125
125
|
if format_version < 1.3:
|
|
126
126
|
raise ValueError('This validator can only be used with format version 1.3 or later')
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
|
|
128
|
+
|
|
129
129
|
## Category validation
|
|
130
|
-
|
|
130
|
+
|
|
131
131
|
if 'detection_categories' not in d:
|
|
132
132
|
raise ValueError('Input does not contain detection_categories field')
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
for k in d['detection_categories'].keys():
|
|
135
135
|
# Category ID should be string-formatted ints
|
|
136
136
|
if not isinstance(k,str):
|
|
@@ -139,7 +139,7 @@ def validate_batch_results(json_filename,options=None):
|
|
|
139
139
|
if not isinstance(d['detection_categories'][k],str):
|
|
140
140
|
raise ValueError('Invalid detection category name: {}'.format(
|
|
141
141
|
d['detection_categories'][k]))
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
if 'classification_categories' in d:
|
|
144
144
|
for k in d['classification_categories'].keys():
|
|
145
145
|
# Categories should be string-formatted ints
|
|
@@ -149,28 +149,28 @@ def validate_batch_results(json_filename,options=None):
|
|
|
149
149
|
if not isinstance(d['classification_categories'][k],str):
|
|
150
150
|
raise ValueError('Invalid classification category name: {}'.format(
|
|
151
151
|
d['classification_categories'][k]))
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
|
|
153
|
+
|
|
154
154
|
## Image validation
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
if 'images' not in d:
|
|
157
157
|
raise ValueError('images field not present')
|
|
158
158
|
if not isinstance(d['images'],list):
|
|
159
159
|
raise ValueError('Invalid images field')
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
if options.verbose:
|
|
162
162
|
print('Validating images')
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
# im = d['images'][0]
|
|
165
165
|
for i_im,im in tqdm(enumerate(d['images']),total=len(d['images']),disable=(not options.verbose)):
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
if not isinstance(im,dict):
|
|
168
168
|
raise ValueError('Invalid image at index {}'.format(i_im))
|
|
169
169
|
if 'file' not in im:
|
|
170
170
|
raise ValueError('Image without filename at index {}'.format(i_im))
|
|
171
|
-
|
|
171
|
+
|
|
172
172
|
file = im['file']
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
if 'detections' in im and im['detections'] is not None:
|
|
175
175
|
for det in im['detections']:
|
|
176
176
|
assert 'category' in det, 'Image {} has a detection with no category'.format(file)
|
|
@@ -180,17 +180,17 @@ def validate_batch_results(json_filename,options=None):
|
|
|
180
180
|
assert 'bbox' in det, 'Image {} has a detection with no box'.format(file)
|
|
181
181
|
assert det['category'] in d['detection_categories'], \
|
|
182
182
|
'Image {} has a detection with an unmapped category {}'.format(
|
|
183
|
-
file,det['category'])
|
|
184
|
-
|
|
183
|
+
file,det['category'])
|
|
184
|
+
|
|
185
185
|
if options.check_image_existence:
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
if options.relative_path_base is None:
|
|
188
188
|
file_abs = file
|
|
189
189
|
else:
|
|
190
190
|
file_abs = os.path.join(options.relative_path_base,file)
|
|
191
191
|
if not os.path.isfile(file_abs):
|
|
192
192
|
raise ValueError('Cannot find file {}'.format(file_abs))
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
if 'failure' in im:
|
|
195
195
|
if im['failure'] is not None:
|
|
196
196
|
if not isinstance(im['failure'],str):
|
|
@@ -206,9 +206,9 @@ def validate_batch_results(json_filename,options=None):
|
|
|
206
206
|
else:
|
|
207
207
|
if not isinstance(im['detections'],list):
|
|
208
208
|
raise ValueError('Invalid detections list for image {}'.format(im['file']))
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
if is_video_file(im['file']) and (format_version >= 1.4):
|
|
211
|
-
|
|
211
|
+
|
|
212
212
|
if 'frame_rate' not in im:
|
|
213
213
|
raise ValueError('Video without frame rate: {}'.format(im['file']))
|
|
214
214
|
if im['frame_rate'] < 0:
|
|
@@ -220,34 +220,34 @@ def validate_batch_results(json_filename,options=None):
|
|
|
220
220
|
raise ValueError('Frame without frame number in video {}'.format(
|
|
221
221
|
im['file']))
|
|
222
222
|
frame_numbers = [det['frame_number'] for det in im['detections']] # noqa
|
|
223
|
-
# assert is_list_sorted(frame_numbers)
|
|
224
|
-
|
|
223
|
+
# assert is_list_sorted(frame_numbers)
|
|
224
|
+
|
|
225
225
|
# ...for each image
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
|
|
227
|
+
|
|
228
228
|
## Validation of other keys
|
|
229
|
-
|
|
230
|
-
for k in d.keys():
|
|
229
|
+
|
|
230
|
+
for k in d.keys():
|
|
231
231
|
if (k not in typical_keys) and (k not in required_keys):
|
|
232
232
|
validation_results['warnings'].append(
|
|
233
|
-
'Warning: non-standard key {} present at file level'.format(k))
|
|
234
|
-
|
|
233
|
+
'Warning: non-standard key {} present at file level'.format(k))
|
|
234
|
+
|
|
235
235
|
except Exception as e:
|
|
236
|
-
|
|
236
|
+
|
|
237
237
|
if options.raise_errors:
|
|
238
238
|
raise
|
|
239
239
|
else:
|
|
240
240
|
validation_results['errors'].append(str(e))
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
# ...try/except
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
if options.return_data:
|
|
245
245
|
to_return = d
|
|
246
246
|
else:
|
|
247
247
|
to_return = {}
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
to_return['validation_results'] = validation_results
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
return to_return
|
|
252
252
|
|
|
253
253
|
# ...def validate_batch_results(...)
|
|
@@ -258,16 +258,16 @@ def validate_batch_results(json_filename,options=None):
|
|
|
258
258
|
if False:
|
|
259
259
|
|
|
260
260
|
#%% Validate all .json files in the MD test suite
|
|
261
|
-
|
|
261
|
+
|
|
262
262
|
from megadetector.utils.path_utils import recursive_file_list
|
|
263
263
|
filenames = recursive_file_list(os.path.expanduser('~/AppData/Local/Temp/md-tests'))
|
|
264
264
|
filenames = [fn for fn in filenames if fn.endswith('.json')]
|
|
265
265
|
filenames = [fn for fn in filenames if 'detectionIndex' not in fn]
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
options = ValidateBatchResultsOptions()
|
|
268
268
|
options.check_image_existence = False
|
|
269
269
|
options.relative_path_base = None # r'g:\temp\test-videos'
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
for json_filename in filenames:
|
|
272
272
|
results = validate_batch_results(json_filename,options)
|
|
273
273
|
if len(results['validation_results']['warnings']) > 0:
|
|
@@ -275,13 +275,13 @@ if False:
|
|
|
275
275
|
for s in results['validation_results']['warnings']:
|
|
276
276
|
print(s)
|
|
277
277
|
print('')
|
|
278
|
-
assert len(results['validation_results']['errors']) == 0
|
|
279
|
-
|
|
278
|
+
assert len(results['validation_results']['errors']) == 0
|
|
279
|
+
|
|
280
280
|
|
|
281
281
|
#%% Command-line driver
|
|
282
282
|
|
|
283
|
-
def main():
|
|
284
|
-
|
|
283
|
+
def main(): # noqa
|
|
284
|
+
|
|
285
285
|
options = ValidateBatchResultsOptions()
|
|
286
286
|
|
|
287
287
|
parser = argparse.ArgumentParser()
|
|
@@ -299,11 +299,11 @@ def main():
|
|
|
299
299
|
parser.exit()
|
|
300
300
|
|
|
301
301
|
args = parser.parse_args()
|
|
302
|
-
|
|
303
|
-
args_to_object(args, options)
|
|
302
|
+
|
|
303
|
+
args_to_object(args, options)
|
|
304
304
|
|
|
305
305
|
validate_batch_results(args.json_filename,options)
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
|
|
307
|
+
|
|
308
308
|
if __name__ == '__main__':
|
|
309
309
|
main()
|