megadetector 5.0.11__py3-none-any.whl → 5.0.13__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/__init__.py +0 -0
- megadetector/api/batch_processing/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
- megadetector/api/batch_processing/api_core/server.py +294 -0
- megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
- megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
- megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +88 -0
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +627 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +855 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +607 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +699 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +506 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +34 -0
- megadetector/data_management/camtrap_dp_to_coco.py +237 -0
- megadetector/data_management/cct_json_utils.py +404 -0
- megadetector/data_management/cct_to_md.py +176 -0
- megadetector/data_management/cct_to_wi.py +289 -0
- megadetector/data_management/coco_to_labelme.py +283 -0
- megadetector/data_management/coco_to_yolo.py +662 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
- megadetector/data_management/databases/integrity_check_json_db.py +493 -0
- megadetector/data_management/databases/subset_json_db.py +115 -0
- megadetector/data_management/generate_crops_from_cct.py +149 -0
- megadetector/data_management/get_image_sizes.py +189 -0
- megadetector/data_management/importers/add_nacti_sizes.py +52 -0
- megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
- megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
- megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
- megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
- megadetector/data_management/importers/awc_to_json.py +191 -0
- megadetector/data_management/importers/bellevue_to_json.py +273 -0
- megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
- megadetector/data_management/importers/cct_field_adjustments.py +58 -0
- megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
- megadetector/data_management/importers/ena24_to_json.py +276 -0
- megadetector/data_management/importers/filenames_to_json.py +386 -0
- megadetector/data_management/importers/helena_to_cct.py +283 -0
- megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
- megadetector/data_management/importers/jb_csv_to_json.py +150 -0
- megadetector/data_management/importers/mcgill_to_json.py +250 -0
- megadetector/data_management/importers/missouri_to_json.py +490 -0
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
- megadetector/data_management/importers/noaa_seals_2019.py +181 -0
- megadetector/data_management/importers/pc_to_json.py +365 -0
- megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
- megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
- megadetector/data_management/importers/rspb_to_json.py +356 -0
- megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
- megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
- megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
- megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
- megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
- megadetector/data_management/importers/sulross_get_exif.py +65 -0
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
- megadetector/data_management/importers/ubc_to_json.py +399 -0
- megadetector/data_management/importers/umn_to_json.py +507 -0
- megadetector/data_management/importers/wellington_to_json.py +263 -0
- megadetector/data_management/importers/wi_to_json.py +442 -0
- megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
- megadetector/data_management/labelme_to_coco.py +547 -0
- megadetector/data_management/labelme_to_yolo.py +272 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
- megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
- megadetector/data_management/lila/create_lila_blank_set.py +558 -0
- megadetector/data_management/lila/create_lila_test_set.py +152 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +178 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +300 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
- megadetector/data_management/ocr_tools.py +870 -0
- megadetector/data_management/read_exif.py +809 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/rename_images.py +187 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +446 -0
- megadetector/data_management/yolo_to_coco.py +676 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +114 -0
- megadetector/detection/process_video.py +846 -0
- megadetector/detection/pytorch_detector.py +355 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +192 -0
- megadetector/detection/video_utils.py +698 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +165 -0
- megadetector/postprocessing/classification_postprocessing.py +716 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +966 -0
- megadetector/postprocessing/convert_output_format.py +396 -0
- megadetector/postprocessing/load_api_results.py +195 -0
- megadetector/postprocessing/md_to_coco.py +310 -0
- megadetector/postprocessing/md_to_labelme.py +330 -0
- megadetector/postprocessing/merge_detections.py +412 -0
- megadetector/postprocessing/postprocess_batch_results.py +1908 -0
- megadetector/postprocessing/remap_detection_categories.py +170 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +700 -0
- megadetector/postprocessing/top_folders_to_bottom.py +223 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +219 -0
- megadetector/taxonomy_mapping/species_lookup.py +834 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/azure_utils.py +178 -0
- megadetector/utils/ct_utils.py +613 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +1164 -0
- megadetector/utils/path_utils.py +1045 -0
- megadetector/utils/process_utils.py +160 -0
- megadetector/utils/sas_blob_utils.py +509 -0
- megadetector/utils/split_locations_into_train_val.py +228 -0
- megadetector/utils/string_utils.py +92 -0
- megadetector/utils/url_utils.py +323 -0
- megadetector/utils/write_html_image_list.py +225 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +293 -0
- megadetector/visualization/render_images_with_thumbnails.py +275 -0
- megadetector/visualization/visualization_utils.py +1536 -0
- megadetector/visualization/visualize_db.py +552 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
- megadetector-5.0.13.dist-info/RECORD +201 -0
- megadetector-5.0.13.dist-info/top_level.txt +1 -0
- megadetector-5.0.11.dist-info/RECORD +0 -5
- megadetector-5.0.11.dist-info/top_level.txt +0 -1
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
combine_coco_camera_traps_files.py
|
|
4
|
+
|
|
5
|
+
Merges two or more .json files in COCO Camera Traps format, optionally
|
|
6
|
+
writing the results to another .json file.
|
|
7
|
+
|
|
8
|
+
- Concatenates image lists, erroring if images are not unique.
|
|
9
|
+
- Errors on unrecognized fields.
|
|
10
|
+
- Checks compatibility in info structs, within reason.
|
|
11
|
+
|
|
12
|
+
*Example command-line invocation*
|
|
13
|
+
|
|
14
|
+
combine_coco_camera_traps_files input1.json input2.json ... inputN.json output.json
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
#%% Constants and imports
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#%% Merge functions
|
|
26
|
+
|
|
27
|
+
def combine_cct_files(input_files, output_file=None, require_uniqueness=True,
|
|
28
|
+
filename_prefixes=None):
|
|
29
|
+
"""
|
|
30
|
+
Merges the list of COCO Camera Traps files [input_files] into a single
|
|
31
|
+
dictionary, optionally writing the result to [output_file].
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
input_files (list): paths to CCT .json files
|
|
35
|
+
output_file (str, optional): path to write merged .json file
|
|
36
|
+
require_uniqueness (bool): whether to require that the images in
|
|
37
|
+
each input_dict be unique
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
dict: the merged COCO-formatted .json dict
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
input_dicts = []
|
|
44
|
+
print('Loading input files')
|
|
45
|
+
for fn in input_files:
|
|
46
|
+
with open(fn, 'r', encoding='utf-8') as f:
|
|
47
|
+
d = json.load(f)
|
|
48
|
+
if filename_prefixes is not None:
|
|
49
|
+
assert fn in filename_prefixes
|
|
50
|
+
d['filename_prefix'] = filename_prefixes[fn]
|
|
51
|
+
input_dicts.append(d)
|
|
52
|
+
|
|
53
|
+
print('Merging results')
|
|
54
|
+
merged_dict = combine_cct_dictionaries(
|
|
55
|
+
input_dicts, require_uniqueness=require_uniqueness)
|
|
56
|
+
|
|
57
|
+
print('Writing output')
|
|
58
|
+
if output_file is not None:
|
|
59
|
+
with open(output_file, 'w') as f:
|
|
60
|
+
json.dump(merged_dict, f, indent=1)
|
|
61
|
+
|
|
62
|
+
return merged_dict
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def combine_cct_dictionaries(input_dicts, require_uniqueness=True):
|
|
66
|
+
"""
|
|
67
|
+
Merges the list of COCO Camera Traps dictionaries [input_dicts]. See module header
|
|
68
|
+
comment for details on merge rules.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
input_dicts: list of CCT dicts
|
|
72
|
+
require_uniqueness: bool, whether to require that the images in
|
|
73
|
+
each input_dict be unique
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
dict: the merged COCO-formatted .json dict
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
filename_to_image = {}
|
|
80
|
+
all_annotations = []
|
|
81
|
+
info = None
|
|
82
|
+
|
|
83
|
+
category_name_to_id = {}
|
|
84
|
+
category_name_to_id['empty'] = 0
|
|
85
|
+
next_category_id = 1
|
|
86
|
+
|
|
87
|
+
known_fields = ['info', 'categories', 'annotations','images','filename_prefix']
|
|
88
|
+
|
|
89
|
+
# i_input_dict = 0; input_dict = input_dicts[i_input_dict]
|
|
90
|
+
for i_input_dict,input_dict in enumerate(input_dicts):
|
|
91
|
+
|
|
92
|
+
filename_prefix = ''
|
|
93
|
+
if ('filename_prefix' in input_dict.keys()):
|
|
94
|
+
filename_prefix = input_dict['filename_prefix']
|
|
95
|
+
|
|
96
|
+
for k in input_dict.keys():
|
|
97
|
+
if k not in known_fields:
|
|
98
|
+
raise ValueError(f'Unrecognized CCT field: {k}')
|
|
99
|
+
|
|
100
|
+
# We will prepend an index to every ID to guarantee uniqueness
|
|
101
|
+
index_string = 'ds' + str(i_input_dict).zfill(3) + '_'
|
|
102
|
+
|
|
103
|
+
old_cat_id_to_new_cat_id = {}
|
|
104
|
+
|
|
105
|
+
# Map detection categories from the original data set into the merged data set
|
|
106
|
+
for original_category in input_dict['categories']:
|
|
107
|
+
|
|
108
|
+
original_cat_id = original_category['id']
|
|
109
|
+
cat_name = original_category['name']
|
|
110
|
+
if cat_name in category_name_to_id:
|
|
111
|
+
new_cat_id = category_name_to_id[cat_name]
|
|
112
|
+
else:
|
|
113
|
+
new_cat_id = next_category_id
|
|
114
|
+
next_category_id += 1
|
|
115
|
+
category_name_to_id[cat_name] = new_cat_id
|
|
116
|
+
|
|
117
|
+
if original_cat_id in old_cat_id_to_new_cat_id:
|
|
118
|
+
assert old_cat_id_to_new_cat_id[original_cat_id] == new_cat_id
|
|
119
|
+
else:
|
|
120
|
+
old_cat_id_to_new_cat_id[original_cat_id] = new_cat_id
|
|
121
|
+
|
|
122
|
+
# ...for each category
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Merge original image list into the merged data set
|
|
126
|
+
for im in input_dict['images']:
|
|
127
|
+
|
|
128
|
+
if 'seq_id' in im:
|
|
129
|
+
im['seq_id'] = index_string + im['seq_id']
|
|
130
|
+
if 'location' in im:
|
|
131
|
+
im['location'] = index_string + im['location']
|
|
132
|
+
|
|
133
|
+
im_file = filename_prefix + im['file_name']
|
|
134
|
+
im['file_name'] = im_file
|
|
135
|
+
if require_uniqueness:
|
|
136
|
+
assert im_file not in filename_to_image, f'Duplicate image: {im_file}'
|
|
137
|
+
else:
|
|
138
|
+
if im_file in filename_to_image:
|
|
139
|
+
print('Redundant image {}'.format(im_file))
|
|
140
|
+
|
|
141
|
+
# Create a unique ID
|
|
142
|
+
im['id'] = index_string + im['id']
|
|
143
|
+
filename_to_image[im_file] = im
|
|
144
|
+
|
|
145
|
+
# ...for each image
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Same for annotations
|
|
149
|
+
for ann in input_dict['annotations']:
|
|
150
|
+
|
|
151
|
+
ann['image_id'] = index_string + ann['image_id']
|
|
152
|
+
ann['id'] = index_string + ann['id']
|
|
153
|
+
assert ann['category_id'] in old_cat_id_to_new_cat_id
|
|
154
|
+
ann['category_id'] = old_cat_id_to_new_cat_id[ann['category_id']]
|
|
155
|
+
|
|
156
|
+
# ...for each annotation
|
|
157
|
+
|
|
158
|
+
all_annotations.extend(input_dict['annotations'])
|
|
159
|
+
|
|
160
|
+
# Merge info dicts, don't check completion time fields
|
|
161
|
+
if info is None:
|
|
162
|
+
import copy
|
|
163
|
+
info = copy.deepcopy(input_dict['info'])
|
|
164
|
+
info['original_info'] = [input_dict['info']]
|
|
165
|
+
else:
|
|
166
|
+
info['original_info'].append(input_dict['info'])
|
|
167
|
+
|
|
168
|
+
# ...for each dictionary
|
|
169
|
+
|
|
170
|
+
# Convert merged image dictionaries to a sorted list
|
|
171
|
+
sorted_images = sorted(filename_to_image.values(), key=lambda im: im['file_name'])
|
|
172
|
+
|
|
173
|
+
all_categories = [{'id':category_name_to_id[cat_name],'name':cat_name} for\
|
|
174
|
+
cat_name in category_name_to_id.keys()]
|
|
175
|
+
|
|
176
|
+
merged_dict = {'info': info,
|
|
177
|
+
'categories': all_categories,
|
|
178
|
+
'images': sorted_images,
|
|
179
|
+
'annotations': all_annotations}
|
|
180
|
+
|
|
181
|
+
return merged_dict
|
|
182
|
+
|
|
183
|
+
# ...combine_cct_dictionaries(...)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
#%% Command-line driver
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
|
|
190
|
+
parser = argparse.ArgumentParser()
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
'input_paths', nargs='+',
|
|
193
|
+
help='List of input .json files')
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
'output_path',
|
|
196
|
+
help='Output .json file')
|
|
197
|
+
|
|
198
|
+
if len(sys.argv[1:]) == 0:
|
|
199
|
+
parser.print_help()
|
|
200
|
+
parser.exit()
|
|
201
|
+
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
combine_cct_files(args.input_paths, args.output_path)
|
|
204
|
+
|
|
205
|
+
if __name__ == '__main__':
|
|
206
|
+
main()
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
integrity_check_json_db.py
|
|
4
|
+
|
|
5
|
+
Does some integrity-checking and computes basic statistics on a COCO Camera Traps .json file, specifically:
|
|
6
|
+
|
|
7
|
+
* Verifies that required fields are present and have the right types
|
|
8
|
+
* Verifies that annotations refer to valid images
|
|
9
|
+
* Verifies that annotations refer to valid categories
|
|
10
|
+
* Verifies that image, category, and annotation IDs are unique
|
|
11
|
+
* Optionally checks file existence
|
|
12
|
+
* Finds un-annotated images
|
|
13
|
+
* Finds unused categories
|
|
14
|
+
* Prints a list of categories sorted by count
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
#%% Constants and environment
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from multiprocessing.pool import ThreadPool
|
|
26
|
+
from operator import itemgetter
|
|
27
|
+
from tqdm import tqdm
|
|
28
|
+
|
|
29
|
+
from megadetector.visualization.visualization_utils import open_image
|
|
30
|
+
from megadetector.utils import ct_utils
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#%% Classes and environment
|
|
34
|
+
|
|
35
|
+
class IntegrityCheckOptions:
|
|
36
|
+
"""
|
|
37
|
+
Options for integrity_check_json_db()
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
|
|
42
|
+
#: Image path; the filenames in the .json file should be relative to this folder
|
|
43
|
+
self.baseDir = ''
|
|
44
|
+
|
|
45
|
+
#: Should we validate the image sizes?
|
|
46
|
+
self.bCheckImageSizes = False
|
|
47
|
+
|
|
48
|
+
#: Should we check that all the images in the .json file exist on disk?
|
|
49
|
+
self.bCheckImageExistence = False
|
|
50
|
+
|
|
51
|
+
#: Should we search [baseDir] for images that are not used in the .json file?
|
|
52
|
+
self.bFindUnusedImages = False
|
|
53
|
+
|
|
54
|
+
#: Should we require that all images in the .json file have a 'location' field?
|
|
55
|
+
self.bRequireLocation = True
|
|
56
|
+
|
|
57
|
+
#: For debugging, limit the number of images we'll process
|
|
58
|
+
self.iMaxNumImages = -1
|
|
59
|
+
|
|
60
|
+
#: Number of threads to use for parallelization, set to <= 1 to disable parallelization
|
|
61
|
+
self.nThreads = 10
|
|
62
|
+
|
|
63
|
+
#: Enable additional debug output
|
|
64
|
+
self.verbose = True
|
|
65
|
+
|
|
66
|
+
#: Allow integer-valued image and annotation IDs (COCO uses this, CCT files use strings)
|
|
67
|
+
self.allowIntIDs = False
|
|
68
|
+
|
|
69
|
+
# This is used in a medium-hacky way to share modified options across threads
|
|
70
|
+
defaultOptions = IntegrityCheckOptions()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#%% Functions
|
|
74
|
+
|
|
75
|
+
def _check_image_existence_and_size(image,options=None):
|
|
76
|
+
"""
|
|
77
|
+
Validate the image represented in the CCT image dict [image], which should have fields:
|
|
78
|
+
|
|
79
|
+
* file_name
|
|
80
|
+
* width
|
|
81
|
+
* height
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
image (dict): image to validate
|
|
85
|
+
options (IntegrityCheckOptions): parameters impacting validation
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
bool: whether this image passes validation
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
if options is None:
|
|
92
|
+
options = defaultOptions
|
|
93
|
+
|
|
94
|
+
assert options.bCheckImageExistence
|
|
95
|
+
|
|
96
|
+
filePath = os.path.join(options.baseDir,image['file_name'])
|
|
97
|
+
if not os.path.isfile(filePath):
|
|
98
|
+
# print('Image path {} does not exist'.format(filePath))
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if options.bCheckImageSizes:
|
|
102
|
+
if not ('height' in image and 'width' in image):
|
|
103
|
+
print('Missing image size in {}'.format(filePath))
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# width, height = Image.open(filePath).size
|
|
107
|
+
pil_im = open_image(filePath)
|
|
108
|
+
width,height = pil_im.size
|
|
109
|
+
if (not (width == image['width'] and height == image['height'])):
|
|
110
|
+
print('Size mismatch for image {}: {} (reported {},{}, actual {},{})'.format(
|
|
111
|
+
image['id'], filePath, image['width'], image['height'], width, height))
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def integrity_check_json_db(jsonFile, options=None):
|
|
118
|
+
"""
|
|
119
|
+
Does some integrity-checking and computes basic statistics on a COCO Camera Traps .json file; see
|
|
120
|
+
module header comment for a list of the validation steps.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
jsonFile (str): filename to validate, or an already-loaded dict
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
tuple: tuple containing:
|
|
127
|
+
- sortedCategories (dict): list of categories used in [jsonFile], sorted by frequency
|
|
128
|
+
- data (dict): the data loaded from [jsonFile]
|
|
129
|
+
- errorInfo (dict): specific validation errors
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if options is None:
|
|
133
|
+
options = IntegrityCheckOptions()
|
|
134
|
+
|
|
135
|
+
if options.bCheckImageSizes:
|
|
136
|
+
options.bCheckImageExistence = True
|
|
137
|
+
|
|
138
|
+
if options.verbose:
|
|
139
|
+
print(options.__dict__)
|
|
140
|
+
|
|
141
|
+
if options.baseDir is None:
|
|
142
|
+
options.baseDir = ''
|
|
143
|
+
|
|
144
|
+
baseDir = options.baseDir
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
##%% Read .json file if necessary, integrity-check fields
|
|
148
|
+
|
|
149
|
+
if isinstance(jsonFile,dict):
|
|
150
|
+
|
|
151
|
+
data = jsonFile
|
|
152
|
+
|
|
153
|
+
elif isinstance(jsonFile,str):
|
|
154
|
+
|
|
155
|
+
assert os.path.isfile(jsonFile), '.json file {} does not exist'.format(jsonFile)
|
|
156
|
+
|
|
157
|
+
if options.verbose:
|
|
158
|
+
print('Reading .json {} with base dir [{}]...'.format(
|
|
159
|
+
jsonFile,baseDir))
|
|
160
|
+
|
|
161
|
+
with open(jsonFile,'r') as f:
|
|
162
|
+
data = json.load(f)
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
|
|
166
|
+
raise ValueError('Illegal value for jsonFile')
|
|
167
|
+
|
|
168
|
+
images = data['images']
|
|
169
|
+
annotations = data['annotations']
|
|
170
|
+
categories = data['categories']
|
|
171
|
+
# info = data['info']
|
|
172
|
+
assert 'info' in data, 'No info struct in database'
|
|
173
|
+
|
|
174
|
+
if len(baseDir) > 0:
|
|
175
|
+
assert os.path.isdir(baseDir), 'Base directory {} does not exist'.format(baseDir)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
##%% Build dictionaries, checking ID uniqueness and internal validity as we go
|
|
179
|
+
|
|
180
|
+
imageIdToImage = {}
|
|
181
|
+
annIdToAnn = {}
|
|
182
|
+
catIdToCat = {}
|
|
183
|
+
catNameToCat = {}
|
|
184
|
+
imageLocationSet = set()
|
|
185
|
+
|
|
186
|
+
if options.verbose:
|
|
187
|
+
print('Checking categories...')
|
|
188
|
+
|
|
189
|
+
for cat in tqdm(categories):
|
|
190
|
+
|
|
191
|
+
# Confirm that required fields are present
|
|
192
|
+
assert 'name' in cat
|
|
193
|
+
assert 'id' in cat
|
|
194
|
+
|
|
195
|
+
assert isinstance(cat['id'],int), 'Illegal category ID type: [{}]'.format(str(cat['id']))
|
|
196
|
+
assert isinstance(cat['name'],str), 'Illegal category name type [{}]'.format(str(cat['name']))
|
|
197
|
+
|
|
198
|
+
catId = cat['id']
|
|
199
|
+
catName = cat['name']
|
|
200
|
+
|
|
201
|
+
# Confirm ID uniqueness
|
|
202
|
+
assert catId not in catIdToCat, 'Category ID {} is used more than once'.format(catId)
|
|
203
|
+
catIdToCat[catId] = cat
|
|
204
|
+
cat['_count'] = 0
|
|
205
|
+
|
|
206
|
+
assert catName not in catNameToCat, 'Category name {} is used more than once'.format(catName)
|
|
207
|
+
catNameToCat[catName] = cat
|
|
208
|
+
|
|
209
|
+
# ...for each category
|
|
210
|
+
|
|
211
|
+
if options.verbose:
|
|
212
|
+
print('\nChecking images...')
|
|
213
|
+
|
|
214
|
+
if options.iMaxNumImages > 0 and len(images) > options.iMaxNumImages:
|
|
215
|
+
|
|
216
|
+
if options.verbose:
|
|
217
|
+
print('Trimming image list to {}'.format(options.iMaxNumImages))
|
|
218
|
+
images = images[0:options.iMaxNumImages]
|
|
219
|
+
|
|
220
|
+
imagePathsInJson = set()
|
|
221
|
+
|
|
222
|
+
sequences = set()
|
|
223
|
+
|
|
224
|
+
# image = images[0]
|
|
225
|
+
for image in tqdm(images):
|
|
226
|
+
|
|
227
|
+
image['_count'] = 0
|
|
228
|
+
|
|
229
|
+
# Confirm that required fields are present
|
|
230
|
+
assert 'file_name' in image
|
|
231
|
+
assert 'id' in image
|
|
232
|
+
|
|
233
|
+
image['file_name'] = os.path.normpath(image['file_name'])
|
|
234
|
+
|
|
235
|
+
imagePathsInJson.add(image['file_name'])
|
|
236
|
+
|
|
237
|
+
assert isinstance(image['file_name'],str), 'Illegal image filename type'
|
|
238
|
+
|
|
239
|
+
if options.allowIntIDs:
|
|
240
|
+
assert isinstance(image['id'],str) or isinstance(image['id'],int), \
|
|
241
|
+
'Illegal image ID type'
|
|
242
|
+
else:
|
|
243
|
+
assert isinstance(image['id'],str), 'Illegal image ID type'
|
|
244
|
+
|
|
245
|
+
imageId = image['id']
|
|
246
|
+
|
|
247
|
+
# Confirm ID uniqueness
|
|
248
|
+
assert imageId not in imageIdToImage, 'Duplicate image ID {}'.format(imageId)
|
|
249
|
+
|
|
250
|
+
imageIdToImage[imageId] = image
|
|
251
|
+
|
|
252
|
+
if 'height' in image:
|
|
253
|
+
assert 'width' in image, 'Image with height but no width: {}'.format(image['id'])
|
|
254
|
+
|
|
255
|
+
if 'width' in image:
|
|
256
|
+
assert 'height' in image, 'Image with width but no height: {}'.format(image['id'])
|
|
257
|
+
|
|
258
|
+
if options.bRequireLocation:
|
|
259
|
+
assert 'location' in image, 'No location available for: {}'.format(image['id'])
|
|
260
|
+
|
|
261
|
+
if 'location' in image:
|
|
262
|
+
# We previously supported ints here; this should be strings now
|
|
263
|
+
# assert isinstance(image['location'], str) or isinstance(image['location'], int), \
|
|
264
|
+
# 'Illegal image location type'
|
|
265
|
+
assert isinstance(image['location'], str)
|
|
266
|
+
imageLocationSet.add(image['location'])
|
|
267
|
+
|
|
268
|
+
if 'seq_id' in image:
|
|
269
|
+
sequences.add(image['seq_id'])
|
|
270
|
+
|
|
271
|
+
assert not ('sequence_id' in image or 'sequence' in image), 'Illegal sequence identifier'
|
|
272
|
+
|
|
273
|
+
unusedFiles = []
|
|
274
|
+
|
|
275
|
+
# Are we checking for unused images?
|
|
276
|
+
if (len(baseDir) > 0) and options.bFindUnusedImages:
|
|
277
|
+
|
|
278
|
+
if options.verbose:
|
|
279
|
+
print('\nEnumerating images...')
|
|
280
|
+
|
|
281
|
+
# Recursively enumerate images
|
|
282
|
+
imagePaths = []
|
|
283
|
+
for root, dirs, files in os.walk(baseDir):
|
|
284
|
+
for file in files:
|
|
285
|
+
if file.lower().endswith(('.jpeg', '.jpg', '.png')):
|
|
286
|
+
relDir = os.path.relpath(root, baseDir)
|
|
287
|
+
relFile = os.path.join(relDir,file)
|
|
288
|
+
relFile = os.path.normpath(relFile)
|
|
289
|
+
if len(relFile) > 2 and \
|
|
290
|
+
(relFile[0:2] == './' or relFile[0:2] == '.\\'):
|
|
291
|
+
relFile = relFile[2:]
|
|
292
|
+
imagePaths.append(relFile)
|
|
293
|
+
|
|
294
|
+
for p in imagePaths:
|
|
295
|
+
if p not in imagePathsInJson:
|
|
296
|
+
# print('Image {} is unused'.format(p))
|
|
297
|
+
unusedFiles.append(p)
|
|
298
|
+
|
|
299
|
+
validationErrors = []
|
|
300
|
+
|
|
301
|
+
# Are we checking file existence and/or image size?
|
|
302
|
+
if options.bCheckImageSizes or options.bCheckImageExistence:
|
|
303
|
+
|
|
304
|
+
if len(baseDir) == 0:
|
|
305
|
+
print('Warning: checking image sizes without a base directory, assuming "."')
|
|
306
|
+
|
|
307
|
+
if options.verbose:
|
|
308
|
+
print('Checking image existence and/or image sizes...')
|
|
309
|
+
|
|
310
|
+
if options.nThreads is not None and options.nThreads > 1:
|
|
311
|
+
pool = ThreadPool(options.nThreads)
|
|
312
|
+
# results = pool.imap_unordered(lambda x: fetch_url(x,nImages), indexedUrlList)
|
|
313
|
+
defaultOptions.baseDir = options.baseDir
|
|
314
|
+
defaultOptions.bCheckImageSizes = options.bCheckImageSizes
|
|
315
|
+
defaultOptions.bCheckImageExistence = options.bCheckImageExistence
|
|
316
|
+
results = tqdm(pool.imap(_check_image_existence_and_size, images), total=len(images))
|
|
317
|
+
else:
|
|
318
|
+
results = []
|
|
319
|
+
for im in tqdm(images):
|
|
320
|
+
results.append(_check_image_existence_and_size(im,options))
|
|
321
|
+
|
|
322
|
+
for iImage,r in enumerate(results):
|
|
323
|
+
if not r:
|
|
324
|
+
validationErrors.append(os.path.join(options.baseDir,images[iImage]['file_name']))
|
|
325
|
+
|
|
326
|
+
# ...for each image
|
|
327
|
+
|
|
328
|
+
if options.verbose:
|
|
329
|
+
print('{} validation errors (of {})'.format(len(validationErrors),len(images)))
|
|
330
|
+
print('Checking annotations...')
|
|
331
|
+
|
|
332
|
+
nBoxes = 0
|
|
333
|
+
|
|
334
|
+
for ann in tqdm(annotations):
|
|
335
|
+
|
|
336
|
+
# Confirm that required fields are present
|
|
337
|
+
assert 'image_id' in ann
|
|
338
|
+
assert 'id' in ann
|
|
339
|
+
assert 'category_id' in ann
|
|
340
|
+
|
|
341
|
+
if options.allowIntIDs:
|
|
342
|
+
assert isinstance(ann['id'],str) or isinstance(ann['id'],int), \
|
|
343
|
+
'Illegal annotation ID type'
|
|
344
|
+
assert isinstance(ann['image_id'],str) or isinstance(ann['image_id'],int), \
|
|
345
|
+
'Illegal annotation image ID type'
|
|
346
|
+
else:
|
|
347
|
+
assert isinstance(ann['id'],str), 'Illegal annotation ID type'
|
|
348
|
+
assert isinstance(ann['image_id'],str), 'Illegal annotation image ID type'
|
|
349
|
+
|
|
350
|
+
assert isinstance(ann['category_id'],int), 'Illegal annotation category ID type'
|
|
351
|
+
|
|
352
|
+
if 'bbox' in ann:
|
|
353
|
+
nBoxes += 1
|
|
354
|
+
|
|
355
|
+
annId = ann['id']
|
|
356
|
+
|
|
357
|
+
# Confirm ID uniqueness
|
|
358
|
+
assert annId not in annIdToAnn
|
|
359
|
+
annIdToAnn[annId] = ann
|
|
360
|
+
|
|
361
|
+
# Confirm validity
|
|
362
|
+
assert ann['category_id'] in catIdToCat, \
|
|
363
|
+
'Category {} not found in category list'.format(ann['category_id'])
|
|
364
|
+
assert ann['image_id'] in imageIdToImage, \
|
|
365
|
+
'Image ID {} referred to by annotation {}, not available'.format(
|
|
366
|
+
ann['image_id'],ann['id'])
|
|
367
|
+
|
|
368
|
+
imageIdToImage[ann['image_id']]['_count'] += 1
|
|
369
|
+
catIdToCat[ann['category_id']]['_count'] +=1
|
|
370
|
+
|
|
371
|
+
# ...for each annotation
|
|
372
|
+
|
|
373
|
+
sortedCategories = sorted(categories, key=itemgetter('_count'), reverse=True)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
##%% Print statistics
|
|
377
|
+
|
|
378
|
+
if options.verbose:
|
|
379
|
+
|
|
380
|
+
# Find un-annotated images and multi-annotation images
|
|
381
|
+
nUnannotated = 0
|
|
382
|
+
nMultiAnnotated = 0
|
|
383
|
+
|
|
384
|
+
for image in images:
|
|
385
|
+
if image['_count'] == 0:
|
|
386
|
+
nUnannotated += 1
|
|
387
|
+
elif image['_count'] > 1:
|
|
388
|
+
nMultiAnnotated += 1
|
|
389
|
+
|
|
390
|
+
print('Found {} unannotated images, {} images with multiple annotations'.format(
|
|
391
|
+
nUnannotated,nMultiAnnotated))
|
|
392
|
+
|
|
393
|
+
if (len(baseDir) > 0) and options.bFindUnusedImages:
|
|
394
|
+
print('Found {} unused image files'.format(len(unusedFiles)))
|
|
395
|
+
|
|
396
|
+
nUnusedCategories = 0
|
|
397
|
+
|
|
398
|
+
# Find unused categories
|
|
399
|
+
for cat in categories:
|
|
400
|
+
if cat['_count'] == 0:
|
|
401
|
+
print('Unused category: {}'.format(cat['name']))
|
|
402
|
+
nUnusedCategories += 1
|
|
403
|
+
|
|
404
|
+
print('Found {} unused categories'.format(nUnusedCategories))
|
|
405
|
+
|
|
406
|
+
sequenceString = 'no sequence info'
|
|
407
|
+
if len(sequences) > 0:
|
|
408
|
+
sequenceString = '{} sequences'.format(len(sequences))
|
|
409
|
+
|
|
410
|
+
print('\nDB contains {} images, {} annotations, {} bboxes, {} categories, {}\n'.format(
|
|
411
|
+
len(images),len(annotations),nBoxes,len(categories),sequenceString))
|
|
412
|
+
|
|
413
|
+
if len(imageLocationSet) > 0:
|
|
414
|
+
print('DB contains images from {} locations\n'.format(len(imageLocationSet)))
|
|
415
|
+
|
|
416
|
+
print('Categories and annotation (not image) counts:\n')
|
|
417
|
+
|
|
418
|
+
for cat in sortedCategories:
|
|
419
|
+
print('{:6} {}'.format(cat['_count'],cat['name']))
|
|
420
|
+
|
|
421
|
+
print('')
|
|
422
|
+
|
|
423
|
+
errorInfo = {}
|
|
424
|
+
errorInfo['unusedFiles'] = unusedFiles
|
|
425
|
+
errorInfo['validationErrors'] = validationErrors
|
|
426
|
+
|
|
427
|
+
return sortedCategories, data, errorInfo
|
|
428
|
+
|
|
429
|
+
# ...def integrity_check_json_db()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
#%% Command-line driver
|
|
433
|
+
|
|
434
|
+
def main():
|
|
435
|
+
|
|
436
|
+
parser = argparse.ArgumentParser()
|
|
437
|
+
parser.add_argument('jsonFile')
|
|
438
|
+
parser.add_argument('--bCheckImageSizes', action='store_true',
|
|
439
|
+
help='Validate image size, requires baseDir to be specified. ' + \
|
|
440
|
+
'Implies existence checking.')
|
|
441
|
+
parser.add_argument('--bCheckImageExistence', action='store_true',
|
|
442
|
+
help='Validate image existence, requires baseDir to be specified')
|
|
443
|
+
parser.add_argument('--bFindUnusedImages', action='store_true',
|
|
444
|
+
help='Check for images in baseDir that aren\'t in the database, ' + \
|
|
445
|
+
'requires baseDir to be specified')
|
|
446
|
+
parser.add_argument('--baseDir', action='store', type=str, default='',
|
|
447
|
+
help='Base directory for images')
|
|
448
|
+
parser.add_argument('--bAllowNoLocation', action='store_true',
|
|
449
|
+
help='Disable errors when no location is specified for an image')
|
|
450
|
+
parser.add_argument('--iMaxNumImages', action='store', type=int, default=-1,
|
|
451
|
+
help='Cap on total number of images to check')
|
|
452
|
+
parser.add_argument('--nThreads', action='store', type=int, default=10,
|
|
453
|
+
help='Number of threads (only relevant when verifying image ' + \
|
|
454
|
+
'sizes and/or existence)')
|
|
455
|
+
|
|
456
|
+
if len(sys.argv[1:])==0:
|
|
457
|
+
parser.print_help()
|
|
458
|
+
parser.exit()
|
|
459
|
+
|
|
460
|
+
args = parser.parse_args()
|
|
461
|
+
args.bRequireLocation = (not args.bAllowNoLocation)
|
|
462
|
+
options = IntegrityCheckOptions()
|
|
463
|
+
ct_utils.args_to_object(args, options)
|
|
464
|
+
integrity_check_json_db(args.jsonFile,options)
|
|
465
|
+
|
|
466
|
+
if __name__ == '__main__':
|
|
467
|
+
main()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
#%% Interactive driver(s)
|
|
471
|
+
|
|
472
|
+
if False:
|
|
473
|
+
|
|
474
|
+
#%%
|
|
475
|
+
|
|
476
|
+
"""
|
|
477
|
+
python integrity_check_json_db.py ~/data/ena24.json --baseDir ~/data/ENA24 --bAllowNoLocation
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
# Integrity-check .json files for LILA
|
|
481
|
+
json_files = [os.path.expanduser('~/data/ena24.json')]
|
|
482
|
+
|
|
483
|
+
options = IntegrityCheckOptions()
|
|
484
|
+
options.baseDir = os.path.expanduser('~/data/ENA24')
|
|
485
|
+
options.bCheckImageSizes = False
|
|
486
|
+
options.bFindUnusedImages = True
|
|
487
|
+
options.bRequireLocation = False
|
|
488
|
+
|
|
489
|
+
# options.iMaxNumImages = 10
|
|
490
|
+
|
|
491
|
+
for json_file in json_files:
|
|
492
|
+
|
|
493
|
+
sortedCategories,data,_ = integrity_check_json_db(json_file, options)
|