megadetector 5.0.27__py3-none-any.whl → 5.0.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of megadetector might be problematic. Click here for more details.
- megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
- megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
- megadetector/classification/aggregate_classifier_probs.py +3 -3
- megadetector/classification/analyze_failed_images.py +5 -5
- megadetector/classification/cache_batchapi_outputs.py +5 -5
- megadetector/classification/create_classification_dataset.py +11 -12
- megadetector/classification/crop_detections.py +10 -10
- megadetector/classification/csv_to_json.py +8 -8
- megadetector/classification/detect_and_crop.py +13 -15
- megadetector/classification/evaluate_model.py +7 -7
- megadetector/classification/identify_mislabeled_candidates.py +6 -6
- megadetector/classification/json_to_azcopy_list.py +1 -1
- megadetector/classification/json_validator.py +29 -32
- megadetector/classification/map_classification_categories.py +9 -9
- megadetector/classification/merge_classification_detection_output.py +12 -9
- megadetector/classification/prepare_classification_script.py +19 -19
- megadetector/classification/prepare_classification_script_mc.py +23 -23
- megadetector/classification/run_classifier.py +4 -4
- megadetector/classification/save_mislabeled.py +6 -6
- megadetector/classification/train_classifier.py +1 -1
- megadetector/classification/train_classifier_tf.py +9 -9
- megadetector/classification/train_utils.py +10 -10
- megadetector/data_management/annotations/annotation_constants.py +1 -1
- megadetector/data_management/camtrap_dp_to_coco.py +45 -45
- megadetector/data_management/cct_json_utils.py +101 -101
- megadetector/data_management/cct_to_md.py +49 -49
- megadetector/data_management/cct_to_wi.py +33 -33
- megadetector/data_management/coco_to_labelme.py +75 -75
- megadetector/data_management/coco_to_yolo.py +189 -189
- megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
- megadetector/data_management/databases/integrity_check_json_db.py +202 -188
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +38 -38
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +130 -124
- megadetector/data_management/labelme_to_yolo.py +78 -72
- megadetector/data_management/lila/create_lila_blank_set.py +81 -83
- megadetector/data_management/lila/create_lila_test_set.py +32 -31
- megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
- megadetector/data_management/lila/download_lila_subset.py +21 -24
- megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
- megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +70 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
- megadetector/data_management/mewc_to_md.py +339 -340
- megadetector/data_management/ocr_tools.py +258 -252
- megadetector/data_management/read_exif.py +232 -223
- megadetector/data_management/remap_coco_categories.py +26 -26
- megadetector/data_management/remove_exif.py +31 -20
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +41 -41
- megadetector/data_management/speciesnet_to_md.py +41 -41
- megadetector/data_management/wi_download_csv_to_coco.py +55 -55
- megadetector/data_management/yolo_output_to_md_output.py +117 -120
- megadetector/data_management/yolo_to_coco.py +195 -188
- megadetector/detection/change_detection.py +831 -0
- megadetector/detection/process_video.py +341 -338
- megadetector/detection/pytorch_detector.py +308 -266
- megadetector/detection/run_detector.py +186 -166
- megadetector/detection/run_detector_batch.py +366 -364
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +312 -253
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +291 -283
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +808 -311
- megadetector/postprocessing/combine_batch_outputs.py +20 -21
- megadetector/postprocessing/compare_batch_results.py +528 -517
- megadetector/postprocessing/convert_output_format.py +97 -97
- megadetector/postprocessing/create_crop_folder.py +220 -147
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -0
- megadetector/postprocessing/load_api_results.py +25 -22
- megadetector/postprocessing/md_to_coco.py +129 -98
- megadetector/postprocessing/md_to_labelme.py +89 -83
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +87 -114
- megadetector/postprocessing/postprocess_batch_results.py +319 -302
- megadetector/postprocessing/remap_detection_categories.py +36 -36
- megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
- megadetector/postprocessing/separate_detections_into_folders.py +226 -211
- megadetector/postprocessing/subset_json_detector_output.py +265 -262
- megadetector/postprocessing/top_folders_to_bottom.py +45 -45
- megadetector/postprocessing/validate_batch_results.py +70 -70
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -69
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +33 -33
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
- megadetector/taxonomy_mapping/taxonomy_graph.py +11 -11
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1019 -200
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +541 -518
- megadetector/utils/path_utils.py +1511 -406
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +73 -60
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2868
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +21 -21
- megadetector/visualization/render_images_with_thumbnails.py +37 -73
- megadetector/visualization/visualization_utils.py +424 -404
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +126 -98
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- megadetector/data_management/importers/add_nacti_sizes.py +0 -52
- megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
- megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
- megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
- megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
- megadetector/data_management/importers/awc_to_json.py +0 -191
- megadetector/data_management/importers/bellevue_to_json.py +0 -272
- megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
- megadetector/data_management/importers/cct_field_adjustments.py +0 -58
- megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
- megadetector/data_management/importers/ena24_to_json.py +0 -276
- megadetector/data_management/importers/filenames_to_json.py +0 -386
- megadetector/data_management/importers/helena_to_cct.py +0 -283
- megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
- megadetector/data_management/importers/jb_csv_to_json.py +0 -150
- megadetector/data_management/importers/mcgill_to_json.py +0 -250
- megadetector/data_management/importers/missouri_to_json.py +0 -490
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
- megadetector/data_management/importers/noaa_seals_2019.py +0 -181
- megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
- megadetector/data_management/importers/pc_to_json.py +0 -365
- megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
- megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
- megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
- megadetector/data_management/importers/rspb_to_json.py +0 -356
- megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
- megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
- megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
- megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
- megadetector/data_management/importers/sulross_get_exif.py +0 -65
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
- megadetector/data_management/importers/ubc_to_json.py +0 -399
- megadetector/data_management/importers/umn_to_json.py +0 -507
- megadetector/data_management/importers/wellington_to_json.py +0 -263
- megadetector/data_management/importers/wi_to_json.py +0 -442
- megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
- megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
- megadetector-5.0.27.dist-info/RECORD +0 -208
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
megadetector/utils/ct_utils.py
CHANGED
|
@@ -13,6 +13,10 @@ import json
|
|
|
13
13
|
import math
|
|
14
14
|
import os
|
|
15
15
|
import builtins
|
|
16
|
+
import datetime
|
|
17
|
+
import tempfile
|
|
18
|
+
import shutil
|
|
19
|
+
import uuid
|
|
16
20
|
|
|
17
21
|
import jsonpickle
|
|
18
22
|
import numpy as np
|
|
@@ -28,13 +32,13 @@ image_extensions = ['.jpg', '.jpeg', '.gif', '.png']
|
|
|
28
32
|
|
|
29
33
|
def truncate_float_array(xs, precision=3):
|
|
30
34
|
"""
|
|
31
|
-
Truncates the fractional portion of each floating-point value in the array [xs]
|
|
35
|
+
Truncates the fractional portion of each floating-point value in the array [xs]
|
|
32
36
|
to a specific number of floating-point digits.
|
|
33
37
|
|
|
34
38
|
Args:
|
|
35
39
|
xs (list): list of floats to truncate
|
|
36
|
-
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
37
|
-
|
|
40
|
+
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
41
|
+
|
|
38
42
|
Returns:
|
|
39
43
|
list: list of truncated floats
|
|
40
44
|
"""
|
|
@@ -44,52 +48,52 @@ def truncate_float_array(xs, precision=3):
|
|
|
44
48
|
|
|
45
49
|
def round_float_array(xs, precision=3):
|
|
46
50
|
"""
|
|
47
|
-
Truncates the fractional portion of each floating-point value in the array [xs]
|
|
51
|
+
Truncates the fractional portion of each floating-point value in the array [xs]
|
|
48
52
|
to a specific number of floating-point digits.
|
|
49
53
|
|
|
50
54
|
Args:
|
|
51
55
|
xs (list): list of floats to round
|
|
52
|
-
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
53
|
-
|
|
56
|
+
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
57
|
+
|
|
54
58
|
Returns:
|
|
55
|
-
list: list of rounded floats
|
|
59
|
+
list: list of rounded floats
|
|
56
60
|
"""
|
|
57
|
-
|
|
61
|
+
|
|
58
62
|
return [round_float(x,precision) for x in xs]
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
def round_float(x, precision=3):
|
|
62
66
|
"""
|
|
63
67
|
Convenience wrapper for the native Python round()
|
|
64
|
-
|
|
68
|
+
|
|
65
69
|
Args:
|
|
66
70
|
x (float): number to truncate
|
|
67
71
|
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
68
|
-
|
|
72
|
+
|
|
69
73
|
Returns:
|
|
70
74
|
float: rounded value
|
|
71
75
|
"""
|
|
72
|
-
|
|
76
|
+
|
|
73
77
|
return round(x,precision)
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
|
|
79
|
+
|
|
76
80
|
def truncate_float(x, precision=3):
|
|
77
81
|
"""
|
|
78
|
-
Truncates the fractional portion of a floating-point value to a specific number of
|
|
82
|
+
Truncates the fractional portion of a floating-point value to a specific number of
|
|
79
83
|
floating-point digits.
|
|
80
|
-
|
|
81
|
-
For example:
|
|
82
|
-
|
|
84
|
+
|
|
85
|
+
For example:
|
|
86
|
+
|
|
83
87
|
truncate_float(0.0003214884) --> 0.000321
|
|
84
88
|
truncate_float(1.0003214884) --> 1.000321
|
|
85
|
-
|
|
89
|
+
|
|
86
90
|
This function is primarily used to achieve a certain float representation
|
|
87
91
|
before exporting to JSON.
|
|
88
92
|
|
|
89
93
|
Args:
|
|
90
94
|
x (float): scalar to truncate
|
|
91
95
|
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
92
|
-
|
|
96
|
+
|
|
93
97
|
Returns:
|
|
94
98
|
float: truncated version of [x]
|
|
95
99
|
"""
|
|
@@ -106,11 +110,11 @@ def args_to_object(args, obj):
|
|
|
106
110
|
Args:
|
|
107
111
|
args (argparse.Namespace): the namespace to convert to an object
|
|
108
112
|
obj (object): object whose whose attributes will be updated
|
|
109
|
-
|
|
113
|
+
|
|
110
114
|
Returns:
|
|
111
115
|
object: the modified object (modified in place, but also returned)
|
|
112
116
|
"""
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
for n, v in inspect.getmembers(args):
|
|
115
119
|
if not n.startswith('_'):
|
|
116
120
|
setattr(obj, n, v)
|
|
@@ -120,17 +124,17 @@ def args_to_object(args, obj):
|
|
|
120
124
|
|
|
121
125
|
def dict_to_object(d, obj):
|
|
122
126
|
"""
|
|
123
|
-
Copies all fields from a dict to an object. Skips fields starting with _.
|
|
127
|
+
Copies all fields from a dict to an object. Skips fields starting with _.
|
|
124
128
|
Does not check existence in the target object.
|
|
125
129
|
|
|
126
130
|
Args:
|
|
127
131
|
d (dict): the dict to convert to an object
|
|
128
132
|
obj (object): object whose whose attributes will be updated
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
Returns:
|
|
131
135
|
object: the modified object (modified in place, but also returned)
|
|
132
136
|
"""
|
|
133
|
-
|
|
137
|
+
|
|
134
138
|
for k in d.keys():
|
|
135
139
|
if not k.startswith('_'):
|
|
136
140
|
setattr(obj, k, d[k])
|
|
@@ -141,11 +145,11 @@ def dict_to_object(d, obj):
|
|
|
141
145
|
def pretty_print_object(obj, b_print=True):
|
|
142
146
|
"""
|
|
143
147
|
Converts an arbitrary object to .json, optionally printing the .json representation.
|
|
144
|
-
|
|
148
|
+
|
|
145
149
|
Args:
|
|
146
150
|
obj (object): object to print
|
|
147
151
|
b_print (bool, optional): whether to print the object
|
|
148
|
-
|
|
152
|
+
|
|
149
153
|
Returns:
|
|
150
154
|
str: .json reprepresentation of [obj]
|
|
151
155
|
"""
|
|
@@ -162,44 +166,85 @@ def pretty_print_object(obj, b_print=True):
|
|
|
162
166
|
return s
|
|
163
167
|
|
|
164
168
|
|
|
165
|
-
def is_list_sorted(L, reverse=False):
|
|
169
|
+
def is_list_sorted(L, reverse=False): # noqa
|
|
166
170
|
"""
|
|
167
171
|
Returns True if the list L appears to be sorted, otherwise False.
|
|
168
|
-
|
|
172
|
+
|
|
169
173
|
Calling is_list_sorted(L,reverse=True) is the same as calling
|
|
170
174
|
is_list_sorted(L.reverse(),reverse=False).
|
|
171
|
-
|
|
175
|
+
|
|
172
176
|
Args:
|
|
173
177
|
L (list): list to evaluate
|
|
174
|
-
reverse (bool, optional): whether to reverse the list before evaluating sort status
|
|
175
|
-
|
|
178
|
+
reverse (bool, optional): whether to reverse the list before evaluating sort status
|
|
179
|
+
|
|
176
180
|
Returns:
|
|
177
181
|
bool: True if the list L appears to be sorted, otherwise False
|
|
178
182
|
"""
|
|
179
|
-
|
|
183
|
+
|
|
180
184
|
if reverse:
|
|
181
185
|
return all(L[i] >= L[i + 1] for i in range(len(L)-1))
|
|
182
186
|
else:
|
|
183
187
|
return all(L[i] <= L[i + 1] for i in range(len(L)-1))
|
|
184
|
-
|
|
185
188
|
|
|
186
|
-
|
|
189
|
+
|
|
190
|
+
def json_serialize_datetime(obj):
|
|
191
|
+
"""
|
|
192
|
+
Serializes datetime.datetime and datetime.date objects to ISO format.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
obj (object): The object to serialize.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str: The ISO format string representation of the datetime object.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
TypeError: If the object is not a datetime.datetime or datetime.date instance.
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
204
|
+
return obj.isoformat()
|
|
205
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable by json_serialize_datetime")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def write_json(path,
|
|
209
|
+
content,
|
|
210
|
+
indent=1,
|
|
211
|
+
force_str=False,
|
|
212
|
+
serialize_datetimes=False,
|
|
213
|
+
ensure_ascii=True,
|
|
214
|
+
encoding='utf-8'):
|
|
187
215
|
"""
|
|
188
216
|
Standardized wrapper for json.dump().
|
|
189
|
-
|
|
217
|
+
|
|
190
218
|
Args:
|
|
191
219
|
path (str): filename to write to
|
|
192
220
|
content (object): object to dump
|
|
193
221
|
indent (int, optional): indentation depth passed to json.dump
|
|
222
|
+
force_str (bool, optional): whether to force string conversion for non-serializable objects
|
|
223
|
+
serialize_datetimes (bool, optional): whether to serialize datetime objects to ISO format
|
|
224
|
+
ensure_ascii (bool, optional): whether to ensure ASCII characters in the output
|
|
194
225
|
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
226
|
+
|
|
227
|
+
default_handler = None
|
|
228
|
+
|
|
229
|
+
if serialize_datetimes:
|
|
230
|
+
default_handler = json_serialize_datetime
|
|
231
|
+
if force_str:
|
|
232
|
+
def serialize_or_str(obj):
|
|
233
|
+
try:
|
|
234
|
+
return json_serialize_datetime(obj)
|
|
235
|
+
except TypeError:
|
|
236
|
+
return str(obj)
|
|
237
|
+
default_handler = serialize_or_str
|
|
238
|
+
elif force_str:
|
|
239
|
+
default_handler = str
|
|
240
|
+
|
|
241
|
+
with open(path, 'w', newline='\n', encoding=encoding) as f:
|
|
242
|
+
json.dump(content, f, indent=indent, default=default_handler, ensure_ascii=ensure_ascii)
|
|
198
243
|
|
|
199
244
|
|
|
200
245
|
def convert_yolo_to_xywh(yolo_box):
|
|
201
246
|
"""
|
|
202
|
-
Converts a YOLO format bounding box [x_center, y_center, w, h] to
|
|
247
|
+
Converts a YOLO format bounding box [x_center, y_center, w, h] to
|
|
203
248
|
[x_min, y_min, width_of_box, height_of_box].
|
|
204
249
|
|
|
205
250
|
Args:
|
|
@@ -208,7 +253,7 @@ def convert_yolo_to_xywh(yolo_box):
|
|
|
208
253
|
Returns:
|
|
209
254
|
list: bbox with coordinates represented as [x_min, y_min, width_of_box, height_of_box]
|
|
210
255
|
"""
|
|
211
|
-
|
|
256
|
+
|
|
212
257
|
x_center, y_center, width_of_box, height_of_box = yolo_box
|
|
213
258
|
x_min = x_center - width_of_box / 2.0
|
|
214
259
|
y_min = y_center - height_of_box / 2.0
|
|
@@ -238,7 +283,7 @@ def get_iou(bb1, bb2):
|
|
|
238
283
|
Calculates the intersection over union (IoU) of two bounding boxes.
|
|
239
284
|
|
|
240
285
|
Adapted from:
|
|
241
|
-
|
|
286
|
+
|
|
242
287
|
https://stackoverflow.com/questions/25349178/calculating-percentage-of-bounding-box-overlap-for-image-detector-evaluation
|
|
243
288
|
|
|
244
289
|
Args:
|
|
@@ -288,7 +333,7 @@ def _get_max_conf_from_detections(detections):
|
|
|
288
333
|
"""
|
|
289
334
|
Internal function used by get_max_conf(); don't call this directly.
|
|
290
335
|
"""
|
|
291
|
-
|
|
336
|
+
|
|
292
337
|
max_conf = 0.0
|
|
293
338
|
if detections is not None and len(detections) > 0:
|
|
294
339
|
confidences = [det['conf'] for det in detections]
|
|
@@ -298,16 +343,17 @@ def _get_max_conf_from_detections(detections):
|
|
|
298
343
|
|
|
299
344
|
def get_max_conf(im):
|
|
300
345
|
"""
|
|
301
|
-
Given an image dict in the MD output format, computes the maximum detection confidence for any
|
|
302
|
-
class. Returns 0.0
|
|
303
|
-
|
|
346
|
+
Given an image dict in the MD output format, computes the maximum detection confidence for any
|
|
347
|
+
class. Returns 0.0 if there were no detections, if there was a failure, or if 'detections' isn't
|
|
348
|
+
present.
|
|
349
|
+
|
|
304
350
|
Args:
|
|
305
351
|
im (dict): image dictionary in the MD output format (with a 'detections' field)
|
|
306
|
-
|
|
352
|
+
|
|
307
353
|
Returns:
|
|
308
354
|
float: the maximum detection confidence across all classes
|
|
309
355
|
"""
|
|
310
|
-
|
|
356
|
+
|
|
311
357
|
max_conf = 0.0
|
|
312
358
|
if 'detections' in im and im['detections'] is not None and len(im['detections']) > 0:
|
|
313
359
|
max_conf = _get_max_conf_from_detections(im['detections'])
|
|
@@ -317,7 +363,7 @@ def get_max_conf(im):
|
|
|
317
363
|
def sort_results_for_image(im):
|
|
318
364
|
"""
|
|
319
365
|
Sort classification and detection results in descending order by confidence (in place).
|
|
320
|
-
|
|
366
|
+
|
|
321
367
|
Args:
|
|
322
368
|
im (dict): image dictionary in the MD output format (with a 'detections' field)
|
|
323
369
|
"""
|
|
@@ -326,55 +372,56 @@ def sort_results_for_image(im):
|
|
|
326
372
|
|
|
327
373
|
# Sort detections in descending order by confidence
|
|
328
374
|
im['detections'] = sort_list_of_dicts_by_key(im['detections'],k='conf',reverse=True)
|
|
329
|
-
|
|
375
|
+
|
|
330
376
|
for det in im['detections']:
|
|
331
|
-
|
|
377
|
+
|
|
332
378
|
# Sort classifications (which are (class,conf) tuples) in descending order by confidence
|
|
333
379
|
if 'classifications' in det and \
|
|
334
380
|
(det['classifications'] is not None) and \
|
|
335
381
|
(len(det['classifications']) > 0):
|
|
336
|
-
|
|
337
|
-
det['classifications'] =
|
|
382
|
+
classifications = det['classifications']
|
|
383
|
+
det['classifications'] = \
|
|
384
|
+
sorted(classifications,key=itemgetter(1),reverse=True)
|
|
338
385
|
|
|
339
386
|
|
|
340
387
|
def point_dist(p1,p2):
|
|
341
388
|
"""
|
|
342
389
|
Computes the distance between two points, represented as length-two tuples.
|
|
343
|
-
|
|
390
|
+
|
|
344
391
|
Args:
|
|
345
392
|
p1: point, formatted as (x,y)
|
|
346
393
|
p2: point, formatted as (x,y)
|
|
347
|
-
|
|
394
|
+
|
|
348
395
|
Returns:
|
|
349
396
|
float: the Euclidean distance between p1 and p2
|
|
350
397
|
"""
|
|
351
|
-
|
|
398
|
+
|
|
352
399
|
return math.sqrt( ((p1[0]-p2[0])**2) + ((p1[1]-p2[1])**2) )
|
|
353
400
|
|
|
354
401
|
|
|
355
402
|
def rect_distance(r1, r2, format='x0y0x1y1'):
|
|
356
403
|
"""
|
|
357
|
-
Computes the minimum distance between two axis-aligned rectangles, each represented as
|
|
404
|
+
Computes the minimum distance between two axis-aligned rectangles, each represented as
|
|
358
405
|
(x0,y0,x1,y1) by default.
|
|
359
|
-
|
|
406
|
+
|
|
360
407
|
Can also specify "format" as x0y0wh for MD-style bbox formatting (x0,y0,w,h).
|
|
361
|
-
|
|
408
|
+
|
|
362
409
|
Args:
|
|
363
410
|
r1: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
364
411
|
r2: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
365
412
|
format (str, optional): whether the boxes are formatted as 'x0y0x1y1' (default) or 'x0y0wh'
|
|
366
|
-
|
|
413
|
+
|
|
367
414
|
Returns:
|
|
368
415
|
float: the minimum distance between r1 and r2
|
|
369
416
|
"""
|
|
370
|
-
|
|
417
|
+
|
|
371
418
|
assert format in ('x0y0x1y1','x0y0wh'), 'Illegal rectangle format {}'.format(format)
|
|
372
|
-
|
|
419
|
+
|
|
373
420
|
if format == 'x0y0wh':
|
|
374
421
|
# Convert to x0y0x1y1 without modifying the original rectangles
|
|
375
422
|
r1 = [r1[0],r1[1],r1[0]+r1[2],r1[1]+r1[3]]
|
|
376
423
|
r2 = [r2[0],r2[1],r2[0]+r2[2],r2[1]+r2[3]]
|
|
377
|
-
|
|
424
|
+
|
|
378
425
|
# https://stackoverflow.com/a/26178015
|
|
379
426
|
x1, y1, x1b, y1b = r1
|
|
380
427
|
x2, y2, x2b, y2b = r2
|
|
@@ -402,40 +449,40 @@ def rect_distance(r1, r2, format='x0y0x1y1'):
|
|
|
402
449
|
return 0.0
|
|
403
450
|
|
|
404
451
|
|
|
405
|
-
def split_list_into_fixed_size_chunks(L,n):
|
|
452
|
+
def split_list_into_fixed_size_chunks(L,n): # noqa
|
|
406
453
|
"""
|
|
407
|
-
Split the list or tuple L into chunks of size n (allowing at most one chunk with size
|
|
454
|
+
Split the list or tuple L into chunks of size n (allowing at most one chunk with size
|
|
408
455
|
less than N, i.e. len(L) does not have to be a multiple of n).
|
|
409
|
-
|
|
456
|
+
|
|
410
457
|
Args:
|
|
411
458
|
L (list): list to split into chunks
|
|
412
459
|
n (int): preferred chunk size
|
|
413
|
-
|
|
460
|
+
|
|
414
461
|
Returns:
|
|
415
462
|
list: list of chunks, where each chunk is a list of length n or n-1
|
|
416
463
|
"""
|
|
417
|
-
|
|
464
|
+
|
|
418
465
|
return [L[i * n:(i + 1) * n] for i in range((len(L) + n - 1) // n )]
|
|
419
466
|
|
|
420
467
|
|
|
421
|
-
def split_list_into_n_chunks(L, n, chunk_strategy='greedy'):
|
|
468
|
+
def split_list_into_n_chunks(L, n, chunk_strategy='greedy'): # noqa
|
|
422
469
|
"""
|
|
423
|
-
Splits the list or tuple L into n equally-sized chunks (some chunks may be one
|
|
470
|
+
Splits the list or tuple L into n equally-sized chunks (some chunks may be one
|
|
424
471
|
element smaller than others, i.e. len(L) does not have to be a multiple of n).
|
|
425
|
-
|
|
472
|
+
|
|
426
473
|
chunk_strategy can be "greedy" (default, if there are k samples per chunk, the first
|
|
427
474
|
k go into the first chunk) or "balanced" (alternate between chunks when pulling
|
|
428
475
|
items from the list).
|
|
429
|
-
|
|
476
|
+
|
|
430
477
|
Args:
|
|
431
478
|
L (list): list to split into chunks
|
|
432
479
|
n (int): number of chunks
|
|
433
480
|
chunk_strategy (str, optiopnal): "greedy" or "balanced"; see above
|
|
434
|
-
|
|
481
|
+
|
|
435
482
|
Returns:
|
|
436
483
|
list: list of chunks, each of which is a list
|
|
437
484
|
"""
|
|
438
|
-
|
|
485
|
+
|
|
439
486
|
if chunk_strategy == 'greedy':
|
|
440
487
|
k, m = divmod(len(L), n)
|
|
441
488
|
return list(L[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(n))
|
|
@@ -449,54 +496,54 @@ def split_list_into_n_chunks(L, n, chunk_strategy='greedy'):
|
|
|
449
496
|
raise ValueError('Invalid chunk strategy: {}'.format(chunk_strategy))
|
|
450
497
|
|
|
451
498
|
|
|
452
|
-
def sort_list_of_dicts_by_key(L,k,reverse=False):
|
|
499
|
+
def sort_list_of_dicts_by_key(L,k,reverse=False): # noqa
|
|
453
500
|
"""
|
|
454
501
|
Sorts the list of dictionaries [L] by the key [k].
|
|
455
|
-
|
|
502
|
+
|
|
456
503
|
Args:
|
|
457
504
|
L (list): list of dictionaries to sort
|
|
458
505
|
k (object, typically str): the sort key
|
|
459
506
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
460
|
-
|
|
507
|
+
|
|
461
508
|
Returns:
|
|
462
509
|
dict: sorted copy of [d]
|
|
463
510
|
"""
|
|
464
511
|
return sorted(L, key=lambda d: d[k], reverse=reverse)
|
|
465
|
-
|
|
466
|
-
|
|
512
|
+
|
|
513
|
+
|
|
467
514
|
def sort_dictionary_by_key(d,reverse=False):
|
|
468
515
|
"""
|
|
469
516
|
Sorts the dictionary [d] by key.
|
|
470
|
-
|
|
517
|
+
|
|
471
518
|
Args:
|
|
472
519
|
d (dict): dictionary to sort
|
|
473
520
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
474
|
-
|
|
521
|
+
|
|
475
522
|
Returns:
|
|
476
523
|
dict: sorted copy of [d]
|
|
477
524
|
"""
|
|
478
|
-
|
|
525
|
+
|
|
479
526
|
d = dict(sorted(d.items(),reverse=reverse))
|
|
480
527
|
return d
|
|
481
|
-
|
|
528
|
+
|
|
482
529
|
|
|
483
530
|
def sort_dictionary_by_value(d,sort_values=None,reverse=False):
|
|
484
531
|
"""
|
|
485
532
|
Sorts the dictionary [d] by value. If sort_values is None, uses d.values(),
|
|
486
|
-
otherwise uses the dictionary sort_values as the sorting criterion. Always
|
|
487
|
-
returns a new standard dict, so if [d] is, for example, a defaultdict, the
|
|
533
|
+
otherwise uses the dictionary sort_values as the sorting criterion. Always
|
|
534
|
+
returns a new standard dict, so if [d] is, for example, a defaultdict, the
|
|
488
535
|
returned value is not.
|
|
489
|
-
|
|
536
|
+
|
|
490
537
|
Args:
|
|
491
538
|
d (dict): dictionary to sort
|
|
492
|
-
sort_values (dict, optional): dictionary mapping keys in [d] to sort values (defaults
|
|
539
|
+
sort_values (dict, optional): dictionary mapping keys in [d] to sort values (defaults
|
|
493
540
|
to None, uses [d] itself for sorting)
|
|
494
541
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
495
|
-
|
|
542
|
+
|
|
496
543
|
Returns:
|
|
497
544
|
dict: sorted copy of [d
|
|
498
545
|
"""
|
|
499
|
-
|
|
546
|
+
|
|
500
547
|
if sort_values is None:
|
|
501
548
|
d = {k: v for k, v in sorted(d.items(), key=lambda item: item[1], reverse=reverse)}
|
|
502
549
|
else:
|
|
@@ -508,111 +555,133 @@ def invert_dictionary(d):
|
|
|
508
555
|
"""
|
|
509
556
|
Creates a new dictionary that maps d.values() to d.keys(). Does not check
|
|
510
557
|
uniqueness.
|
|
511
|
-
|
|
558
|
+
|
|
512
559
|
Args:
|
|
513
560
|
d (dict): dictionary to invert
|
|
514
|
-
|
|
561
|
+
|
|
515
562
|
Returns:
|
|
516
563
|
dict: inverted copy of [d]
|
|
517
564
|
"""
|
|
518
|
-
|
|
565
|
+
|
|
519
566
|
return {v: k for k, v in d.items()}
|
|
520
567
|
|
|
521
568
|
|
|
522
|
-
def round_floats_in_nested_dict(obj, decimal_places=5):
|
|
569
|
+
def round_floats_in_nested_dict(obj, decimal_places=5, allow_iterator_conversion=False):
|
|
523
570
|
"""
|
|
524
|
-
Recursively rounds all floating point values in a nested structure to the
|
|
525
|
-
specified number of decimal places. Handles dictionaries, lists, tuples,
|
|
571
|
+
Recursively rounds all floating point values in a nested structure to the
|
|
572
|
+
specified number of decimal places. Handles dictionaries, lists, tuples,
|
|
526
573
|
sets, and other iterables. Modifies mutable objects in place.
|
|
527
|
-
|
|
574
|
+
|
|
528
575
|
Args:
|
|
529
576
|
obj: The object to process (can be a dict, list, set, tuple, or primitive value)
|
|
530
|
-
decimal_places: Number of decimal places to round to
|
|
531
|
-
|
|
577
|
+
decimal_places (int, optional): Number of decimal places to round to
|
|
578
|
+
allow_iterator_conversion (bool, optional): for iterator types, should we convert
|
|
579
|
+
to lists? Otherwise we error.
|
|
580
|
+
|
|
532
581
|
Returns:
|
|
533
582
|
The processed object (useful for recursive calls)
|
|
534
583
|
"""
|
|
535
584
|
if isinstance(obj, dict):
|
|
536
585
|
for key in obj:
|
|
537
|
-
obj[key] = round_floats_in_nested_dict(obj[key], decimal_places
|
|
586
|
+
obj[key] = round_floats_in_nested_dict(obj[key], decimal_places=decimal_places,
|
|
587
|
+
allow_iterator_conversion=allow_iterator_conversion)
|
|
538
588
|
return obj
|
|
539
|
-
|
|
589
|
+
|
|
540
590
|
elif isinstance(obj, list):
|
|
541
591
|
for i in range(len(obj)):
|
|
542
|
-
obj[i] = round_floats_in_nested_dict(obj[i], decimal_places
|
|
592
|
+
obj[i] = round_floats_in_nested_dict(obj[i], decimal_places=decimal_places,
|
|
593
|
+
allow_iterator_conversion=allow_iterator_conversion)
|
|
543
594
|
return obj
|
|
544
|
-
|
|
595
|
+
|
|
545
596
|
elif isinstance(obj, tuple):
|
|
546
597
|
# Tuples are immutable, so we create a new one
|
|
547
|
-
return tuple(round_floats_in_nested_dict(item, decimal_places
|
|
548
|
-
|
|
598
|
+
return tuple(round_floats_in_nested_dict(item, decimal_places=decimal_places,
|
|
599
|
+
allow_iterator_conversion=allow_iterator_conversion) for item in obj)
|
|
600
|
+
|
|
549
601
|
elif isinstance(obj, set):
|
|
550
602
|
# Sets are mutable but we can't modify elements in-place
|
|
551
603
|
# Convert to list, process, and convert back to set
|
|
552
|
-
return set(round_floats_in_nested_dict(list(obj), decimal_places
|
|
553
|
-
|
|
604
|
+
return set(round_floats_in_nested_dict(list(obj), decimal_places=decimal_places,
|
|
605
|
+
allow_iterator_conversion=allow_iterator_conversion))
|
|
606
|
+
|
|
554
607
|
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
|
|
555
|
-
# Handle other iterable types
|
|
556
|
-
|
|
557
|
-
|
|
608
|
+
# Handle other iterable types: convert to list, process, and convert back
|
|
609
|
+
processed_list = [round_floats_in_nested_dict(item,
|
|
610
|
+
decimal_places=decimal_places,
|
|
611
|
+
allow_iterator_conversion=allow_iterator_conversion) \
|
|
612
|
+
for item in obj]
|
|
613
|
+
|
|
614
|
+
# Try to recreate the original type, but fall back to list for iterators
|
|
615
|
+
try:
|
|
616
|
+
return type(obj)(processed_list)
|
|
617
|
+
except (TypeError, ValueError):
|
|
618
|
+
if allow_iterator_conversion:
|
|
619
|
+
# For iterators and other types that can't be reconstructed, return a list
|
|
620
|
+
return processed_list
|
|
621
|
+
else:
|
|
622
|
+
raise ValueError('Cannot process iterator types when allow_iterator_conversion is False')
|
|
623
|
+
|
|
558
624
|
elif isinstance(obj, float):
|
|
559
625
|
return round(obj, decimal_places)
|
|
560
|
-
|
|
626
|
+
|
|
561
627
|
else:
|
|
562
628
|
# For other types (int, str, bool, None, etc.), return as is
|
|
563
629
|
return obj
|
|
564
630
|
|
|
565
|
-
# ...def round_floats_in_nested_dict(...)
|
|
631
|
+
# ...def round_floats_in_nested_dict(...)
|
|
566
632
|
|
|
567
633
|
|
|
568
634
|
def image_file_to_camera_folder(image_fn):
|
|
569
635
|
r"""
|
|
570
636
|
Removes common overflow folders (e.g. RECNX101, RECNX102) from paths, i.e. turn:
|
|
571
|
-
|
|
637
|
+
|
|
572
638
|
a\b\c\RECNX101\image001.jpg
|
|
573
|
-
|
|
639
|
+
|
|
574
640
|
...into:
|
|
575
|
-
|
|
641
|
+
|
|
576
642
|
a\b\c
|
|
577
643
|
|
|
578
|
-
Returns the same thing as os.dirname() (i.e., just the folder name) if no overflow folders are
|
|
644
|
+
Returns the same thing as os.dirname() (i.e., just the folder name) if no overflow folders are
|
|
579
645
|
present.
|
|
580
646
|
|
|
581
647
|
Always converts backslashes to slashes.
|
|
582
|
-
|
|
648
|
+
|
|
583
649
|
Args:
|
|
584
650
|
image_fn (str): the image filename from which we should remove overflow folders
|
|
585
|
-
|
|
651
|
+
|
|
586
652
|
Returns:
|
|
587
653
|
str: a version of [image_fn] from which camera overflow folders have been removed
|
|
588
654
|
"""
|
|
589
|
-
|
|
655
|
+
|
|
590
656
|
import re
|
|
591
|
-
|
|
657
|
+
|
|
592
658
|
# 100RECNX is the overflow folder style for Reconyx cameras
|
|
593
659
|
# 100EK113 is (for some reason) the overflow folder style for Bushnell cameras
|
|
594
660
|
# 100_BTCF is the overflow folder style for Browning cameras
|
|
595
661
|
# 100MEDIA is the overflow folder style used on a number of consumer-grade cameras
|
|
596
662
|
patterns = [r'/\d+RECNX/',r'/\d+EK\d+/',r'/\d+_BTCF/',r'/\d+MEDIA/']
|
|
597
|
-
|
|
598
|
-
image_fn = image_fn.replace('\\','/')
|
|
663
|
+
|
|
664
|
+
image_fn = image_fn.replace('\\','/')
|
|
599
665
|
for pat in patterns:
|
|
600
666
|
image_fn = re.sub(pat,'/',image_fn)
|
|
601
667
|
camera_folder = os.path.dirname(image_fn)
|
|
602
|
-
|
|
668
|
+
|
|
603
669
|
return camera_folder
|
|
604
|
-
|
|
670
|
+
|
|
605
671
|
|
|
606
672
|
def is_float(v):
|
|
607
673
|
"""
|
|
608
674
|
Determines whether v is either a float or a string representation of a float.
|
|
609
|
-
|
|
675
|
+
|
|
610
676
|
Args:
|
|
611
677
|
v (object): object to evaluate
|
|
612
|
-
|
|
678
|
+
|
|
613
679
|
Returns:
|
|
614
680
|
bool: True if [v] is a float or a string representation of a float, otherwise False
|
|
615
681
|
"""
|
|
682
|
+
|
|
683
|
+
if v is None:
|
|
684
|
+
return False
|
|
616
685
|
|
|
617
686
|
try:
|
|
618
687
|
_ = float(v)
|
|
@@ -624,17 +693,17 @@ def is_float(v):
|
|
|
624
693
|
def is_iterable(x):
|
|
625
694
|
"""
|
|
626
695
|
Uses duck typing to assess whether [x] is iterable (list, set, dict, etc.).
|
|
627
|
-
|
|
696
|
+
|
|
628
697
|
Args:
|
|
629
698
|
x (object): the object to test
|
|
630
|
-
|
|
699
|
+
|
|
631
700
|
Returns:
|
|
632
701
|
bool: True if [x] appears to be iterable, otherwise False
|
|
633
702
|
"""
|
|
634
|
-
|
|
703
|
+
|
|
635
704
|
try:
|
|
636
705
|
_ = iter(x)
|
|
637
|
-
except:
|
|
706
|
+
except Exception:
|
|
638
707
|
return False
|
|
639
708
|
return True
|
|
640
709
|
|
|
@@ -643,10 +712,10 @@ def is_empty(v):
|
|
|
643
712
|
"""
|
|
644
713
|
A common definition of "empty" used throughout the repo, particularly when loading
|
|
645
714
|
data from .csv files. "empty" includes None, '', and NaN.
|
|
646
|
-
|
|
715
|
+
|
|
647
716
|
Args:
|
|
648
717
|
v: the object to evaluate for emptiness
|
|
649
|
-
|
|
718
|
+
|
|
650
719
|
Returns:
|
|
651
720
|
bool: True if [v] is None, '', or NaN, otherwise False
|
|
652
721
|
"""
|
|
@@ -659,15 +728,55 @@ def is_empty(v):
|
|
|
659
728
|
return False
|
|
660
729
|
|
|
661
730
|
|
|
731
|
+
def to_bool(v):
|
|
732
|
+
"""
|
|
733
|
+
Convert an object to a bool with specific rules.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
obj (object): The object to convert
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
bool or None:
|
|
740
|
+
- For strings: True if 'true' (case-insensitive), False if 'false', recursively applied if int-like
|
|
741
|
+
- For int/bytes: False if 0, True otherwise
|
|
742
|
+
- For bool: returns the bool as-is
|
|
743
|
+
- For other types: None
|
|
744
|
+
"""
|
|
745
|
+
|
|
746
|
+
if isinstance(v, bool):
|
|
747
|
+
return v
|
|
748
|
+
|
|
749
|
+
if isinstance(v, str):
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
v = int(v)
|
|
753
|
+
return to_bool(v)
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
756
|
+
|
|
757
|
+
v = v.lower().strip()
|
|
758
|
+
if v == 'true':
|
|
759
|
+
return True
|
|
760
|
+
elif v == 'false':
|
|
761
|
+
return False
|
|
762
|
+
else:
|
|
763
|
+
return None
|
|
764
|
+
|
|
765
|
+
if isinstance(v, (int, bytes)):
|
|
766
|
+
return v != 0
|
|
767
|
+
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
|
|
662
771
|
def min_none(a,b):
|
|
663
772
|
"""
|
|
664
|
-
Returns the minimum of a and b. If both are None, returns None. If one is None,
|
|
773
|
+
Returns the minimum of a and b. If both are None, returns None. If one is None,
|
|
665
774
|
returns the other.
|
|
666
|
-
|
|
775
|
+
|
|
667
776
|
Args:
|
|
668
777
|
a (numeric): the first value to compare
|
|
669
778
|
b (numeric): the second value to compare
|
|
670
|
-
|
|
779
|
+
|
|
671
780
|
Returns:
|
|
672
781
|
numeric: the minimum of a and b, or None
|
|
673
782
|
"""
|
|
@@ -679,17 +788,17 @@ def min_none(a,b):
|
|
|
679
788
|
return a
|
|
680
789
|
else:
|
|
681
790
|
return min(a,b)
|
|
682
|
-
|
|
791
|
+
|
|
683
792
|
|
|
684
793
|
def max_none(a,b):
|
|
685
794
|
"""
|
|
686
|
-
Returns the maximum of a and b. If both are None, returns None. If one is None,
|
|
795
|
+
Returns the maximum of a and b. If both are None, returns None. If one is None,
|
|
687
796
|
returns the other.
|
|
688
|
-
|
|
797
|
+
|
|
689
798
|
Args:
|
|
690
799
|
a (numeric): the first value to compare
|
|
691
800
|
b (numeric): the second value to compare
|
|
692
|
-
|
|
801
|
+
|
|
693
802
|
Returns:
|
|
694
803
|
numeric: the maximum of a and b, or None
|
|
695
804
|
"""
|
|
@@ -702,19 +811,19 @@ def max_none(a,b):
|
|
|
702
811
|
else:
|
|
703
812
|
return max(a,b)
|
|
704
813
|
|
|
705
|
-
|
|
814
|
+
|
|
706
815
|
def isnan(v):
|
|
707
816
|
"""
|
|
708
817
|
Returns True if v is a nan-valued float, otherwise returns False.
|
|
709
|
-
|
|
818
|
+
|
|
710
819
|
Args:
|
|
711
820
|
v: the object to evaluate for nan-ness
|
|
712
|
-
|
|
821
|
+
|
|
713
822
|
Returns:
|
|
714
823
|
bool: True if v is a nan-valued float, otherwise False
|
|
715
824
|
"""
|
|
716
|
-
|
|
717
|
-
try:
|
|
825
|
+
|
|
826
|
+
try:
|
|
718
827
|
return np.isnan(v)
|
|
719
828
|
except Exception:
|
|
720
829
|
return False
|
|
@@ -723,55 +832,56 @@ def isnan(v):
|
|
|
723
832
|
def sets_overlap(set1, set2):
|
|
724
833
|
"""
|
|
725
834
|
Determines whether two sets overlap.
|
|
726
|
-
|
|
835
|
+
|
|
727
836
|
Args:
|
|
728
837
|
set1 (set): the first set to compare (converted to a set if it's not already)
|
|
729
838
|
set2 (set): the second set to compare (converted to a set if it's not already)
|
|
730
|
-
|
|
839
|
+
|
|
731
840
|
Returns:
|
|
732
841
|
bool: True if any elements are shared between set1 and set2
|
|
733
842
|
"""
|
|
734
|
-
|
|
843
|
+
|
|
735
844
|
return not set(set1).isdisjoint(set(set2))
|
|
736
845
|
|
|
737
846
|
|
|
738
847
|
def is_function_name(s,calling_namespace):
|
|
739
848
|
"""
|
|
740
|
-
Determines whether [s] is a callable function in the global or local scope, or a
|
|
849
|
+
Determines whether [s] is a callable function in the global or local scope, or a
|
|
741
850
|
built-in function.
|
|
742
|
-
|
|
851
|
+
|
|
743
852
|
Args:
|
|
744
853
|
s (str): the string to test for function-ness
|
|
745
854
|
calling_namespace (dict): typically pass the output of locals()
|
|
746
855
|
"""
|
|
747
|
-
|
|
856
|
+
|
|
748
857
|
assert isinstance(s,str), 'Input is not a string'
|
|
749
|
-
|
|
858
|
+
|
|
750
859
|
return callable(globals().get(s)) or \
|
|
751
860
|
callable(locals().get(s)) or \
|
|
752
861
|
callable(calling_namespace.get(s)) or \
|
|
753
862
|
callable(getattr(builtins, s, None))
|
|
754
863
|
|
|
755
|
-
|
|
864
|
+
|
|
756
865
|
# From https://gist.github.com/fralau/061a4f6c13251367ef1d9a9a99fb3e8d
|
|
757
866
|
def parse_kvp(s,kv_separator='='):
|
|
758
867
|
"""
|
|
759
868
|
Parse a key/value pair, separated by [kv_separator]. Errors if s is not
|
|
760
|
-
a valid key/value pair string.
|
|
761
|
-
|
|
869
|
+
a valid key/value pair string. Strips leading/trailing whitespace from
|
|
870
|
+
the key and value.
|
|
871
|
+
|
|
762
872
|
Args:
|
|
763
873
|
s (str): the string to parse
|
|
764
874
|
kv_separator (str, optional): the string separating keys from values.
|
|
765
|
-
|
|
875
|
+
|
|
766
876
|
Returns:
|
|
767
877
|
tuple: a 2-tuple formatted as (key,value)
|
|
768
878
|
"""
|
|
769
|
-
|
|
879
|
+
|
|
770
880
|
items = s.split(kv_separator)
|
|
771
881
|
assert len(items) > 1, 'Illegal key-value pair'
|
|
772
882
|
key = items[0].strip()
|
|
773
883
|
if len(items) > 1:
|
|
774
|
-
value = kv_separator.join(items[1:])
|
|
884
|
+
value = kv_separator.join(items[1:]).strip()
|
|
775
885
|
return (key, value)
|
|
776
886
|
|
|
777
887
|
|
|
@@ -779,26 +889,26 @@ def parse_kvp_list(items,kv_separator='=',d=None):
|
|
|
779
889
|
"""
|
|
780
890
|
Parse a list key-value pairs into a dictionary. If items is None or [],
|
|
781
891
|
returns {}.
|
|
782
|
-
|
|
892
|
+
|
|
783
893
|
Args:
|
|
784
894
|
items (list): the list of KVPs to parse
|
|
785
895
|
kv_separator (str, optional): the string separating keys from values.
|
|
786
896
|
d (dict, optional): the initial dictionary, defaults to {}
|
|
787
|
-
|
|
897
|
+
|
|
788
898
|
Returns:
|
|
789
899
|
dict: a dict mapping keys to values
|
|
790
900
|
"""
|
|
791
|
-
|
|
901
|
+
|
|
792
902
|
if d is None:
|
|
793
903
|
d = {}
|
|
794
904
|
|
|
795
905
|
if items is None or len(items) == 0:
|
|
796
906
|
return d
|
|
797
|
-
|
|
907
|
+
|
|
798
908
|
for item in items:
|
|
799
|
-
key, value = parse_kvp(item)
|
|
909
|
+
key, value = parse_kvp(item,kv_separator=kv_separator)
|
|
800
910
|
d[key] = value
|
|
801
|
-
|
|
911
|
+
|
|
802
912
|
return d
|
|
803
913
|
|
|
804
914
|
|
|
@@ -810,24 +920,24 @@ def dict_to_kvp_list(d,
|
|
|
810
920
|
Convert a string <--> string dict into a string containing list of list of
|
|
811
921
|
key-value pairs. I.e., converts {'a':'dog','b':'cat'} to 'a=dog b=cat'. If
|
|
812
922
|
d is None, returns None. If d is empty, returns ''.
|
|
813
|
-
|
|
923
|
+
|
|
814
924
|
Args:
|
|
815
925
|
d (dict): the dictionary to convert, must contain only strings
|
|
816
926
|
item_separator (str, optional): the delimiter between KV pairs
|
|
817
927
|
kv_separator (str, optional): the separator betweena a key and its value
|
|
818
928
|
non_string_value_handling (str, optional): what do do with non-string values,
|
|
819
929
|
can be "omit", "error", or "convert"
|
|
820
|
-
|
|
930
|
+
|
|
821
931
|
Returns:
|
|
822
932
|
str: the string representation of [d]
|
|
823
933
|
"""
|
|
824
|
-
|
|
934
|
+
|
|
825
935
|
if d is None:
|
|
826
936
|
return None
|
|
827
|
-
|
|
937
|
+
|
|
828
938
|
if len(d) == 0:
|
|
829
939
|
return ''
|
|
830
|
-
|
|
940
|
+
|
|
831
941
|
s = None
|
|
832
942
|
for k in d.keys():
|
|
833
943
|
assert isinstance(k,str), 'Input {} is not a str <--> str dict'.format(str(d))
|
|
@@ -847,25 +957,25 @@ def dict_to_kvp_list(d,
|
|
|
847
957
|
else:
|
|
848
958
|
s += item_separator
|
|
849
959
|
s += k + kv_separator + v
|
|
850
|
-
|
|
960
|
+
|
|
851
961
|
if s is None:
|
|
852
962
|
s = ''
|
|
853
|
-
|
|
963
|
+
|
|
854
964
|
return s
|
|
855
|
-
|
|
965
|
+
|
|
856
966
|
|
|
857
967
|
def parse_bool_string(s):
|
|
858
968
|
"""
|
|
859
969
|
Convert the strings "true" or "false" to boolean values. Case-insensitive, discards
|
|
860
970
|
leading and trailing whitespace. If s is already a bool, returns s.
|
|
861
|
-
|
|
971
|
+
|
|
862
972
|
Args:
|
|
863
973
|
s (str or bool): the string to parse, or the bool to return
|
|
864
|
-
|
|
974
|
+
|
|
865
975
|
Returns:
|
|
866
976
|
bool: the parsed value
|
|
867
977
|
"""
|
|
868
|
-
|
|
978
|
+
|
|
869
979
|
if isinstance(s,bool):
|
|
870
980
|
return s
|
|
871
981
|
s = s.lower().strip()
|
|
@@ -875,57 +985,766 @@ def parse_bool_string(s):
|
|
|
875
985
|
return False
|
|
876
986
|
else:
|
|
877
987
|
raise ValueError('Cannot parse bool from string {}'.format(str(s)))
|
|
878
|
-
|
|
879
988
|
|
|
880
|
-
#%% Test driver
|
|
881
989
|
|
|
882
|
-
def
|
|
990
|
+
def make_temp_folder(top_level_folder='megadetector',subfolder=None,append_guid=True):
|
|
883
991
|
"""
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
992
|
+
Creates a temporary folder within the system temp folder, by default in a subfolder
|
|
993
|
+
called megadetector/some_guid. Used for testing without making too much of a mess.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
top_level_folder (str, optional): the top-level folder to use within the system temp folder
|
|
997
|
+
subfolder (str, optional): the subfolder within [top_level_folder]
|
|
998
|
+
append_guid (bool, optional): append a guid to the subfolder
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
str: the new directory
|
|
1002
|
+
"""
|
|
1003
|
+
|
|
1004
|
+
to_return = os.path.join(tempfile.gettempdir(),top_level_folder)
|
|
1005
|
+
if subfolder is not None:
|
|
1006
|
+
to_return = os.path.join(to_return,subfolder)
|
|
1007
|
+
if append_guid:
|
|
1008
|
+
to_return = os.path.join(to_return,str(uuid.uuid1()))
|
|
1009
|
+
to_return = os.path.normpath(to_return)
|
|
1010
|
+
os.makedirs(to_return,exist_ok=True)
|
|
1011
|
+
return to_return
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def make_test_folder(subfolder=None):
|
|
1015
|
+
"""
|
|
1016
|
+
Wrapper around make_temp_folder that creates folders within megadetector/tests
|
|
1017
|
+
"""
|
|
1018
|
+
|
|
1019
|
+
return make_temp_folder(top_level_folder='megadetector/tests',
|
|
1020
|
+
subfolder=subfolder,
|
|
1021
|
+
append_guid=True)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
#%% Tests
|
|
1025
|
+
|
|
1026
|
+
def test_write_json():
|
|
1027
|
+
"""
|
|
1028
|
+
Test driver for write_json.
|
|
1029
|
+
"""
|
|
1030
|
+
|
|
1031
|
+
temp_dir = make_test_folder()
|
|
1032
|
+
|
|
1033
|
+
def _verify_json_file(file_path, expected_content_str):
|
|
1034
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
1035
|
+
content = json.load(f)
|
|
1036
|
+
assert isinstance(content,dict)
|
|
1037
|
+
content = sort_dictionary_by_key(content)
|
|
1038
|
+
expected_content = json.loads(expected_content_str)
|
|
1039
|
+
expected_content = sort_dictionary_by_key(expected_content)
|
|
1040
|
+
assert content == expected_content, \
|
|
1041
|
+
f"File {file_path} content mismatch.\nExpected:\n{expected_content}\nGot:\n{content}"
|
|
1042
|
+
|
|
1043
|
+
# Test default indent (1)
|
|
1044
|
+
data_default = {'a': 1, 'b': 2}
|
|
1045
|
+
file_path_default = os.path.join(temp_dir, 'test_default_indent.json')
|
|
1046
|
+
write_json(file_path_default, data_default)
|
|
1047
|
+
# Default indent is 1
|
|
1048
|
+
_verify_json_file(file_path_default, '{\n "a": 1,\n "b": 2\n}')
|
|
1049
|
+
|
|
1050
|
+
# Test custom indent (e.g., 4)
|
|
1051
|
+
data_custom_indent = {'a': 1, 'b': 2}
|
|
1052
|
+
file_path_custom_indent = os.path.join(temp_dir, 'test_custom_indent.json')
|
|
1053
|
+
write_json(file_path_custom_indent, data_custom_indent, indent=4)
|
|
1054
|
+
_verify_json_file(file_path_custom_indent, '{\n "a": 1,\n "b": 2\n}')
|
|
1055
|
+
|
|
1056
|
+
# Test indent=None (compact)
|
|
1057
|
+
data_no_indent = {'a': 1, 'b': 2}
|
|
1058
|
+
file_path_no_indent = os.path.join(temp_dir, 'test_no_indent.json')
|
|
1059
|
+
write_json(file_path_no_indent, data_no_indent, indent=None)
|
|
1060
|
+
_verify_json_file(file_path_no_indent, '{"a": 1, "b": 2}')
|
|
1061
|
+
|
|
1062
|
+
# Test force_str=True
|
|
1063
|
+
data_force_str = {'a': 1, 's': {1, 2, 3}} # Set is not normally JSON serializable
|
|
1064
|
+
file_path_force_str = os.path.join(temp_dir, 'test_force_str.json')
|
|
1065
|
+
write_json(file_path_force_str, data_force_str, force_str=True)
|
|
1066
|
+
with open(file_path_force_str, 'r', encoding='utf-8') as f:
|
|
1067
|
+
result_force_str = json.load(f)
|
|
1068
|
+
assert isinstance(result_force_str['s'], str)
|
|
1069
|
+
assert eval(result_force_str['s']) == {1, 2, 3}
|
|
1070
|
+
|
|
1071
|
+
# Test serialize_datetimes=True
|
|
1072
|
+
dt = datetime.datetime(2023, 1, 1, 10, 30, 0)
|
|
1073
|
+
d_date = datetime.date(2023, 2, 15)
|
|
1074
|
+
data_serialize_datetimes = {'dt_obj': dt, 'd_obj': d_date}
|
|
1075
|
+
file_path_serialize_datetimes = os.path.join(temp_dir, 'test_serialize_datetimes.json')
|
|
1076
|
+
write_json(file_path_serialize_datetimes, data_serialize_datetimes, serialize_datetimes=True)
|
|
1077
|
+
_verify_json_file(file_path_serialize_datetimes, '{\n "d_obj": "2023-02-15",\n "dt_obj": "2023-01-01T10:30:00"\n}')
|
|
1078
|
+
|
|
1079
|
+
# Test serialize_datetimes=True and force_str=True
|
|
1080
|
+
dt_combo = datetime.datetime(2023, 1, 1, 12, 0, 0)
|
|
1081
|
+
data_datetime_force_str = {'dt_obj': dt_combo, 's_obj': {4, 5}}
|
|
1082
|
+
file_path_datetime_force_str = os.path.join(temp_dir, 'test_datetime_and_force_str.json')
|
|
1083
|
+
write_json(file_path_datetime_force_str, data_datetime_force_str, serialize_datetimes=True, force_str=True)
|
|
1084
|
+
with open(file_path_datetime_force_str, 'r', encoding='utf-8') as f:
|
|
1085
|
+
result_datetime_force_str = json.load(f)
|
|
1086
|
+
assert result_datetime_force_str['dt_obj'] == "2023-01-01T12:00:00"
|
|
1087
|
+
assert isinstance(result_datetime_force_str['s_obj'], str)
|
|
1088
|
+
assert eval(result_datetime_force_str['s_obj']) == {4, 5}
|
|
1089
|
+
|
|
1090
|
+
# Test ensure_ascii=False (with non-ASCII chars)
|
|
1091
|
+
data_ensure_ascii_false = {'name': 'Jules César'}
|
|
1092
|
+
file_path_ensure_ascii_false = os.path.join(temp_dir, 'test_ensure_ascii_false.json')
|
|
1093
|
+
write_json(file_path_ensure_ascii_false, data_ensure_ascii_false, ensure_ascii=False)
|
|
1094
|
+
with open(file_path_ensure_ascii_false, 'r', encoding='utf-8') as f:
|
|
1095
|
+
content_ensure_ascii_false = f.read()
|
|
1096
|
+
assert content_ensure_ascii_false == '{\n "name": "Jules César"\n}'
|
|
1097
|
+
|
|
1098
|
+
# Test ensure_ascii=True (with non-ASCII chars, default)
|
|
1099
|
+
data_ensure_ascii_true = {'name': 'Jules César'}
|
|
1100
|
+
file_path_ensure_ascii_true = os.path.join(temp_dir, 'test_ensure_ascii_true.json')
|
|
1101
|
+
write_json(file_path_ensure_ascii_true, data_ensure_ascii_true, ensure_ascii=True)
|
|
1102
|
+
with open(file_path_ensure_ascii_true, 'r', encoding='utf-8') as f:
|
|
1103
|
+
content_ensure_ascii_true = f.read()
|
|
1104
|
+
assert content_ensure_ascii_true == '{\n "name": "Jules C\\u00e9sar"\n}'
|
|
1105
|
+
|
|
1106
|
+
shutil.rmtree(temp_dir)
|
|
1107
|
+
|
|
1108
|
+
# ...def test_write_json(...)
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def test_path_operations():
|
|
1112
|
+
"""
|
|
1113
|
+
Test path manipulation functions.
|
|
1114
|
+
"""
|
|
1115
|
+
|
|
887
1116
|
##%% Camera folder mapping
|
|
888
|
-
|
|
889
|
-
assert image_file_to_camera_folder('a/b/c/d/100EK113/blah.jpg') == 'a/b/c/d'
|
|
1117
|
+
assert image_file_to_camera_folder('a/b/c/d/100EK113/blah.jpg') == 'a/b/c/d'
|
|
890
1118
|
assert image_file_to_camera_folder('a/b/c/d/100RECNX/blah.jpg') == 'a/b/c/d'
|
|
891
|
-
|
|
892
|
-
|
|
1119
|
+
assert image_file_to_camera_folder('a/b/c/d/blah.jpg') == 'a/b/c/d'
|
|
1120
|
+
assert image_file_to_camera_folder(r'a\b\c\d\100RECNX\blah.jpg') == 'a/b/c/d'
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def test_geometric_operations():
|
|
1124
|
+
"""
|
|
1125
|
+
Test geometric calculations like distances.
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
893
1128
|
##%% Test a few rectangle distances
|
|
894
|
-
|
|
1129
|
+
|
|
895
1130
|
r1 = [0,0,1,1]; r2 = [0,0,1,1]; assert rect_distance(r1,r2)==0
|
|
896
1131
|
r1 = [0,0,1,1]; r2 = [0,0,1,100]; assert rect_distance(r1,r2)==0
|
|
897
1132
|
r1 = [0,0,1,1]; r2 = [1,1,2,2]; assert rect_distance(r1,r2)==0
|
|
898
1133
|
r1 = [0,0,1,1]; r2 = [1.1,0,0,1.1]; assert abs(rect_distance(r1,r2)-.1) < 0.00001
|
|
899
|
-
|
|
1134
|
+
|
|
900
1135
|
r1 = [0.4,0.8,10,22]; r2 = [100, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-119.753) < 0.001
|
|
901
|
-
r1 = [0.4,0.8,10,22]; r2 = [101, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-120.507) < 0.001
|
|
1136
|
+
r1 = [0.4,0.8,10,22]; r2 = [101, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-120.507) < 0.001
|
|
902
1137
|
r1 = [0.4,0.8,10,22]; r2 = [120, 120, 200, 210.4]; assert abs(rect_distance(r1,r2)-147.323) < 0.001
|
|
903
|
-
|
|
904
1138
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1139
|
+
# Test with 'x0y0wh' format
|
|
1140
|
+
r1_wh = [0,0,1,1]; r2_wh = [1,0,1,1]; assert rect_distance(r1_wh, r2_wh, format='x0y0wh') == 0
|
|
1141
|
+
r1_wh = [0,0,1,1]; r2_wh = [1.5,0,1,1]; assert abs(rect_distance(r1_wh, r2_wh, format='x0y0wh') - 0.5) < 0.00001
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
##%% Test point_dist
|
|
1145
|
+
|
|
1146
|
+
assert point_dist((0,0), (3,4)) == 5.0
|
|
1147
|
+
assert point_dist((1,1), (1,1)) == 0.0
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def test_dictionary_operations():
|
|
1151
|
+
"""
|
|
1152
|
+
Test dictionary manipulation and sorting functions.
|
|
1153
|
+
"""
|
|
1154
|
+
|
|
1155
|
+
##%% Test sort_list_of_dicts_by_key
|
|
1156
|
+
|
|
1157
|
+
L = [{'a':5},{'a':0},{'a':10}]
|
|
908
1158
|
k = 'a'
|
|
909
|
-
sort_list_of_dicts_by_key(L, k
|
|
1159
|
+
sorted_L = sort_list_of_dicts_by_key(L, k)
|
|
1160
|
+
assert sorted_L[0]['a'] == 0; assert sorted_L[1]['a'] == 5; assert sorted_L[2]['a'] == 10
|
|
1161
|
+
sorted_L_rev = sort_list_of_dicts_by_key(L, k, reverse=True)
|
|
1162
|
+
assert sorted_L_rev[0]['a'] == 10; assert sorted_L_rev[1]['a'] == 5; assert sorted_L_rev[2]['a'] == 0
|
|
910
1163
|
|
|
911
1164
|
|
|
912
|
-
##%% Test
|
|
913
|
-
|
|
914
|
-
|
|
1165
|
+
##%% Test sort_dictionary_by_key
|
|
1166
|
+
|
|
1167
|
+
d_key = {'b': 2, 'a': 1, 'c': 3}
|
|
1168
|
+
sorted_d_key = sort_dictionary_by_key(d_key)
|
|
1169
|
+
assert list(sorted_d_key.keys()) == ['a', 'b', 'c']
|
|
1170
|
+
sorted_d_key_rev = sort_dictionary_by_key(d_key, reverse=True)
|
|
1171
|
+
assert list(sorted_d_key_rev.keys()) == ['c', 'b', 'a']
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
##%% Test sort_dictionary_by_value
|
|
1175
|
+
|
|
1176
|
+
d_val = {'a': 2, 'b': 1, 'c': 3}
|
|
1177
|
+
sorted_d_val = sort_dictionary_by_value(d_val)
|
|
1178
|
+
assert list(sorted_d_val.keys()) == ['b', 'a', 'c']
|
|
1179
|
+
sorted_d_val_rev = sort_dictionary_by_value(d_val, reverse=True)
|
|
1180
|
+
assert list(sorted_d_val_rev.keys()) == ['c', 'a', 'b']
|
|
1181
|
+
|
|
1182
|
+
# With sort_values
|
|
1183
|
+
sort_vals = {'a': 10, 'b': 0, 'c': 5}
|
|
1184
|
+
sorted_d_custom = sort_dictionary_by_value(d_val, sort_values=sort_vals)
|
|
1185
|
+
assert list(sorted_d_custom.keys()) == ['b', 'c', 'a']
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
##%% Test invert_dictionary
|
|
1189
|
+
|
|
1190
|
+
d_inv = {'a': 'x', 'b': 'y'}
|
|
1191
|
+
inverted_d = invert_dictionary(d_inv)
|
|
1192
|
+
assert inverted_d == {'x': 'a', 'y': 'b'}
|
|
1193
|
+
|
|
1194
|
+
# Does not check for uniqueness, last one wins
|
|
1195
|
+
d_inv_dup = {'a': 'x', 'b': 'x'}
|
|
1196
|
+
inverted_d_dup = invert_dictionary(d_inv_dup)
|
|
1197
|
+
assert inverted_d_dup == {'x': 'b'}
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def test_float_rounding_and_truncation():
|
|
1201
|
+
"""
|
|
1202
|
+
Test float rounding, truncation, and nested rounding functions.
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
##%% Test round_floats_in_nested_dict
|
|
1206
|
+
|
|
915
1207
|
data = {
|
|
916
1208
|
"name": "Project X",
|
|
917
1209
|
"values": [1.23456789, 2.3456789],
|
|
918
1210
|
"tuple_values": (3.45678901, 4.56789012),
|
|
919
|
-
"set_values": {5.67890123, 6.78901234},
|
|
1211
|
+
"set_values": {5.67890123, 6.78901234}, # Order not guaranteed in set, test min/max
|
|
920
1212
|
"metrics": {
|
|
921
1213
|
"score": 98.7654321,
|
|
922
1214
|
"components": [5.6789012, 6.7890123]
|
|
923
|
-
}
|
|
1215
|
+
},
|
|
1216
|
+
"other_iter": iter([7.89012345]) # Test other iterables
|
|
924
1217
|
}
|
|
925
|
-
|
|
926
|
-
result = round_floats_in_nested_dict(data)
|
|
1218
|
+
|
|
1219
|
+
result = round_floats_in_nested_dict(data, decimal_places=5, allow_iterator_conversion=True)
|
|
927
1220
|
assert result['values'][0] == 1.23457
|
|
928
1221
|
assert result['tuple_values'][0] == 3.45679
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1222
|
+
|
|
1223
|
+
# For sets, convert to list and sort for consistent testing
|
|
1224
|
+
assert sorted(list(result['set_values'])) == sorted([5.67890, 6.78901])
|
|
1225
|
+
assert result['metrics']['score'] == 98.76543
|
|
1226
|
+
|
|
1227
|
+
# Test other iterables by converting back to list
|
|
1228
|
+
assert list(result['other_iter'])[0] == 7.89012
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
##%% Test truncate_float_array and truncate_float
|
|
1232
|
+
|
|
1233
|
+
assert truncate_float_array([0.12345, 0.67890], precision=3) == [0.123, 0.678]
|
|
1234
|
+
assert truncate_float_array([1.0, 2.0], precision=2) == [1.0, 2.0]
|
|
1235
|
+
assert truncate_float(0.12345, precision=3) == 0.123
|
|
1236
|
+
assert truncate_float(1.999, precision=2) == 1.99
|
|
1237
|
+
assert truncate_float(0.0003214884, precision=6) == 0.000321
|
|
1238
|
+
assert truncate_float(1.0003214884, precision=6) == 1.000321
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
##%% Test round_float_array and round_float
|
|
1242
|
+
|
|
1243
|
+
assert round_float_array([0.12345, 0.67890], precision=3) == [0.123, 0.679]
|
|
1244
|
+
assert round_float_array([1.0, 2.0], precision=2) == [1.0, 2.0]
|
|
1245
|
+
assert round_float(0.12345, precision=3) == 0.123
|
|
1246
|
+
assert round_float(0.12378, precision=3) == 0.124
|
|
1247
|
+
assert round_float(1.999, precision=2) == 2.00
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def test_object_conversion_and_presentation():
|
|
1251
|
+
"""
|
|
1252
|
+
Test functions that convert or present objects.
|
|
1253
|
+
"""
|
|
1254
|
+
|
|
1255
|
+
##%% Test args_to_object
|
|
1256
|
+
|
|
1257
|
+
class ArgsObject:
|
|
1258
|
+
pass
|
|
1259
|
+
args_namespace = type('ArgsNameSpace', (), {'a': 1, 'b': 'test', '_c': 'ignored'})
|
|
1260
|
+
obj = ArgsObject()
|
|
1261
|
+
args_to_object(args_namespace, obj)
|
|
1262
|
+
assert obj.a == 1
|
|
1263
|
+
assert obj.b == 'test'
|
|
1264
|
+
assert not hasattr(obj, '_c')
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
##%% Test dict_to_object
|
|
1268
|
+
|
|
1269
|
+
class DictObject:
|
|
1270
|
+
pass
|
|
1271
|
+
d = {'a': 1, 'b': 'test', '_c': 'ignored'}
|
|
1272
|
+
obj = DictObject()
|
|
1273
|
+
dict_to_object(d, obj)
|
|
1274
|
+
assert obj.a == 1
|
|
1275
|
+
assert obj.b == 'test'
|
|
1276
|
+
assert not hasattr(obj, '_c')
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
##%% Test pretty_print_object
|
|
1280
|
+
|
|
1281
|
+
class PrettyPrintable:
|
|
1282
|
+
def __init__(self):
|
|
1283
|
+
self.a = 1
|
|
1284
|
+
self.b = "test"
|
|
1285
|
+
obj_to_print = PrettyPrintable()
|
|
1286
|
+
json_str = pretty_print_object(obj_to_print, b_print=False)
|
|
1287
|
+
|
|
1288
|
+
# Basic check for valid json and presence of attributes
|
|
1289
|
+
parsed_json = json.loads(json_str) # Relies on json.loads
|
|
1290
|
+
assert parsed_json['a'] == 1
|
|
1291
|
+
assert parsed_json['b'] == "test"
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def test_list_operations():
|
|
1295
|
+
"""
|
|
1296
|
+
Test list sorting and chunking functions.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
##%% Test is_list_sorted
|
|
1300
|
+
|
|
1301
|
+
assert is_list_sorted([1, 2, 3])
|
|
1302
|
+
assert not is_list_sorted([1, 3, 2])
|
|
1303
|
+
assert is_list_sorted([3, 2, 1], reverse=True)
|
|
1304
|
+
assert not is_list_sorted([1, 2, 3], reverse=True)
|
|
1305
|
+
assert is_list_sorted([]) # Empty list is considered sorted
|
|
1306
|
+
assert is_list_sorted([1]) # Single element list is sorted
|
|
1307
|
+
assert is_list_sorted([1,1,1])
|
|
1308
|
+
assert is_list_sorted([1,1,1], reverse=True)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
##%% Test split_list_into_fixed_size_chunks
|
|
1312
|
+
|
|
1313
|
+
assert split_list_into_fixed_size_chunks([1,2,3,4,5,6], 2) == [[1,2],[3,4],[5,6]]
|
|
1314
|
+
assert split_list_into_fixed_size_chunks([1,2,3,4,5], 2) == [[1,2],[3,4],[5]]
|
|
1315
|
+
assert split_list_into_fixed_size_chunks([], 3) == []
|
|
1316
|
+
assert split_list_into_fixed_size_chunks([1,2,3], 5) == [[1,2,3]]
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
##%% Test split_list_into_n_chunks
|
|
1320
|
+
|
|
1321
|
+
# Greedy
|
|
1322
|
+
assert split_list_into_n_chunks([1,2,3,4,5,6], 3, chunk_strategy='greedy') == [[1,2],[3,4],[5,6]]
|
|
1323
|
+
assert split_list_into_n_chunks([1,2,3,4,5], 3, chunk_strategy='greedy') == [[1,2],[3,4],[5]]
|
|
1324
|
+
assert split_list_into_n_chunks([1,2,3,4,5,6,7], 3, chunk_strategy='greedy') == [[1,2,3],[4,5],[6,7]]
|
|
1325
|
+
assert split_list_into_n_chunks([], 3) == [[],[],[]]
|
|
1326
|
+
|
|
1327
|
+
# Balanced
|
|
1328
|
+
assert split_list_into_n_chunks([1,2,3,4,5,6], 3, chunk_strategy='balanced') == [[1,4],[2,5],[3,6]]
|
|
1329
|
+
assert split_list_into_n_chunks([1,2,3,4,5], 3, chunk_strategy='balanced') == [[1,4],[2,5],[3]]
|
|
1330
|
+
assert split_list_into_n_chunks([], 3, chunk_strategy='balanced') == [[],[],[]]
|
|
1331
|
+
try:
|
|
1332
|
+
split_list_into_n_chunks([1,2,3], 2, chunk_strategy='invalid')
|
|
1333
|
+
assert False, "ValueError not raised for invalid chunk_strategy"
|
|
1334
|
+
except ValueError:
|
|
1335
|
+
pass
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def test_datetime_serialization():
|
|
1339
|
+
"""
|
|
1340
|
+
Test datetime serialization functions.
|
|
1341
|
+
"""
|
|
1342
|
+
|
|
1343
|
+
##%% Test json_serialize_datetime
|
|
1344
|
+
|
|
1345
|
+
now = datetime.datetime.now()
|
|
1346
|
+
today = datetime.date.today()
|
|
1347
|
+
assert json_serialize_datetime(now) == now.isoformat()
|
|
1348
|
+
assert json_serialize_datetime(today) == today.isoformat()
|
|
1349
|
+
try:
|
|
1350
|
+
json_serialize_datetime("not a datetime")
|
|
1351
|
+
assert False, "TypeError not raised for non-datetime object"
|
|
1352
|
+
except TypeError:
|
|
1353
|
+
pass
|
|
1354
|
+
try:
|
|
1355
|
+
json_serialize_datetime(123)
|
|
1356
|
+
assert False, "TypeError not raised for non-datetime object"
|
|
1357
|
+
except TypeError:
|
|
1358
|
+
pass
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def test_bounding_box_operations():
|
|
1362
|
+
"""
|
|
1363
|
+
Test bounding box conversion and IoU calculation.
|
|
1364
|
+
"""
|
|
1365
|
+
|
|
1366
|
+
##%% Test convert_yolo_to_xywh
|
|
1367
|
+
|
|
1368
|
+
# [x_center, y_center, w, h]
|
|
1369
|
+
yolo_box = [0.5, 0.5, 0.2, 0.2]
|
|
1370
|
+
# [x_min, y_min, width_of_box, height_of_box]
|
|
1371
|
+
expected_xywh = [0.4, 0.4, 0.2, 0.2]
|
|
1372
|
+
assert np.allclose(convert_yolo_to_xywh(yolo_box), expected_xywh)
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
##%% Test convert_xywh_to_xyxy
|
|
1376
|
+
|
|
1377
|
+
# [x_min, y_min, width_of_box, height_of_box]
|
|
1378
|
+
xywh_box = [0.1, 0.1, 0.3, 0.3]
|
|
1379
|
+
# [x_min, y_min, x_max, y_max]
|
|
1380
|
+
expected_xyxy = [0.1, 0.1, 0.4, 0.4]
|
|
1381
|
+
assert np.allclose(convert_xywh_to_xyxy(xywh_box), expected_xyxy)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
##%% Test get_iou
|
|
1385
|
+
|
|
1386
|
+
bb1 = [0, 0, 0.5, 0.5] # x, y, w, h
|
|
1387
|
+
bb2 = [0.25, 0.25, 0.5, 0.5]
|
|
1388
|
+
assert abs(get_iou(bb1, bb2) - 0.142857) < 1e-5
|
|
1389
|
+
bb3 = [0, 0, 1, 1]
|
|
1390
|
+
bb4 = [0.5, 0.5, 1, 1]
|
|
1391
|
+
assert abs(get_iou(bb3, bb4) - (0.25 / 1.75)) < 1e-5
|
|
1392
|
+
bb5 = [0,0,1,1]
|
|
1393
|
+
bb6 = [1,1,1,1] # No overlap
|
|
1394
|
+
assert get_iou(bb5, bb6) == 0.0
|
|
1395
|
+
|
|
1396
|
+
# Test malformed boxes (should ideally raise error or handle gracefully based on spec, current impl asserts)
|
|
1397
|
+
bb_malformed1 = [0.6, 0.0, 0.5, 0.5] # x_min > x_max after conversion
|
|
1398
|
+
bb_ok = [0.0, 0.0, 0.5, 0.5]
|
|
1399
|
+
try:
|
|
1400
|
+
get_iou(bb_malformed1, bb_ok)
|
|
1401
|
+
# This assert False will only be reached if the expected AssertionError is not raised by get_iou
|
|
1402
|
+
# assert False, "AssertionError for malformed bounding box (x2 >= x1) not raised in get_iou"
|
|
1403
|
+
except AssertionError as e:
|
|
1404
|
+
assert 'Malformed bounding box' in str(e)
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def test_detection_processing():
|
|
1408
|
+
"""
|
|
1409
|
+
Test functions related to processing detection results.
|
|
1410
|
+
"""
|
|
1411
|
+
|
|
1412
|
+
##%% Test _get_max_conf_from_detections and get_max_conf
|
|
1413
|
+
|
|
1414
|
+
detections1 = [{'conf': 0.8}, {'conf': 0.9}, {'conf': 0.75}]
|
|
1415
|
+
assert _get_max_conf_from_detections(detections1) == 0.9
|
|
1416
|
+
assert _get_max_conf_from_detections([]) == 0.0
|
|
1417
|
+
assert _get_max_conf_from_detections(None) == 0.0
|
|
1418
|
+
|
|
1419
|
+
im1 = {'detections': detections1}
|
|
1420
|
+
assert get_max_conf(im1) == 0.9
|
|
1421
|
+
im2 = {'detections': []}
|
|
1422
|
+
assert get_max_conf(im2) == 0.0
|
|
1423
|
+
im3 = {} # No 'detections' key
|
|
1424
|
+
assert get_max_conf(im3) == 0.0
|
|
1425
|
+
im4 = {'detections': None}
|
|
1426
|
+
assert get_max_conf(im4) == 0.0
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
##%% Test sort_results_for_image
|
|
1430
|
+
|
|
1431
|
+
img_data = {
|
|
1432
|
+
'detections': [
|
|
1433
|
+
{'conf': 0.7, 'classifications': [('c', 0.6), ('a', 0.9), ('b', 0.8)]},
|
|
1434
|
+
{'conf': 0.9, 'classifications': [('x', 0.95), ('y', 0.85)]},
|
|
1435
|
+
{'conf': 0.8} # No classifications field
|
|
1436
|
+
]
|
|
1437
|
+
}
|
|
1438
|
+
sort_results_for_image(img_data)
|
|
1439
|
+
|
|
1440
|
+
# Check detections sorted by conf
|
|
1441
|
+
assert img_data['detections'][0]['conf'] == 0.9
|
|
1442
|
+
assert img_data['detections'][1]['conf'] == 0.8
|
|
1443
|
+
assert img_data['detections'][2]['conf'] == 0.7
|
|
1444
|
+
|
|
1445
|
+
# Check classifications sorted by conf (only for the first original detection, now at index 0 after sort)
|
|
1446
|
+
assert img_data['detections'][0]['classifications'][0] == ('x', 0.95)
|
|
1447
|
+
assert img_data['detections'][0]['classifications'][1] == ('y', 0.85)
|
|
1448
|
+
|
|
1449
|
+
# Check classifications for the second original detection (now at index 2)
|
|
1450
|
+
assert img_data['detections'][2]['classifications'][0] == ('a', 0.9)
|
|
1451
|
+
assert img_data['detections'][2]['classifications'][1] == ('b', 0.8)
|
|
1452
|
+
assert img_data['detections'][2]['classifications'][2] == ('c', 0.6)
|
|
1453
|
+
|
|
1454
|
+
# Test with no detections or no classifications field
|
|
1455
|
+
img_data_no_det = {'detections': None}
|
|
1456
|
+
sort_results_for_image(img_data_no_det)
|
|
1457
|
+
assert img_data_no_det['detections'] is None
|
|
1458
|
+
|
|
1459
|
+
img_data_empty_det = {'detections': []}
|
|
1460
|
+
sort_results_for_image(img_data_empty_det)
|
|
1461
|
+
assert img_data_empty_det['detections'] == []
|
|
1462
|
+
|
|
1463
|
+
img_data_no_classifications_field = {'detections': [{'conf': 0.8}]}
|
|
1464
|
+
sort_results_for_image(img_data_no_classifications_field)
|
|
1465
|
+
assert 'classifications' not in img_data_no_classifications_field['detections'][0]
|
|
1466
|
+
|
|
1467
|
+
img_data_none_classifications = {'detections': [{'conf': 0.8, 'classifications':None}]}
|
|
1468
|
+
sort_results_for_image(img_data_none_classifications)
|
|
1469
|
+
assert img_data_none_classifications['detections'][0]['classifications'] is None
|
|
1470
|
+
|
|
1471
|
+
img_data_empty_classifications = {'detections': [{'conf': 0.8, 'classifications':[]}]}
|
|
1472
|
+
sort_results_for_image(img_data_empty_classifications)
|
|
1473
|
+
assert img_data_empty_classifications['detections'][0]['classifications'] == []
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def test_type_checking_and_validation():
|
|
1477
|
+
"""
|
|
1478
|
+
Test type checking and validation utility functions.
|
|
1479
|
+
"""
|
|
1480
|
+
|
|
1481
|
+
##%% Test is_float
|
|
1482
|
+
|
|
1483
|
+
assert is_float(1.23)
|
|
1484
|
+
assert is_float("1.23")
|
|
1485
|
+
assert is_float("-1.23")
|
|
1486
|
+
assert is_float(" 1.23 ")
|
|
1487
|
+
assert not is_float("abc")
|
|
1488
|
+
assert not is_float(None)
|
|
1489
|
+
assert is_float(1) # int is also a float (current behavior)
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
##%% Test is_iterable
|
|
1493
|
+
|
|
1494
|
+
assert is_iterable([1,2,3])
|
|
1495
|
+
assert is_iterable("hello")
|
|
1496
|
+
assert is_iterable({'a':1})
|
|
1497
|
+
assert is_iterable(range(5))
|
|
1498
|
+
assert not is_iterable(123)
|
|
1499
|
+
assert not is_iterable(None)
|
|
1500
|
+
assert is_iterable(np.array([1,2]))
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
##%% Test is_empty
|
|
1504
|
+
|
|
1505
|
+
assert is_empty(None)
|
|
1506
|
+
assert is_empty("")
|
|
1507
|
+
assert is_empty(np.nan)
|
|
1508
|
+
assert not is_empty(0)
|
|
1509
|
+
assert not is_empty(" ")
|
|
1510
|
+
assert not is_empty([])
|
|
1511
|
+
assert not is_empty({})
|
|
1512
|
+
assert not is_empty(False) # False is not empty
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
##%% Test min_none and max_none
|
|
1516
|
+
|
|
1517
|
+
assert min_none(1, 2) == 1
|
|
1518
|
+
assert min_none(None, 2) == 2
|
|
1519
|
+
assert min_none(1, None) == 1
|
|
1520
|
+
assert min_none(None, None) is None
|
|
1521
|
+
assert max_none(1, 2) == 2
|
|
1522
|
+
assert max_none(None, 2) == 2
|
|
1523
|
+
assert max_none(1, None) == 1
|
|
1524
|
+
assert max_none(None, None) is None
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
##%% Test isnan
|
|
1528
|
+
|
|
1529
|
+
assert isnan(np.nan)
|
|
1530
|
+
assert not isnan(0.0)
|
|
1531
|
+
assert not isnan("text")
|
|
1532
|
+
assert not isnan(None)
|
|
1533
|
+
assert not isnan(float('inf'))
|
|
1534
|
+
assert not isnan(float('-inf'))
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
##%% Test sets_overlap
|
|
1538
|
+
|
|
1539
|
+
assert sets_overlap({1,2,3}, {3,4,5})
|
|
1540
|
+
assert not sets_overlap({1,2}, {3,4})
|
|
1541
|
+
assert sets_overlap([1,2,3], [3,4,5]) # Test with lists
|
|
1542
|
+
assert sets_overlap(set(), {1}) is False
|
|
1543
|
+
assert sets_overlap({1},{1})
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
##%% Test is_function_name
|
|
1547
|
+
|
|
1548
|
+
def _test_local_func(): pass
|
|
1549
|
+
assert is_function_name("is_float", locals()) # Test a function in ct_utils
|
|
1550
|
+
assert is_function_name("_test_local_func", locals()) # Test a local function
|
|
1551
|
+
assert is_function_name("print", locals()) # Test a builtin
|
|
1552
|
+
assert not is_function_name("non_existent_func", locals())
|
|
1553
|
+
global _test_global_func_ct_utils # Renamed to avoid conflict if run multiple times
|
|
1554
|
+
def _test_global_func_ct_utils(): pass
|
|
1555
|
+
assert is_function_name("_test_global_func_ct_utils", globals())
|
|
1556
|
+
# Clean up global
|
|
1557
|
+
del _test_global_func_ct_utils
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
def test_string_parsing():
|
|
1561
|
+
"""
|
|
1562
|
+
Test string parsing utilities like KVP and boolean parsing.
|
|
1563
|
+
"""
|
|
1564
|
+
|
|
1565
|
+
##%% Test parse_kvp and parse_kvp_list
|
|
1566
|
+
|
|
1567
|
+
assert parse_kvp("key=value") == ("key", "value")
|
|
1568
|
+
assert parse_kvp("key = value with spaces") == ("key", "value with spaces")
|
|
1569
|
+
assert parse_kvp("key=value1=value2", kv_separator='=') == ("key", "value1=value2")
|
|
1570
|
+
try:
|
|
1571
|
+
parse_kvp("keyvalue")
|
|
1572
|
+
assert False, "AssertionError not raised for invalid KVP"
|
|
1573
|
+
except AssertionError:
|
|
1574
|
+
pass
|
|
1575
|
+
|
|
1576
|
+
kvp_list = ["a=1", "b = 2", "c=foo=bar"]
|
|
1577
|
+
parsed_list = parse_kvp_list(kvp_list)
|
|
1578
|
+
assert parsed_list == {"a": "1", "b": "2", "c": "foo=bar"}
|
|
1579
|
+
assert parse_kvp_list(None) == {}
|
|
1580
|
+
assert parse_kvp_list([]) == {}
|
|
1581
|
+
d_initial = {'z': '0'}
|
|
1582
|
+
|
|
1583
|
+
# parse_kvp_list modifies d in place if provided
|
|
1584
|
+
parse_kvp_list(kvp_list, d=d_initial)
|
|
1585
|
+
assert d_initial == {"z": "0", "a": "1", "b": "2", "c": "foo=bar"}
|
|
1586
|
+
|
|
1587
|
+
# Test with a different separator
|
|
1588
|
+
assert parse_kvp("key:value", kv_separator=":") == ("key", "value")
|
|
1589
|
+
assert parse_kvp_list(["a:1","b:2"], kv_separator=":") == {"a":"1", "b":"2"}
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
##%% Test dict_to_kvp_list
|
|
1593
|
+
|
|
1594
|
+
d_kvp = {"a": "1", "b": "dog", "c": "foo=bar"}
|
|
1595
|
+
kvp_str = dict_to_kvp_list(d_kvp)
|
|
1596
|
+
|
|
1597
|
+
# Order isn't guaranteed, so check for presence of all items and length
|
|
1598
|
+
assert "a=1" in kvp_str
|
|
1599
|
+
assert "b=dog" in kvp_str
|
|
1600
|
+
assert "c=foo=bar" in kvp_str
|
|
1601
|
+
assert len(kvp_str.split(' ')) == 3
|
|
1602
|
+
|
|
1603
|
+
assert dict_to_kvp_list({}) == ""
|
|
1604
|
+
assert dict_to_kvp_list(None) is None
|
|
1605
|
+
d_kvp_int = {"a":1, "b":"text"}
|
|
1606
|
+
try:
|
|
1607
|
+
dict_to_kvp_list(d_kvp_int, non_string_value_handling='error')
|
|
1608
|
+
assert False, "ValueError not raised for non-string value with 'error' handling"
|
|
1609
|
+
except ValueError:
|
|
1610
|
+
pass
|
|
1611
|
+
convert_result = dict_to_kvp_list(d_kvp_int, non_string_value_handling='convert')
|
|
1612
|
+
assert "a=1" in convert_result and "b=text" in convert_result
|
|
1613
|
+
|
|
1614
|
+
omit_result = dict_to_kvp_list({"a":1, "b":"text"}, non_string_value_handling='omit')
|
|
1615
|
+
assert "a=1" not in omit_result and "b=text" in omit_result
|
|
1616
|
+
assert omit_result == "b=text"
|
|
1617
|
+
|
|
1618
|
+
assert dict_to_kvp_list({"key":"val"}, item_separator="&", kv_separator=":") == "key:val"
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
##%% Test parse_bool_string
|
|
1622
|
+
|
|
1623
|
+
assert parse_bool_string("true")
|
|
1624
|
+
assert parse_bool_string("True")
|
|
1625
|
+
assert parse_bool_string(" TRUE ")
|
|
1626
|
+
assert not parse_bool_string("false")
|
|
1627
|
+
assert not parse_bool_string("False")
|
|
1628
|
+
assert not parse_bool_string(" FALSE ")
|
|
1629
|
+
assert parse_bool_string(True) is True # Test with existing bool
|
|
1630
|
+
assert parse_bool_string(False) is False
|
|
1631
|
+
try:
|
|
1632
|
+
parse_bool_string("maybe")
|
|
1633
|
+
assert False, "ValueError not raised for invalid bool string"
|
|
1634
|
+
except ValueError:
|
|
1635
|
+
pass
|
|
1636
|
+
try:
|
|
1637
|
+
parse_bool_string("1") # Should not parse to True
|
|
1638
|
+
assert False, "ValueError not raised for '1'"
|
|
1639
|
+
except ValueError:
|
|
1640
|
+
pass
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
def test_temp_folder_creation():
|
|
1644
|
+
"""
|
|
1645
|
+
Test temporary folder creation and cleanup.
|
|
1646
|
+
"""
|
|
1647
|
+
|
|
1648
|
+
# Store original tempdir for restoration if modified by tests (though unlikely for make_temp_folder)
|
|
1649
|
+
original_tempdir = tempfile.gettempdir()
|
|
1650
|
+
|
|
1651
|
+
# Test make_temp_folder
|
|
1652
|
+
custom_top_level = "my_custom_temp_app_test" # Unique name for this test run
|
|
1653
|
+
custom_subfolder = "specific_test_run"
|
|
1654
|
+
|
|
1655
|
+
# Test with default subfolder (UUID)
|
|
1656
|
+
temp_folder1_base = os.path.join(tempfile.gettempdir(), custom_top_level)
|
|
1657
|
+
temp_folder1 = make_temp_folder(top_level_folder=custom_top_level)
|
|
1658
|
+
assert os.path.exists(temp_folder1)
|
|
1659
|
+
assert os.path.basename(os.path.dirname(temp_folder1)) == custom_top_level
|
|
1660
|
+
assert temp_folder1_base == os.path.dirname(temp_folder1) # Path up to UUID should match
|
|
1661
|
+
|
|
1662
|
+
# Cleanup: remove the custom_top_level which contains the UUID folder
|
|
1663
|
+
if os.path.exists(temp_folder1_base):
|
|
1664
|
+
shutil.rmtree(temp_folder1_base)
|
|
1665
|
+
assert not os.path.exists(temp_folder1_base)
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
# Test with specified subfolder
|
|
1669
|
+
temp_folder2_base = os.path.join(tempfile.gettempdir(), custom_top_level)
|
|
1670
|
+
temp_folder2 = make_temp_folder(top_level_folder=custom_top_level,
|
|
1671
|
+
subfolder=custom_subfolder,
|
|
1672
|
+
append_guid=False)
|
|
1673
|
+
assert os.path.exists(temp_folder2)
|
|
1674
|
+
assert os.path.basename(temp_folder2) == custom_subfolder
|
|
1675
|
+
assert os.path.basename(os.path.dirname(temp_folder2)) == custom_top_level
|
|
1676
|
+
assert temp_folder2 == os.path.join(tempfile.gettempdir(), custom_top_level, custom_subfolder)
|
|
1677
|
+
|
|
1678
|
+
# Cleanup
|
|
1679
|
+
if os.path.exists(temp_folder2_base):
|
|
1680
|
+
shutil.rmtree(temp_folder2_base)
|
|
1681
|
+
assert not os.path.exists(temp_folder2_base)
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
# Test make_test_folder (which uses 'megadetector/tests' as top_level)
|
|
1685
|
+
#
|
|
1686
|
+
# This will create tempfile.gettempdir()/megadetector/tests/some_uuid or specified_subfolder
|
|
1687
|
+
megadetector_temp_base = os.path.join(tempfile.gettempdir(), "megadetector")
|
|
1688
|
+
test_subfolder = "my_specific_module_test"
|
|
1689
|
+
|
|
1690
|
+
# Test with default subfolder for make_test_folder
|
|
1691
|
+
test_folder1 = make_test_folder() # Creates megadetector/tests/uuid_folder
|
|
1692
|
+
assert os.path.exists(test_folder1)
|
|
1693
|
+
assert os.path.basename(os.path.dirname(test_folder1)) == "tests"
|
|
1694
|
+
assert os.path.basename(os.path.dirname(os.path.dirname(test_folder1))) == "megadetector"
|
|
1695
|
+
|
|
1696
|
+
# Cleanup for make_test_folder default: remove the 'megadetector' base temp dir
|
|
1697
|
+
if os.path.exists(megadetector_temp_base):
|
|
1698
|
+
shutil.rmtree(megadetector_temp_base)
|
|
1699
|
+
assert not os.path.exists(megadetector_temp_base)
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
# Test with specified subfolder for make_test_folder
|
|
1703
|
+
test_folder2 = make_test_folder(subfolder=test_subfolder) # megadetector/tests/my_specific_module_test
|
|
1704
|
+
assert os.path.exists(test_folder2)
|
|
1705
|
+
assert test_subfolder in test_folder2
|
|
1706
|
+
assert "megadetector" in test_folder2
|
|
1707
|
+
|
|
1708
|
+
# Cleanup for make_test_folder specific: remove the 'megadetector' base temp dir
|
|
1709
|
+
if os.path.exists(megadetector_temp_base):
|
|
1710
|
+
shutil.rmtree(megadetector_temp_base)
|
|
1711
|
+
assert not os.path.exists(megadetector_temp_base)
|
|
1712
|
+
|
|
1713
|
+
# Verify cleanup if top level folder was 'megadetector' (default for make_temp_folder)
|
|
1714
|
+
#
|
|
1715
|
+
# This means it creates tempfile.gettempdir()/megadetector/uuid_folder
|
|
1716
|
+
default_temp_folder = make_temp_folder()
|
|
1717
|
+
assert os.path.exists(default_temp_folder)
|
|
1718
|
+
assert os.path.basename(os.path.dirname(default_temp_folder)) == "megadetector"
|
|
1719
|
+
|
|
1720
|
+
# Cleanup: remove the 'megadetector' base temp dir created by default make_temp_folder
|
|
1721
|
+
if os.path.exists(megadetector_temp_base):
|
|
1722
|
+
shutil.rmtree(megadetector_temp_base)
|
|
1723
|
+
assert not os.path.exists(megadetector_temp_base)
|
|
1724
|
+
|
|
1725
|
+
# Restore original tempdir if it was changed (though not expected for these functions)
|
|
1726
|
+
tempfile.tempdir = original_tempdir
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def run_all_module_tests():
|
|
1730
|
+
"""
|
|
1731
|
+
Run all tests in the ct_utils module. This is not invoked by pytest; this is
|
|
1732
|
+
just a convenience wrapper for debuggig the tests.
|
|
1733
|
+
"""
|
|
1734
|
+
|
|
1735
|
+
test_write_json()
|
|
1736
|
+
test_path_operations()
|
|
1737
|
+
test_geometric_operations()
|
|
1738
|
+
test_dictionary_operations()
|
|
1739
|
+
test_float_rounding_and_truncation()
|
|
1740
|
+
test_object_conversion_and_presentation()
|
|
1741
|
+
test_list_operations()
|
|
1742
|
+
test_datetime_serialization()
|
|
1743
|
+
test_bounding_box_operations()
|
|
1744
|
+
test_detection_processing()
|
|
1745
|
+
test_type_checking_and_validation()
|
|
1746
|
+
test_string_parsing()
|
|
1747
|
+
test_temp_folder_creation()
|
|
1748
|
+
|
|
1749
|
+
# from IPython import embed; embed()
|
|
1750
|
+
# run_all_module_tests()
|