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,613 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
ct_utils.py
|
|
4
|
+
|
|
5
|
+
Numeric/geometry/array utility functions.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
#%% Imports and constants
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
import jsonpickle
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
# List of file extensions we'll consider images; comparisons will be case-insensitive
|
|
20
|
+
# (i.e., no need to include both .jpg and .JPG on this list).
|
|
21
|
+
image_extensions = ['.jpg', '.jpeg', '.gif', '.png']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
#%% Functions
|
|
25
|
+
|
|
26
|
+
def truncate_float_array(xs, precision=3):
|
|
27
|
+
"""
|
|
28
|
+
Vectorized version of truncate_float(...), truncates the fractional portion of each
|
|
29
|
+
floating-point value to a specific number of floating-point digits.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
xs (list): list of floats to truncate
|
|
33
|
+
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
list: list of truncated floats
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return [truncate_float(x, precision=precision) for x in xs]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def truncate_float(x, precision=3):
|
|
43
|
+
"""
|
|
44
|
+
Truncates the fractional portion of a floating-point value to a specific number of
|
|
45
|
+
floating-point digits.
|
|
46
|
+
|
|
47
|
+
For example:
|
|
48
|
+
|
|
49
|
+
truncate_float(0.0003214884) --> 0.000321
|
|
50
|
+
truncate_float(1.0003214884) --> 1.000321
|
|
51
|
+
|
|
52
|
+
This function is primarily used to achieve a certain float representation
|
|
53
|
+
before exporting to JSON.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
x (float): scalar to truncate
|
|
57
|
+
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
float: truncated version of [x]
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
assert precision > 0
|
|
64
|
+
|
|
65
|
+
if np.isclose(x, 0):
|
|
66
|
+
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
elif (x > 1):
|
|
70
|
+
|
|
71
|
+
fractional_component = x - 1.0
|
|
72
|
+
return 1 + truncate_float(fractional_component)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
|
|
76
|
+
# Determine the factor, which shifts the decimal point of x
|
|
77
|
+
# just behind the last significant digit.
|
|
78
|
+
factor = math.pow(10, precision - 1 - math.floor(math.log10(abs(x))))
|
|
79
|
+
|
|
80
|
+
# Shift decimal point by multiplication with factor, flooring, and
|
|
81
|
+
# division by factor.
|
|
82
|
+
return math.floor(x * factor)/factor
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def args_to_object(args, obj):
|
|
86
|
+
"""
|
|
87
|
+
Copies all fields from a Namespace (typically the output from parse_args) to an
|
|
88
|
+
object. Skips fields starting with _. Does not check existence in the target
|
|
89
|
+
object.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
args (argparse.Namespace): the namespace to convert to an object
|
|
93
|
+
obj (object): object whose whose attributes will be updated
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
object: the modified object (modified in place, but also returned)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
for n, v in inspect.getmembers(args):
|
|
100
|
+
if not n.startswith('_'):
|
|
101
|
+
setattr(obj, n, v)
|
|
102
|
+
|
|
103
|
+
return obj
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def pretty_print_object(obj, b_print=True):
|
|
107
|
+
"""
|
|
108
|
+
Converts an arbitrary object to .json, optionally printing the .json representation.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
obj (object): object to print
|
|
112
|
+
b_print (bool, optional): whether to print the object
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: .json reprepresentation of [obj]
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
# _ = pretty_print_object(obj)
|
|
119
|
+
|
|
120
|
+
# TODO: it's sloppy that I'm making a module-wide change here, consider at least
|
|
121
|
+
# recording these operations and re-setting them at the end of this function.
|
|
122
|
+
jsonpickle.set_encoder_options('json', sort_keys=True, indent=2)
|
|
123
|
+
a = jsonpickle.encode(obj)
|
|
124
|
+
s = '{}'.format(a)
|
|
125
|
+
if b_print:
|
|
126
|
+
print(s)
|
|
127
|
+
return s
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_list_sorted(L, reverse=False):
|
|
131
|
+
"""
|
|
132
|
+
Returns True if the list L appears to be sorted, otherwise False.
|
|
133
|
+
|
|
134
|
+
Calling is_list_sorted(L,reverse=True) is the same as calling
|
|
135
|
+
is_list_sorted(L.reverse(),reverse=False).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
L (list): list to evaluate
|
|
139
|
+
reverse (bool, optional): whether to reverse the list before evaluating sort status
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if the list L appears to be sorted, otherwise False
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if reverse:
|
|
146
|
+
return all(L[i] >= L[i + 1] for i in range(len(L)-1))
|
|
147
|
+
else:
|
|
148
|
+
return all(L[i] <= L[i + 1] for i in range(len(L)-1))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def write_json(path, content, indent=1):
|
|
152
|
+
"""
|
|
153
|
+
Standardized wrapper for json.dump().
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
path (str): filename to write to
|
|
157
|
+
content (object): object to dump
|
|
158
|
+
indent (int, optional): indentation depth passed to json.dump
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
with open(path, 'w') as f:
|
|
162
|
+
json.dump(content, f, indent=indent)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def convert_yolo_to_xywh(yolo_box):
|
|
166
|
+
"""
|
|
167
|
+
Converts a YOLO format bounding box to [x_min, y_min, width_of_box, height_of_box].
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
yolo_box (list): bounding box of format [x_center, y_center, width_of_box, height_of_box]
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
list: bbox with coordinates represented as [x_min, y_min, width_of_box, height_of_box]
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
x_center, y_center, width_of_box, height_of_box = yolo_box
|
|
177
|
+
x_min = x_center - width_of_box / 2.0
|
|
178
|
+
y_min = y_center - height_of_box / 2.0
|
|
179
|
+
return [x_min, y_min, width_of_box, height_of_box]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def convert_xywh_to_tf(api_box):
|
|
183
|
+
"""
|
|
184
|
+
Converts an xywh bounding box (the format used in MD output) to the [y_min, x_min, y_max, x_max]
|
|
185
|
+
format that the TensorFlow Object Detection API uses.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
api_box: bbox output by the batch processing API [x_min, y_min, width_of_box, height_of_box]
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
list: bbox with coordinates represented as [y_min, x_min, y_max, x_max]
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
x_min, y_min, width_of_box, height_of_box = api_box
|
|
195
|
+
x_max = x_min + width_of_box
|
|
196
|
+
y_max = y_min + height_of_box
|
|
197
|
+
return [y_min, x_min, y_max, x_max]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def convert_xywh_to_xyxy(api_bbox):
|
|
201
|
+
"""
|
|
202
|
+
Converts an xywh bounding box (the MD output format) to an xyxy bounding box.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
api_bbox (list): bbox formatted as [x_min, y_min, width_of_box, height_of_box]
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
list: bbox formatted as [x_min, y_min, x_max, y_max]
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
x_min, y_min, width_of_box, height_of_box = api_bbox
|
|
212
|
+
x_max, y_max = x_min + width_of_box, y_min + height_of_box
|
|
213
|
+
return [x_min, y_min, x_max, y_max]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_iou(bb1, bb2):
|
|
217
|
+
"""
|
|
218
|
+
Calculates the intersection over union (IoU) of two bounding boxes.
|
|
219
|
+
|
|
220
|
+
Adapted from:
|
|
221
|
+
|
|
222
|
+
https://stackoverflow.com/questions/25349178/calculating-percentage-of-bounding-box-overlap-for-image-detector-evaluation
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
bb1 (list): [x_min, y_min, width_of_box, height_of_box]
|
|
226
|
+
bb2 (list): [x_min, y_min, width_of_box, height_of_box]
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
float: intersection_over_union, a float in [0, 1]
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
bb1 = convert_xywh_to_xyxy(bb1)
|
|
233
|
+
bb2 = convert_xywh_to_xyxy(bb2)
|
|
234
|
+
|
|
235
|
+
assert bb1[0] < bb1[2], 'Malformed bounding box (x2 >= x1)'
|
|
236
|
+
assert bb1[1] < bb1[3], 'Malformed bounding box (y2 >= y1)'
|
|
237
|
+
|
|
238
|
+
assert bb2[0] < bb2[2], 'Malformed bounding box (x2 >= x1)'
|
|
239
|
+
assert bb2[1] < bb2[3], 'Malformed bounding box (y2 >= y1)'
|
|
240
|
+
|
|
241
|
+
# Determine the coordinates of the intersection rectangle
|
|
242
|
+
x_left = max(bb1[0], bb2[0])
|
|
243
|
+
y_top = max(bb1[1], bb2[1])
|
|
244
|
+
x_right = min(bb1[2], bb2[2])
|
|
245
|
+
y_bottom = min(bb1[3], bb2[3])
|
|
246
|
+
|
|
247
|
+
if x_right < x_left or y_bottom < y_top:
|
|
248
|
+
return 0.0
|
|
249
|
+
|
|
250
|
+
# The intersection of two axis-aligned bounding boxes is always an
|
|
251
|
+
# axis-aligned bounding box
|
|
252
|
+
intersection_area = (x_right - x_left) * (y_bottom - y_top)
|
|
253
|
+
|
|
254
|
+
# Compute the area of both AABBs
|
|
255
|
+
bb1_area = (bb1[2] - bb1[0]) * (bb1[3] - bb1[1])
|
|
256
|
+
bb2_area = (bb2[2] - bb2[0]) * (bb2[3] - bb2[1])
|
|
257
|
+
|
|
258
|
+
# Compute the intersection over union by taking the intersection
|
|
259
|
+
# area and dividing it by the sum of prediction + ground-truth
|
|
260
|
+
# areas - the intersection area.
|
|
261
|
+
iou = intersection_area / float(bb1_area + bb2_area - intersection_area)
|
|
262
|
+
assert iou >= 0.0, 'Illegal IOU < 0'
|
|
263
|
+
assert iou <= 1.0, 'Illegal IOU > 1'
|
|
264
|
+
return iou
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _get_max_conf_from_detections(detections):
|
|
268
|
+
"""
|
|
269
|
+
Internal function used by get_max_conf(); don't call this directly.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
max_conf = 0.0
|
|
273
|
+
if detections is not None and len(detections) > 0:
|
|
274
|
+
confidences = [det['conf'] for det in detections]
|
|
275
|
+
max_conf = max(confidences)
|
|
276
|
+
return max_conf
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_max_conf(im):
|
|
280
|
+
"""
|
|
281
|
+
Given an image dict in the MD output format, computes the maximum detection confidence for any
|
|
282
|
+
class. Returns 0.0 (rather than None) if there was a failure or 'detections' isn't present.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
im (dict): image dictionary in the MD output format (with a 'detections' field)
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
float: the maximum detection confidence across all classes
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
max_conf = 0.0
|
|
292
|
+
if 'detections' in im and im['detections'] is not None and len(im['detections']) > 0:
|
|
293
|
+
max_conf = _get_max_conf_from_detections(im['detections'])
|
|
294
|
+
return max_conf
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def point_dist(p1,p2):
|
|
298
|
+
"""
|
|
299
|
+
Computes the distance between two points, represented as length-two tuples.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
p1: point, formatted as (x,y)
|
|
303
|
+
p2: point, formatted as (x,y)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
float: the Euclidean distance between p1 and p2
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
return math.sqrt( ((p1[0]-p2[0])**2) + ((p1[1]-p2[1])**2) )
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def rect_distance(r1, r2, format='x0y0x1y1'):
|
|
313
|
+
"""
|
|
314
|
+
Computes the minimum distance between two axis-aligned rectangles, each represented as
|
|
315
|
+
(x0,y0,x1,y1) by default.
|
|
316
|
+
|
|
317
|
+
Can also specify "format" as x0y0wh for MD-style bbox formatting (x0,y0,w,h).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
r1: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
321
|
+
r2: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
322
|
+
format (str, optional): whether the boxes are formatted as 'x0y0x1y1' (default) or 'x0y0wh'
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
float: the minimum distance between r1 and r2
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
assert format in ('x0y0x1y1','x0y0wh'), 'Illegal rectangle format {}'.format(format)
|
|
329
|
+
|
|
330
|
+
if format == 'x0y0wh':
|
|
331
|
+
# Convert to x0y0x1y1 without modifying the original rectangles
|
|
332
|
+
r1 = [r1[0],r1[1],r1[0]+r1[2],r1[1]+r1[3]]
|
|
333
|
+
r2 = [r2[0],r2[1],r2[0]+r2[2],r2[1]+r2[3]]
|
|
334
|
+
|
|
335
|
+
# https://stackoverflow.com/a/26178015
|
|
336
|
+
x1, y1, x1b, y1b = r1
|
|
337
|
+
x2, y2, x2b, y2b = r2
|
|
338
|
+
left = x2b < x1
|
|
339
|
+
right = x1b < x2
|
|
340
|
+
bottom = y2b < y1
|
|
341
|
+
top = y1b < y2
|
|
342
|
+
if top and left:
|
|
343
|
+
return point_dist((x1, y1b), (x2b, y2))
|
|
344
|
+
elif left and bottom:
|
|
345
|
+
return point_dist((x1, y1), (x2b, y2b))
|
|
346
|
+
elif bottom and right:
|
|
347
|
+
return point_dist((x1b, y1), (x2, y2b))
|
|
348
|
+
elif right and top:
|
|
349
|
+
return point_dist((x1b, y1b), (x2, y2))
|
|
350
|
+
elif left:
|
|
351
|
+
return x1 - x2b
|
|
352
|
+
elif right:
|
|
353
|
+
return x2 - x1b
|
|
354
|
+
elif bottom:
|
|
355
|
+
return y1 - y2b
|
|
356
|
+
elif top:
|
|
357
|
+
return y2 - y1b
|
|
358
|
+
else:
|
|
359
|
+
return 0.0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def split_list_into_fixed_size_chunks(L,n):
|
|
363
|
+
"""
|
|
364
|
+
Split the list or tuple L into chunks of size n (allowing at most one chunk with size
|
|
365
|
+
less than N, i.e. len(L) does not have to be a multiple of n).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
L (list): list to split into chunks
|
|
369
|
+
n (int): preferred chunk size
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
list: list of chunks, where each chunk is a list of length n or n-1
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
return [L[i * n:(i + 1) * n] for i in range((len(L) + n - 1) // n )]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def split_list_into_n_chunks(L, n, chunk_strategy='greedy'):
|
|
379
|
+
"""
|
|
380
|
+
Splits the list or tuple L into n equally-sized chunks (some chunks may be one
|
|
381
|
+
element smaller than others, i.e. len(L) does not have to be a multiple of n).
|
|
382
|
+
|
|
383
|
+
chunk_strategy can be "greedy" (default, if there are k samples per chunk, the first
|
|
384
|
+
k go into the first chunk) or "balanced" (alternate between chunks when pulling
|
|
385
|
+
items from the list).
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
L (list): list to split into chunks
|
|
389
|
+
n (int): number of chunks
|
|
390
|
+
chunk_strategy (str, optiopnal): "greedy" or "balanced"; see above
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
list: list of chunks, each of which is a list
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
if chunk_strategy == 'greedy':
|
|
397
|
+
k, m = divmod(len(L), n)
|
|
398
|
+
return list(L[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(n))
|
|
399
|
+
elif chunk_strategy == 'balanced':
|
|
400
|
+
chunks = [ [] for _ in range(n) ]
|
|
401
|
+
for i_item,item in enumerate(L):
|
|
402
|
+
i_chunk = i_item % n
|
|
403
|
+
chunks[i_chunk].append(item)
|
|
404
|
+
return chunks
|
|
405
|
+
else:
|
|
406
|
+
raise ValueError('Invalid chunk strategy: {}'.format(chunk_strategy))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def sort_dictionary_by_key(d,reverse=False):
|
|
410
|
+
"""
|
|
411
|
+
Sorts the dictionary [d] by key.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
d (dict): dictionary to sort
|
|
415
|
+
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
dict: sorted copy of [d]
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
d = dict(sorted(d.items(),reverse=reverse))
|
|
422
|
+
return d
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def sort_dictionary_by_value(d,sort_values=None,reverse=False):
|
|
426
|
+
"""
|
|
427
|
+
Sorts the dictionary [d] by value. If sort_values is None, uses d.values(),
|
|
428
|
+
otherwise uses the dictionary sort_values as the sorting criterion.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
d (dict): dictionary to sort
|
|
432
|
+
sort_values (dict, optional): dictionary mapping keys in [d] to sort values (defaults
|
|
433
|
+
to None, uses [d] itself for sorting)
|
|
434
|
+
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
dict: sorted copy of [d]
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
if sort_values is None:
|
|
441
|
+
d = {k: v for k, v in sorted(d.items(), key=lambda item: item[1], reverse=reverse)}
|
|
442
|
+
else:
|
|
443
|
+
d = {k: v for k, v in sorted(d.items(), key=lambda item: sort_values[item[0]], reverse=reverse)}
|
|
444
|
+
return d
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def invert_dictionary(d):
|
|
448
|
+
"""
|
|
449
|
+
Creates a new dictionary that maps d.values() to d.keys(). Does not check
|
|
450
|
+
uniqueness.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
d (dict): dictionary to invert
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
dict: inverted copy of [d]
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
return {v: k for k, v in d.items()}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def image_file_to_camera_folder(image_fn):
|
|
463
|
+
r"""
|
|
464
|
+
Removes common overflow folders (e.g. RECNX101, RECNX102) from paths, i.e. turn:
|
|
465
|
+
|
|
466
|
+
a\b\c\RECNX101\image001.jpg
|
|
467
|
+
|
|
468
|
+
...into:
|
|
469
|
+
|
|
470
|
+
a\b\c
|
|
471
|
+
|
|
472
|
+
Returns the same thing as os.dirname() (i.e., just the folder name) if no overflow folders are
|
|
473
|
+
present.
|
|
474
|
+
|
|
475
|
+
Always converts backslashes to slashes.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
image_fn (str): the image filename from which we should remove overflow folders
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
str: a version of [image_fn] from which camera overflow folders have been removed
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
import re
|
|
485
|
+
|
|
486
|
+
# 100RECNX is the overflow folder style for Reconyx cameras
|
|
487
|
+
# 100EK113 is (for some reason) the overflow folder style for Bushnell cameras
|
|
488
|
+
# 100_BTCF is the overflow folder style for Browning cameras
|
|
489
|
+
# 100MEDIA is the overflow folder style used on a number of consumer-grade cameras
|
|
490
|
+
patterns = ['\/\d+RECNX\/','\/\d+EK\d+\/','\/\d+_BTCF\/','\/\d+MEDIA\/']
|
|
491
|
+
|
|
492
|
+
image_fn = image_fn.replace('\\','/')
|
|
493
|
+
for pat in patterns:
|
|
494
|
+
image_fn = re.sub(pat,'/',image_fn)
|
|
495
|
+
camera_folder = os.path.dirname(image_fn)
|
|
496
|
+
|
|
497
|
+
return camera_folder
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def is_float(v):
|
|
501
|
+
"""
|
|
502
|
+
Determines whether v is either a float or a string representation of a float.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
v (object): object to evaluate
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
bool: True if [v] is a float or a string representation of a float, otherwise False
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
_ = float(v)
|
|
513
|
+
return True
|
|
514
|
+
except ValueError:
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def is_iterable(x):
|
|
519
|
+
"""
|
|
520
|
+
Uses duck typing to assess whether [x] is iterable (list, set, dict, etc.).
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
x (object): the object to test
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
bool: True if [x] appears to be iterable, otherwise False
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
_ = iter(x)
|
|
531
|
+
except:
|
|
532
|
+
return False
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def is_empty(v):
|
|
537
|
+
"""
|
|
538
|
+
A common definition of "empty" used throughout the repo, particularly when loading
|
|
539
|
+
data from .csv files. "empty" includes None, '', and NaN.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
v: the object to evaluate for emptiness
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
bool: True if [v] is None, '', or NaN, otherwise False
|
|
546
|
+
"""
|
|
547
|
+
if v is None:
|
|
548
|
+
return True
|
|
549
|
+
if isinstance(v,str) and v == '':
|
|
550
|
+
return True
|
|
551
|
+
if isinstance(v,float) and np.isnan(v):
|
|
552
|
+
return True
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def isnan(v):
|
|
557
|
+
"""
|
|
558
|
+
Returns True if v is a nan-valued float, otherwise returns False.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
v: the object to evaluate for nan-ness
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
bool: True if v is a nan-valued float, otherwise False
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
return np.isnan(v)
|
|
569
|
+
except Exception:
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def sets_overlap(set1, set2):
|
|
574
|
+
"""
|
|
575
|
+
Determines whether two sets overlap.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
set1 (set): the first set to compare (converted to a set if it's not already)
|
|
579
|
+
set2 (set): the second set to compare (converted to a set if it's not already)
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
bool: True if any elements are shared between set1 and set2
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
return not set(set1).isdisjoint(set(set2))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
#%% Test drivers
|
|
590
|
+
|
|
591
|
+
if False:
|
|
592
|
+
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
#%% Test image_file_to_camera_folder()
|
|
596
|
+
|
|
597
|
+
relative_path = 'a/b/c/d/100EK113/blah.jpg'
|
|
598
|
+
print(image_file_to_camera_folder(relative_path))
|
|
599
|
+
|
|
600
|
+
relative_path = 'a/b/c/d/100RECNX/blah.jpg'
|
|
601
|
+
print(image_file_to_camera_folder(relative_path))
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
#%% Test a few rectangle distances
|
|
605
|
+
|
|
606
|
+
r1 = [0,0,1,1]; r2 = [0,0,1,1]; assert rect_distance(r1,r2)==0
|
|
607
|
+
r1 = [0,0,1,1]; r2 = [0,0,1,100]; assert rect_distance(r1,r2)==0
|
|
608
|
+
r1 = [0,0,1,1]; r2 = [1,1,2,2]; assert rect_distance(r1,r2)==0
|
|
609
|
+
r1 = [0,0,1,1]; r2 = [1.1,0,0,1.1]; assert abs(rect_distance(r1,r2)-.1) < 0.00001
|
|
610
|
+
|
|
611
|
+
r1 = [0.4,0.8,10,22]; r2 = [100, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-119.753) < 0.001
|
|
612
|
+
r1 = [0.4,0.8,10,22]; r2 = [101, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-120.507) < 0.001
|
|
613
|
+
r1 = [0.4,0.8,10,22]; r2 = [120, 120, 200, 210.4]; assert abs(rect_distance(r1,r2)-147.323) < 0.001
|