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