megadetector 10.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/__init__.py +0 -0
- megadetector/api/__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/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 +626 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +853 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +608 -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 +696 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +509 -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/animl_to_md.py +161 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +33 -0
- megadetector/data_management/camtrap_dp_to_coco.py +270 -0
- megadetector/data_management/cct_json_utils.py +566 -0
- megadetector/data_management/cct_to_md.py +184 -0
- megadetector/data_management/cct_to_wi.py +293 -0
- megadetector/data_management/coco_to_labelme.py +284 -0
- megadetector/data_management/coco_to_yolo.py +702 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
- megadetector/data_management/databases/integrity_check_json_db.py +528 -0
- megadetector/data_management/databases/subset_json_db.py +195 -0
- megadetector/data_management/generate_crops_from_cct.py +200 -0
- megadetector/data_management/get_image_sizes.py +164 -0
- megadetector/data_management/labelme_to_coco.py +559 -0
- megadetector/data_management/labelme_to_yolo.py +349 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/create_lila_blank_set.py +556 -0
- megadetector/data_management/lila/create_lila_test_set.py +187 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +182 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +319 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
- megadetector/data_management/mewc_to_md.py +344 -0
- megadetector/data_management/ocr_tools.py +873 -0
- megadetector/data_management/read_exif.py +964 -0
- megadetector/data_management/remap_coco_categories.py +195 -0
- megadetector/data_management/remove_exif.py +156 -0
- megadetector/data_management/rename_images.py +194 -0
- megadetector/data_management/resize_coco_dataset.py +663 -0
- megadetector/data_management/speciesnet_to_md.py +41 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +594 -0
- megadetector/data_management/yolo_to_coco.py +876 -0
- megadetector/data_management/zamba_to_md.py +188 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/change_detection.py +840 -0
- megadetector/detection/process_video.py +479 -0
- megadetector/detection/pytorch_detector.py +1451 -0
- megadetector/detection/run_detector.py +1267 -0
- megadetector/detection/run_detector_batch.py +2159 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
- megadetector/detection/run_md_and_speciesnet.py +1494 -0
- megadetector/detection/run_tiled_inference.py +1038 -0
- megadetector/detection/tf_detector.py +209 -0
- megadetector/detection/video_utils.py +1379 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +72 -0
- megadetector/postprocessing/categorize_detections_by_size.py +166 -0
- megadetector/postprocessing/classification_postprocessing.py +1752 -0
- megadetector/postprocessing/combine_batch_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +2110 -0
- megadetector/postprocessing/convert_output_format.py +403 -0
- megadetector/postprocessing/create_crop_folder.py +629 -0
- megadetector/postprocessing/detector_calibration.py +570 -0
- megadetector/postprocessing/generate_csv_report.py +522 -0
- megadetector/postprocessing/load_api_results.py +223 -0
- megadetector/postprocessing/md_to_coco.py +428 -0
- megadetector/postprocessing/md_to_labelme.py +351 -0
- megadetector/postprocessing/md_to_wi.py +41 -0
- megadetector/postprocessing/merge_detections.py +392 -0
- megadetector/postprocessing/postprocess_batch_results.py +2077 -0
- megadetector/postprocessing/remap_detection_categories.py +226 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
- megadetector/postprocessing/separate_detections_into_folders.py +795 -0
- megadetector/postprocessing/subset_json_detector_output.py +964 -0
- megadetector/postprocessing/top_folders_to_bottom.py +238 -0
- megadetector/postprocessing/validate_batch_results.py +332 -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 +213 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +224 -0
- megadetector/taxonomy_mapping/species_lookup.py +1008 -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/tests/__init__.py +0 -0
- megadetector/tests/test_nms_synthetic.py +335 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/ct_utils.py +1857 -0
- megadetector/utils/directory_listing.py +199 -0
- megadetector/utils/extract_frames_from_video.py +307 -0
- megadetector/utils/gpu_test.py +125 -0
- megadetector/utils/md_tests.py +2072 -0
- megadetector/utils/path_utils.py +2832 -0
- megadetector/utils/process_utils.py +172 -0
- megadetector/utils/split_locations_into_train_val.py +237 -0
- megadetector/utils/string_utils.py +234 -0
- megadetector/utils/url_utils.py +825 -0
- megadetector/utils/wi_platform_utils.py +968 -0
- megadetector/utils/wi_taxonomy_utils.py +1759 -0
- megadetector/utils/write_html_image_list.py +239 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +309 -0
- megadetector/visualization/render_images_with_thumbnails.py +243 -0
- megadetector/visualization/visualization_utils.py +1940 -0
- megadetector/visualization/visualize_db.py +630 -0
- megadetector/visualization/visualize_detector_output.py +479 -0
- megadetector/visualization/visualize_video_output.py +705 -0
- megadetector-10.0.13.dist-info/METADATA +134 -0
- megadetector-10.0.13.dist-info/RECORD +147 -0
- megadetector-10.0.13.dist-info/WHEEL +5 -0
- megadetector-10.0.13.dist-info/licenses/LICENSE +19 -0
- megadetector-10.0.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
|
|
3
|
+
subset_json_detector_output.py
|
|
4
|
+
|
|
5
|
+
Creates one or more subsets of a detector results file (.json), doing either
|
|
6
|
+
or both of the following (if both are requested, they happen in this order):
|
|
7
|
+
|
|
8
|
+
1) Retrieve all elements where filenames contain a specified query string,
|
|
9
|
+
optionally replacing that query with a replacement token. If the query is blank,
|
|
10
|
+
can also be used to prepend content to all filenames.
|
|
11
|
+
|
|
12
|
+
Does not support regex's, but supports a special case of ^string to indicate "must start with
|
|
13
|
+
to match".
|
|
14
|
+
|
|
15
|
+
2) Create separate .jsons for each unique path, optionally making the filenames
|
|
16
|
+
in those .json's relative paths. In this case, you specify an output directory,
|
|
17
|
+
rather than an output path. All images in the folder blah/foo/bar will end up
|
|
18
|
+
in a .json file called blah_foo_bar.json.
|
|
19
|
+
|
|
20
|
+
Can also apply a confidence threshold.
|
|
21
|
+
|
|
22
|
+
Can also subset by categories above a threshold (programmatic invocation only, this is
|
|
23
|
+
not supported at the command line yet).
|
|
24
|
+
|
|
25
|
+
To subset a COCO Camera Traps .json database, see subset_json_db.py
|
|
26
|
+
|
|
27
|
+
**Sample invocation (splitting into multiple json's)**
|
|
28
|
+
|
|
29
|
+
Read from "1800_idfg_statewide_wolf_detections_w_classifications.json", split up into
|
|
30
|
+
individual .jsons in 'd:/temp/idfg/output', making filenames relative to their individual
|
|
31
|
+
folders:
|
|
32
|
+
|
|
33
|
+
python subset_json_detector_output.py ^
|
|
34
|
+
"d:/temp/idfg/1800_idfg_statewide_wolf_detections_w_classifications.json" "d:/temp/idfg/output" ^
|
|
35
|
+
--split_folders --make_folder_relative
|
|
36
|
+
|
|
37
|
+
Now do the same thing, but instead of writing .json's to d:/temp/idfg/output, write them to *subfolders*
|
|
38
|
+
corresponding to the subfolders for each .json file.
|
|
39
|
+
|
|
40
|
+
python subset_json_detector_output.py ^
|
|
41
|
+
"d:/temp/idfg/1800_detections_S2.json" "d:/temp/idfg/output_to_folders" ^
|
|
42
|
+
--split_folders --make_folder_relative --copy_jsons_to_folders
|
|
43
|
+
|
|
44
|
+
**Sample invocation (creating a single subset matching a query)**
|
|
45
|
+
|
|
46
|
+
Read from "1800_detections.json", write to "1800_detections_2017.json"
|
|
47
|
+
|
|
48
|
+
Include only images matching "2017", and change "2017" to "blah"
|
|
49
|
+
|
|
50
|
+
python subset_json_detector_output.py "d:/temp/1800_detections.json" "d:/temp/1800_detections_2017_blah.json" ^
|
|
51
|
+
--query 2017 --replacement blah
|
|
52
|
+
|
|
53
|
+
Include all images, prepend with "prefix/"
|
|
54
|
+
|
|
55
|
+
python subset_json_detector_output.py "d:/temp/1800_detections.json" "d:/temp/1800_detections_prefix.json" ^
|
|
56
|
+
--replacement "prefix/"
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
#%% Constants and imports
|
|
61
|
+
|
|
62
|
+
import argparse
|
|
63
|
+
import sys
|
|
64
|
+
import copy
|
|
65
|
+
import json
|
|
66
|
+
import os
|
|
67
|
+
import re
|
|
68
|
+
|
|
69
|
+
from tqdm import tqdm
|
|
70
|
+
|
|
71
|
+
from megadetector.utils import ct_utils
|
|
72
|
+
from megadetector.utils.ct_utils import args_to_object, get_max_conf, invert_dictionary
|
|
73
|
+
from megadetector.utils.path_utils import recursive_file_list
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#%% Helper classes
|
|
77
|
+
|
|
78
|
+
class SubsetJsonDetectorOutputOptions:
|
|
79
|
+
"""
|
|
80
|
+
Options used to parameterize subset_json_detector_output()
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self):
|
|
84
|
+
|
|
85
|
+
#: Only process files containing the token 'query'
|
|
86
|
+
#:
|
|
87
|
+
#: Does not support general regexes, but supports ^ as a special case
|
|
88
|
+
#: regex-like notation for "starts with"
|
|
89
|
+
self.query = None
|
|
90
|
+
|
|
91
|
+
#: Replace 'query' with 'replacement' if 'replacement' is not None. If 'query' is None,
|
|
92
|
+
#: prepend 'replacement'
|
|
93
|
+
self.replacement = None
|
|
94
|
+
|
|
95
|
+
#: Should we split output into individual .json files for each folder?
|
|
96
|
+
self.split_folders = False
|
|
97
|
+
|
|
98
|
+
#: Folder level to use for splitting ['bottom','n_from_bottom','n_from_top','dict']
|
|
99
|
+
#:
|
|
100
|
+
#: 'dict' requires 'split_folder_param' to be a dictionary mapping each filename
|
|
101
|
+
#: to a token.
|
|
102
|
+
self.split_folder_mode = 'bottom'
|
|
103
|
+
|
|
104
|
+
#: When using the 'n_from_bottom' parameter to define folder splitting, this
|
|
105
|
+
#: defines the number of directories from the bottom. 'n_from_bottom' with
|
|
106
|
+
#: a parameter of zero is the same as 'bottom'.
|
|
107
|
+
#:
|
|
108
|
+
#: Same story with 'n_from_top'.
|
|
109
|
+
#:
|
|
110
|
+
#: When 'split_folder_mode' is 'dict', this should be a dictionary mapping each filename
|
|
111
|
+
#: to a token.
|
|
112
|
+
self.split_folder_param = 0
|
|
113
|
+
|
|
114
|
+
#: Only meaningful if split_folders is True: should we convert pathnames to be relative
|
|
115
|
+
#: the folder for each .json file?
|
|
116
|
+
self.make_folder_relative = False
|
|
117
|
+
|
|
118
|
+
#: Only meaningful if split_folders and make_folder_relative are True: if not None,
|
|
119
|
+
#: will copy .json files to their corresponding output directories, relative to
|
|
120
|
+
#: output_filename
|
|
121
|
+
self.copy_jsons_to_folders = False
|
|
122
|
+
|
|
123
|
+
#: Should we over-write .json files?
|
|
124
|
+
self.overwrite_json_files = False
|
|
125
|
+
|
|
126
|
+
#: If copy_jsons_to_folders is true, do we require that directories already exist?
|
|
127
|
+
self.copy_jsons_to_folders_directories_must_exist = True
|
|
128
|
+
|
|
129
|
+
#: Optional confidence threshold; if not None, detections below this confidence won't be
|
|
130
|
+
#: included in the output.
|
|
131
|
+
self.confidence_threshold = None
|
|
132
|
+
|
|
133
|
+
#: Should we remove failed images?
|
|
134
|
+
self.remove_failed_images = False
|
|
135
|
+
|
|
136
|
+
#: Either a list of category IDs (as string-ints) (not names), or a dictionary mapping category *IDs*
|
|
137
|
+
#: (as string-ints) (not names) to thresholds. Removes non-matching detections, does not
|
|
138
|
+
#: remove images. Not technically mutually exclusize with category_names_to_keep, but it's an esoteric
|
|
139
|
+
#: scenario indeed where you would want to specify both.
|
|
140
|
+
self.categories_to_keep = None
|
|
141
|
+
|
|
142
|
+
#: Either a list of category names (not IDs), or a dictionary mapping category *names* (not IDs) to thresholds.
|
|
143
|
+
#: Removes non-matching detections, does not remove images. Not technically mutually exclusize with
|
|
144
|
+
#: category_ids_to_keep, but it's an esoteric scenario indeed where you would want to specify both.
|
|
145
|
+
self.category_names_to_keep = None
|
|
146
|
+
|
|
147
|
+
#: Set to >0 during testing to limit the number of images that get processed.
|
|
148
|
+
self.debug_max_images = -1
|
|
149
|
+
|
|
150
|
+
#: Keep only files in this list, which can be a list, a .json results file, or a folder.
|
|
151
|
+
#
|
|
152
|
+
#: Assumes that the input .json file contains relative paths when comparing to a folder.
|
|
153
|
+
self.keep_files_in_list = None
|
|
154
|
+
|
|
155
|
+
#: Remove classification with <= N instances. Does not re-map categories
|
|
156
|
+
#: to be contiguous. Set to 1 to remove empty categories only.
|
|
157
|
+
self.remove_classification_categories_below_count = None
|
|
158
|
+
|
|
159
|
+
#: Remove detections above a threshold size (as a fraction of the image size)
|
|
160
|
+
self.maximum_detection_size = None
|
|
161
|
+
|
|
162
|
+
#: Remove detections below a threshold size (as a fraction of the image size)
|
|
163
|
+
self.minimum_detection_size = None
|
|
164
|
+
|
|
165
|
+
# ...class SubsetJsonDetectorOutputOptions
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#%% Main function
|
|
169
|
+
|
|
170
|
+
def _write_detection_results(data, output_filename, options):
|
|
171
|
+
"""
|
|
172
|
+
Writes the detector-output-formatted dict *data* to *output_filename*.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if (not options.overwrite_json_files) and os.path.isfile(output_filename):
|
|
176
|
+
raise ValueError('File {} exists'.format(output_filename))
|
|
177
|
+
|
|
178
|
+
basedir = os.path.dirname(output_filename)
|
|
179
|
+
|
|
180
|
+
if options.copy_jsons_to_folders and options.copy_jsons_to_folders_directories_must_exist:
|
|
181
|
+
if not os.path.isdir(basedir):
|
|
182
|
+
raise ValueError('Directory {} does not exist'.format(basedir))
|
|
183
|
+
else:
|
|
184
|
+
os.makedirs(basedir, exist_ok=True)
|
|
185
|
+
|
|
186
|
+
n_images = len(data['images'])
|
|
187
|
+
|
|
188
|
+
print('Writing detection output (with {} images) to {}'.format(n_images,output_filename))
|
|
189
|
+
ct_utils.write_json(output_filename, data)
|
|
190
|
+
|
|
191
|
+
# ...def _write_detection_results(...)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def remove_classification_categories_below_count(data, options):
|
|
195
|
+
"""
|
|
196
|
+
Removes all classification categories below a threshold count. Does not re-map
|
|
197
|
+
classification category IDs.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
data (dict): data loaded from a MD results file
|
|
201
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if options.remove_classification_categories_below_count is None:
|
|
208
|
+
return data
|
|
209
|
+
if 'classification_categories' not in data:
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
classification_category_id_to_count = {}
|
|
213
|
+
|
|
214
|
+
for classification_category_id in data['classification_categories']:
|
|
215
|
+
classification_category_id_to_count[classification_category_id] = 0
|
|
216
|
+
|
|
217
|
+
# Count the number of occurrences of each classification category
|
|
218
|
+
for im in data['images']:
|
|
219
|
+
if 'detections' not in im or im['detections'] is None:
|
|
220
|
+
continue
|
|
221
|
+
for det in im['detections']:
|
|
222
|
+
if 'classifications' not in det:
|
|
223
|
+
continue
|
|
224
|
+
for classification in det['classifications']:
|
|
225
|
+
classification_category_id_to_count[classification[0]] = \
|
|
226
|
+
classification_category_id_to_count[classification[0]] + 1
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Which categories have above-threshold counts?
|
|
230
|
+
classification_category_ids_to_keep = set()
|
|
231
|
+
|
|
232
|
+
for classification_category_id in classification_category_id_to_count:
|
|
233
|
+
if classification_category_id_to_count[classification_category_id] >= \
|
|
234
|
+
options.remove_classification_categories_below_count:
|
|
235
|
+
classification_category_ids_to_keep.add(classification_category_id)
|
|
236
|
+
|
|
237
|
+
n_categories_removed = \
|
|
238
|
+
len(classification_category_id_to_count) - \
|
|
239
|
+
len(classification_category_ids_to_keep)
|
|
240
|
+
|
|
241
|
+
print('Removing {} of {} classification categories'.format(
|
|
242
|
+
n_categories_removed,len(classification_category_id_to_count)))
|
|
243
|
+
|
|
244
|
+
if n_categories_removed == 0:
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
# Filter the category list
|
|
248
|
+
output_classification_categories = {}
|
|
249
|
+
for category_id in data['classification_categories']:
|
|
250
|
+
if category_id in classification_category_ids_to_keep:
|
|
251
|
+
output_classification_categories[category_id] = \
|
|
252
|
+
data['classification_categories'][category_id]
|
|
253
|
+
data['classification_categories'] = output_classification_categories
|
|
254
|
+
assert len(data['classification_categories']) == len(classification_category_ids_to_keep)
|
|
255
|
+
|
|
256
|
+
# If necessary, filter the category descriptions
|
|
257
|
+
if 'classification_category_descriptions' in data:
|
|
258
|
+
output_classification_category_descriptions = {}
|
|
259
|
+
for category_id in data['classification_category_descriptions']:
|
|
260
|
+
if category_id in classification_category_ids_to_keep:
|
|
261
|
+
output_classification_category_descriptions[category_id] = \
|
|
262
|
+
data['classification_category_descriptions'][category_id]
|
|
263
|
+
data['classification_category_descriptions'] = output_classification_category_descriptions
|
|
264
|
+
|
|
265
|
+
# Filter images
|
|
266
|
+
for im in data['images']:
|
|
267
|
+
if 'detections' not in im or im['detections'] is None:
|
|
268
|
+
continue
|
|
269
|
+
for det in im['detections']:
|
|
270
|
+
if 'classifications' not in det:
|
|
271
|
+
continue
|
|
272
|
+
classifications_to_keep = []
|
|
273
|
+
for classification in det['classifications']:
|
|
274
|
+
if classification[0] in classification_category_ids_to_keep:
|
|
275
|
+
classifications_to_keep.append(classification)
|
|
276
|
+
det['classifications'] = classifications_to_keep
|
|
277
|
+
|
|
278
|
+
return data
|
|
279
|
+
|
|
280
|
+
# ...def remove_classification_categories_below_count(...)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def subset_json_detector_output_by_size(data, options):
|
|
284
|
+
"""
|
|
285
|
+
Remove detections above or below threshold sizes (as a fraction
|
|
286
|
+
of the image size).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
data (dict): data loaded from a MD results file
|
|
290
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
if (options.maximum_detection_size is None) and \
|
|
297
|
+
(options.minimum_detection_size is None):
|
|
298
|
+
return data
|
|
299
|
+
|
|
300
|
+
if options.maximum_detection_size is None:
|
|
301
|
+
options.maximum_detection_size = 1000
|
|
302
|
+
|
|
303
|
+
if options.minimum_detection_size is None:
|
|
304
|
+
options.minimum_detection_size = -1000
|
|
305
|
+
|
|
306
|
+
print('Subsetting by size ({} <--> {})'.format(
|
|
307
|
+
options.minimum_detection_size,
|
|
308
|
+
options.maximum_detection_size))
|
|
309
|
+
|
|
310
|
+
images_in = data['images']
|
|
311
|
+
images_out = []
|
|
312
|
+
|
|
313
|
+
# im = images_in[0]
|
|
314
|
+
for i_image, im in tqdm(enumerate(images_in), total=len(images_in)):
|
|
315
|
+
|
|
316
|
+
# Always keep failed images; if the caller wants to remove these, they
|
|
317
|
+
# will use remove_failed_images
|
|
318
|
+
if ('detections' not in im) or (im['detections'] is None):
|
|
319
|
+
images_out.append(im)
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
detections_to_keep = []
|
|
323
|
+
|
|
324
|
+
for det in im['detections']:
|
|
325
|
+
|
|
326
|
+
# [x_min, y_min, width_of_box, height_of_box]
|
|
327
|
+
detection_size = det['bbox'][2] * det['bbox'][3]
|
|
328
|
+
|
|
329
|
+
if (detection_size >= options.minimum_detection_size) and \
|
|
330
|
+
(detection_size <= options.maximum_detection_size):
|
|
331
|
+
detections_to_keep.append(det)
|
|
332
|
+
|
|
333
|
+
im['detections'] = detections_to_keep
|
|
334
|
+
|
|
335
|
+
images_out.append(im)
|
|
336
|
+
|
|
337
|
+
# ...for each image
|
|
338
|
+
|
|
339
|
+
data['images'] = images_out
|
|
340
|
+
print('done, found {} matches (of {})'.format(
|
|
341
|
+
len(data['images']),len(images_in)))
|
|
342
|
+
|
|
343
|
+
return data
|
|
344
|
+
|
|
345
|
+
# ...def subset_json_detector_output_by_size(...)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def subset_json_detector_output_by_confidence(data, options):
|
|
349
|
+
"""
|
|
350
|
+
Removes all detections below options.confidence_threshold.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
data (dict): data loaded from a MD results file
|
|
354
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
if options.confidence_threshold is None:
|
|
361
|
+
return data
|
|
362
|
+
|
|
363
|
+
images_in = data['images']
|
|
364
|
+
images_out = []
|
|
365
|
+
|
|
366
|
+
print('Subsetting by confidence >= {}'.format(options.confidence_threshold))
|
|
367
|
+
|
|
368
|
+
n_max_changes = 0
|
|
369
|
+
|
|
370
|
+
# im = images_in[0]
|
|
371
|
+
for i_image, im in tqdm(enumerate(images_in), total=len(images_in)):
|
|
372
|
+
|
|
373
|
+
# Always keep failed images; if the caller wants to remove these, they
|
|
374
|
+
# will use remove_failed_images
|
|
375
|
+
if ('detections' not in im) or (im['detections'] is None):
|
|
376
|
+
images_out.append(im)
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
p_orig = get_max_conf(im)
|
|
380
|
+
|
|
381
|
+
# Find all detections above threshold for this image
|
|
382
|
+
detections = [d for d in im['detections'] if d['conf'] >= options.confidence_threshold]
|
|
383
|
+
|
|
384
|
+
# If there are no detections above threshold, set the max probability
|
|
385
|
+
# to -1, unless it already had a negative probability.
|
|
386
|
+
if len(detections) == 0:
|
|
387
|
+
if p_orig <= 0:
|
|
388
|
+
p = p_orig
|
|
389
|
+
else:
|
|
390
|
+
p = -1
|
|
391
|
+
|
|
392
|
+
# Otherwise find the max confidence
|
|
393
|
+
else:
|
|
394
|
+
p = max([d['conf'] for d in detections])
|
|
395
|
+
|
|
396
|
+
im['detections'] = detections
|
|
397
|
+
|
|
398
|
+
# Did this thresholding result in a max-confidence change?
|
|
399
|
+
if abs(p_orig - p) > 0.00001:
|
|
400
|
+
|
|
401
|
+
# We should only be *lowering* max confidence values (i.e., making them negative)
|
|
402
|
+
assert (p_orig <= 0) or (p < p_orig), \
|
|
403
|
+
'Confidence changed from {} to {}'.format(p_orig, p)
|
|
404
|
+
n_max_changes += 1
|
|
405
|
+
|
|
406
|
+
if 'max_detection_conf' in im:
|
|
407
|
+
im['max_detection_conf'] = p
|
|
408
|
+
|
|
409
|
+
images_out.append(im)
|
|
410
|
+
|
|
411
|
+
# ...for each image
|
|
412
|
+
|
|
413
|
+
data['images'] = images_out
|
|
414
|
+
print('done, found {} matches (of {}), {} max conf changes'.format(
|
|
415
|
+
len(data['images']),len(images_in),n_max_changes))
|
|
416
|
+
|
|
417
|
+
return data
|
|
418
|
+
|
|
419
|
+
# ...def subset_json_detector_output_by_confidence(...)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def subset_json_detector_output_by_list(data, options):
|
|
423
|
+
"""
|
|
424
|
+
Keeps only files in options.keep_files_in_list, which can be a .json results file or a folder.
|
|
425
|
+
Assumes that the input .json file contains relative paths when comparing to a folder.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
data (dict): data loaded from a MD results file
|
|
429
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
if options.keep_files_in_list is None:
|
|
436
|
+
return data
|
|
437
|
+
|
|
438
|
+
files_to_keep = None
|
|
439
|
+
|
|
440
|
+
if isinstance(options.keep_files_in_list,list):
|
|
441
|
+
files_to_keep = options.keep_files_in_list
|
|
442
|
+
elif os.path.isfile(options.keep_files_in_list):
|
|
443
|
+
with open(options.keep_files_in_list,'r') as f:
|
|
444
|
+
d = json.load(f)
|
|
445
|
+
files_to_keep = [im['file'] for im in d['images']]
|
|
446
|
+
elif os.path.isdir(options.keep_files_in_list):
|
|
447
|
+
files_to_keep = \
|
|
448
|
+
recursive_file_list(options.keep_files_in_list,return_relative_paths=True)
|
|
449
|
+
else:
|
|
450
|
+
raise ValueError('Subsetting .json file by list: {} is neither a .json results file nor a folder'.format(
|
|
451
|
+
options.keep_files_in_list))
|
|
452
|
+
|
|
453
|
+
files_to_keep = [fn.replace('\\','/') for fn in files_to_keep]
|
|
454
|
+
files_to_keep_set = set(files_to_keep)
|
|
455
|
+
|
|
456
|
+
images_to_keep = []
|
|
457
|
+
|
|
458
|
+
for im in data['images']:
|
|
459
|
+
fn = im['file'].replace('\\','/')
|
|
460
|
+
if fn in files_to_keep_set:
|
|
461
|
+
images_to_keep.append(im)
|
|
462
|
+
|
|
463
|
+
print('Subsetting by list kept {} of {} files (expected {})'.format(
|
|
464
|
+
len(images_to_keep),len(data['images']),len(files_to_keep)))
|
|
465
|
+
|
|
466
|
+
data['images'] = images_to_keep
|
|
467
|
+
|
|
468
|
+
return data
|
|
469
|
+
|
|
470
|
+
# ...def subset_json_detector_output_by_list(...)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def subset_json_detector_output_by_categories(data, options):
|
|
474
|
+
"""
|
|
475
|
+
Removes all detections without detections above a threshold for specific categories.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
data (dict): data loaded from a MD results file
|
|
479
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
# If categories_to_keep is supplied as a list, convert to a dict
|
|
486
|
+
if options.categories_to_keep is not None:
|
|
487
|
+
if not isinstance(options.categories_to_keep, dict):
|
|
488
|
+
dict_categories_to_keep = {}
|
|
489
|
+
for category_id in options.categories_to_keep:
|
|
490
|
+
# Set unspecified thresholds to a silly negative value
|
|
491
|
+
dict_categories_to_keep[category_id] = -100000.0
|
|
492
|
+
options.categories_to_keep = dict_categories_to_keep
|
|
493
|
+
|
|
494
|
+
# If category_names_to_keep is supplied as a list, convert to a dict
|
|
495
|
+
if options.category_names_to_keep is not None:
|
|
496
|
+
if not isinstance(options.category_names_to_keep, dict):
|
|
497
|
+
dict_category_names_to_keep = {}
|
|
498
|
+
for category_name in options.category_names_to_keep:
|
|
499
|
+
# Set unspecified thresholds to a silly negative value
|
|
500
|
+
dict_category_names_to_keep[category_name] = -100000.0
|
|
501
|
+
options.category_names_to_keep = dict_category_names_to_keep
|
|
502
|
+
|
|
503
|
+
category_name_to_category_id = invert_dictionary(data['detection_categories'])
|
|
504
|
+
|
|
505
|
+
# If some categories are supplied as names, convert all to IDs and add to "categories_to_keep"
|
|
506
|
+
if options.category_names_to_keep is not None:
|
|
507
|
+
if options.categories_to_keep is None:
|
|
508
|
+
options.categories_to_keep = {}
|
|
509
|
+
for category_name in options.category_names_to_keep:
|
|
510
|
+
assert category_name in category_name_to_category_id, \
|
|
511
|
+
'Category {} not in detection categories'.format(category_name)
|
|
512
|
+
category_id = category_name_to_category_id[category_name]
|
|
513
|
+
assert category_id not in options.categories_to_keep, \
|
|
514
|
+
'Category {} ({}) specified as both a name and an ID'.format(
|
|
515
|
+
category_name,category_id)
|
|
516
|
+
options.categories_to_keep[category_id] = options.category_names_to_keep[category_name]
|
|
517
|
+
|
|
518
|
+
if options.categories_to_keep is None:
|
|
519
|
+
return data
|
|
520
|
+
|
|
521
|
+
images_in = data['images']
|
|
522
|
+
images_out = []
|
|
523
|
+
|
|
524
|
+
print('Subsetting by categories (keeping {} categories):'.format(
|
|
525
|
+
len(options.categories_to_keep)))
|
|
526
|
+
|
|
527
|
+
for category_id in sorted(list(options.categories_to_keep.keys())):
|
|
528
|
+
if category_id not in data['detection_categories']:
|
|
529
|
+
print('Warning: category ID {} not in category map in this file'.format(category_id))
|
|
530
|
+
else:
|
|
531
|
+
print('{} ({}) (threshold {})'.format(
|
|
532
|
+
category_id,
|
|
533
|
+
data['detection_categories'][category_id],
|
|
534
|
+
options.categories_to_keep[category_id]))
|
|
535
|
+
|
|
536
|
+
n_detections_in = 0
|
|
537
|
+
n_detections_kept = 0
|
|
538
|
+
|
|
539
|
+
# im = images_in[0]
|
|
540
|
+
for i_image, im in tqdm(enumerate(images_in), total=len(images_in)):
|
|
541
|
+
|
|
542
|
+
# Always keep failed images; if the caller wants to remove these, they
|
|
543
|
+
# will use remove_failed_images
|
|
544
|
+
if ('detections' not in im) or (im['detections'] is None):
|
|
545
|
+
images_out.append(im)
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
n_detections_in += len(im['detections'])
|
|
549
|
+
|
|
550
|
+
# Find all matching detections for this image
|
|
551
|
+
detections = []
|
|
552
|
+
for d in im['detections']:
|
|
553
|
+
if (d['category'] in options.categories_to_keep) and \
|
|
554
|
+
(d['conf'] > options.categories_to_keep[d['category']]):
|
|
555
|
+
detections.append(d)
|
|
556
|
+
|
|
557
|
+
im['detections'] = detections
|
|
558
|
+
|
|
559
|
+
if 'max_detection_conf' in im:
|
|
560
|
+
if len(detections) == 0:
|
|
561
|
+
p = 0
|
|
562
|
+
else:
|
|
563
|
+
p = max([d['conf'] for d in detections])
|
|
564
|
+
im['max_detection_conf'] = p
|
|
565
|
+
|
|
566
|
+
n_detections_kept += len(im['detections'])
|
|
567
|
+
|
|
568
|
+
images_out.append(im)
|
|
569
|
+
|
|
570
|
+
# ...for each image
|
|
571
|
+
|
|
572
|
+
data['images'] = images_out
|
|
573
|
+
print('done, kept {} detections (of {})'.format(
|
|
574
|
+
n_detections_kept,n_detections_in))
|
|
575
|
+
|
|
576
|
+
return data
|
|
577
|
+
|
|
578
|
+
# ...def subset_json_detector_output_by_categories(...)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def remove_failed_images(data,options):
|
|
582
|
+
"""
|
|
583
|
+
Removed failed images from [data]
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
data (dict): data loaded from a MD results file
|
|
587
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
images_in = data['images']
|
|
594
|
+
images_out = []
|
|
595
|
+
|
|
596
|
+
if not options.remove_failed_images:
|
|
597
|
+
return data
|
|
598
|
+
|
|
599
|
+
print('Removing failed images...', end='')
|
|
600
|
+
|
|
601
|
+
# i_image = 0; im = images_in[0]
|
|
602
|
+
for i_image, im in tqdm(enumerate(images_in), total=len(images_in)):
|
|
603
|
+
|
|
604
|
+
if 'failure' in im and isinstance(im['failure'],str):
|
|
605
|
+
continue
|
|
606
|
+
else:
|
|
607
|
+
images_out.append(im)
|
|
608
|
+
|
|
609
|
+
# ...for each image
|
|
610
|
+
|
|
611
|
+
data['images'] = images_out
|
|
612
|
+
n_removed = len(images_in) - len(data['images'])
|
|
613
|
+
print('Done, removed {} of {}'.format(n_removed, len(images_in)))
|
|
614
|
+
|
|
615
|
+
return data
|
|
616
|
+
|
|
617
|
+
# ...def remove_failed_images(...)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def subset_json_detector_output_by_query(data, options):
|
|
621
|
+
"""
|
|
622
|
+
Subsets to images whose filename matches options.query; replace all instances of
|
|
623
|
+
options.query with options.replacement. No-op if options.query_string is None or ''.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
data (dict): data loaded from a MD results file
|
|
627
|
+
options (SubsetJsonDetectorOutputOptions): parameters for subsetting
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
dict: Possibly-modified version of [data] (also modifies in place)
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
images_in = data['images']
|
|
634
|
+
images_out = []
|
|
635
|
+
|
|
636
|
+
print('Subsetting by query {}, replacement {}...'.format(options.query, options.replacement), end='')
|
|
637
|
+
|
|
638
|
+
query_string = options.query
|
|
639
|
+
query_starts_with = False
|
|
640
|
+
|
|
641
|
+
# Support a special case regex-like notation for "starts with"
|
|
642
|
+
if query_string is not None and query_string.startswith('^'):
|
|
643
|
+
query_string = query_string[1:]
|
|
644
|
+
query_starts_with = True
|
|
645
|
+
|
|
646
|
+
# i_image = 0; im = images_in[0]
|
|
647
|
+
for i_image, im in tqdm(enumerate(images_in), total=len(images_in)):
|
|
648
|
+
|
|
649
|
+
fn = im['file']
|
|
650
|
+
|
|
651
|
+
# Only take images that match the query
|
|
652
|
+
if query_string is not None:
|
|
653
|
+
if query_starts_with:
|
|
654
|
+
if (not fn.startswith(query_string)):
|
|
655
|
+
continue
|
|
656
|
+
else:
|
|
657
|
+
if query_string not in fn:
|
|
658
|
+
continue
|
|
659
|
+
|
|
660
|
+
if options.replacement is not None:
|
|
661
|
+
if query_string is not None:
|
|
662
|
+
fn = fn.replace(query_string, options.replacement)
|
|
663
|
+
else:
|
|
664
|
+
fn = options.replacement + fn
|
|
665
|
+
|
|
666
|
+
im['file'] = fn
|
|
667
|
+
|
|
668
|
+
images_out.append(im)
|
|
669
|
+
|
|
670
|
+
# ...for each image
|
|
671
|
+
|
|
672
|
+
data['images'] = images_out
|
|
673
|
+
print('done, found {} matches (of {})'.format(len(data['images']), len(images_in)))
|
|
674
|
+
|
|
675
|
+
return data
|
|
676
|
+
|
|
677
|
+
# ...def subset_json_detector_output_by_query(...)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def subset_json_detector_output(input_filename, output_filename, options, data=None):
|
|
681
|
+
"""
|
|
682
|
+
Main entry point; creates one or more subsets of a detector results file. See the
|
|
683
|
+
module header comment for more information about the available subsetting approaches.
|
|
684
|
+
|
|
685
|
+
Makes a copy of [data] before modifying if a data dictionary is supplied.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
input_filename (str): filename to load and subset; can be None if [data] is supplied
|
|
689
|
+
output_filename (str): file or folder name (depending on [options]) to which we should
|
|
690
|
+
write subset results.
|
|
691
|
+
options (SubsetJsonDetectorOutputOptions): parameters for .json splitting/subsetting;
|
|
692
|
+
see SubsetJsonDetectorOutputOptions for details.
|
|
693
|
+
data (dict, optional): data loaded from a .json file; if this is not None, [input_filename]
|
|
694
|
+
will be ignored. If supplied, this will be copied before it's modified.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
dict: Results that are either loaded from [input_filename] and processed, or copied
|
|
698
|
+
from [data] and processed.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
if options is None:
|
|
702
|
+
options = SubsetJsonDetectorOutputOptions()
|
|
703
|
+
else:
|
|
704
|
+
options = copy.deepcopy(options)
|
|
705
|
+
|
|
706
|
+
# Input validation
|
|
707
|
+
if options.copy_jsons_to_folders:
|
|
708
|
+
assert options.split_folders and options.make_folder_relative, \
|
|
709
|
+
'copy_jsons_to_folders set without make_folder_relative and split_folders'
|
|
710
|
+
|
|
711
|
+
if options.split_folders:
|
|
712
|
+
if os.path.isfile(output_filename):
|
|
713
|
+
raise ValueError('When splitting by folders, output must be a valid directory name, ' + \
|
|
714
|
+
'you specified an existing file')
|
|
715
|
+
|
|
716
|
+
if data is None:
|
|
717
|
+
print('Reading file {}'.format(input_filename))
|
|
718
|
+
with open(input_filename) as f:
|
|
719
|
+
data = json.load(f)
|
|
720
|
+
print('Read {} images'.format(len(data['images'])))
|
|
721
|
+
if options.debug_max_images > 0:
|
|
722
|
+
print('Trimming to {} images'.format(options.debug_max_images))
|
|
723
|
+
data['images'] = data['images'][:options.debug_max_images]
|
|
724
|
+
else:
|
|
725
|
+
print('Copying data')
|
|
726
|
+
data = copy.deepcopy(data)
|
|
727
|
+
print('...done')
|
|
728
|
+
|
|
729
|
+
if options.query is not None:
|
|
730
|
+
|
|
731
|
+
data = subset_json_detector_output_by_query(data, options)
|
|
732
|
+
|
|
733
|
+
if options.remove_failed_images:
|
|
734
|
+
|
|
735
|
+
data = remove_failed_images(data, options)
|
|
736
|
+
|
|
737
|
+
if options.confidence_threshold is not None:
|
|
738
|
+
|
|
739
|
+
data = subset_json_detector_output_by_confidence(data, options)
|
|
740
|
+
|
|
741
|
+
if (options.categories_to_keep is not None) or (options.category_names_to_keep is not None):
|
|
742
|
+
|
|
743
|
+
data = subset_json_detector_output_by_categories(data, options)
|
|
744
|
+
|
|
745
|
+
if options.remove_classification_categories_below_count is not None:
|
|
746
|
+
|
|
747
|
+
data = remove_classification_categories_below_count(data, options)
|
|
748
|
+
|
|
749
|
+
if options.keep_files_in_list is not None:
|
|
750
|
+
|
|
751
|
+
data = subset_json_detector_output_by_list(data, options)
|
|
752
|
+
|
|
753
|
+
if (options.maximum_detection_size is not None) or \
|
|
754
|
+
(options.minimum_detection_size is not None):
|
|
755
|
+
|
|
756
|
+
data = subset_json_detector_output_by_size(data, options)
|
|
757
|
+
|
|
758
|
+
if not options.split_folders:
|
|
759
|
+
|
|
760
|
+
_write_detection_results(data, output_filename, options)
|
|
761
|
+
return data
|
|
762
|
+
|
|
763
|
+
else:
|
|
764
|
+
|
|
765
|
+
# Map images to unique folders
|
|
766
|
+
print('Finding unique folders')
|
|
767
|
+
|
|
768
|
+
folders_to_images = {}
|
|
769
|
+
|
|
770
|
+
# im = data['images'][0]
|
|
771
|
+
for im in tqdm(data['images']):
|
|
772
|
+
|
|
773
|
+
fn = im['file']
|
|
774
|
+
|
|
775
|
+
if options.split_folder_mode == 'bottom':
|
|
776
|
+
|
|
777
|
+
dirname = os.path.dirname(fn)
|
|
778
|
+
|
|
779
|
+
elif options.split_folder_mode == 'n_from_bottom':
|
|
780
|
+
|
|
781
|
+
dirname = os.path.dirname(fn)
|
|
782
|
+
for n in range(0, options.split_folder_param):
|
|
783
|
+
dirname = os.path.dirname(dirname)
|
|
784
|
+
|
|
785
|
+
elif options.split_folder_mode == 'n_from_top':
|
|
786
|
+
|
|
787
|
+
# Split string into folders, keeping delimiters
|
|
788
|
+
|
|
789
|
+
# Don't use this, it removes delimiters
|
|
790
|
+
# tokens = _split_path(fn)
|
|
791
|
+
tokens = re.split(r'([\\/])',fn)
|
|
792
|
+
|
|
793
|
+
n_tokens_to_keep = ((options.split_folder_param + 1) * 2) - 1
|
|
794
|
+
|
|
795
|
+
if n_tokens_to_keep > len(tokens):
|
|
796
|
+
raise ValueError('Cannot walk {} folders from the top in path {}'.format(
|
|
797
|
+
options.split_folder_param, fn))
|
|
798
|
+
dirname = ''.join(tokens[0:n_tokens_to_keep])
|
|
799
|
+
|
|
800
|
+
elif options.split_folder_mode == 'dict':
|
|
801
|
+
|
|
802
|
+
assert isinstance(options.split_folder_param, dict)
|
|
803
|
+
dirname = options.split_folder_param[fn]
|
|
804
|
+
|
|
805
|
+
else:
|
|
806
|
+
|
|
807
|
+
raise ValueError('Unrecognized folder split mode {}'.format(options.split_folder_mode))
|
|
808
|
+
|
|
809
|
+
folders_to_images.setdefault(dirname, []).append(im)
|
|
810
|
+
|
|
811
|
+
# ...for each image
|
|
812
|
+
|
|
813
|
+
print('Found {} unique folders'.format(len(folders_to_images)))
|
|
814
|
+
|
|
815
|
+
# Optionally make paths relative
|
|
816
|
+
# dirname = list(folders_to_images.keys())[0]
|
|
817
|
+
if options.make_folder_relative:
|
|
818
|
+
|
|
819
|
+
print('Converting database-relative paths to individual-json-relative paths...')
|
|
820
|
+
|
|
821
|
+
for dirname in tqdm(folders_to_images):
|
|
822
|
+
# im = folders_to_images[dirname][0]
|
|
823
|
+
for im in folders_to_images[dirname]:
|
|
824
|
+
fn = im['file']
|
|
825
|
+
relfn = os.path.relpath(fn, dirname).replace('\\', '/')
|
|
826
|
+
im['file'] = relfn
|
|
827
|
+
|
|
828
|
+
# ...if we need to convert paths to be folder-relative
|
|
829
|
+
|
|
830
|
+
print('Finished converting to json-relative paths, writing output')
|
|
831
|
+
|
|
832
|
+
os.makedirs(output_filename, exist_ok=True)
|
|
833
|
+
all_images = data['images']
|
|
834
|
+
|
|
835
|
+
# dirname = list(folders_to_images.keys())[0]
|
|
836
|
+
for dirname in tqdm(folders_to_images):
|
|
837
|
+
|
|
838
|
+
json_fn = dirname.replace('/', '_').replace('\\', '_') + '.json'
|
|
839
|
+
|
|
840
|
+
if options.copy_jsons_to_folders:
|
|
841
|
+
json_fn = os.path.join(output_filename, dirname, json_fn)
|
|
842
|
+
else:
|
|
843
|
+
json_fn = os.path.join(output_filename, json_fn)
|
|
844
|
+
|
|
845
|
+
# Recycle the 'data' struct, replacing 'images' every time... medium-hacky, but
|
|
846
|
+
# forward-compatible in that I don't take dependencies on the other fields
|
|
847
|
+
dir_data = data
|
|
848
|
+
dir_data['images'] = folders_to_images[dirname]
|
|
849
|
+
_write_detection_results(dir_data, json_fn, options)
|
|
850
|
+
print('Wrote {} images to {}'.format(len(dir_data['images']), json_fn))
|
|
851
|
+
|
|
852
|
+
# ...for each directory
|
|
853
|
+
|
|
854
|
+
data['images'] = all_images
|
|
855
|
+
|
|
856
|
+
return data
|
|
857
|
+
|
|
858
|
+
# ...if we're splitting folders
|
|
859
|
+
|
|
860
|
+
# ...def subset_json_detector_output(...)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
#%% Interactive driver
|
|
864
|
+
|
|
865
|
+
if False:
|
|
866
|
+
|
|
867
|
+
#%%
|
|
868
|
+
|
|
869
|
+
#%% Subset a file without splitting
|
|
870
|
+
|
|
871
|
+
input_filename = r"c:\temp\sample.json"
|
|
872
|
+
output_filename = r"c:\temp\output.json"
|
|
873
|
+
|
|
874
|
+
options = SubsetJsonDetectorOutputOptions()
|
|
875
|
+
options.replacement = None
|
|
876
|
+
options.query = 'S2'
|
|
877
|
+
|
|
878
|
+
data = subset_json_detector_output(input_filename,output_filename,options,None)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
#%% Subset and split, but don't copy to individual folders
|
|
882
|
+
|
|
883
|
+
input_filename = r"C:\temp\xxx-export.json"
|
|
884
|
+
output_filename = r"c:\temp\out"
|
|
885
|
+
|
|
886
|
+
options = SubsetJsonDetectorOutputOptions()
|
|
887
|
+
options.split_folders = True
|
|
888
|
+
options.make_folder_relative = True
|
|
889
|
+
options.split_folder_mode = 'n_from_top'
|
|
890
|
+
options.split_folder_param = 1
|
|
891
|
+
|
|
892
|
+
data = subset_json_detector_output(input_filename,output_filename,options,None)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
#%% Subset and split, copying to individual folders
|
|
896
|
+
|
|
897
|
+
input_filename = r"c:\temp\sample.json"
|
|
898
|
+
output_filename = r"c:\temp\out"
|
|
899
|
+
|
|
900
|
+
options = SubsetJsonDetectorOutputOptions()
|
|
901
|
+
options.split_folders = True
|
|
902
|
+
options.make_folder_relative = True
|
|
903
|
+
options.copy_jsons_to_folders = True
|
|
904
|
+
|
|
905
|
+
data = subset_json_detector_output(input_filename,output_filename,options,data)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
#%% Command-line driver
|
|
909
|
+
|
|
910
|
+
def main(): # noqa
|
|
911
|
+
|
|
912
|
+
parser = argparse.ArgumentParser()
|
|
913
|
+
parser.add_argument('input_file', type=str, help='Input .json filename')
|
|
914
|
+
parser.add_argument('output_file', type=str, help='Output .json filename')
|
|
915
|
+
parser.add_argument('--query', type=str, default=None,
|
|
916
|
+
help='Query string to search for (omitting this matches all)')
|
|
917
|
+
parser.add_argument('--replacement', type=str, default=None,
|
|
918
|
+
help='Replace [query] with this')
|
|
919
|
+
parser.add_argument('--confidence_threshold', type=float, default=None,
|
|
920
|
+
help='Remove detections below this confidence level')
|
|
921
|
+
parser.add_argument('--maximum_detection_size', type=float, default=None,
|
|
922
|
+
help='Remove detections above this size (as a fraction of the image size)')
|
|
923
|
+
parser.add_argument('--minimum_detection_size', type=float, default=None,
|
|
924
|
+
help='Remove detections below this size (as a fraction of the image size)')
|
|
925
|
+
parser.add_argument('--keep_files_in_list', type=str, default=None,
|
|
926
|
+
help='Keep only files in this list, which can be a .json results file or a folder.' + \
|
|
927
|
+
' Assumes that the input .json file contains relative paths when comparing to a folder.')
|
|
928
|
+
parser.add_argument('--split_folders', action='store_true',
|
|
929
|
+
help='Split .json files by leaf-node folder')
|
|
930
|
+
parser.add_argument('--split_folder_param', type=int,
|
|
931
|
+
help='Directory level count for n_from_bottom and n_from_top splitting')
|
|
932
|
+
parser.add_argument('--split_folder_mode', type=str,
|
|
933
|
+
help='Folder level to use for splitting ("bottom", "n_from_bottom", or "n_from_top")')
|
|
934
|
+
parser.add_argument('--make_folder_relative', action='store_true',
|
|
935
|
+
help='Make image paths relative to their containing folder ' + \
|
|
936
|
+
'(only meaningful with split_folders)')
|
|
937
|
+
parser.add_argument('--overwrite_json_files', action='store_true',
|
|
938
|
+
help='Overwrite output files')
|
|
939
|
+
parser.add_argument('--copy_jsons_to_folders', action='store_true',
|
|
940
|
+
help='When using split_folders and make_folder_relative, copy jsons to their ' + \
|
|
941
|
+
'corresponding folders (relative to output_file)')
|
|
942
|
+
parser.add_argument('--create_folders', action='store_true',
|
|
943
|
+
help='When using copy_jsons_to_folders, create folders that don''t exist')
|
|
944
|
+
parser.add_argument('--remove_classification_categories_below_count', type=int, default=None,
|
|
945
|
+
help='Remove classification categories with less than this many instances ' + \
|
|
946
|
+
'(no removal by default)')
|
|
947
|
+
|
|
948
|
+
if len(sys.argv[1:]) == 0:
|
|
949
|
+
parser.print_help()
|
|
950
|
+
parser.exit()
|
|
951
|
+
|
|
952
|
+
args = parser.parse_args()
|
|
953
|
+
|
|
954
|
+
# Convert to an options object
|
|
955
|
+
options = SubsetJsonDetectorOutputOptions()
|
|
956
|
+
if args.create_folders:
|
|
957
|
+
options.copy_jsons_to_folders_directories_must_exist = False
|
|
958
|
+
|
|
959
|
+
args_to_object(args, options)
|
|
960
|
+
|
|
961
|
+
subset_json_detector_output(args.input_file, args.output_file, options)
|
|
962
|
+
|
|
963
|
+
if __name__ == '__main__':
|
|
964
|
+
main()
|