matrice-analytics 0.1.60__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.
Files changed (196) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +146 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3291 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  44. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  45. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  46. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  47. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  48. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  49. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  50. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  51. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  52. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  53. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  54. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  55. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  56. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  57. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  58. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  59. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  60. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  61. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  62. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  63. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  64. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  65. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  66. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  67. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  68. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  69. matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
  70. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  71. matrice_analytics/post_processing/post_processor.py +1175 -0
  72. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  73. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  74. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  75. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  76. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  77. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  78. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  79. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  80. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  81. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  82. matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
  83. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  84. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  85. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  86. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  87. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  88. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  89. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  90. matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
  91. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  92. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  93. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  94. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  95. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  96. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  97. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  98. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  99. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  100. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  101. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  102. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  103. matrice_analytics/post_processing/usecases/color/clip.py +660 -0
  104. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  105. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  106. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  107. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  108. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  109. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  110. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  111. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  112. matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
  113. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  114. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  115. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  116. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  117. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  118. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  119. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
  120. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  121. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  122. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  123. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  124. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  125. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  126. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  127. matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
  128. matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
  129. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  130. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  131. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  132. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  133. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  134. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  135. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  136. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  137. matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
  138. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
  139. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  140. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  141. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  142. matrice_analytics/post_processing/usecases/parking.py +787 -0
  143. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  144. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  145. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  146. matrice_analytics/post_processing/usecases/people_counting.py +706 -0
  147. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  148. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  149. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  150. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  151. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  152. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  153. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  154. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  155. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  156. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  157. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  158. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  159. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  160. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  161. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  162. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  163. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  164. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  165. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  166. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  167. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  168. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  169. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  170. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
  171. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  172. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  173. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  174. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  175. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  176. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  177. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  178. matrice_analytics/post_processing/utils/__init__.py +150 -0
  179. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  180. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  181. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  182. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  183. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  184. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  185. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  186. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  187. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  188. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  189. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  190. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  191. matrice_analytics/py.typed +0 -0
  192. matrice_analytics-0.1.60.dist-info/METADATA +481 -0
  193. matrice_analytics-0.1.60.dist-info/RECORD +196 -0
  194. matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
  195. matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
  196. matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
