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,524 @@
1
+ """
2
+ Comprehensive tests for the PostProcessor class.
3
+
4
+ This module tests the main PostProcessor functionality including:
5
+ - Simple processing interface
6
+ - Configuration-based processing
7
+ - File-based configuration
8
+ - Error handling
9
+ - Performance characteristics
10
+ """
11
+
12
+ import unittest
13
+ import json
14
+ import os
15
+ import time
16
+ from unittest.mock import patch, MagicMock
17
+
18
+ from .test_utilities import BasePostProcessingTest
19
+ from .test_data_generators import (
20
+ create_detection_results, create_tracking_results,
21
+ create_people_counting_config, create_customer_service_config,
22
+ create_edge_case_data, create_performance_test_data
23
+ )
24
+
25
+ from src.matrice_analytics.post_processing import (
26
+ PostProcessor, ProcessingStatus, ProcessingContext,
27
+ PeopleCountingConfig, CustomerServiceConfig, ConfigValidationError
28
+ )
29
+
30
+
31
+ class TestPostProcessor(BasePostProcessingTest):
32
+ """Test cases for PostProcessor class."""
33
+
34
+ def test_processor_initialization(self):
35
+ """Test that PostProcessor initializes correctly."""
36
+ processor = PostProcessor()
37
+
38
+ # Check that statistics are initialized
39
+ stats = processor.get_statistics()
40
+ self.assertIsInstance(stats, dict)
41
+ self.assertEqual(stats["total_processed"], 0)
42
+ self.assertEqual(stats["successful"], 0)
43
+ self.assertEqual(stats["failed"], 0)
44
+
45
+ # Check that use cases are available
46
+ use_cases = processor.list_available_usecases()
47
+ self.assertIsInstance(use_cases, dict)
48
+ self.assertIn("general", use_cases)
49
+ self.assertIn("sales", use_cases)
50
+
51
+ def test_simple_people_counting_processing(self):
52
+ """Test simple people counting processing."""
53
+ # Create test data
54
+ test_data = create_detection_results(
55
+ num_detections=10,
56
+ categories=["person", "car", "bicycle"]
57
+ )
58
+
59
+ # Process with simple interface
60
+ result = self.processor.process_simple(
61
+ test_data,
62
+ usecase="people_counting",
63
+ confidence_threshold=0.5,
64
+ enable_tracking=True
65
+ )
66
+
67
+ # Validate result
68
+ self.assert_processing_result_valid(
69
+ result,
70
+ expected_usecase="people_counting",
71
+ expected_category="general"
72
+ )
73
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
74
+ self.assert_insights_generated(result, min_insights=1)
75
+
76
+ # Check for expected metrics (using actual names from implementation)
77
+ expected_metrics = ["total_people", "processing_time", "input_format"]
78
+ self.assert_metrics_present(result, expected_metrics)
79
+
80
+ def test_simple_customer_service_processing(self):
81
+ """Test simple customer service processing."""
82
+ # Create test data with relevant categories
83
+ test_data = create_detection_results(
84
+ num_detections=15,
85
+ categories=["person", "staff", "customer"]
86
+ )
87
+
88
+ # Process with simple interface
89
+ result = self.processor.process_simple(
90
+ test_data,
91
+ usecase="customer_service",
92
+ confidence_threshold=0.6,
93
+ service_proximity_threshold=100.0
94
+ )
95
+
96
+ # Validate result (may return WARNING status for some scenarios)
97
+ self.assert_processing_result_valid(
98
+ result,
99
+ expected_usecase="customer_service",
100
+ expected_category="sales"
101
+ )
102
+ self.assertIn(result.status, [ProcessingStatus.SUCCESS, ProcessingStatus.WARNING])
103
+ self.assert_insights_generated(result, min_insights=1)
104
+
105
+ def test_configuration_based_processing(self):
106
+ """Test processing with configuration objects."""
107
+ # Create test data
108
+ test_data = create_detection_results(num_detections=8)
109
+
110
+ # Create configuration
111
+ config = create_people_counting_config(
112
+ confidence_threshold=0.7,
113
+ enable_tracking=True,
114
+ include_zones=True,
115
+ include_alerts=True
116
+ )
117
+
118
+ # Process with configuration
119
+ result = self.processor.process(test_data, config)
120
+
121
+ # Validate result
122
+ self.assert_processing_result_valid(
123
+ result,
124
+ expected_usecase="people_counting",
125
+ expected_category="general"
126
+ )
127
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
128
+
129
+ # Check that zone analysis was performed (using actual metric names)
130
+ if result.metrics.get("zones_analyzed", 0) > 0:
131
+ self.assertIn("zone_metrics", result.metrics)
132
+
133
+ def test_file_based_configuration(self):
134
+ """Test processing with configuration files."""
135
+ # Create test data
136
+ test_data = create_detection_results(num_detections=5)
137
+
138
+ # Create configuration
139
+ config = create_people_counting_config(confidence_threshold=0.6)
140
+
141
+ # Save configuration to file
142
+ config_file = self.create_temp_config_file(config, "people_counting_test.json")
143
+
144
+ # Process from file
145
+ result = self.processor.process_from_file(test_data, config_file)
146
+
147
+ # Validate result
148
+ self.assert_processing_result_valid(
149
+ result,
150
+ expected_usecase="people_counting",
151
+ expected_category="general"
152
+ )
153
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
154
+
155
+ def test_invalid_configuration_handling(self):
156
+ """Test handling of invalid configurations."""
157
+ test_data = create_detection_results(num_detections=3)
158
+
159
+ # Test with invalid confidence threshold
160
+ config = PeopleCountingConfig(
161
+ category="general",
162
+ usecase="people_counting",
163
+ confidence_threshold=1.5 # Invalid: >1.0
164
+ )
165
+
166
+ result = self.processor.process(test_data, config)
167
+
168
+ # Should handle gracefully
169
+ self.assertIsInstance(result.status, ProcessingStatus)
170
+ if result.status == ProcessingStatus.ERROR:
171
+ self.assertIsNotNone(result.error_message)
172
+
173
+ def test_unknown_usecase_handling(self):
174
+ """Test handling of unknown use cases."""
175
+ test_data = create_detection_results(num_detections=3)
176
+
177
+ # Try to process with unknown use case
178
+ result = self.processor.process_simple(
179
+ test_data,
180
+ usecase="unknown_usecase",
181
+ confidence_threshold=0.5
182
+ )
183
+
184
+ # Should return error result
185
+ self.assertEqual(result.status, ProcessingStatus.ERROR)
186
+ self.assertIsNotNone(result.error_message)
187
+ # Check for appropriate error message
188
+ self.assertTrue(
189
+ "unknown" in result.error_message.lower() or
190
+ "not found" in result.error_message.lower()
191
+ )
192
+
193
+ def test_empty_data_handling(self):
194
+ """Test handling of empty input data."""
195
+ # Test with empty list
196
+ result = self.processor.process_simple(
197
+ [],
198
+ usecase="people_counting",
199
+ confidence_threshold=0.5
200
+ )
201
+
202
+ # Should handle gracefully
203
+ self.assertIsInstance(result.status, ProcessingStatus)
204
+ if result.status == ProcessingStatus.SUCCESS:
205
+ # Check for appropriate message indicating no data
206
+ self.assertTrue(
207
+ "no people" in result.summary.lower() or
208
+ "no detections" in result.summary.lower() or
209
+ "empty" in result.summary.lower()
210
+ )
211
+
212
+ def test_malformed_data_handling(self):
213
+ """Test handling of malformed input data."""
214
+ # Test with malformed detections
215
+ malformed_data = create_edge_case_data()
216
+
217
+ result = self.processor.process_simple(
218
+ malformed_data,
219
+ usecase="people_counting",
220
+ confidence_threshold=0.5
221
+ )
222
+
223
+ # Should handle gracefully - either success with warnings or error
224
+ self.assertIsInstance(result.status, ProcessingStatus)
225
+ if result.status == ProcessingStatus.WARNING:
226
+ self.assertTrue(len(result.warnings) > 0)
227
+
228
+ def test_context_propagation(self):
229
+ """Test that processing context is properly propagated."""
230
+ test_data = create_detection_results(num_detections=3)
231
+
232
+ # Create context with specific parameters
233
+ context = ProcessingContext(
234
+ confidence_threshold=0.8,
235
+ enable_tracking=True,
236
+ enable_analytics=True,
237
+ metadata={"source": "test_camera", "location": "test_location"}
238
+ )
239
+
240
+ # Process with context
241
+ result = self.processor.process_simple(
242
+ test_data,
243
+ usecase="people_counting",
244
+ confidence_threshold=0.8,
245
+ context=context
246
+ )
247
+
248
+ # Check context propagation
249
+ self.assertIsNotNone(result.context)
250
+ self.assertEqual(result.context.confidence_threshold, 0.8)
251
+ self.assertTrue(result.context.enable_tracking)
252
+ self.assertTrue(result.context.enable_analytics)
253
+ self.assertEqual(result.context.metadata.get("source"), "test_camera")
254
+ self.assertEqual(result.context.metadata.get("location"), "test_location")
255
+
256
+ def test_statistics_tracking(self):
257
+ """Test that processing statistics are tracked correctly."""
258
+ # Get initial statistics
259
+ initial_stats = self.processor.get_statistics()
260
+
261
+ # Process some data
262
+ test_data = create_detection_results(num_detections=3)
263
+
264
+ # Successful processing
265
+ result1 = self.processor.process_simple(
266
+ test_data,
267
+ usecase="people_counting",
268
+ confidence_threshold=0.5
269
+ )
270
+
271
+ # Check intermediate statistics
272
+ intermediate_stats = self.processor.get_statistics()
273
+ self.assertEqual(intermediate_stats["total_processed"], initial_stats["total_processed"] + 1)
274
+ self.assertEqual(intermediate_stats["successful"], initial_stats["successful"] + 1)
275
+
276
+ # Failed processing (unknown use case) - this should not increment counters
277
+ try:
278
+ result2 = self.processor.process_simple(
279
+ test_data,
280
+ usecase="unknown_usecase"
281
+ )
282
+ except Exception:
283
+ pass # Expected to fail
284
+
285
+ # Check final statistics - failed request shouldn't be counted
286
+ final_stats = self.processor.get_statistics()
287
+ self.assertEqual(final_stats["total_processed"], initial_stats["total_processed"] + 1)
288
+ self.assertEqual(final_stats["successful"], initial_stats["successful"] + 1)
289
+
290
+ def test_config_template_generation(self):
291
+ """Test configuration template generation."""
292
+ # Test people counting template
293
+ template = self.processor.get_config_template("people_counting")
294
+
295
+ self.assertIsInstance(template, dict)
296
+ self.assertIn("confidence_threshold", template)
297
+ self.assertIn("enable_tracking", template)
298
+ self.assertIn("person_categories", template)
299
+
300
+ # Test customer service template
301
+ template = self.processor.get_config_template("customer_service")
302
+
303
+ self.assertIsInstance(template, dict)
304
+ self.assertIn("service_proximity_threshold", template)
305
+ self.assertIn("staff_categories", template)
306
+ self.assertIn("customer_categories", template)
307
+
308
+ def test_config_validation(self):
309
+ """Test configuration validation."""
310
+ # Valid configuration
311
+ valid_config = create_people_counting_config(confidence_threshold=0.5)
312
+ errors = self.processor.validate_config(valid_config)
313
+ self.assertEqual(len(errors), 0)
314
+
315
+ # Invalid configuration
316
+ invalid_config = PeopleCountingConfig(
317
+ category="general",
318
+ usecase="people_counting",
319
+ confidence_threshold=2.0, # Invalid
320
+ time_window_minutes=-5 # Invalid
321
+ )
322
+ errors = self.processor.validate_config(invalid_config)
323
+ self.assertGreater(len(errors), 0)
324
+
325
+ def test_available_usecases_listing(self):
326
+ """Test listing of available use cases."""
327
+ use_cases = self.processor.list_available_usecases()
328
+
329
+ self.assertIsInstance(use_cases, dict)
330
+
331
+ # Check expected categories and use cases
332
+ self.assertIn("general", use_cases)
333
+ self.assertIn("people_counting", use_cases["general"])
334
+
335
+ self.assertIn("sales", use_cases)
336
+ self.assertIn("customer_service", use_cases["sales"])
337
+
338
+ def test_supported_usecases_listing(self):
339
+ """Test getting list of supported use case names."""
340
+ supported = self.processor.get_supported_usecases()
341
+
342
+ self.assertIsInstance(supported, list)
343
+ self.assertIn("people_counting", supported)
344
+ self.assertIn("customer_service", supported)
345
+
346
+ def test_processing_with_tracking_data(self):
347
+ """Test processing with tracking format data."""
348
+ # Create tracking data
349
+ tracking_data = create_tracking_results(
350
+ num_tracks=5,
351
+ categories=["person", "car"]
352
+ )
353
+
354
+ result = self.processor.process_simple(
355
+ tracking_data,
356
+ usecase="people_counting",
357
+ confidence_threshold=0.5,
358
+ enable_tracking=True
359
+ )
360
+
361
+ # Should handle tracking data format
362
+ self.assert_processing_result_valid(result)
363
+
364
+ # Should have basic metrics
365
+ self.assertIn("total_people", result.metrics)
366
+ self.assertIn("processing_time", result.metrics)
367
+
368
+ def test_config_save_and_load(self):
369
+ """Test saving and loading configurations."""
370
+ # Create configuration
371
+ original_config = create_customer_service_config(
372
+ confidence_threshold=0.7,
373
+ service_proximity_threshold=150.0
374
+ )
375
+
376
+ # Save configuration
377
+ config_file = os.path.join(self.temp_dir, "customer_service_config.json")
378
+ self.processor.save_config(original_config, config_file)
379
+
380
+ # Load configuration
381
+ loaded_config = self.processor.load_config(config_file)
382
+
383
+ # Compare configurations
384
+ self.assertEqual(original_config.usecase, loaded_config.usecase)
385
+ self.assertEqual(original_config.category, loaded_config.category)
386
+ self.assertEqual(original_config.confidence_threshold, loaded_config.confidence_threshold)
387
+ self.assertEqual(original_config.service_proximity_threshold, loaded_config.service_proximity_threshold)
388
+
389
+ def test_reset_statistics(self):
390
+ """Test resetting processing statistics."""
391
+ # Process some data to generate statistics
392
+ test_data = create_detection_results(num_detections=3)
393
+ self.processor.process_simple(test_data, usecase="people_counting")
394
+
395
+ # Check that statistics exist
396
+ stats_before = self.processor.get_statistics()
397
+ self.assertGreater(stats_before["total_processed"], 0)
398
+
399
+ # Reset statistics
400
+ self.processor.reset_statistics()
401
+
402
+ # Check that statistics are reset
403
+ stats_after = self.processor.get_statistics()
404
+ self.assertEqual(stats_after["total_processed"], 0)
405
+ self.assertEqual(stats_after["successful"], 0)
406
+ self.assertEqual(stats_after["failed"], 0)
407
+ self.assertEqual(stats_after["total_processing_time"], 0.0)
408
+
409
+
410
+ class TestPostProcessorPerformance(BasePostProcessingTest):
411
+ """Performance tests for PostProcessor."""
412
+
413
+ def test_large_dataset_processing(self):
414
+ """Test processing performance with large datasets."""
415
+ # Create large dataset
416
+ large_data = create_performance_test_data(size=1000)
417
+
418
+ # Measure processing time
419
+ result, processing_time = self.measure_processing_time(
420
+ self.processor.process_simple,
421
+ large_data,
422
+ usecase="people_counting",
423
+ confidence_threshold=0.5
424
+ )
425
+
426
+ # Check that processing completed successfully
427
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
428
+
429
+ # Check performance (should complete within reasonable time)
430
+ self.assert_performance_acceptable(processing_time, max_time=5.0)
431
+
432
+ # Check basic metrics are present
433
+ self.assertIn("total_people", result.metrics)
434
+ self.assertIn("processing_time", result.metrics)
435
+
436
+ def test_batch_processing_performance(self):
437
+ """Test performance with multiple batch processing calls."""
438
+ # Create multiple small batches
439
+ batches = [create_detection_results(num_detections=50) for _ in range(10)]
440
+
441
+ total_start_time = time.time()
442
+ results = []
443
+
444
+ for batch in batches:
445
+ result = self.processor.process_simple(
446
+ batch,
447
+ usecase="people_counting",
448
+ confidence_threshold=0.5
449
+ )
450
+ results.append(result)
451
+
452
+ total_processing_time = time.time() - total_start_time
453
+
454
+ # Check that all batches processed successfully
455
+ for result in results:
456
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
457
+
458
+ # Check overall performance
459
+ self.assert_performance_acceptable(total_processing_time, max_time=10.0)
460
+
461
+ # Check statistics
462
+ stats = self.processor.get_statistics()
463
+ self.assertEqual(stats["total_processed"], len(batches))
464
+ self.assertEqual(stats["successful"], len(batches))
465
+
466
+ def test_memory_usage_stability(self):
467
+ """Test that memory usage remains stable during processing."""
468
+ import psutil
469
+ import os
470
+
471
+ # Get initial memory usage
472
+ process = psutil.Process(os.getpid())
473
+ initial_memory = process.memory_info().rss
474
+
475
+ # Process multiple datasets
476
+ for i in range(20):
477
+ test_data = create_detection_results(num_detections=100)
478
+ result = self.processor.process_simple(
479
+ test_data,
480
+ usecase="people_counting",
481
+ confidence_threshold=0.5
482
+ )
483
+ self.assertEqual(result.status, ProcessingStatus.SUCCESS)
484
+
485
+ # Check final memory usage
486
+ final_memory = process.memory_info().rss
487
+ memory_increase = final_memory - initial_memory
488
+
489
+ # Memory increase should be reasonable (less than 100MB)
490
+ max_memory_increase = 100 * 1024 * 1024 # 100MB
491
+ self.assertLess(memory_increase, max_memory_increase,
492
+ f"Memory increased by {memory_increase / 1024 / 1024:.2f}MB")
493
+
494
+
495
+ if __name__ == "__main__":
496
+ import time
497
+
498
+ # Create test suite
499
+ suite = unittest.TestSuite()
500
+
501
+ # Add test cases
502
+ suite.addTest(unittest.makeSuite(TestPostProcessor))
503
+ suite.addTest(unittest.makeSuite(TestPostProcessorPerformance))
504
+
505
+ # Run tests
506
+ runner = unittest.TextTestRunner(verbosity=2)
507
+ start_time = time.time()
508
+ result = runner.run(suite)
509
+ end_time = time.time()
510
+
511
+ print(f"\nTest execution completed in {end_time - start_time:.2f} seconds")
512
+ print(f"Tests run: {result.testsRun}")
513
+ print(f"Failures: {len(result.failures)}")
514
+ print(f"Errors: {len(result.errors)}")
515
+
516
+ if result.failures:
517
+ print("\nFailures:")
518
+ for test, traceback in result.failures:
519
+ print(f"- {test}: {traceback}")
520
+
521
+ if result.errors:
522
+ print("\nErrors:")
523
+ for test, traceback in result.errors:
524
+ print(f"- {test}: {traceback}")
@@ -0,0 +1,165 @@
1
+ import os
2
+ import sys
3
+ import cv2
4
+ import json
5
+ import importlib
6
+ import argparse
7
+ from ultralytics import YOLO
8
+ from src.matrice_analytics.post_processing.core.base import ProcessingContext
9
+
10
+
11
+ class UseCaseTestProcessor:
12
+ """
13
+ A flexible YOLO-based video processor for testing different post-processing use cases.
14
+ """
15
+
16
+ def __init__(self, file_name, config_name, usecase_name, model_path, video_path, post_process=None, max_frames=None):
17
+ self.file_name = file_name
18
+ self.config_name = config_name
19
+ self.usecase_name = usecase_name
20
+ self.model_path = model_path
21
+ self.video_path = video_path
22
+ self.post_process = post_process
23
+ self.max_frames = max_frames
24
+ self.json_dir = "jsons"
25
+
26
+ self._setup_environment()
27
+ self.ConfigClass, self.UsecaseClass = self._load_usecase()
28
+ self.config = self._initialize_config()
29
+ self.processor = self.UsecaseClass()
30
+ self.model = YOLO(self.model_path)
31
+ os.makedirs(self.json_dir, exist_ok=True)
32
+
33
+ def _setup_environment(self):
34
+ """Ensure project root is added to sys.path."""
35
+ project_root = os.path.abspath("/content/py_analytics")
36
+ if project_root not in sys.path:
37
+ sys.path.append(project_root)
38
+
39
+ def _load_usecase(self):
40
+ """Dynamically import config and usecase classes."""
41
+ module_path = f"src.matrice_analytics.post_processing.usecases.{self.file_name}"
42
+ module = importlib.import_module(module_path)
43
+ return getattr(module, self.config_name), getattr(module, self.usecase_name)
44
+
45
+ def _initialize_config(self):
46
+ """Initialize config object, applying overrides if provided."""
47
+ if self.post_process:
48
+ return self.ConfigClass(**self.post_process)
49
+ return self.ConfigClass()
50
+
51
+ def _serialize_result(self, result):
52
+ """Convert result object into JSON-serializable dict."""
53
+ def to_serializable(obj):
54
+ if hasattr(obj, "to_dict"):
55
+ return obj.to_dict()
56
+ if hasattr(obj, "__dict__"):
57
+ return obj.__dict__
58
+ return str(obj)
59
+ return json.loads(json.dumps(result, default=to_serializable))
60
+
61
+
62
+ def process_video(self):
63
+ """Run YOLO inference on video and post-process frame by frame."""
64
+ cap = cv2.VideoCapture(self.video_path)
65
+ if not cap.isOpened():
66
+ raise ValueError(f"Failed to open video at {self.video_path}")
67
+
68
+ frame_idx = 0
69
+ stream_info = {
70
+ 'input_settings': {
71
+ 'start_frame': 0,
72
+ 'original_fps': cap.get(cv2.CAP_PROP_FPS),
73
+ 'camera_info': {'id': 'cam1', 'name': 'Test Camera'}
74
+ }
75
+ }
76
+
77
+ print(f"\nStarting video processing: {self.video_path}")
78
+ print(f"Model: {self.model_path}")
79
+ print(f"Output directory: {self.json_dir}\n")
80
+
81
+ while cap.isOpened():
82
+ ret, frame = cap.read()
83
+ if not ret:
84
+ break
85
+
86
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
87
+ results = self.model(frame_rgb)
88
+
89
+ detections = []
90
+ for xyxy, conf, cls in zip(results[0].boxes.xyxy, results[0].boxes.conf, results[0].boxes.cls):
91
+ x1, y1, x2, y2 = xyxy.tolist()
92
+ detections.append({
93
+ 'category_id': int(cls),
94
+ 'confidence': conf.item(),
95
+ 'bounding_box': {
96
+ 'xmin': int(x1),
97
+ 'ymin': int(y1),
98
+ 'xmax': int(x2),
99
+ 'ymax': int(y2)
100
+ }
101
+ })
102
+
103
+ success, encoded_image = cv2.imencode(".jpg", frame)
104
+ input_bytes = encoded_image.tobytes() if success else None
105
+
106
+ try:
107
+ result = self.processor.process(
108
+ detections, self.config, input_bytes, ProcessingContext(), stream_info
109
+ )
110
+ except TypeError:
111
+ result = self.processor.process(
112
+ detections, self.config, ProcessingContext(), stream_info
113
+ )
114
+
115
+ json_path = os.path.join(self.json_dir, f"frame_{frame_idx:04d}.json")
116
+ with open(json_path, "w") as f:
117
+ json.dump(self._serialize_result(result), f, indent=2)
118
+
119
+ print(f"Frame {frame_idx} processed — detections: {len(detections)} — saved: {json_path}")
120
+
121
+ frame_idx += 1
122
+ stream_info['input_settings']['start_frame'] += 1
123
+
124
+ if self.max_frames and frame_idx >= self.max_frames:
125
+ print(f"\nMax frame limit ({self.max_frames}) reached.")
126
+ break
127
+
128
+ cap.release()
129
+ print(f"\nProcessing complete. JSON outputs saved in: {self.json_dir}")
130
+
131
+
132
+ def main():
133
+ parser = argparse.ArgumentParser(description="YOLO Use Case Test Processor")
134
+
135
+ parser.add_argument("--file_name", type=str, required=True,
136
+ help="Usecase file name under src/matrice_analytics/post_processing/usecases/")
137
+ parser.add_argument("--config_name", type=str, required=True,
138
+ help="Config class name (e.g., PeopleCountingConfig)")
139
+ parser.add_argument("--usecase_name", type=str, required=True,
140
+ help="Use case class name (e.g., PeopleCountingUseCase)")
141
+ parser.add_argument("--model_path", type=str, required=True,
142
+ help="Path to YOLO model file (.pt)")
143
+ parser.add_argument("--video_path", type=str, required=True,
144
+ help="Path to input video")
145
+ parser.add_argument("--post_process", type=json.loads, default=None,
146
+ help="JSON string for config overrides, e.g. '{\"min_confidence\": 0.5}'")
147
+ parser.add_argument("--max_frames", type=int, default=None,
148
+ help="Limit number of frames processed")
149
+
150
+ args = parser.parse_args()
151
+
152
+ processor = UseCaseTestProcessor(
153
+ file_name=args.file_name,
154
+ config_name=args.config_name,
155
+ usecase_name=args.usecase_name,
156
+ model_path=args.model_path,
157
+ video_path=args.video_path,
158
+ post_process=args.post_process,
159
+ max_frames=args.max_frames
160
+ )
161
+ processor.process_video()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()