megadetector 5.0.28__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 +231 -224
- 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 +340 -337
- megadetector/detection/pytorch_detector.py +304 -262
- megadetector/detection/run_detector.py +177 -164
- megadetector/detection/run_detector_batch.py +364 -363
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +256 -249
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +290 -282
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +415 -415
- 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 +219 -146
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -499
- megadetector/postprocessing/load_api_results.py +23 -20
- 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 +313 -298
- 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 -66
- 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 +10 -10
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1018 -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 +1457 -398
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +61 -61
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2526
- 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 +401 -397
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +79 -73
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
- 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.28.dist-info/RECORD +0 -209
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,17 +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 if there were no detections, if there was a failure, or if 'detections' isn't
|
|
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
|
|
303
348
|
present.
|
|
304
|
-
|
|
349
|
+
|
|
305
350
|
Args:
|
|
306
351
|
im (dict): image dictionary in the MD output format (with a 'detections' field)
|
|
307
|
-
|
|
352
|
+
|
|
308
353
|
Returns:
|
|
309
354
|
float: the maximum detection confidence across all classes
|
|
310
355
|
"""
|
|
311
|
-
|
|
356
|
+
|
|
312
357
|
max_conf = 0.0
|
|
313
358
|
if 'detections' in im and im['detections'] is not None and len(im['detections']) > 0:
|
|
314
359
|
max_conf = _get_max_conf_from_detections(im['detections'])
|
|
@@ -318,7 +363,7 @@ def get_max_conf(im):
|
|
|
318
363
|
def sort_results_for_image(im):
|
|
319
364
|
"""
|
|
320
365
|
Sort classification and detection results in descending order by confidence (in place).
|
|
321
|
-
|
|
366
|
+
|
|
322
367
|
Args:
|
|
323
368
|
im (dict): image dictionary in the MD output format (with a 'detections' field)
|
|
324
369
|
"""
|
|
@@ -327,55 +372,56 @@ def sort_results_for_image(im):
|
|
|
327
372
|
|
|
328
373
|
# Sort detections in descending order by confidence
|
|
329
374
|
im['detections'] = sort_list_of_dicts_by_key(im['detections'],k='conf',reverse=True)
|
|
330
|
-
|
|
375
|
+
|
|
331
376
|
for det in im['detections']:
|
|
332
|
-
|
|
377
|
+
|
|
333
378
|
# Sort classifications (which are (class,conf) tuples) in descending order by confidence
|
|
334
379
|
if 'classifications' in det and \
|
|
335
380
|
(det['classifications'] is not None) and \
|
|
336
381
|
(len(det['classifications']) > 0):
|
|
337
|
-
|
|
338
|
-
det['classifications'] =
|
|
382
|
+
classifications = det['classifications']
|
|
383
|
+
det['classifications'] = \
|
|
384
|
+
sorted(classifications,key=itemgetter(1),reverse=True)
|
|
339
385
|
|
|
340
386
|
|
|
341
387
|
def point_dist(p1,p2):
|
|
342
388
|
"""
|
|
343
389
|
Computes the distance between two points, represented as length-two tuples.
|
|
344
|
-
|
|
390
|
+
|
|
345
391
|
Args:
|
|
346
392
|
p1: point, formatted as (x,y)
|
|
347
393
|
p2: point, formatted as (x,y)
|
|
348
|
-
|
|
394
|
+
|
|
349
395
|
Returns:
|
|
350
396
|
float: the Euclidean distance between p1 and p2
|
|
351
397
|
"""
|
|
352
|
-
|
|
398
|
+
|
|
353
399
|
return math.sqrt( ((p1[0]-p2[0])**2) + ((p1[1]-p2[1])**2) )
|
|
354
400
|
|
|
355
401
|
|
|
356
402
|
def rect_distance(r1, r2, format='x0y0x1y1'):
|
|
357
403
|
"""
|
|
358
|
-
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
|
|
359
405
|
(x0,y0,x1,y1) by default.
|
|
360
|
-
|
|
406
|
+
|
|
361
407
|
Can also specify "format" as x0y0wh for MD-style bbox formatting (x0,y0,w,h).
|
|
362
|
-
|
|
408
|
+
|
|
363
409
|
Args:
|
|
364
410
|
r1: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
365
411
|
r2: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
|
|
366
412
|
format (str, optional): whether the boxes are formatted as 'x0y0x1y1' (default) or 'x0y0wh'
|
|
367
|
-
|
|
413
|
+
|
|
368
414
|
Returns:
|
|
369
415
|
float: the minimum distance between r1 and r2
|
|
370
416
|
"""
|
|
371
|
-
|
|
417
|
+
|
|
372
418
|
assert format in ('x0y0x1y1','x0y0wh'), 'Illegal rectangle format {}'.format(format)
|
|
373
|
-
|
|
419
|
+
|
|
374
420
|
if format == 'x0y0wh':
|
|
375
421
|
# Convert to x0y0x1y1 without modifying the original rectangles
|
|
376
422
|
r1 = [r1[0],r1[1],r1[0]+r1[2],r1[1]+r1[3]]
|
|
377
423
|
r2 = [r2[0],r2[1],r2[0]+r2[2],r2[1]+r2[3]]
|
|
378
|
-
|
|
424
|
+
|
|
379
425
|
# https://stackoverflow.com/a/26178015
|
|
380
426
|
x1, y1, x1b, y1b = r1
|
|
381
427
|
x2, y2, x2b, y2b = r2
|
|
@@ -403,40 +449,40 @@ def rect_distance(r1, r2, format='x0y0x1y1'):
|
|
|
403
449
|
return 0.0
|
|
404
450
|
|
|
405
451
|
|
|
406
|
-
def split_list_into_fixed_size_chunks(L,n):
|
|
452
|
+
def split_list_into_fixed_size_chunks(L,n): # noqa
|
|
407
453
|
"""
|
|
408
|
-
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
|
|
409
455
|
less than N, i.e. len(L) does not have to be a multiple of n).
|
|
410
|
-
|
|
456
|
+
|
|
411
457
|
Args:
|
|
412
458
|
L (list): list to split into chunks
|
|
413
459
|
n (int): preferred chunk size
|
|
414
|
-
|
|
460
|
+
|
|
415
461
|
Returns:
|
|
416
462
|
list: list of chunks, where each chunk is a list of length n or n-1
|
|
417
463
|
"""
|
|
418
|
-
|
|
464
|
+
|
|
419
465
|
return [L[i * n:(i + 1) * n] for i in range((len(L) + n - 1) // n )]
|
|
420
466
|
|
|
421
467
|
|
|
422
|
-
def split_list_into_n_chunks(L, n, chunk_strategy='greedy'):
|
|
468
|
+
def split_list_into_n_chunks(L, n, chunk_strategy='greedy'): # noqa
|
|
423
469
|
"""
|
|
424
|
-
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
|
|
425
471
|
element smaller than others, i.e. len(L) does not have to be a multiple of n).
|
|
426
|
-
|
|
472
|
+
|
|
427
473
|
chunk_strategy can be "greedy" (default, if there are k samples per chunk, the first
|
|
428
474
|
k go into the first chunk) or "balanced" (alternate between chunks when pulling
|
|
429
475
|
items from the list).
|
|
430
|
-
|
|
476
|
+
|
|
431
477
|
Args:
|
|
432
478
|
L (list): list to split into chunks
|
|
433
479
|
n (int): number of chunks
|
|
434
480
|
chunk_strategy (str, optiopnal): "greedy" or "balanced"; see above
|
|
435
|
-
|
|
481
|
+
|
|
436
482
|
Returns:
|
|
437
483
|
list: list of chunks, each of which is a list
|
|
438
484
|
"""
|
|
439
|
-
|
|
485
|
+
|
|
440
486
|
if chunk_strategy == 'greedy':
|
|
441
487
|
k, m = divmod(len(L), n)
|
|
442
488
|
return list(L[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(n))
|
|
@@ -450,54 +496,54 @@ def split_list_into_n_chunks(L, n, chunk_strategy='greedy'):
|
|
|
450
496
|
raise ValueError('Invalid chunk strategy: {}'.format(chunk_strategy))
|
|
451
497
|
|
|
452
498
|
|
|
453
|
-
def sort_list_of_dicts_by_key(L,k,reverse=False):
|
|
499
|
+
def sort_list_of_dicts_by_key(L,k,reverse=False): # noqa
|
|
454
500
|
"""
|
|
455
501
|
Sorts the list of dictionaries [L] by the key [k].
|
|
456
|
-
|
|
502
|
+
|
|
457
503
|
Args:
|
|
458
504
|
L (list): list of dictionaries to sort
|
|
459
505
|
k (object, typically str): the sort key
|
|
460
506
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
461
|
-
|
|
507
|
+
|
|
462
508
|
Returns:
|
|
463
509
|
dict: sorted copy of [d]
|
|
464
510
|
"""
|
|
465
511
|
return sorted(L, key=lambda d: d[k], reverse=reverse)
|
|
466
|
-
|
|
467
|
-
|
|
512
|
+
|
|
513
|
+
|
|
468
514
|
def sort_dictionary_by_key(d,reverse=False):
|
|
469
515
|
"""
|
|
470
516
|
Sorts the dictionary [d] by key.
|
|
471
|
-
|
|
517
|
+
|
|
472
518
|
Args:
|
|
473
519
|
d (dict): dictionary to sort
|
|
474
520
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
475
|
-
|
|
521
|
+
|
|
476
522
|
Returns:
|
|
477
523
|
dict: sorted copy of [d]
|
|
478
524
|
"""
|
|
479
|
-
|
|
525
|
+
|
|
480
526
|
d = dict(sorted(d.items(),reverse=reverse))
|
|
481
527
|
return d
|
|
482
|
-
|
|
528
|
+
|
|
483
529
|
|
|
484
530
|
def sort_dictionary_by_value(d,sort_values=None,reverse=False):
|
|
485
531
|
"""
|
|
486
532
|
Sorts the dictionary [d] by value. If sort_values is None, uses d.values(),
|
|
487
|
-
otherwise uses the dictionary sort_values as the sorting criterion. Always
|
|
488
|
-
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
|
|
489
535
|
returned value is not.
|
|
490
|
-
|
|
536
|
+
|
|
491
537
|
Args:
|
|
492
538
|
d (dict): dictionary to sort
|
|
493
|
-
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
|
|
494
540
|
to None, uses [d] itself for sorting)
|
|
495
541
|
reverse (bool, optional): whether to sort in reverse (descending) order
|
|
496
|
-
|
|
542
|
+
|
|
497
543
|
Returns:
|
|
498
544
|
dict: sorted copy of [d
|
|
499
545
|
"""
|
|
500
|
-
|
|
546
|
+
|
|
501
547
|
if sort_values is None:
|
|
502
548
|
d = {k: v for k, v in sorted(d.items(), key=lambda item: item[1], reverse=reverse)}
|
|
503
549
|
else:
|
|
@@ -509,111 +555,133 @@ def invert_dictionary(d):
|
|
|
509
555
|
"""
|
|
510
556
|
Creates a new dictionary that maps d.values() to d.keys(). Does not check
|
|
511
557
|
uniqueness.
|
|
512
|
-
|
|
558
|
+
|
|
513
559
|
Args:
|
|
514
560
|
d (dict): dictionary to invert
|
|
515
|
-
|
|
561
|
+
|
|
516
562
|
Returns:
|
|
517
563
|
dict: inverted copy of [d]
|
|
518
564
|
"""
|
|
519
|
-
|
|
565
|
+
|
|
520
566
|
return {v: k for k, v in d.items()}
|
|
521
567
|
|
|
522
568
|
|
|
523
|
-
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):
|
|
524
570
|
"""
|
|
525
|
-
Recursively rounds all floating point values in a nested structure to the
|
|
526
|
-
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,
|
|
527
573
|
sets, and other iterables. Modifies mutable objects in place.
|
|
528
|
-
|
|
574
|
+
|
|
529
575
|
Args:
|
|
530
576
|
obj: The object to process (can be a dict, list, set, tuple, or primitive value)
|
|
531
|
-
decimal_places: Number of decimal places to round to
|
|
532
|
-
|
|
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
|
+
|
|
533
581
|
Returns:
|
|
534
582
|
The processed object (useful for recursive calls)
|
|
535
583
|
"""
|
|
536
584
|
if isinstance(obj, dict):
|
|
537
585
|
for key in obj:
|
|
538
|
-
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)
|
|
539
588
|
return obj
|
|
540
|
-
|
|
589
|
+
|
|
541
590
|
elif isinstance(obj, list):
|
|
542
591
|
for i in range(len(obj)):
|
|
543
|
-
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)
|
|
544
594
|
return obj
|
|
545
|
-
|
|
595
|
+
|
|
546
596
|
elif isinstance(obj, tuple):
|
|
547
597
|
# Tuples are immutable, so we create a new one
|
|
548
|
-
return tuple(round_floats_in_nested_dict(item, decimal_places
|
|
549
|
-
|
|
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
|
+
|
|
550
601
|
elif isinstance(obj, set):
|
|
551
602
|
# Sets are mutable but we can't modify elements in-place
|
|
552
603
|
# Convert to list, process, and convert back to set
|
|
553
|
-
return set(round_floats_in_nested_dict(list(obj), decimal_places
|
|
554
|
-
|
|
604
|
+
return set(round_floats_in_nested_dict(list(obj), decimal_places=decimal_places,
|
|
605
|
+
allow_iterator_conversion=allow_iterator_conversion))
|
|
606
|
+
|
|
555
607
|
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
|
|
556
|
-
# Handle other iterable types
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
|
|
559
624
|
elif isinstance(obj, float):
|
|
560
625
|
return round(obj, decimal_places)
|
|
561
|
-
|
|
626
|
+
|
|
562
627
|
else:
|
|
563
628
|
# For other types (int, str, bool, None, etc.), return as is
|
|
564
629
|
return obj
|
|
565
630
|
|
|
566
|
-
# ...def round_floats_in_nested_dict(...)
|
|
631
|
+
# ...def round_floats_in_nested_dict(...)
|
|
567
632
|
|
|
568
633
|
|
|
569
634
|
def image_file_to_camera_folder(image_fn):
|
|
570
635
|
r"""
|
|
571
636
|
Removes common overflow folders (e.g. RECNX101, RECNX102) from paths, i.e. turn:
|
|
572
|
-
|
|
637
|
+
|
|
573
638
|
a\b\c\RECNX101\image001.jpg
|
|
574
|
-
|
|
639
|
+
|
|
575
640
|
...into:
|
|
576
|
-
|
|
641
|
+
|
|
577
642
|
a\b\c
|
|
578
643
|
|
|
579
|
-
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
|
|
580
645
|
present.
|
|
581
646
|
|
|
582
647
|
Always converts backslashes to slashes.
|
|
583
|
-
|
|
648
|
+
|
|
584
649
|
Args:
|
|
585
650
|
image_fn (str): the image filename from which we should remove overflow folders
|
|
586
|
-
|
|
651
|
+
|
|
587
652
|
Returns:
|
|
588
653
|
str: a version of [image_fn] from which camera overflow folders have been removed
|
|
589
654
|
"""
|
|
590
|
-
|
|
655
|
+
|
|
591
656
|
import re
|
|
592
|
-
|
|
657
|
+
|
|
593
658
|
# 100RECNX is the overflow folder style for Reconyx cameras
|
|
594
659
|
# 100EK113 is (for some reason) the overflow folder style for Bushnell cameras
|
|
595
660
|
# 100_BTCF is the overflow folder style for Browning cameras
|
|
596
661
|
# 100MEDIA is the overflow folder style used on a number of consumer-grade cameras
|
|
597
662
|
patterns = [r'/\d+RECNX/',r'/\d+EK\d+/',r'/\d+_BTCF/',r'/\d+MEDIA/']
|
|
598
|
-
|
|
599
|
-
image_fn = image_fn.replace('\\','/')
|
|
663
|
+
|
|
664
|
+
image_fn = image_fn.replace('\\','/')
|
|
600
665
|
for pat in patterns:
|
|
601
666
|
image_fn = re.sub(pat,'/',image_fn)
|
|
602
667
|
camera_folder = os.path.dirname(image_fn)
|
|
603
|
-
|
|
668
|
+
|
|
604
669
|
return camera_folder
|
|
605
|
-
|
|
670
|
+
|
|
606
671
|
|
|
607
672
|
def is_float(v):
|
|
608
673
|
"""
|
|
609
674
|
Determines whether v is either a float or a string representation of a float.
|
|
610
|
-
|
|
675
|
+
|
|
611
676
|
Args:
|
|
612
677
|
v (object): object to evaluate
|
|
613
|
-
|
|
678
|
+
|
|
614
679
|
Returns:
|
|
615
680
|
bool: True if [v] is a float or a string representation of a float, otherwise False
|
|
616
681
|
"""
|
|
682
|
+
|
|
683
|
+
if v is None:
|
|
684
|
+
return False
|
|
617
685
|
|
|
618
686
|
try:
|
|
619
687
|
_ = float(v)
|
|
@@ -625,17 +693,17 @@ def is_float(v):
|
|
|
625
693
|
def is_iterable(x):
|
|
626
694
|
"""
|
|
627
695
|
Uses duck typing to assess whether [x] is iterable (list, set, dict, etc.).
|
|
628
|
-
|
|
696
|
+
|
|
629
697
|
Args:
|
|
630
698
|
x (object): the object to test
|
|
631
|
-
|
|
699
|
+
|
|
632
700
|
Returns:
|
|
633
701
|
bool: True if [x] appears to be iterable, otherwise False
|
|
634
702
|
"""
|
|
635
|
-
|
|
703
|
+
|
|
636
704
|
try:
|
|
637
705
|
_ = iter(x)
|
|
638
|
-
except:
|
|
706
|
+
except Exception:
|
|
639
707
|
return False
|
|
640
708
|
return True
|
|
641
709
|
|
|
@@ -644,10 +712,10 @@ def is_empty(v):
|
|
|
644
712
|
"""
|
|
645
713
|
A common definition of "empty" used throughout the repo, particularly when loading
|
|
646
714
|
data from .csv files. "empty" includes None, '', and NaN.
|
|
647
|
-
|
|
715
|
+
|
|
648
716
|
Args:
|
|
649
717
|
v: the object to evaluate for emptiness
|
|
650
|
-
|
|
718
|
+
|
|
651
719
|
Returns:
|
|
652
720
|
bool: True if [v] is None, '', or NaN, otherwise False
|
|
653
721
|
"""
|
|
@@ -660,15 +728,55 @@ def is_empty(v):
|
|
|
660
728
|
return False
|
|
661
729
|
|
|
662
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
|
+
|
|
663
771
|
def min_none(a,b):
|
|
664
772
|
"""
|
|
665
|
-
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,
|
|
666
774
|
returns the other.
|
|
667
|
-
|
|
775
|
+
|
|
668
776
|
Args:
|
|
669
777
|
a (numeric): the first value to compare
|
|
670
778
|
b (numeric): the second value to compare
|
|
671
|
-
|
|
779
|
+
|
|
672
780
|
Returns:
|
|
673
781
|
numeric: the minimum of a and b, or None
|
|
674
782
|
"""
|
|
@@ -680,17 +788,17 @@ def min_none(a,b):
|
|
|
680
788
|
return a
|
|
681
789
|
else:
|
|
682
790
|
return min(a,b)
|
|
683
|
-
|
|
791
|
+
|
|
684
792
|
|
|
685
793
|
def max_none(a,b):
|
|
686
794
|
"""
|
|
687
|
-
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,
|
|
688
796
|
returns the other.
|
|
689
|
-
|
|
797
|
+
|
|
690
798
|
Args:
|
|
691
799
|
a (numeric): the first value to compare
|
|
692
800
|
b (numeric): the second value to compare
|
|
693
|
-
|
|
801
|
+
|
|
694
802
|
Returns:
|
|
695
803
|
numeric: the maximum of a and b, or None
|
|
696
804
|
"""
|
|
@@ -703,19 +811,19 @@ def max_none(a,b):
|
|
|
703
811
|
else:
|
|
704
812
|
return max(a,b)
|
|
705
813
|
|
|
706
|
-
|
|
814
|
+
|
|
707
815
|
def isnan(v):
|
|
708
816
|
"""
|
|
709
817
|
Returns True if v is a nan-valued float, otherwise returns False.
|
|
710
|
-
|
|
818
|
+
|
|
711
819
|
Args:
|
|
712
820
|
v: the object to evaluate for nan-ness
|
|
713
|
-
|
|
821
|
+
|
|
714
822
|
Returns:
|
|
715
823
|
bool: True if v is a nan-valued float, otherwise False
|
|
716
824
|
"""
|
|
717
|
-
|
|
718
|
-
try:
|
|
825
|
+
|
|
826
|
+
try:
|
|
719
827
|
return np.isnan(v)
|
|
720
828
|
except Exception:
|
|
721
829
|
return False
|
|
@@ -724,55 +832,56 @@ def isnan(v):
|
|
|
724
832
|
def sets_overlap(set1, set2):
|
|
725
833
|
"""
|
|
726
834
|
Determines whether two sets overlap.
|
|
727
|
-
|
|
835
|
+
|
|
728
836
|
Args:
|
|
729
837
|
set1 (set): the first set to compare (converted to a set if it's not already)
|
|
730
838
|
set2 (set): the second set to compare (converted to a set if it's not already)
|
|
731
|
-
|
|
839
|
+
|
|
732
840
|
Returns:
|
|
733
841
|
bool: True if any elements are shared between set1 and set2
|
|
734
842
|
"""
|
|
735
|
-
|
|
843
|
+
|
|
736
844
|
return not set(set1).isdisjoint(set(set2))
|
|
737
845
|
|
|
738
846
|
|
|
739
847
|
def is_function_name(s,calling_namespace):
|
|
740
848
|
"""
|
|
741
|
-
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
|
|
742
850
|
built-in function.
|
|
743
|
-
|
|
851
|
+
|
|
744
852
|
Args:
|
|
745
853
|
s (str): the string to test for function-ness
|
|
746
854
|
calling_namespace (dict): typically pass the output of locals()
|
|
747
855
|
"""
|
|
748
|
-
|
|
856
|
+
|
|
749
857
|
assert isinstance(s,str), 'Input is not a string'
|
|
750
|
-
|
|
858
|
+
|
|
751
859
|
return callable(globals().get(s)) or \
|
|
752
860
|
callable(locals().get(s)) or \
|
|
753
861
|
callable(calling_namespace.get(s)) or \
|
|
754
862
|
callable(getattr(builtins, s, None))
|
|
755
863
|
|
|
756
|
-
|
|
864
|
+
|
|
757
865
|
# From https://gist.github.com/fralau/061a4f6c13251367ef1d9a9a99fb3e8d
|
|
758
866
|
def parse_kvp(s,kv_separator='='):
|
|
759
867
|
"""
|
|
760
868
|
Parse a key/value pair, separated by [kv_separator]. Errors if s is not
|
|
761
|
-
a valid key/value pair string.
|
|
762
|
-
|
|
869
|
+
a valid key/value pair string. Strips leading/trailing whitespace from
|
|
870
|
+
the key and value.
|
|
871
|
+
|
|
763
872
|
Args:
|
|
764
873
|
s (str): the string to parse
|
|
765
874
|
kv_separator (str, optional): the string separating keys from values.
|
|
766
|
-
|
|
875
|
+
|
|
767
876
|
Returns:
|
|
768
877
|
tuple: a 2-tuple formatted as (key,value)
|
|
769
878
|
"""
|
|
770
|
-
|
|
879
|
+
|
|
771
880
|
items = s.split(kv_separator)
|
|
772
881
|
assert len(items) > 1, 'Illegal key-value pair'
|
|
773
882
|
key = items[0].strip()
|
|
774
883
|
if len(items) > 1:
|
|
775
|
-
value = kv_separator.join(items[1:])
|
|
884
|
+
value = kv_separator.join(items[1:]).strip()
|
|
776
885
|
return (key, value)
|
|
777
886
|
|
|
778
887
|
|
|
@@ -780,26 +889,26 @@ def parse_kvp_list(items,kv_separator='=',d=None):
|
|
|
780
889
|
"""
|
|
781
890
|
Parse a list key-value pairs into a dictionary. If items is None or [],
|
|
782
891
|
returns {}.
|
|
783
|
-
|
|
892
|
+
|
|
784
893
|
Args:
|
|
785
894
|
items (list): the list of KVPs to parse
|
|
786
895
|
kv_separator (str, optional): the string separating keys from values.
|
|
787
896
|
d (dict, optional): the initial dictionary, defaults to {}
|
|
788
|
-
|
|
897
|
+
|
|
789
898
|
Returns:
|
|
790
899
|
dict: a dict mapping keys to values
|
|
791
900
|
"""
|
|
792
|
-
|
|
901
|
+
|
|
793
902
|
if d is None:
|
|
794
903
|
d = {}
|
|
795
904
|
|
|
796
905
|
if items is None or len(items) == 0:
|
|
797
906
|
return d
|
|
798
|
-
|
|
907
|
+
|
|
799
908
|
for item in items:
|
|
800
|
-
key, value = parse_kvp(item)
|
|
909
|
+
key, value = parse_kvp(item,kv_separator=kv_separator)
|
|
801
910
|
d[key] = value
|
|
802
|
-
|
|
911
|
+
|
|
803
912
|
return d
|
|
804
913
|
|
|
805
914
|
|
|
@@ -811,24 +920,24 @@ def dict_to_kvp_list(d,
|
|
|
811
920
|
Convert a string <--> string dict into a string containing list of list of
|
|
812
921
|
key-value pairs. I.e., converts {'a':'dog','b':'cat'} to 'a=dog b=cat'. If
|
|
813
922
|
d is None, returns None. If d is empty, returns ''.
|
|
814
|
-
|
|
923
|
+
|
|
815
924
|
Args:
|
|
816
925
|
d (dict): the dictionary to convert, must contain only strings
|
|
817
926
|
item_separator (str, optional): the delimiter between KV pairs
|
|
818
927
|
kv_separator (str, optional): the separator betweena a key and its value
|
|
819
928
|
non_string_value_handling (str, optional): what do do with non-string values,
|
|
820
929
|
can be "omit", "error", or "convert"
|
|
821
|
-
|
|
930
|
+
|
|
822
931
|
Returns:
|
|
823
932
|
str: the string representation of [d]
|
|
824
933
|
"""
|
|
825
|
-
|
|
934
|
+
|
|
826
935
|
if d is None:
|
|
827
936
|
return None
|
|
828
|
-
|
|
937
|
+
|
|
829
938
|
if len(d) == 0:
|
|
830
939
|
return ''
|
|
831
|
-
|
|
940
|
+
|
|
832
941
|
s = None
|
|
833
942
|
for k in d.keys():
|
|
834
943
|
assert isinstance(k,str), 'Input {} is not a str <--> str dict'.format(str(d))
|
|
@@ -848,25 +957,25 @@ def dict_to_kvp_list(d,
|
|
|
848
957
|
else:
|
|
849
958
|
s += item_separator
|
|
850
959
|
s += k + kv_separator + v
|
|
851
|
-
|
|
960
|
+
|
|
852
961
|
if s is None:
|
|
853
962
|
s = ''
|
|
854
|
-
|
|
963
|
+
|
|
855
964
|
return s
|
|
856
|
-
|
|
965
|
+
|
|
857
966
|
|
|
858
967
|
def parse_bool_string(s):
|
|
859
968
|
"""
|
|
860
969
|
Convert the strings "true" or "false" to boolean values. Case-insensitive, discards
|
|
861
970
|
leading and trailing whitespace. If s is already a bool, returns s.
|
|
862
|
-
|
|
971
|
+
|
|
863
972
|
Args:
|
|
864
973
|
s (str or bool): the string to parse, or the bool to return
|
|
865
|
-
|
|
974
|
+
|
|
866
975
|
Returns:
|
|
867
976
|
bool: the parsed value
|
|
868
977
|
"""
|
|
869
|
-
|
|
978
|
+
|
|
870
979
|
if isinstance(s,bool):
|
|
871
980
|
return s
|
|
872
981
|
s = s.lower().strip()
|
|
@@ -876,57 +985,766 @@ def parse_bool_string(s):
|
|
|
876
985
|
return False
|
|
877
986
|
else:
|
|
878
987
|
raise ValueError('Cannot parse bool from string {}'.format(str(s)))
|
|
879
|
-
|
|
880
988
|
|
|
881
|
-
#%% Test driver
|
|
882
989
|
|
|
883
|
-
def
|
|
990
|
+
def make_temp_folder(top_level_folder='megadetector',subfolder=None,append_guid=True):
|
|
884
991
|
"""
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
+
|
|
888
1116
|
##%% Camera folder mapping
|
|
889
|
-
|
|
890
|
-
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'
|
|
891
1118
|
assert image_file_to_camera_folder('a/b/c/d/100RECNX/blah.jpg') == 'a/b/c/d'
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
|
|
894
1128
|
##%% Test a few rectangle distances
|
|
895
|
-
|
|
1129
|
+
|
|
896
1130
|
r1 = [0,0,1,1]; r2 = [0,0,1,1]; assert rect_distance(r1,r2)==0
|
|
897
1131
|
r1 = [0,0,1,1]; r2 = [0,0,1,100]; assert rect_distance(r1,r2)==0
|
|
898
1132
|
r1 = [0,0,1,1]; r2 = [1,1,2,2]; assert rect_distance(r1,r2)==0
|
|
899
1133
|
r1 = [0,0,1,1]; r2 = [1.1,0,0,1.1]; assert abs(rect_distance(r1,r2)-.1) < 0.00001
|
|
900
|
-
|
|
1134
|
+
|
|
901
1135
|
r1 = [0.4,0.8,10,22]; r2 = [100, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-119.753) < 0.001
|
|
902
|
-
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
|
|
903
1137
|
r1 = [0.4,0.8,10,22]; r2 = [120, 120, 200, 210.4]; assert abs(rect_distance(r1,r2)-147.323) < 0.001
|
|
904
|
-
|
|
905
1138
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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}]
|
|
909
1158
|
k = 'a'
|
|
910
|
-
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
|
|
911
1163
|
|
|
912
1164
|
|
|
913
|
-
##%% Test
|
|
914
|
-
|
|
915
|
-
|
|
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
|
+
|
|
916
1207
|
data = {
|
|
917
1208
|
"name": "Project X",
|
|
918
1209
|
"values": [1.23456789, 2.3456789],
|
|
919
1210
|
"tuple_values": (3.45678901, 4.56789012),
|
|
920
|
-
"set_values": {5.67890123, 6.78901234},
|
|
1211
|
+
"set_values": {5.67890123, 6.78901234}, # Order not guaranteed in set, test min/max
|
|
921
1212
|
"metrics": {
|
|
922
1213
|
"score": 98.7654321,
|
|
923
1214
|
"components": [5.6789012, 6.7890123]
|
|
924
|
-
}
|
|
1215
|
+
},
|
|
1216
|
+
"other_iter": iter([7.89012345]) # Test other iterables
|
|
925
1217
|
}
|
|
926
|
-
|
|
927
|
-
result = round_floats_in_nested_dict(data)
|
|
1218
|
+
|
|
1219
|
+
result = round_floats_in_nested_dict(data, decimal_places=5, allow_iterator_conversion=True)
|
|
928
1220
|
assert result['values'][0] == 1.23457
|
|
929
1221
|
assert result['tuple_values'][0] == 3.45679
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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()
|