megadetector 5.0.29__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.

Files changed (95) hide show
  1. megadetector/classification/efficientnet/model.py +8 -8
  2. megadetector/classification/efficientnet/utils.py +6 -5
  3. megadetector/classification/prepare_classification_script_mc.py +3 -3
  4. megadetector/data_management/annotations/annotation_constants.py +0 -1
  5. megadetector/data_management/camtrap_dp_to_coco.py +34 -1
  6. megadetector/data_management/cct_json_utils.py +2 -2
  7. megadetector/data_management/coco_to_yolo.py +22 -5
  8. megadetector/data_management/databases/add_width_and_height_to_db.py +85 -12
  9. megadetector/data_management/databases/combine_coco_camera_traps_files.py +2 -2
  10. megadetector/data_management/databases/integrity_check_json_db.py +29 -15
  11. megadetector/data_management/generate_crops_from_cct.py +50 -1
  12. megadetector/data_management/labelme_to_coco.py +4 -2
  13. megadetector/data_management/labelme_to_yolo.py +82 -2
  14. megadetector/data_management/lila/generate_lila_per_image_labels.py +276 -18
  15. megadetector/data_management/lila/get_lila_annotation_counts.py +5 -3
  16. megadetector/data_management/lila/lila_common.py +3 -0
  17. megadetector/data_management/lila/test_lila_metadata_urls.py +15 -5
  18. megadetector/data_management/mewc_to_md.py +5 -0
  19. megadetector/data_management/ocr_tools.py +4 -3
  20. megadetector/data_management/read_exif.py +20 -5
  21. megadetector/data_management/remap_coco_categories.py +66 -4
  22. megadetector/data_management/remove_exif.py +50 -1
  23. megadetector/data_management/rename_images.py +3 -3
  24. megadetector/data_management/resize_coco_dataset.py +563 -95
  25. megadetector/data_management/yolo_output_to_md_output.py +131 -2
  26. megadetector/data_management/yolo_to_coco.py +140 -5
  27. megadetector/detection/change_detection.py +4 -3
  28. megadetector/detection/pytorch_detector.py +60 -22
  29. megadetector/detection/run_detector.py +225 -25
  30. megadetector/detection/run_detector_batch.py +42 -16
  31. megadetector/detection/run_inference_with_yolov5_val.py +12 -2
  32. megadetector/detection/run_tiled_inference.py +1 -0
  33. megadetector/detection/video_utils.py +53 -24
  34. megadetector/postprocessing/add_max_conf.py +4 -0
  35. megadetector/postprocessing/categorize_detections_by_size.py +1 -1
  36. megadetector/postprocessing/classification_postprocessing.py +55 -20
  37. megadetector/postprocessing/combine_batch_outputs.py +3 -2
  38. megadetector/postprocessing/compare_batch_results.py +64 -10
  39. megadetector/postprocessing/convert_output_format.py +12 -8
  40. megadetector/postprocessing/create_crop_folder.py +137 -10
  41. megadetector/postprocessing/load_api_results.py +26 -8
  42. megadetector/postprocessing/md_to_coco.py +4 -4
  43. megadetector/postprocessing/md_to_labelme.py +18 -7
  44. megadetector/postprocessing/merge_detections.py +5 -0
  45. megadetector/postprocessing/postprocess_batch_results.py +6 -3
  46. megadetector/postprocessing/remap_detection_categories.py +55 -2
  47. megadetector/postprocessing/render_detection_confusion_matrix.py +9 -6
  48. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
  49. megadetector/taxonomy_mapping/map_new_lila_datasets.py +3 -4
  50. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +40 -19
  51. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +1 -1
  52. megadetector/taxonomy_mapping/species_lookup.py +123 -41
  53. megadetector/utils/ct_utils.py +133 -113
  54. megadetector/utils/md_tests.py +93 -13
  55. megadetector/utils/path_utils.py +137 -107
  56. megadetector/utils/split_locations_into_train_val.py +2 -2
  57. megadetector/utils/string_utils.py +7 -7
  58. megadetector/utils/url_utils.py +81 -58
  59. megadetector/utils/wi_utils.py +46 -17
  60. megadetector/visualization/plot_utils.py +13 -9
  61. megadetector/visualization/render_images_with_thumbnails.py +2 -1
  62. megadetector/visualization/visualization_utils.py +94 -46
  63. megadetector/visualization/visualize_db.py +36 -9
  64. megadetector/visualization/visualize_detector_output.py +4 -4
  65. {megadetector-5.0.29.dist-info → megadetector-10.0.0.dist-info}/METADATA +135 -135
  66. megadetector-10.0.0.dist-info/RECORD +139 -0
  67. {megadetector-5.0.29.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
  68. {megadetector-5.0.29.dist-info → megadetector-10.0.0.dist-info}/top_level.txt +0 -0
  69. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  70. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  71. megadetector/api/batch_processing/api_core/batch_service/score.py +0 -438
  72. megadetector/api/batch_processing/api_core/server.py +0 -294
  73. megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
  74. megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
  75. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  76. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
  77. megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
  78. megadetector/api/batch_processing/api_core/server_utils.py +0 -88
  79. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  80. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  81. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  82. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  83. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  84. megadetector/api/synchronous/__init__.py +0 -0
  85. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  86. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
  87. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
  88. megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
  89. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  90. megadetector/api/synchronous/api_core/tests/load_test.py +0 -109
  91. megadetector/utils/azure_utils.py +0 -178
  92. megadetector/utils/sas_blob_utils.py +0 -513
  93. megadetector-5.0.29.dist-info/RECORD +0 -163
  94. /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
  95. {megadetector-5.0.29.dist-info → megadetector-10.0.0.dist-info}/WHEEL +0 -0
@@ -206,9 +206,9 @@ def json_serialize_datetime(obj):
206
206
 
207
207
 
208
208
  def write_json(path,
209
- content,
210
- indent=1,
211
- force_str=False,
209
+ content,
210
+ indent=1,
211
+ force_str=False,
212
212
  serialize_datetimes=False,
213
213
  ensure_ascii=True,
214
214
  encoding='utf-8'):
@@ -222,6 +222,7 @@ def write_json(path,
222
222
  force_str (bool, optional): whether to force string conversion for non-serializable objects
223
223
  serialize_datetimes (bool, optional): whether to serialize datetime objects to ISO format
224
224
  ensure_ascii (bool, optional): whether to ensure ASCII characters in the output
225
+ encoding (str, optional): string encoding to use
225
226
  """
226
227
 
227
228
  default_handler = None
@@ -238,9 +239,13 @@ def write_json(path,
238
239
  elif force_str:
239
240
  default_handler = str
240
241
 
242
+ os.makedirs(os.path.dirname(path), exist_ok=True)
243
+
241
244
  with open(path, 'w', newline='\n', encoding=encoding) as f:
242
245
  json.dump(content, f, indent=indent, default=default_handler, ensure_ascii=ensure_ascii)
243
246
 
247
+ # ...def write_json(...)
248
+
244
249
 
245
250
  def convert_yolo_to_xywh(yolo_box):
246
251
  """
@@ -389,8 +394,8 @@ def point_dist(p1,p2):
389
394
  Computes the distance between two points, represented as length-two tuples.
390
395
 
391
396
  Args:
392
- p1: point, formatted as (x,y)
393
- p2: point, formatted as (x,y)
397
+ p1 (list or tuple): point, formatted as (x,y)
398
+ p2 (list or tuple): point, formatted as (x,y)
394
399
 
395
400
  Returns:
396
401
  float: the Euclidean distance between p1 and p2
@@ -407,8 +412,8 @@ def rect_distance(r1, r2, format='x0y0x1y1'):
407
412
  Can also specify "format" as x0y0wh for MD-style bbox formatting (x0,y0,w,h).
408
413
 
409
414
  Args:
410
- r1: rectangle, formatted as (x0,y0,x1,y1) or (x0,y0,xy,y1)
411
- 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)
412
417
  format (str, optional): whether the boxes are formatted as 'x0y0x1y1' (default) or 'x0y0wh'
413
418
 
414
419
  Returns:
@@ -477,7 +482,7 @@ def split_list_into_n_chunks(L, n, chunk_strategy='greedy'): # noqa
477
482
  Args:
478
483
  L (list): list to split into chunks
479
484
  n (int): number of chunks
480
- chunk_strategy (str, optiopnal): "greedy" or "balanced"; see above
485
+ chunk_strategy (str, optional): "greedy" or "balanced"; see above
481
486
 
482
487
  Returns:
483
488
  list: list of chunks, each of which is a list
@@ -496,7 +501,7 @@ def split_list_into_n_chunks(L, n, chunk_strategy='greedy'): # noqa
496
501
  raise ValueError('Invalid chunk strategy: {}'.format(chunk_strategy))
497
502
 
498
503
 
499
- def sort_list_of_dicts_by_key(L,k,reverse=False): # noqa
504
+ def sort_list_of_dicts_by_key(L, k, reverse=False, none_handling='smallest'): # noqa ("L" should be lowercase)
500
505
  """
501
506
  Sorts the list of dictionaries [L] by the key [k].
502
507
 
@@ -504,11 +509,25 @@ def sort_list_of_dicts_by_key(L,k,reverse=False): # noqa
504
509
  L (list): list of dictionaries to sort
505
510
  k (object, typically str): the sort key
506
511
  reverse (bool, optional): whether to sort in reverse (descending) order
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
507
516
 
508
517
  Returns:
509
- dict: sorted copy of [d]
518
+ list: sorted copy of [L]
510
519
  """
511
- return sorted(L, key=lambda d: d[k], reverse=reverse)
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))
512
531
 