@@ -0,0 +1,743 @@
1
+ """
2
+ Tests for post processing utility functions.
3
+
4
+ This module tests all utility functions including geometry calculations,
5
+ format conversions, filtering, counting, and tracking utilities.
6
+ """
7
+
8
+ import unittest
9
+ import math
10
+ from typing import Dict, List, Any
11
+
12
+ # Fix imports for proper module resolution
13
+ import sys
14
+ import os
15
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
16
+
17
+ from src.matrice_analytics.post_processing.utils import (
18
+ # Geometry utilities
19
+ point_in_polygon, get_bbox_center, calculate_distance, calculate_bbox_overlap,
20
+ calculate_iou, get_bbox_area, normalize_bbox, denormalize_bbox, line_segments_intersect,
21
+
22
+ # Format utilities
23
+ convert_to_coco_format, convert_to_yolo_format, convert_to_tracking_format,
24
+ convert_detection_to_tracking_format, convert_tracking_to_detection_format,
25
+ match_results_structure,
26
+
27
+ # Filter utilities
28
+ filter_by_confidence, filter_by_categories, calculate_bbox_fingerprint,
29
+ clean_expired_tracks, remove_duplicate_detections, apply_category_mapping,
30
+ filter_by_area,
31
+
32
+ # Counting utilities
33
+ count_objects_by_category, count_objects_in_zones, count_unique_tracks,
34
+ calculate_counting_summary,
35
+
36
+ # Tracking utilities
37
+ track_objects_in_zone, detect_line_crossings, analyze_track_movements,
38
+ filter_tracks_by_duration
39
+ )
40
+
41
+ from .test_utilities import BasePostProcessingTest
42
+ from .test_data_generators import (
43
+ create_detection_results, create_tracking_results, create_zone_polygons,
44
+ create_line_crossing_data, create_edge_case_data
45
+ )
46
+
47
+
48
+ class TestGeometryUtils(BasePostProcessingTest):
49
+ """Test geometry utility functions."""
50
+
51
+ def test_point_in_polygon(self):
52
+ """Test point in polygon detection."""
53
+ # Square polygon
54
+ square = [[0, 0], [100, 0], [100, 100], [0, 100]]
55
+
56
+ # Test points inside
57
+ self.assertTrue(point_in_polygon([50, 50], square))
58
+ self.assertTrue(point_in_polygon([10, 10], square))
59
+ self.assertTrue(point_in_polygon([90, 90], square))
60
+
61
+ # Test points outside
62
+ self.assertFalse(point_in_polygon([150, 50], square))
63
+ self.assertFalse(point_in_polygon([50, 150], square))
64
+ self.assertFalse(point_in_polygon([-10, 50], square))
65
+
66
+ # Test boundary points
67
+ self.assertTrue(point_in_polygon([0, 50], square)) # Edge case
68
+ self.assertTrue(point_in_polygon([50, 0], square)) # Edge case
69
+
70
+ def test_point_in_polygon_complex(self):
71
+ """Test point in polygon with complex shapes."""
72
+ # Triangle
73
+ triangle = [[0, 0], [100, 0], [50, 100]]
74
+
75
+ self.assertTrue(point_in_polygon([50, 30], triangle))
76
+ self.assertFalse(point_in_polygon([10, 90], triangle))
77
+
78
+ # Concave polygon
79
+ concave = [[0, 0], [100, 0], [100, 50], [50, 50], [50, 100], [0, 100]]
80
+
81
+ self.assertTrue(point_in_polygon([25, 25], concave))
82
+ self.assertTrue(point_in_polygon([75, 25], concave))
83
+ self.assertFalse(point_in_polygon([75, 75], concave)) # In the "notch"
84
+
85
+ def test_get_bbox_center(self):
86
+ """Test bounding box center calculation."""
87
+ # Simple rectangle
88
+ bbox = [10, 20, 50, 60]
89
+ center = get_bbox_center(bbox)
90
+ self.assertEqual(center, [30.0, 40.0])
91
+
92
+ # Square
93
+ bbox = [0, 0, 100, 100]
94
+ center = get_bbox_center(bbox)
95
+ self.assertEqual(center, [50.0, 50.0])
96
+
97
+ # Single point (degenerate case)
98
+ bbox = [50, 50, 50, 50]
99
+ center = get_bbox_center(bbox)
100
+ self.assertEqual(center, [50.0, 50.0])
101
+
102
+ def test_calculate_distance(self):
103
+ """Test distance calculation between points."""
104
+ # Simple cases
105
+ self.assertEqual(calculate_distance([0, 0], [3, 4]), 5.0)
106
+ self.assertEqual(calculate_distance([0, 0], [0, 0]), 0.0)
107
+ self.assertEqual(calculate_distance([1, 1], [4, 5]), 5.0)
108
+
109
+ # Negative coordinates
110
+ self.assertEqual(calculate_distance([-3, -4], [0, 0]), 5.0)
111
+
112
+ # Floating point coordinates
113
+ dist = calculate_distance([1.5, 2.5], [4.5, 6.5])
114
+ self.assertAlmostEqual(dist, 5.0, places=5)
115
+
116
+ def test_calculate_bbox_overlap(self):
117
+ """Test bounding box overlap calculation."""
118
+ # Overlapping boxes
119
+ bbox1 = [0, 0, 50, 50]
120
+ bbox2 = [25, 25, 75, 75]
121
+ overlap = calculate_bbox_overlap(bbox1, bbox2)
122
+ expected_overlap = 25 * 25 # 625
123
+ self.assertEqual(overlap, expected_overlap)
124
+
125
+ # Non-overlapping boxes
126
+ bbox1 = [0, 0, 50, 50]
127
+ bbox2 = [100, 100, 150, 150]
128
+ overlap = calculate_bbox_overlap(bbox1, bbox2)
129
+ self.assertEqual(overlap, 0)
130
+
131
+ # Identical boxes
132
+ bbox1 = [0, 0, 100, 100]
133
+ bbox2 = [0, 0, 100, 100]
134
+ overlap = calculate_bbox_overlap(bbox1, bbox2)
135
+ self.assertEqual(overlap, 10000) # 100 * 100
136
+
137
+ # Touching boxes (no overlap)
138
+ bbox1 = [0, 0, 50, 50]
139
+ bbox2 = [50, 0, 100, 50]
140
+ overlap = calculate_bbox_overlap(bbox1, bbox2)
141
+ self.assertEqual(overlap, 0)
142
+
143
+ def test_calculate_iou(self):
144
+ """Test IoU (Intersection over Union) calculation."""
145
+ # Identical boxes
146
+ bbox1 = [0, 0, 100, 100]
147
+ bbox2 = [0, 0, 100, 100]
148
+ iou = calculate_iou(bbox1, bbox2)
149
+ self.assertEqual(iou, 1.0)
150
+
151
+ # Non-overlapping boxes
152
+ bbox1 = [0, 0, 50, 50]
153
+ bbox2 = [100, 100, 150, 150]
154
+ iou = calculate_iou(bbox1, bbox2)
155
+ self.assertEqual(iou, 0.0)
156
+
157
+ # Partially overlapping boxes
158
+ bbox1 = [0, 0, 100, 100]
159
+ bbox2 = [50, 50, 150, 150]
160
+ iou = calculate_iou(bbox1, bbox2)
161
+ # Intersection: 50*50 = 2500, Union: 10000 + 10000 - 2500 = 17500
162
+ expected_iou = 2500 / 17500
163
+ self.assertAlmostEqual(iou, expected_iou, places=5)
164
+
165
+ def test_get_bbox_area(self):
166
+ """Test bounding box area calculation."""
167
+ # Rectangle
168
+ bbox = [0, 0, 100, 50]
169
+ area = get_bbox_area(bbox)
170
+ self.assertEqual(area, 5000)
171
+
172
+ # Square
173
+ bbox = [10, 10, 60, 60]
174
+ area = get_bbox_area(bbox)
175
+ self.assertEqual(area, 2500)
176
+
177
+ # Degenerate case (line)
178
+ bbox = [0, 0, 100, 0]
179
+ area = get_bbox_area(bbox)
180
+ self.assertEqual(area, 0)
181
+
182
+ # Point
183
+ bbox = [50, 50, 50, 50]
184
+ area = get_bbox_area(bbox)
185
+ self.assertEqual(area, 0)
186
+
187
+ def test_normalize_denormalize_bbox(self):
188
+ """Test bbox normalization and denormalization."""
189
+ bbox = [100, 200, 300, 400]
190
+ image_size = (640, 480)
191
+
192
+ # Normalize
193
+ normalized = normalize_bbox(bbox, image_size)
194
+ expected = [100/640, 200/480, 300/640, 400/480]
195
+ self.assertEqual(normalized, expected)
196
+
197
+ # Denormalize
198
+ denormalized = denormalize_bbox(normalized, image_size)
199
+ self.assertEqual(denormalized, bbox)
200
+
201
+ # Round trip test
202
+ original = [50, 75, 150, 225]
203
+ normalized = normalize_bbox(original, image_size)
204
+ denormalized = denormalize_bbox(normalized, image_size)
205
+ self.assertEqual(denormalized, original)
206
+
207
+ def test_line_segments_intersect(self):
208
+ """Test line segment intersection detection."""
209
+ # Intersecting lines
210
+ line1 = [[0, 0], [100, 100]]
211
+ line2 = [[0, 100], [100, 0]]
212
+ self.assertTrue(line_segments_intersect(line1, line2))
213
+
214
+ # Non-intersecting lines
215
+ line1 = [[0, 0], [50, 50]]
216
+ line2 = [[100, 100], [150, 150]]
217
+ self.assertFalse(line_segments_intersect(line1, line2))
218
+
219
+ # Parallel lines
220
+ line1 = [[0, 0], [100, 0]]
221
+ line2 = [[0, 50], [100, 50]]
222
+ self.assertFalse(line_segments_intersect(line1, line2))
223
+
224
+ # Touching at endpoint
225
+ line1 = [[0, 0], [50, 50]]
226
+ line2 = [[50, 50], [100, 100]]
227
+ self.assertTrue(line_segments_intersect(line1, line2))
228
+
229
+
230
+ class TestFormatUtils(BasePostProcessingTest):
231
+ """Test format utility functions."""
232
+
233
+ def test_convert_to_coco_format(self):
234
+ """Test conversion to COCO format."""
235
+ detections = [
236
+ {"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person"},
237
+ {"bbox": [100, 100, 150, 200], "confidence": 0.9, "category": "car"}
238
+ ]
239
+
240
+ coco_format = convert_to_coco_format(detections)
241
+
242
+ # Check structure
243
+ self.assertIn("annotations", coco_format)
244
+ self.assertIn("categories", coco_format)
245
+
246
+ # Check annotations
247
+ annotations = coco_format["annotations"]
248
+ self.assertEqual(len(annotations), 2)
249
+
250
+ # Check first annotation
251
+ ann = annotations[0]
252
+ self.assertEqual(ann["bbox"], [10, 20, 40, 40]) # COCO format: [x, y, width, height]
253
+ self.assertEqual(ann["score"], 0.8)
254
+ self.assertEqual(ann["area"], 1600) # 40 * 40
255
+
256
+ def test_convert_to_yolo_format(self):
257
+ """Test conversion to YOLO format."""
258
+ detections = [
259
+ {"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person"}
260
+ ]
261
+ image_size = (640, 480)
262
+
263
+ yolo_format = convert_to_yolo_format(detections, image_size)
264
+
265
+ self.assertEqual(len(yolo_format), 1)
266
+
267
+ yolo_det = yolo_format[0]
268
+ # YOLO format: [class, confidence, center_x_norm, center_y_norm, width_norm, height_norm]
269
+ self.assertEqual(yolo_det["class"], 0) # Assuming first category gets index 0
270
+ self.assertEqual(yolo_det["confidence"], 0.8)
271
+
272
+ # Check normalized coordinates
273
+ expected_center_x = (10 + 50) / 2 / 640 # 30 / 640
274
+ expected_center_y = (20 + 60) / 2 / 480 # 40 / 480
275
+ expected_width = 40 / 640
276
+ expected_height = 40 / 480
277
+
278
+ self.assertAlmostEqual(yolo_det["bbox"][0], expected_center_x, places=5)
279
+ self.assertAlmostEqual(yolo_det["bbox"][1], expected_center_y, places=5)
280
+ self.assertAlmostEqual(yolo_det["bbox"][2], expected_width, places=5)
281
+ self.assertAlmostEqual(yolo_det["bbox"][3], expected_height, places=5)
282
+
283
+ def test_convert_to_tracking_format(self):
284
+ """Test conversion to tracking format."""
285
+ detections = [
286
+ {"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person", "detection_id": 1}
287
+ ]
288
+
289
+ tracking_format = convert_to_tracking_format(detections)
290
+
291
+ self.assertEqual(len(tracking_format), 1)
292
+
293
+ track = tracking_format[0]
294
+ self.assertIn("track_id", track)
295
+ self.assertEqual(track["bbox"], [10, 20, 50, 60])
296
+ self.assertEqual(track["confidence"], 0.8)
297
+ self.assertEqual(track["category"], "person")
298
+
299
+ def test_match_results_structure(self):
300
+ """Test result structure matching."""
301
+ # Detection format
302
+ detections = [{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"}]
303
+ format_type = match_results_structure(detections)
304
+ self.assertEqual(format_type.value, "detection")
305
+
306
+ # Tracking format
307
+ tracks = [{"track_id": 1, "bbox": [0, 0, 100, 100], "confidence": 0.8, "frame": 1}]
308
+ format_type = match_results_structure(tracks)
309
+ self.assertEqual(format_type.value, "tracking")
310
+
311
+ # Empty data
312
+ format_type = match_results_structure([])
313
+ self.assertEqual(format_type.value, "unknown")
314
+
315
+
316
+ class TestFilterUtils(BasePostProcessingTest):
317
+ """Test filter utility functions."""
318
+
319
+ def test_filter_by_confidence(self):
320
+ """Test confidence filtering."""
321
+ detections = [
322
+ {"bbox": [0, 0, 100, 100], "confidence": 0.3, "category": "person"},
323
+ {"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "car"},
324
+ {"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "person"}
325
+ ]
326
+
327
+ # Filter with threshold 0.5
328
+ filtered = filter_by_confidence(detections, 0.5)
329
+ self.assertEqual(len(filtered), 2)
330
+
331
+ # Check that low confidence detection was removed
332
+ confidences = [det["confidence"] for det in filtered]
333
+ self.assertNotIn(0.3, confidences)
334
+ self.assertIn(0.7, confidences)
335
+ self.assertIn(0.9, confidences)
336
+
337
+ # Filter with high threshold
338
+ filtered = filter_by_confidence(detections, 0.8)
339
+ self.assertEqual(len(filtered), 1)
340
+ self.assertEqual(filtered[0]["confidence"], 0.9)
341
+
342
+ def test_filter_by_categories(self):
343
+ """Test category filtering."""
344
+ detections = [
345
+ {"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
346
+ {"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "car"},
347
+ {"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "bike"}
348
+ ]
349
+
350
+ # Filter for specific categories
351
+ filtered = filter_by_categories(detections, ["person", "bike"])
352
+ self.assertEqual(len(filtered), 2)
353
+
354
+ categories = [det["category"] for det in filtered]
355
+ self.assertIn("person", categories)
356
+ self.assertIn("bike", categories)
357
+ self.assertNotIn("car", categories)
358
+
359
+ # Filter for single category
360
+ filtered = filter_by_categories(detections, ["person"])
361
+ self.assertEqual(len(filtered), 1)
362
+ self.assertEqual(filtered[0]["category"], "person")
363
+
364
+ def test_filter_by_area(self):
365
+ """Test area-based filtering."""
366
+ detections = [
367
+ {"bbox": [0, 0, 10, 10], "confidence": 0.8, "category": "person"}, # Area: 100
368
+ {"bbox": [0, 0, 50, 50], "confidence": 0.8, "category": "person"}, # Area: 2500
369
+ {"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"} # Area: 10000
370
+ ]
371
+
372
+ # Filter by minimum area
373
+ filtered = filter_by_area(detections, min_area=1000)
374
+ self.assertEqual(len(filtered), 2) # Should exclude the smallest
375
+
376
+ # Filter by maximum area
377
+ filtered = filter_by_area(detections, max_area=5000)
378
+ self.assertEqual(len(filtered), 2) # Should exclude the largest
379
+
380
+ # Filter by area range
381
+ filtered = filter_by_area(detections, min_area=1000, max_area=5000)
382
+ self.assertEqual(len(filtered), 1) # Should only include middle one
383
+ self.assertEqual(get_bbox_area(filtered[0]["bbox"]), 2500)
384
+
385
+ def test_apply_category_mapping(self):
386
+ """Test category mapping application."""
387
+ detections = [
388
+ {"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": 0},
389
+ {"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": 1},
390
+ {"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": 2}
391
+ ]
392
+
393
+ # Apply mapping
394
+ mapping = {0: "person", 1: "car", 2: "bike"}
395
+ mapped = apply_category_mapping(detections, mapping)
396
+
397
+ self.assertEqual(len(mapped), 3)
398
+ categories = [det["category"] for det in mapped]
399
+ self.assertIn("person", categories)
400
+ self.assertIn("car", categories)
401
+ self.assertIn("bike", categories)
402
+
403
+ # Test with missing mapping
404
+ incomplete_mapping = {0: "person", 1: "car"}
405
+ mapped = apply_category_mapping(detections, incomplete_mapping)
406
+
407
+ # Should handle missing mapping gracefully
408
+ self.assertEqual(len(mapped), 3)
409
+ self.assertEqual(mapped[2]["category"], 2) # Unchanged
410
+
411
+ def test_remove_duplicate_detections(self):
412
+ """Test duplicate detection removal."""
413
+ detections = [
414
+ {"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
415
+ {"bbox": [5, 5, 105, 105], "confidence": 0.7, "category": "person"}, # Similar bbox
416
+ {"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "car"}
417
+ ]
418
+
419
+ # Remove duplicates with IoU threshold
420
+ unique = remove_duplicate_detections(detections, iou_threshold=0.5)
421
+
422
+ # Should remove one of the overlapping detections
423
+ self.assertLessEqual(len(unique), 2)
424
+
425
+ # Higher confidence detection should be kept
426
+ if len(unique) == 2:
427
+ confidences = [det["confidence"] for det in unique]
428
+ self.assertIn(0.8, confidences) # Higher confidence from overlapping pair
429
+ self.assertIn(0.9, confidences) # Non-overlapping detection
430
+
431
+
432
+ class TestCountingUtils(BasePostProcessingTest):
433
+ """Test counting utility functions."""
434
+
435
+ def test_count_objects_by_category(self):
436
+ """Test object counting by category."""
437
+ detections = [
438
+ {"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
439
+ {"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "person"},
440
+ {"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "car"},
441
+ {"bbox": [300, 300, 400, 400], "confidence": 0.6, "category": "bike"}
442
+ ]
443
+
444
+ counts = count_objects_by_category(detections)
445
+
446
+ self.assertEqual(counts["person"], 2)
447
+ self.assertEqual(counts["car"], 1)
448
+ self.assertEqual(counts["bike"], 1)
449
+ self.assertEqual(len(counts), 3)
450
+
451
+ def test_count_objects_in_zones(self):
452
+ """Test zone-based object counting."""
453
+ detections = [
454
+ {"bbox": [25, 25, 75, 75], "confidence": 0.8, "category": "person"}, # In zone1
455
+ {"bbox": [125, 25, 175, 75], "confidence": 0.7, "category": "person"}, # In zone2
456
+ {"bbox": [225, 225, 275, 275], "confidence": 0.9, "category": "car"} # Outside zones
457
+ ]
458
+
459
+ zones = {
460
+ "zone1": [[0, 0], [100, 0], [100, 100], [0, 100]],
461
+ "zone2": [[100, 0], [200, 0], [200, 100], [100, 100]]
462
+ }
463
+
464
+ zone_counts = count_objects_in_zones(detections, zones)
465
+
466
+ self.assertEqual(zone_counts["zone1"]["total"], 1)
467
+ self.assertEqual(zone_counts["zone2"]["total"], 1)
468
+ self.assertEqual(zone_counts["zone1"]["by_category"]["person"], 1)
469
+ self.assertEqual(zone_counts["zone2"]["by_category"]["person"], 1)
470
+
471
+ def test_count_unique_tracks(self):
472
+ """Test unique track counting."""
473
+ tracks = [
474
+ {"track_id": 1, "bbox": [0, 0, 100, 100], "confidence": 0.8, "frame": 1},
475
+ {"track_id": 1, "bbox": [10, 10, 110, 110], "confidence": 0.8, "frame": 2},
476
+ {"track_id": 2, "bbox": [200, 200, 300, 300], "confidence": 0.7, "frame": 1},
477
+ {"track_id": 2, "bbox": [210, 210, 310, 310], "confidence": 0.7, "frame": 2}
478
+ ]
479
+
480
+ unique_count = count_unique_tracks(tracks)
481
+ self.assertEqual(unique_count, 2)
482
+
483
+ # Test with categories
484
+ tracks_with_categories = [
485
+ {"track_id": 1, "category": "person", "frame": 1},
486
+ {"track_id": 2, "category": "person", "frame": 1},
487
+ {"track_id": 3, "category": "car", "frame": 1}
488
+ ]
489
+
490
+ category_counts = count_unique_tracks(tracks_with_categories, by_category=True)
491
+ self.assertEqual(category_counts["person"], 2)
492
+ self.assertEqual(category_counts["car"], 1)
493
+
494
+ def test_calculate_counting_summary(self):
495
+ """Test comprehensive counting summary."""
496
+ detections = [
497
+ {"bbox": [25, 25, 75, 75], "confidence": 0.8, "category": "person"},
498
+ {"bbox": [125, 25, 175, 75], "confidence": 0.7, "category": "person"},
499
+ {"bbox": [225, 225, 275, 275], "confidence": 0.9, "category": "car"}
500
+ ]
501
+
502
+ zones = {
503
+ "zone1": [[0, 0], [100, 0], [100, 100], [0, 100]]
504
+ }
505
+
506
+ summary = calculate_counting_summary(detections, zones=zones)
507
+
508
+ # Check summary structure
509
+ self.assertIn("total_count", summary)
510
+ self.assertIn("category_counts", summary)
511
+ self.assertIn("zone_analysis", summary)
512
+ self.assertIn("confidence_stats", summary)
513
+
514
+ # Check values
515
+ self.assertEqual(summary["total_count"], 3)
516
+ self.assertEqual(summary["category_counts"]["person"], 2)
517
+ self.assertEqual(summary["category_counts"]["car"], 1)
518
+
519
+
520
+ class TestTrackingUtils(BasePostProcessingTest):
521
+ """Test tracking utility functions."""
522
+
523
+ def test_track_objects_in_zone(self):
524
+ """Test zone-based object tracking."""
525
+ tracks = [
526
+ {"track_id": 1, "bbox": [25, 25, 75, 75], "frame": 1}, # In zone
527
+ {"track_id": 1, "bbox": [125, 125, 175, 175], "frame": 2}, # Outside zone
528
+ {"track_id": 2, "bbox": [50, 50, 100, 100], "frame": 1} # In zone
529
+ ]
530
+
531
+ zone = [[0, 0], [100, 0], [100, 100], [0, 100]]
532
+
533
+ zone_tracks = track_objects_in_zone(tracks, zone)
534
+
535
+ # Should track objects that entered the zone
536
+ self.assertGreater(len(zone_tracks), 0)
537
+
538
+ # Check that we have tracking information
539
+ for track_info in zone_tracks:
540
+ self.assertIn("track_id", track_info)
541
+ self.assertIn("enter_time", track_info)
542
+
543
+ def test_detect_line_crossings(self):
544
+ """Test line crossing detection."""
545
+ # Create tracks that cross a line
546
+ line = [[100, 0], [100, 200]]
547
+ tracks = create_line_crossing_data({"test_line": line}, num_tracks=3, frames=10)
548
+
549
+ crossings = detect_line_crossings(tracks, {"test_line": line})
550
+
551
+ # Should detect crossings
552
+ self.assertIn("test_line", crossings)
553
+ self.assertGreater(len(crossings["test_line"]), 0)
554
+
555
+ # Check crossing information
556
+ for crossing in crossings["test_line"]:
557
+ self.assertIn("track_id", crossing)
558
+ self.assertIn("crossing_frame", crossing)
559
+ self.assertIn("direction", crossing)
560
+
561
+ def test_analyze_track_movements(self):
562
+ """Test track movement analysis."""
563
+ tracks = [
564
+ {"track_id": 1, "bbox": [0, 0, 50, 50], "frame": 1, "timestamp": 1.0},
565
+ {"track_id": 1, "bbox": [10, 10, 60, 60], "frame": 2, "timestamp": 2.0},
566
+ {"track_id": 1, "bbox": [20, 20, 70, 70], "frame": 3, "timestamp": 3.0}
567
+ ]
568
+
569
+ movements = analyze_track_movements(tracks)
570
+
571
+ # Should have movement analysis for track 1
572
+ self.assertIn(1, movements)
573
+
574
+ track_movement = movements[1]
575
+ self.assertIn("total_distance", track_movement)
576
+ self.assertIn("average_speed", track_movement)
577
+ self.assertIn("direction_changes", track_movement)
578
+
579
+ # Check calculated values
580
+ self.assertGreater(track_movement["total_distance"], 0)
581
+ self.assertGreater(track_movement["average_speed"], 0)
582
+
583
+ def test_filter_tracks_by_duration(self):
584
+ """Test track filtering by duration."""
585
+ tracks = [
586
+ {"track_id": 1, "frame": 1, "timestamp": 1.0},
587
+ {"track_id": 1, "frame": 2, "timestamp": 2.0},
588
+ {"track_id": 1, "frame": 3, "timestamp": 3.0}, # Duration: 2 seconds
589
+ {"track_id": 2, "frame": 1, "timestamp": 1.0},
590
+ {"track_id": 2, "frame": 2, "timestamp": 6.0} # Duration: 5 seconds
591
+ ]
592
+
593
+ # Filter tracks with minimum duration
594
+ long_tracks = filter_tracks_by_duration(tracks, min_duration=3.0)
595
+
596
+ # Should only include track 2
597
+ track_ids = set(track["track_id"] for track in long_tracks)
598
+ self.assertIn(2, track_ids)
599
+ self.assertNotIn(1, track_ids)
600
+
601
+ # Filter tracks with maximum duration
602
+ short_tracks = filter_tracks_by_duration(tracks, max_duration=3.0)
603
+
604
+ # Should only include track 1
605
+ track_ids = set(track["track_id"] for track in short_tracks)
606
+ self.assertIn(1, track_ids)
607
+ self.assertNotIn(2, track_ids)
608
+
609
+
610
+ class TestUtilsIntegration(BasePostProcessingTest):
611
+ """Integration tests for utility functions."""
612
+
613
+ def test_detection_processing_pipeline(self):
614
+ """Test complete detection processing pipeline."""
615
+ # Create test data
616
+ detections = create_detection_results(
617
+ num_detections=20,
618
+ categories=["person", "car", "bike"],
619
+ confidence_range=(0.3, 0.95)
620
+ )
621
+
622
+ # Step 1: Filter by confidence
623
+ filtered = filter_by_confidence(detections, 0.5)
624
+
625
+ # Step 2: Apply category mapping
626
+ mapping = {"person": "pedestrian", "car": "vehicle", "bike": "bicycle"}
627
+ mapped = []
628
+ for det in filtered:
629
+ if det["category"] in mapping:
630
+ det_copy = det.copy()
631
+ det_copy["category"] = mapping[det["category"]]
632
+ mapped.append(det_copy)
633
+ else:
634
+ mapped.append(det)
635
+
636
+ # Step 3: Remove duplicates
637
+ unique = remove_duplicate_detections(mapped, iou_threshold=0.5)
638
+
639
+ # Step 4: Count by category
640
+ counts = count_objects_by_category(unique)
641
+
642
+ # Verify pipeline
643
+ self.assertLessEqual(len(unique), len(detections)) # Should filter some
644
+ self.assertGreater(len(counts), 0) # Should have categories
645
+
646
+ # Check that mapping worked
647
+ original_categories = set(det["category"] for det in detections)
648
+ final_categories = set(det["category"] for det in unique)
649
+
650
+ # Should have some mapped categories
651
+ if "person" in original_categories:
652
+ self.assertIn("pedestrian", final_categories)
653
+
654
+ def test_tracking_analysis_pipeline(self):
655
+ """Test complete tracking analysis pipeline."""
656
+ # Create tracking data
657
+ tracks = create_tracking_results(num_tracks=5, frames=15)
658
+
659
+ # Define zones and lines
660
+ zones = create_zone_polygons(["entrance", "lobby"])
661
+ lines = {"crossing_line": [[200, 0], [200, 400]]}
662
+
663
+ # Step 1: Filter by duration
664
+ long_tracks = filter_tracks_by_duration(tracks, min_duration=0.1)
665
+
666
+ # Step 2: Analyze movements
667
+ movements = analyze_track_movements(long_tracks)
668
+
669
+ # Step 3: Count in zones
670
+ zone_counts = count_objects_in_zones(long_tracks, zones)
671
+
672
+ # Step 4: Detect line crossings
673
+ crossings = detect_line_crossings(long_tracks, lines)
674
+
675
+ # Verify pipeline
676
+ self.assertGreater(len(movements), 0)
677
+ self.assertIn("entrance", zone_counts)
678
+ self.assertIn("lobby", zone_counts)
679
+ self.assertIn("crossing_line", crossings)
680
+
681
+ # Check movement analysis
682
+ for track_id, movement in movements.items():
683
+ self.assertIn("total_distance", movement)
684
+ self.assertIn("average_speed", movement)
685
+
686
+ def test_format_conversion_roundtrip(self):
687
+ """Test format conversion round trip."""
688
+ # Create detection data
689
+ original_detections = create_detection_results(5)
690
+
691
+ # Convert to COCO format
692
+ coco_format = convert_to_coco_format(original_detections)
693
+
694
+ # Convert to tracking format
695
+ tracking_format = convert_detection_to_tracking_format(original_detections)
696
+
697
+ # Convert back to detection format
698
+ back_to_detection = convert_tracking_to_detection_format(tracking_format)
699
+
700
+ # Verify conversions
701
+ self.assertEqual(len(back_to_detection), len(original_detections))
702
+
703
+ # Check that essential information is preserved
704
+ for orig, converted in zip(original_detections, back_to_detection):
705
+ self.assertEqual(orig["bbox"], converted["bbox"])
706
+ self.assertEqual(orig["confidence"], converted["confidence"])
707
+ self.assertEqual(orig["category"], converted["category"])
708
+
709
+ def test_edge_cases_handling(self):
710
+ """Test utility functions with edge cases."""
711
+ edge_cases = create_edge_case_data()
712
+
713
+ # Test with empty data
714
+ empty_data = edge_cases["empty_results"]
715
+
716
+ # All functions should handle empty data gracefully
717
+ self.assertEqual(filter_by_confidence(empty_data, 0.5), [])
718
+ self.assertEqual(count_objects_by_category(empty_data), {})
719
+ self.assertEqual(count_unique_tracks(empty_data), 0)
720
+
721
+ # Test with single detection
722
+ single_detection = edge_cases["single_detection"]
723
+
724
+ filtered = filter_by_confidence(single_detection, 0.3)
725
+ self.assertGreaterEqual(len(filtered), 0)
726
+
727
+ counts = count_objects_by_category(single_detection)
728
+ self.assertGreater(len(counts), 0)
729
+
730
+ # Test with boundary cases
731
+ boundary_data = edge_cases["boundary_bboxes"]
732
+
733
+ # Should handle boundary cases without errors
734
+ for bbox_data in boundary_data:
735
+ area = get_bbox_area(bbox_data["bbox"])
736
+ self.assertGreaterEqual(area, 0)
737
+
738
+ center = get_bbox_center(bbox_data["bbox"])
739
+ self.assertEqual(len(center), 2)
740
+
741
+
742
+ if __name__ == "__main__":
743
+ unittest.main()