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.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +146 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3291 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +660 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for the People Counting use case.
|
|
3
|
+
|
|
4
|
+
This module tests detailed functionality for people counting including:
|
|
5
|
+
- Zone-based analysis
|
|
6
|
+
- Tracking integration
|
|
7
|
+
- Alert generation
|
|
8
|
+
- Configuration validation
|
|
9
|
+
- Performance characteristics
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from unittest.mock import patch, MagicMock
|
|
16
|
+
|
|
17
|
+
from .test_utilities import BasePostProcessingTest
|
|
18
|
+
from .test_data_generators import (
|
|
19
|
+
create_detection_results, create_tracking_results,
|
|
20
|
+
create_people_counting_config, create_zone_polygons,
|
|
21
|
+
create_performance_test_data, create_edge_case_data
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from src.matrice_analytics.post_processing import (
|
|
25
|
+
PostProcessor, ProcessingStatus, PeopleCountingConfig,
|
|
26
|
+
ZoneConfig, AlertConfig
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestPeopleCountingUseCase(BasePostProcessingTest):
|
|
31
|
+
"""Test cases for people counting use case."""
|
|
32
|
+
|
|
33
|
+
def test_basic_people_counting(self):
|
|
34
|
+
"""Test basic people counting without zones."""
|
|
35
|
+
# Create test data with people and other objects
|
|
36
|
+
test_data = create_detection_results(
|
|
37
|
+
num_detections=20,
|
|
38
|
+
categories=["person", "car", "bicycle", "dog"]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
result = self.processor.process_simple(
|
|
42
|
+
test_data,
|
|
43
|
+
usecase="people_counting",
|
|
44
|
+
confidence_threshold=0.5,
|
|
45
|
+
person_categories=["person"]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Validate basic result structure
|
|
49
|
+
self.assert_processing_result_valid(result, expected_usecase="people_counting")
|
|
50
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
51
|
+
|
|
52
|
+
# Check for expected metrics (using actual metric names from implementation)
|
|
53
|
+
self.assertIn("total_people", result.metrics)
|
|
54
|
+
self.assertIn("processing_time", result.metrics)
|
|
55
|
+
self.assertIn("input_format", result.metrics)
|
|
56
|
+
self.assertIn("confidence_threshold", result.metrics)
|
|
57
|
+
|
|
58
|
+
# Check zone-based metrics (using actual zone metric structure)
|
|
59
|
+
if result.metrics.get("zones_analyzed", 0) > 0:
|
|
60
|
+
self.assertIn("zone_metrics", result.metrics)
|
|
61
|
+
|
|
62
|
+
# Check people count using correct metric name
|
|
63
|
+
self.assertGreaterEqual(result.metrics["total_people"], 0)
|
|
64
|
+
|
|
65
|
+
# Check that insights were generated
|
|
66
|
+
self.assert_insights_generated(result, min_insights=1)
|
|
67
|
+
|
|
68
|
+
# Verify people count is reasonable
|
|
69
|
+
people_count = result.metrics["total_people"]
|
|
70
|
+
self.assertIsInstance(people_count, int)
|
|
71
|
+
self.assertGreaterEqual(people_count, 0)
|
|
72
|
+
|
|
73
|
+
def test_people_counting_with_zones(self):
|
|
74
|
+
"""Test people counting with zone-based analysis."""
|
|
75
|
+
# Create test data
|
|
76
|
+
test_data = create_detection_results(
|
|
77
|
+
num_detections=15,
|
|
78
|
+
categories=["person", "people"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Create configuration with zones
|
|
82
|
+
config = create_people_counting_config(
|
|
83
|
+
confidence_threshold=0.6,
|
|
84
|
+
include_zones=True,
|
|
85
|
+
include_alerts=False
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
result = self.processor.process(test_data, config)
|
|
89
|
+
|
|
90
|
+
# Validate result
|
|
91
|
+
self.assert_processing_result_valid(result)
|
|
92
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
93
|
+
|
|
94
|
+
# Check that zone analysis was performed
|
|
95
|
+
self.assertIn("zone_metrics", result.metrics)
|
|
96
|
+
self.assertIn("zones_analyzed", result.metrics)
|
|
97
|
+
self.assertGreater(result.metrics["zones_analyzed"], 0)
|
|
98
|
+
|
|
99
|
+
def test_people_counting_with_tracking(self):
|
|
100
|
+
"""Test people counting with tracking enabled."""
|
|
101
|
+
# Create tracking data
|
|
102
|
+
tracking_data = create_tracking_results(
|
|
103
|
+
num_tracks=8,
|
|
104
|
+
categories=["person", "car"]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result = self.processor.process_simple(
|
|
108
|
+
tracking_data,
|
|
109
|
+
usecase="people_counting",
|
|
110
|
+
confidence_threshold=0.5,
|
|
111
|
+
enable_tracking=True,
|
|
112
|
+
enable_unique_counting=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Validate result
|
|
116
|
+
self.assert_processing_result_valid(result)
|
|
117
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
118
|
+
|
|
119
|
+
# Check basic tracking metrics
|
|
120
|
+
self.assertIn("total_people", result.metrics)
|
|
121
|
+
self.assertIn("processing_time", result.metrics)
|
|
122
|
+
|
|
123
|
+
def test_people_counting_with_alerts(self):
|
|
124
|
+
"""Test people counting with alert generation."""
|
|
125
|
+
# Create test data with many people
|
|
126
|
+
test_data = create_detection_results(
|
|
127
|
+
num_detections=25,
|
|
128
|
+
categories=["person"] * 20 + ["car"] * 5 # 20 people, 5 cars
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Create configuration with low alert thresholds
|
|
132
|
+
config = create_people_counting_config(
|
|
133
|
+
confidence_threshold=0.5,
|
|
134
|
+
include_alerts=True
|
|
135
|
+
)
|
|
136
|
+
# Set low thresholds to trigger alerts
|
|
137
|
+
config.alert_config.count_thresholds = {"person": 5, "all": 10}
|
|
138
|
+
config.alert_config.occupancy_thresholds = {"entrance": 3}
|
|
139
|
+
|
|
140
|
+
result = self.processor.process(test_data, config)
|
|
141
|
+
|
|
142
|
+
# Validate result
|
|
143
|
+
self.assert_processing_result_valid(result)
|
|
144
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
145
|
+
|
|
146
|
+
# Check basic people counting metrics
|
|
147
|
+
self.assertIn("total_people", result.metrics)
|
|
148
|
+
self.assertGreater(result.metrics["total_people"], 0)
|
|
149
|
+
|
|
150
|
+
def test_people_counting_confidence_filtering(self):
|
|
151
|
+
"""Test that confidence filtering works correctly."""
|
|
152
|
+
# Create test data with mixed confidence levels
|
|
153
|
+
test_data = []
|
|
154
|
+
|
|
155
|
+
# High confidence people (should be included)
|
|
156
|
+
for i in range(5):
|
|
157
|
+
test_data.append({
|
|
158
|
+
"bbox": [100 + i*50, 100, 140 + i*50, 200],
|
|
159
|
+
"confidence": 0.9,
|
|
160
|
+
"category": "person",
|
|
161
|
+
"category_id": 0
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# Low confidence people (should be filtered out)
|
|
165
|
+
for i in range(5):
|
|
166
|
+
test_data.append({
|
|
167
|
+
"bbox": [100 + i*50, 300, 140 + i*50, 400],
|
|
168
|
+
"confidence": 0.2,
|
|
169
|
+
"category": "person",
|
|
170
|
+
"category_id": 0
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
result = self.processor.process_simple(
|
|
174
|
+
test_data,
|
|
175
|
+
usecase="people_counting",
|
|
176
|
+
confidence_threshold=0.5
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Should only count high confidence detections
|
|
180
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
181
|
+
self.assertEqual(result.metrics["total_people"], 5)
|
|
182
|
+
|
|
183
|
+
def test_people_counting_category_mapping(self):
|
|
184
|
+
"""Test people counting with category mapping."""
|
|
185
|
+
# Create test data with different person categories
|
|
186
|
+
test_data = []
|
|
187
|
+
categories = ["person", "people", "human", "pedestrian"]
|
|
188
|
+
|
|
189
|
+
for i, category in enumerate(categories):
|
|
190
|
+
for j in range(3): # 3 detections per category
|
|
191
|
+
test_data.append({
|
|
192
|
+
"bbox": [100 + j*50, 100 + i*100, 140 + j*50, 180 + i*100],
|
|
193
|
+
"confidence": 0.8,
|
|
194
|
+
"category": category,
|
|
195
|
+
"category_id": i
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
result = self.processor.process_simple(
|
|
199
|
+
test_data,
|
|
200
|
+
usecase="people_counting",
|
|
201
|
+
confidence_threshold=0.5,
|
|
202
|
+
person_categories=["person", "people", "human", "pedestrian"]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Should count all categories as people
|
|
206
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
207
|
+
self.assertEqual(result.metrics["total_people"], 12) # 4 categories * 3 detections
|
|
208
|
+
|
|
209
|
+
def test_people_counting_empty_data(self):
|
|
210
|
+
"""Test people counting with empty input data."""
|
|
211
|
+
result = self.processor.process_simple(
|
|
212
|
+
[],
|
|
213
|
+
usecase="people_counting",
|
|
214
|
+
confidence_threshold=0.5
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Should handle empty data gracefully
|
|
218
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
219
|
+
self.assertEqual(result.metrics["total_people"], 0)
|
|
220
|
+
|
|
221
|
+
def test_people_counting_config_validation(self):
|
|
222
|
+
"""Test people counting configuration validation."""
|
|
223
|
+
# Valid configuration
|
|
224
|
+
valid_config = PeopleCountingConfig(
|
|
225
|
+
category="general",
|
|
226
|
+
usecase="people_counting",
|
|
227
|
+
confidence_threshold=0.5,
|
|
228
|
+
time_window_minutes=60,
|
|
229
|
+
person_categories=["person"]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
errors = valid_config.validate()
|
|
233
|
+
self.assertEqual(len(errors), 0)
|
|
234
|
+
|
|
235
|
+
# Invalid configurations
|
|
236
|
+
invalid_configs = [
|
|
237
|
+
# Invalid confidence threshold
|
|
238
|
+
PeopleCountingConfig(
|
|
239
|
+
category="general",
|
|
240
|
+
usecase="people_counting",
|
|
241
|
+
confidence_threshold=1.5
|
|
242
|
+
),
|
|
243
|
+
# Invalid time window
|
|
244
|
+
PeopleCountingConfig(
|
|
245
|
+
category="general",
|
|
246
|
+
usecase="people_counting",
|
|
247
|
+
time_window_minutes=-10
|
|
248
|
+
),
|
|
249
|
+
# Empty person categories
|
|
250
|
+
PeopleCountingConfig(
|
|
251
|
+
category="general",
|
|
252
|
+
usecase="people_counting",
|
|
253
|
+
person_categories=[]
|
|
254
|
+
)
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
for config in invalid_configs:
|
|
258
|
+
errors = config.validate()
|
|
259
|
+
self.assertGreater(len(errors), 0)
|
|
260
|
+
|
|
261
|
+
def test_people_counting_zone_validation(self):
|
|
262
|
+
"""Test zone configuration validation."""
|
|
263
|
+
# Valid zone configuration
|
|
264
|
+
valid_zones = create_zone_polygons(2, zone_names=["area1", "area2"])
|
|
265
|
+
zone_config = ZoneConfig(zones=valid_zones)
|
|
266
|
+
|
|
267
|
+
errors = zone_config.validate()
|
|
268
|
+
self.assertEqual(len(errors), 0)
|
|
269
|
+
|
|
270
|
+
# Invalid zone configurations
|
|
271
|
+
invalid_zone_configs = [
|
|
272
|
+
# Zone with too few points
|
|
273
|
+
ZoneConfig(zones={"invalid": [[0, 0], [100, 100]]}),
|
|
274
|
+
# Zone with invalid point format
|
|
275
|
+
ZoneConfig(zones={"invalid": [[0, 0, 0], [100, 100, 100], [50, 50, 50]]}),
|
|
276
|
+
# Invalid confidence threshold for zone
|
|
277
|
+
ZoneConfig(
|
|
278
|
+
zones={"valid": [[0, 0], [100, 0], [100, 100], [0, 100]]},
|
|
279
|
+
zone_confidence_thresholds={"valid": 1.5}
|
|
280
|
+
)
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
for zone_config in invalid_zone_configs:
|
|
284
|
+
errors = zone_config.validate()
|
|
285
|
+
self.assertGreater(len(errors), 0)
|
|
286
|
+
|
|
287
|
+
def test_people_counting_alert_validation(self):
|
|
288
|
+
"""Test alert configuration validation."""
|
|
289
|
+
# Valid alert configuration
|
|
290
|
+
valid_alert_config = AlertConfig(
|
|
291
|
+
count_thresholds={"person": 10},
|
|
292
|
+
occupancy_thresholds={"entrance": 5},
|
|
293
|
+
alert_cooldown=30.0
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
errors = valid_alert_config.validate()
|
|
297
|
+
self.assertEqual(len(errors), 0)
|
|
298
|
+
|
|
299
|
+
# Invalid alert configurations
|
|
300
|
+
invalid_alert_configs = [
|
|
301
|
+
# Invalid count threshold
|
|
302
|
+
AlertConfig(count_thresholds={"person": -5}),
|
|
303
|
+
# Invalid occupancy threshold
|
|
304
|
+
AlertConfig(occupancy_thresholds={"entrance": 0}),
|
|
305
|
+
# Invalid alert cooldown
|
|
306
|
+
AlertConfig(alert_cooldown=-10.0),
|
|
307
|
+
# Webhook enabled without URL
|
|
308
|
+
AlertConfig(enable_webhook_alerts=True, webhook_url=None),
|
|
309
|
+
# Email enabled without recipients
|
|
310
|
+
AlertConfig(enable_email_alerts=True, email_recipients=[])
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
for alert_config in invalid_alert_configs:
|
|
314
|
+
errors = alert_config.validate()
|
|
315
|
+
self.assertGreater(len(errors), 0)
|
|
316
|
+
|
|
317
|
+
def test_people_counting_performance(self):
|
|
318
|
+
"""Test people counting performance with various data sizes."""
|
|
319
|
+
# Test different data sizes
|
|
320
|
+
data_sizes = [10, 50, 100]
|
|
321
|
+
|
|
322
|
+
for size in data_sizes:
|
|
323
|
+
with self.subTest(size=size):
|
|
324
|
+
# Create test data
|
|
325
|
+
test_data = create_detection_results(
|
|
326
|
+
num_detections=size,
|
|
327
|
+
categories=["person", "car", "bicycle"]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Measure processing time
|
|
331
|
+
result, processing_time = self.measure_processing_time(
|
|
332
|
+
self.processor.process_simple,
|
|
333
|
+
test_data,
|
|
334
|
+
usecase="people_counting",
|
|
335
|
+
confidence_threshold=0.5
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Should complete successfully
|
|
339
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
340
|
+
|
|
341
|
+
# Performance should be reasonable
|
|
342
|
+
self.assert_performance_acceptable(processing_time, max_time=2.0)
|
|
343
|
+
|
|
344
|
+
# Should have basic metrics
|
|
345
|
+
self.assertIn("total_people", result.metrics)
|
|
346
|
+
self.assertIn("processing_time", result.metrics)
|
|
347
|
+
|
|
348
|
+
def test_people_counting_insights_generation(self):
|
|
349
|
+
"""Test that meaningful insights are generated."""
|
|
350
|
+
# Create test data
|
|
351
|
+
test_data = create_detection_results(
|
|
352
|
+
num_detections=20,
|
|
353
|
+
categories=["person"] * 15 + ["car"] * 5
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Create configuration with zones and alerts
|
|
357
|
+
config = create_people_counting_config(
|
|
358
|
+
confidence_threshold=0.5,
|
|
359
|
+
include_zones=True,
|
|
360
|
+
include_alerts=True
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
result = self.processor.process(test_data, config)
|
|
364
|
+
|
|
365
|
+
# Should generate multiple insights
|
|
366
|
+
self.assert_insights_generated(result, min_insights=1)
|
|
367
|
+
|
|
368
|
+
# Check insight quality
|
|
369
|
+
insights = result.insights
|
|
370
|
+
|
|
371
|
+
# Should mention people count
|
|
372
|
+
count_mentioned = any("people" in insight.lower() or "person" in insight.lower()
|
|
373
|
+
for insight in insights)
|
|
374
|
+
self.assertTrue(count_mentioned)
|
|
375
|
+
|
|
376
|
+
# Should have zone metrics if zones are configured
|
|
377
|
+
if config.zone_config and config.zone_config.zones:
|
|
378
|
+
self.assertIn("zone_metrics", result.metrics)
|
|
379
|
+
|
|
380
|
+
def test_people_counting_metrics_completeness(self):
|
|
381
|
+
"""Test that all expected metrics are generated."""
|
|
382
|
+
# Create test data
|
|
383
|
+
test_data = create_detection_results(
|
|
384
|
+
num_detections=10,
|
|
385
|
+
categories=["person", "car"]
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Create comprehensive configuration
|
|
389
|
+
config = create_people_counting_config(
|
|
390
|
+
confidence_threshold=0.5,
|
|
391
|
+
enable_tracking=True,
|
|
392
|
+
include_zones=True,
|
|
393
|
+
include_alerts=True
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
result = self.processor.process(test_data, config)
|
|
397
|
+
|
|
398
|
+
# Check for core metrics
|
|
399
|
+
expected_core_metrics = [
|
|
400
|
+
"total_people",
|
|
401
|
+
"processing_time",
|
|
402
|
+
"input_format",
|
|
403
|
+
"confidence_threshold"
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
for metric in expected_core_metrics:
|
|
407
|
+
self.assertIn(metric, result.metrics)
|
|
408
|
+
|
|
409
|
+
# Check for zone metrics if zones are configured
|
|
410
|
+
if config.zone_config and config.zone_config.zones:
|
|
411
|
+
self.assertIn("zone_metrics", result.metrics)
|
|
412
|
+
self.assertIn("zones_analyzed", result.metrics)
|
|
413
|
+
|
|
414
|
+
# Check for alert configuration existence (alerts may be generated but not always included in metrics)
|
|
415
|
+
if config.alert_config:
|
|
416
|
+
# Just validate that alert configuration exists, don't require specific metrics
|
|
417
|
+
self.assertIsNotNone(config.alert_config.count_thresholds)
|
|
418
|
+
|
|
419
|
+
def test_people_counting_time_window_analysis(self):
|
|
420
|
+
"""Test time window analysis functionality."""
|
|
421
|
+
# Create tracking data spanning multiple time windows
|
|
422
|
+
tracking_data = create_tracking_results(
|
|
423
|
+
num_tracks=10,
|
|
424
|
+
track_length_range=(20, 50)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
result = self.processor.process_simple(
|
|
428
|
+
tracking_data,
|
|
429
|
+
usecase="people_counting",
|
|
430
|
+
confidence_threshold=0.5,
|
|
431
|
+
enable_tracking=True,
|
|
432
|
+
time_window_minutes=30
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Should handle time window analysis
|
|
436
|
+
self.assertEqual(result.status, ProcessingStatus.SUCCESS)
|
|
437
|
+
|
|
438
|
+
# Should have basic metrics
|
|
439
|
+
self.assertIn("total_people", result.metrics)
|
|
440
|
+
self.assertIn("processing_time", result.metrics)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class TestPeopleCountingIntegration(BasePostProcessingTest):
|
|
444
|
+
"""Integration tests for people counting with other components."""
|
|
445
|
+
|
|
446
|
+
def test_people_counting_with_mixed_data_formats(self):
|
|
447
|
+
"""Test people counting with mixed detection and tracking data."""
|
|
448
|
+
# Create mixed format data
|
|
449
|
+
mixed_data = {
|
|
450
|
+
"detections": create_detection_results(5, categories=["person"]),
|
|
451
|
+
"tracks": create_tracking_results(3, categories=["person"]),
|
|
452
|
+
"metadata": {"source": "mixed_test"}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
result = self.processor.process_simple(
|
|
456
|
+
mixed_data,
|
|
457
|
+
usecase="people_counting",
|
|
458
|
+
confidence_threshold=0.5
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Should handle mixed format gracefully
|
|
462
|
+
self.assertIsInstance(result.status, ProcessingStatus)
|
|
463
|
+
|
|
464
|
+
def test_people_counting_config_serialization(self):
|
|
465
|
+
"""Test configuration serialization and deserialization."""
|
|
466
|
+
# Create configuration
|
|
467
|
+
original_config = create_people_counting_config(
|
|
468
|
+
confidence_threshold=0.7,
|
|
469
|
+
include_zones=True,
|
|
470
|
+
include_alerts=True
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Save and load configuration
|
|
474
|
+
config_file = self.create_temp_config_file(original_config)
|
|
475
|
+
loaded_config = self.processor.load_config(config_file)
|
|
476
|
+
|
|
477
|
+
# Compare key attributes
|
|
478
|
+
self.assertEqual(original_config.usecase, loaded_config.usecase)
|
|
479
|
+
self.assertEqual(original_config.category, loaded_config.category)
|
|
480
|
+
self.assertEqual(original_config.confidence_threshold, loaded_config.confidence_threshold)
|
|
481
|
+
self.assertEqual(original_config.person_categories, loaded_config.person_categories)
|
|
482
|
+
|
|
483
|
+
def test_people_counting_error_recovery(self):
|
|
484
|
+
"""Test error recovery in people counting."""
|
|
485
|
+
# Create partially invalid data
|
|
486
|
+
mixed_quality_data = [
|
|
487
|
+
# Valid detection
|
|
488
|
+
{"bbox": [10, 10, 50, 50], "confidence": 0.8, "category": "person"},
|
|
489
|
+
# Invalid detection (will be filtered or cause warning)
|
|
490
|
+
{"bbox": [100, 100, 90, 90], "confidence": 0.9, "category": "person"}, # Invalid bbox
|
|
491
|
+
# Another valid detection
|
|
492
|
+
{"bbox": [200, 200, 240, 250], "confidence": 0.9, "category": "person"}
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
result = self.processor.process_simple(
|
|
496
|
+
mixed_quality_data,
|
|
497
|
+
usecase="people_counting",
|
|
498
|
+
confidence_threshold=0.5
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Should handle partial failures gracefully
|
|
502
|
+
self.assertIn(result.status, [ProcessingStatus.SUCCESS, ProcessingStatus.WARNING])
|
|
503
|
+
|
|
504
|
+
# Should still provide some results
|
|
505
|
+
self.assertIsNotNone(result.data)
|
|
506
|
+
self.assertIsInstance(result.metrics, dict)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
if __name__ == "__main__":
|
|
510
|
+
unittest.main(verbosity=2)
|