513
532
 
514
533
  def sort_dictionary_by_key(d,reverse=False):
@@ -573,7 +592,7 @@ def round_floats_in_nested_dict(obj, decimal_places=5, allow_iterator_conversion
573
592
  sets, and other iterables. Modifies mutable objects in place.
574
593
 
575
594
  Args:
576
- obj: The object to process (can be a dict, list, set, tuple, or primitive value)
595
+ obj (obj): The object to process (can be a dict, list, set, tuple, or primitive value)
577
596
  decimal_places (int, optional): Number of decimal places to round to
578
597
  allow_iterator_conversion (bool, optional): for iterator types, should we convert
579
598
  to lists? Otherwise we error.
@@ -595,7 +614,7 @@ def round_floats_in_nested_dict(obj, decimal_places=5, allow_iterator_conversion
595
614
 
596
615
  elif isinstance(obj, tuple):
597
616
  # Tuples are immutable, so we create a new one
598
- return tuple(round_floats_in_nested_dict(item, decimal_places=decimal_places,
617
+ return tuple(round_floats_in_nested_dict(item, decimal_places=decimal_places,
599
618
  allow_iterator_conversion=allow_iterator_conversion) for item in obj)
600
619
 
601
620
  elif isinstance(obj, set):
@@ -606,11 +625,11 @@ def round_floats_in_nested_dict(obj, decimal_places=5, allow_iterator_conversion
606
625
 
607
626
  elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
608
627
  # Handle other iterable types: convert to list, process, and convert back
609
- processed_list = [round_floats_in_nested_dict(item,
628
+ processed_list = [round_floats_in_nested_dict(item,
610
629
  decimal_places=decimal_places,
611
630
  allow_iterator_conversion=allow_iterator_conversion) \
612
631
  for item in obj]
613
-
632
+
614
633
  # Try to recreate the original type, but fall back to list for iterators
615
634
  try:
616
635
  return type(obj)(processed_list)
@@ -682,7 +701,7 @@ def is_float(v):
682
701
 
683
702
  if v is None:
684
703
  return False
685
-
704
+
686
705
  try:
687
706
  _ = float(v)
688
707
  return True
@@ -714,7 +733,7 @@ def is_empty(v):
714
733
  data from .csv files. "empty" includes None, '', and NaN.
715
734
 
716
735
  Args:
717
- v: the object to evaluate for emptiness
736
+ v (obj): the object to evaluate for emptiness
718
737
 
719
738
  Returns:
720
739
  bool: True if [v] is None, '', or NaN, otherwise False
@@ -731,12 +750,12 @@ def is_empty(v):
731
750
  def to_bool(v):
732
751
  """
733
752
  Convert an object to a bool with specific rules.
734
-
753
+
735
754
  Args:
736
- obj (object): The object to convert
737
-
755
+ v (object): The object to convert
756
+
738
757
  Returns:
739
- bool or None:
758
+ bool or None:
740
759
  - For strings: True if 'true' (case-insensitive), False if 'false', recursively applied if int-like
741
760
  - For int/bytes: False if 0, True otherwise
742
761
  - For bool: returns the bool as-is
@@ -745,7 +764,7 @@ def to_bool(v):
745
764
 
746
765
  if isinstance(v, bool):
747
766
  return v
748
-
767
+
749
768
  if isinstance(v, str):
750
769
 
751
770
  try:
@@ -764,7 +783,7 @@ def to_bool(v):
764
783
 
765
784
  if isinstance(v, (int, bytes)):
766
785
  return v != 0
767
-
786
+
768
787
  return None
769
788
 
770
789
 
@@ -817,7 +836,7 @@ def isnan(v):
817
836
  Returns True if v is a nan-valued float, otherwise returns False.
818
837
 
819
838
  Args:
820
- v: the object to evaluate for nan-ness
839
+ v (obj): the object to evaluate for nan-ness
821
840
 
822
841
  Returns:
823
842
  bool: True if v is a nan-valued float, otherwise False
@@ -866,7 +885,7 @@ def is_function_name(s,calling_namespace):
866
885
  def parse_kvp(s,kv_separator='='):
867
886
  """
868
887
  Parse a key/value pair, separated by [kv_separator]. Errors if s is not
869
- a valid key/value pair string. Strips leading/trailing whitespace from
888
+ a valid key/value pair string. Strips leading/trailing whitespace from
870
889
  the key and value.
871
890
 
872
891
  Args:
@@ -1014,6 +1033,10 @@ def make_temp_folder(top_level_folder='megadetector',subfolder=None,append_guid=
1014
1033
  def make_test_folder(subfolder=None):
1015
1034
  """
1016
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.
1017
1040
  """
1018
1041
 
1019
1042
  return make_temp_folder(top_level_folder='megadetector/tests',
@@ -1135,14 +1158,14 @@ def test_geometric_operations():
1135
1158
  r1 = [0.4,0.8,10,22]; r2 = [100, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-119.753) < 0.001
1136
1159
  r1 = [0.4,0.8,10,22]; r2 = [101, 101, 200, 210.4]; assert abs(rect_distance(r1,r2)-120.507) < 0.001
1137
1160
  r1 = [0.4,0.8,10,22]; r2 = [120, 120, 200, 210.4]; assert abs(rect_distance(r1,r2)-147.323) < 0.001
1138
-
1161
+
1139
1162
  # Test with 'x0y0wh' format
1140
1163
  r1_wh = [0,0,1,1]; r2_wh = [1,0,1,1]; assert rect_distance(r1_wh, r2_wh, format='x0y0wh') == 0
1141
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
1142
1165
 
1143
1166
 
1144
1167
  ##%% Test point_dist
1145
-
1168
+
1146
1169
  assert point_dist((0,0), (3,4)) == 5.0
1147
1170
  assert point_dist((1,1), (1,1)) == 0.0
1148
1171
 
@@ -1153,17 +1176,17 @@ def test_dictionary_operations():
1153
1176
  """
1154
1177
 
1155
1178
  ##%% Test sort_list_of_dicts_by_key
1156
-
1157
- L = [{'a':5},{'a':0},{'a':10}]
1179
+
1180
+ x = [{'a':5},{'a':0},{'a':10}]
1158
1181
  k = 'a'
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
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
1163
1186
 
1164
1187
 
1165
1188
  ##%% Test sort_dictionary_by_key
1166
-
1189
+
1167
1190
  d_key = {'b': 2, 'a': 1, 'c': 3}
1168
1191
  sorted_d_key = sort_dictionary_by_key(d_key)
1169
1192
  assert list(sorted_d_key.keys()) == ['a', 'b', 'c']
@@ -1190,7 +1213,7 @@ def test_dictionary_operations():
1190
1213
  d_inv = {'a': 'x', 'b': 'y'}
1191
1214
  inverted_d = invert_dictionary(d_inv)
1192
1215
  assert inverted_d == {'x': 'a', 'y': 'b'}
1193
-
1216
+
1194
1217
  # Does not check for uniqueness, last one wins
1195
1218
  d_inv_dup = {'a': 'x', 'b': 'x'}
1196
1219
  inverted_d_dup = invert_dictionary(d_inv_dup)
@@ -1203,7 +1226,7 @@ def test_float_rounding_and_truncation():
1203
1226
  """
1204
1227
 
1205
1228
  ##%% Test round_floats_in_nested_dict
1206
-
1229
+
1207
1230
  data = {
1208
1231
  "name": "Project X",
1209
1232
  "values": [1.23456789, 2.3456789],
@@ -1229,7 +1252,7 @@ def test_float_rounding_and_truncation():
1229
1252
 
1230
1253
 
1231
1254
  ##%% Test truncate_float_array and truncate_float
1232
-
1255
+
1233
1256
  assert truncate_float_array([0.12345, 0.67890], precision=3) == [0.123, 0.678]
1234
1257
  assert truncate_float_array([1.0, 2.0], precision=2) == [1.0, 2.0]
1235
1258
  assert truncate_float(0.12345, precision=3) == 0.123
@@ -1239,7 +1262,7 @@ def test_float_rounding_and_truncation():
1239
1262
 
1240
1263
 
1241
1264
  ##%% Test round_float_array and round_float
1242
-
1265
+
1243
1266
  assert round_float_array([0.12345, 0.67890], precision=3) == [0.123, 0.679]
1244
1267
  assert round_float_array([1.0, 2.0], precision=2) == [1.0, 2.0]
1245
1268
  assert round_float(0.12345, precision=3) == 0.123
@@ -1253,7 +1276,7 @@ def test_object_conversion_and_presentation():
1253
1276
  """
1254
1277
 
1255
1278
  ##%% Test args_to_object
1256
-
1279
+
1257
1280
  class ArgsObject:
1258
1281
  pass
1259
1282
  args_namespace = type('ArgsNameSpace', (), {'a': 1, 'b': 'test', '_c': 'ignored'})
@@ -1265,7 +1288,7 @@ def test_object_conversion_and_presentation():
1265
1288
 
1266
1289
 
1267
1290
  ##%% Test dict_to_object
1268
-
1291
+
1269
1292
  class DictObject:
1270
1293
  pass
1271
1294
  d = {'a': 1, 'b': 'test', '_c': 'ignored'}
@@ -1277,7 +1300,7 @@ def test_object_conversion_and_presentation():
1277
1300
 
1278
1301
 
1279
1302
  ##%% Test pretty_print_object
1280
-
1303
+
1281
1304
  class PrettyPrintable:
1282
1305
  def __init__(self):
1283
1306
  self.a = 1
@@ -1289,7 +1312,7 @@ def test_object_conversion_and_presentation():
1289
1312
  parsed_json = json.loads(json_str) # Relies on json.loads
1290
1313
  assert parsed_json['a'] == 1
1291
1314
  assert parsed_json['b'] == "test"
1292
-
1315
+
1293
1316
 
1294
1317
  def test_list_operations():
1295
1318
  """
@@ -1297,7 +1320,7 @@ def test_list_operations():
1297
1320
  """
1298
1321
 
1299
1322
  ##%% Test is_list_sorted
1300
-
1323
+
1301
1324
  assert is_list_sorted([1, 2, 3])
1302
1325
  assert not is_list_sorted([1, 3, 2])
1303
1326
  assert is_list_sorted([3, 2, 1], reverse=True)
@@ -1306,10 +1329,10 @@ def test_list_operations():
1306
1329
  assert is_list_sorted([1]) # Single element list is sorted
1307
1330
  assert is_list_sorted([1,1,1])
1308
1331
  assert is_list_sorted([1,1,1], reverse=True)
1309
-
1310
-
1332
+
1333
+
1311
1334
  ##%% Test split_list_into_fixed_size_chunks
1312
-
1335
+
1313
1336
  assert split_list_into_fixed_size_chunks([1,2,3,4,5,6], 2) == [[1,2],[3,4],[5,6]]
1314
1337
  assert split_list_into_fixed_size_chunks([1,2,3,4,5], 2) == [[1,2],[3,4],[5]]
1315
1338
  assert split_list_into_fixed_size_chunks([], 3) == []
@@ -1317,11 +1340,11 @@ def test_list_operations():
1317
1340
 
1318
1341
 
1319
1342
  ##%% Test split_list_into_n_chunks
1320
-
1343
+
1321
1344
  # Greedy
1322
1345
  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]]
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]]
1325
1348
  assert split_list_into_n_chunks([], 3) == [[],[],[]]
1326
1349
 
1327
1350
  # Balanced
@@ -1330,7 +1353,7 @@ def test_list_operations():
1330
1353
  assert split_list_into_n_chunks([], 3, chunk_strategy='balanced') == [[],[],[]]
1331
1354
  try:
1332
1355
  split_list_into_n_chunks([1,2,3], 2, chunk_strategy='invalid')
1333
- assert False, "ValueError not raised for invalid chunk_strategy"
1356
+ raise AssertionError("ValueError not raised for invalid chunk_strategy")
1334
1357
  except ValueError:
1335
1358
  pass
1336
1359
 
@@ -1341,19 +1364,19 @@ def test_datetime_serialization():
1341
1364
  """
1342
1365
 
1343
1366
  ##%% Test json_serialize_datetime
1344
-
1367
+
1345
1368
  now = datetime.datetime.now()
1346
1369
  today = datetime.date.today()
1347
1370
  assert json_serialize_datetime(now) == now.isoformat()
1348
1371
  assert json_serialize_datetime(today) == today.isoformat()
1349
1372
  try:
1350
1373
  json_serialize_datetime("not a datetime")
1351
- assert False, "TypeError not raised for non-datetime object"
1374
+ raise AssertionError("TypeError not raised for non-datetime object")
1352
1375
  except TypeError:
1353
1376
  pass
1354
1377
  try:
1355
1378
  json_serialize_datetime(123)
1356
- assert False, "TypeError not raised for non-datetime object"
1379
+ raise AssertionError("TypeError not raised for non-datetime object")
1357
1380
  except TypeError:
1358
1381
  pass
1359
1382
 
@@ -1364,7 +1387,7 @@ def test_bounding_box_operations():
1364
1387
  """
1365
1388
 
1366
1389
  ##%% Test convert_yolo_to_xywh
1367
-
1390
+
1368
1391
  # [x_center, y_center, w, h]
1369
1392
  yolo_box = [0.5, 0.5, 0.2, 0.2]
1370
1393
  # [x_min, y_min, width_of_box, height_of_box]
@@ -1373,7 +1396,7 @@ def test_bounding_box_operations():
1373
1396
 
1374
1397
 
1375
1398
  ##%% Test convert_xywh_to_xyxy
1376
-
1399
+
1377
1400
  # [x_min, y_min, width_of_box, height_of_box]
1378
1401
  xywh_box = [0.1, 0.1, 0.3, 0.3]
1379
1402
  # [x_min, y_min, x_max, y_max]
@@ -1387,7 +1410,7 @@ def test_bounding_box_operations():
1387
1410
  bb2 = [0.25, 0.25, 0.5, 0.5]
1388
1411
  assert abs(get_iou(bb1, bb2) - 0.142857) < 1e-5
1389
1412
  bb3 = [0, 0, 1, 1]
1390
- bb4 = [0.5, 0.5, 1, 1]
1413
+ bb4 = [0.5, 0.5, 1, 1]
1391
1414
  assert abs(get_iou(bb3, bb4) - (0.25 / 1.75)) < 1e-5
1392
1415
  bb5 = [0,0,1,1]
1393
1416
  bb6 = [1,1,1,1] # No overlap
@@ -1410,7 +1433,7 @@ def test_detection_processing():
1410
1433
  """
1411
1434
 
1412
1435
  ##%% Test _get_max_conf_from_detections and get_max_conf
1413
-
1436
+
1414
1437
  detections1 = [{'conf': 0.8}, {'conf': 0.9}, {'conf': 0.75}]
1415
1438
  assert _get_max_conf_from_detections(detections1) == 0.9
1416
1439
  assert _get_max_conf_from_detections([]) == 0.0
@@ -1424,8 +1447,8 @@ def test_detection_processing():
1424
1447
  assert get_max_conf(im3) == 0.0
1425
1448
  im4 = {'detections': None}
1426
1449
  assert get_max_conf(im4) == 0.0
1427
-
1428
-
1450
+
1451
+
1429
1452
  ##%% Test sort_results_for_image
1430
1453
 
1431
1454
  img_data = {
@@ -1436,30 +1459,30 @@ def test_detection_processing():
1436
1459
  ]
1437
1460
  }
1438
1461
  sort_results_for_image(img_data)
1439
-
1440
- # Check detections sorted by conf
1462
+
1463
+ # Check detections sorted by conf
1441
1464
  assert img_data['detections'][0]['conf'] == 0.9
1442
1465
  assert img_data['detections'][1]['conf'] == 0.8
1443
1466
  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)
1467
+
1468
+ # Check classifications sorted by conf (only for the first original detection, now at index 0 after sort)
1446
1469
  assert img_data['detections'][0]['classifications'][0] == ('x', 0.95)
1447
1470
  assert img_data['detections'][0]['classifications'][1] == ('y', 0.85)
1448
-
1449
- # Check classifications for the second original detection (now at index 2)
1471
+
1472
+ # Check classifications for the second original detection (now at index 2)
1450
1473
  assert img_data['detections'][2]['classifications'][0] == ('a', 0.9)
1451
1474
  assert img_data['detections'][2]['classifications'][1] == ('b', 0.8)
1452
1475
  assert img_data['detections'][2]['classifications'][2] == ('c', 0.6)
1453
-
1476
+
1454
1477
  # Test with no detections or no classifications field
1455
1478
  img_data_no_det = {'detections': None}
1456
- sort_results_for_image(img_data_no_det)
1479
+ sort_results_for_image(img_data_no_det)
1457
1480
  assert img_data_no_det['detections'] is None
1458
-
1481
+
1459
1482
  img_data_empty_det = {'detections': []}
1460
1483
  sort_results_for_image(img_data_empty_det)
1461
1484
  assert img_data_empty_det['detections'] == []
1462
-
1485
+
1463
1486
  img_data_no_classifications_field = {'detections': [{'conf': 0.8}]}
1464
1487
  sort_results_for_image(img_data_no_classifications_field)
1465
1488
  assert 'classifications' not in img_data_no_classifications_field['detections'][0]
@@ -1479,7 +1502,7 @@ def test_type_checking_and_validation():
1479
1502
  """
1480
1503
 
1481
1504
  ##%% Test is_float
1482
-
1505
+
1483
1506
  assert is_float(1.23)
1484
1507
  assert is_float("1.23")
1485
1508
  assert is_float("-1.23")
@@ -1490,7 +1513,7 @@ def test_type_checking_and_validation():
1490
1513
 
1491
1514
 
1492
1515
  ##%% Test is_iterable
1493
-
1516
+
1494
1517
  assert is_iterable([1,2,3])
1495
1518
  assert is_iterable("hello")
1496
1519
  assert is_iterable({'a':1})
@@ -1501,7 +1524,7 @@ def test_type_checking_and_validation():
1501
1524
 
1502
1525
 
1503
1526
  ##%% Test is_empty
1504
-
1527
+
1505
1528
  assert is_empty(None)
1506
1529
  assert is_empty("")
1507
1530
  assert is_empty(np.nan)
@@ -1513,7 +1536,7 @@ def test_type_checking_and_validation():
1513
1536
 
1514
1537
 
1515
1538
  ##%% Test min_none and max_none
1516
-
1539
+
1517
1540
  assert min_none(1, 2) == 1
1518
1541
  assert min_none(None, 2) == 2
1519
1542
  assert min_none(1, None) == 1
@@ -1525,7 +1548,7 @@ def test_type_checking_and_validation():
1525
1548
 
1526
1549
 
1527
1550
  ##%% Test isnan
1528
-
1551
+
1529
1552
  assert isnan(np.nan)
1530
1553
  assert not isnan(0.0)
1531
1554
  assert not isnan("text")
@@ -1535,7 +1558,7 @@ def test_type_checking_and_validation():
1535
1558
 
1536
1559
 
1537
1560
  ##%% Test sets_overlap
1538
-
1561
+
1539
1562
  assert sets_overlap({1,2,3}, {3,4,5})
1540
1563
  assert not sets_overlap({1,2}, {3,4})
1541
1564
  assert sets_overlap([1,2,3], [3,4,5]) # Test with lists
@@ -1544,7 +1567,7 @@ def test_type_checking_and_validation():
1544
1567
 
1545
1568
 
1546
1569
  ##%% Test is_function_name
1547
-
1570
+
1548
1571
  def _test_local_func(): pass
1549
1572
  assert is_function_name("is_float", locals()) # Test a function in ct_utils
1550
1573
  assert is_function_name("_test_local_func", locals()) # Test a local function
@@ -1563,38 +1586,38 @@ def test_string_parsing():
1563
1586
  """
1564
1587
 
1565
1588
  ##%% Test parse_kvp and parse_kvp_list
1566
-
1589
+
1567
1590
  assert parse_kvp("key=value") == ("key", "value")
1568
1591
  assert parse_kvp("key = value with spaces") == ("key", "value with spaces")
1569
1592
  assert parse_kvp("key=value1=value2", kv_separator='=') == ("key", "value1=value2")
1570
1593
  try:
1571
1594
  parse_kvp("keyvalue")
1572
- assert False, "AssertionError not raised for invalid KVP"
1595
+ raise AssertionError("AssertionError not raised for invalid KVP")
1573
1596
  except AssertionError:
1574
1597
  pass
1575
-
1598
+
1576
1599
  kvp_list = ["a=1", "b = 2", "c=foo=bar"]
1577
1600
  parsed_list = parse_kvp_list(kvp_list)
1578
1601
  assert parsed_list == {"a": "1", "b": "2", "c": "foo=bar"}
1579
1602
  assert parse_kvp_list(None) == {}
1580
1603
  assert parse_kvp_list([]) == {}
1581
1604
  d_initial = {'z': '0'}
1582
-
1583
- # parse_kvp_list modifies d in place if provided
1584
- parse_kvp_list(kvp_list, d=d_initial)
1605
+
1606
+ # parse_kvp_list modifies d in place if provided
1607
+ parse_kvp_list(kvp_list, d=d_initial)
1585
1608
  assert d_initial == {"z": "0", "a": "1", "b": "2", "c": "foo=bar"}
1586
-
1587
- # Test with a different separator
1609
+
1610
+ # Test with a different separator
1588
1611
  assert parse_kvp("key:value", kv_separator=":") == ("key", "value")
1589
1612
  assert parse_kvp_list(["a:1","b:2"], kv_separator=":") == {"a":"1", "b":"2"}
1590
1613
 
1591
1614
 
1592
1615
  ##%% Test dict_to_kvp_list
1593
-
1616
+
1594
1617
  d_kvp = {"a": "1", "b": "dog", "c": "foo=bar"}
1595
1618
  kvp_str = dict_to_kvp_list(d_kvp)
1596
-
1597
- # Order isn't guaranteed, so check for presence of all items and length
1619
+
1620
+ # Order isn't guaranteed, so check for presence of all items and length
1598
1621
  assert "a=1" in kvp_str
1599
1622
  assert "b=dog" in kvp_str
1600
1623
  assert "c=foo=bar" in kvp_str
@@ -1605,12 +1628,12 @@ def test_string_parsing():
1605
1628
  d_kvp_int = {"a":1, "b":"text"}
1606
1629
  try:
1607
1630
  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"
1631
+ raise AssertionError("ValueError not raised for non-string value with 'error' handling")
1609
1632
  except ValueError:
1610
1633
  pass
1611
1634
  convert_result = dict_to_kvp_list(d_kvp_int, non_string_value_handling='convert')
1612
1635
  assert "a=1" in convert_result and "b=text" in convert_result
1613
-
1636
+
1614
1637
  omit_result = dict_to_kvp_list({"a":1, "b":"text"}, non_string_value_handling='omit')
1615
1638
  assert "a=1" not in omit_result and "b=text" in omit_result
1616
1639
  assert omit_result == "b=text"
@@ -1619,7 +1642,7 @@ def test_string_parsing():
1619
1642
 
1620
1643
 
1621
1644
  ##%% Test parse_bool_string
1622
-
1645
+
1623
1646
  assert parse_bool_string("true")
1624
1647
  assert parse_bool_string("True")
1625
1648
  assert parse_bool_string(" TRUE ")
@@ -1630,12 +1653,12 @@ def test_string_parsing():
1630
1653
  assert parse_bool_string(False) is False
1631
1654
  try:
1632
1655
  parse_bool_string("maybe")
1633
- assert False, "ValueError not raised for invalid bool string"
1656
+ raise AssertionError("ValueError not raised for invalid bool string")
1634
1657
  except ValueError:
1635
1658
  pass
1636
1659
  try:
1637
1660
  parse_bool_string("1") # Should not parse to True
1638
- assert False, "ValueError not raised for '1'"
1661
+ raise AssertionError("ValueError not raised for '1'")
1639
1662
  except ValueError:
1640
1663
  pass
1641
1664
 
@@ -1647,18 +1670,18 @@ def test_temp_folder_creation():
1647
1670
 
1648
1671
  # Store original tempdir for restoration if modified by tests (though unlikely for make_temp_folder)
1649
1672
  original_tempdir = tempfile.gettempdir()
1650
-
1673
+
1651
1674
  # Test make_temp_folder
1652
1675
  custom_top_level = "my_custom_temp_app_test" # Unique name for this test run
1653
1676
  custom_subfolder = "specific_test_run"
1654
-
1677
+
1655
1678
  # Test with default subfolder (UUID)
1656
1679
  temp_folder1_base = os.path.join(tempfile.gettempdir(), custom_top_level)
1657
1680
  temp_folder1 = make_temp_folder(top_level_folder=custom_top_level)
1658
1681
  assert os.path.exists(temp_folder1)
1659
1682
  assert os.path.basename(os.path.dirname(temp_folder1)) == custom_top_level
1660
1683
  assert temp_folder1_base == os.path.dirname(temp_folder1) # Path up to UUID should match
1661
-
1684
+
1662
1685
  # Cleanup: remove the custom_top_level which contains the UUID folder
1663
1686
  if os.path.exists(temp_folder1_base):
1664
1687
  shutil.rmtree(temp_folder1_base)
@@ -1674,26 +1697,26 @@ def test_temp_folder_creation():
1674
1697
  assert os.path.basename(temp_folder2) == custom_subfolder
1675
1698
  assert os.path.basename(os.path.dirname(temp_folder2)) == custom_top_level
1676
1699
  assert temp_folder2 == os.path.join(tempfile.gettempdir(), custom_top_level, custom_subfolder)
1677
-
1678
- # Cleanup
1700
+
1701
+ # Cleanup
1679
1702
  if os.path.exists(temp_folder2_base):
1680
1703
  shutil.rmtree(temp_folder2_base)
1681
1704
  assert not os.path.exists(temp_folder2_base)
1682
1705
 
1683
1706
 
1684
1707
  # Test make_test_folder (which uses 'megadetector/tests' as top_level)
1685
- #
1708
+ #
1686
1709
  # This will create tempfile.gettempdir()/megadetector/tests/some_uuid or specified_subfolder
1687
1710
  megadetector_temp_base = os.path.join(tempfile.gettempdir(), "megadetector")
1688
1711
  test_subfolder = "my_specific_module_test"
1689
-
1712
+
1690
1713
  # Test with default subfolder for make_test_folder
1691
1714
  test_folder1 = make_test_folder() # Creates megadetector/tests/uuid_folder
1692
1715
  assert os.path.exists(test_folder1)
1693
1716
  assert os.path.basename(os.path.dirname(test_folder1)) == "tests"
1694
1717
  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
1718
+
1719
+ # Cleanup for make_test_folder default: remove the 'megadetector' base temp dir
1697
1720
  if os.path.exists(megadetector_temp_base):
1698
1721
  shutil.rmtree(megadetector_temp_base)
1699
1722
  assert not os.path.exists(megadetector_temp_base)
@@ -1703,21 +1726,21 @@ def test_temp_folder_creation():
1703
1726
  test_folder2 = make_test_folder(subfolder=test_subfolder) # megadetector/tests/my_specific_module_test
1704
1727
  assert os.path.exists(test_folder2)
1705
1728
  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
1729
+ assert "megadetector" in test_folder2
1730
+
1731
+ # Cleanup for make_test_folder specific: remove the 'megadetector' base temp dir
1709
1732
  if os.path.exists(megadetector_temp_base):
1710
1733
  shutil.rmtree(megadetector_temp_base)
1711
1734
  assert not os.path.exists(megadetector_temp_base)
1712
-
1735
+
1713
1736
  # Verify cleanup if top level folder was 'megadetector' (default for make_temp_folder)
1714
- #
1737
+ #
1715
1738
  # This means it creates tempfile.gettempdir()/megadetector/uuid_folder
1716
- default_temp_folder = make_temp_folder()
1739
+ default_temp_folder = make_temp_folder()
1717
1740
  assert os.path.exists(default_temp_folder)
1718
1741
  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
1742
+
1743
+ # Cleanup: remove the 'megadetector' base temp dir created by default make_temp_folder
1721
1744
  if os.path.exists(megadetector_temp_base):
1722
1745
  shutil.rmtree(megadetector_temp_base)
1723
1746
  assert not os.path.exists(megadetector_temp_base)
@@ -1729,7 +1752,7 @@ def test_temp_folder_creation():
1729
1752
  def run_all_module_tests():
1730
1753
  """
1731
1754
  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.
1755
+ just a convenience wrapper for debugging the tests.
1733
1756
  """
1734
1757
 
1735
1758
  test_write_json()
@@ -1745,6 +1768,3 @@ def run_all_module_tests():
1745
1768
  test_type_checking_and_validation()
1746
1769
  test_string_parsing()
1747
1770
  test_temp_folder_creation()
1748
-
1749
- # from IPython import embed; embed()
1750
- # run_all_module_tests()