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,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic counting with tracking use case implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a simplified counting use case that combines basic object counting
|
|
5
|
+
with essential tracking and alerting features. It's designed for scenarios where you need
|
|
6
|
+
simple counting with track continuity and basic alert notifications.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
|
|
13
|
+
from ..core.config import BaseConfig, TrackingConfig, AlertConfig
|
|
14
|
+
from ..utils import (
|
|
15
|
+
filter_by_confidence,
|
|
16
|
+
apply_category_mapping,
|
|
17
|
+
count_objects_by_category,
|
|
18
|
+
count_objects_in_zones,
|
|
19
|
+
count_unique_tracks,
|
|
20
|
+
calculate_counting_summary,
|
|
21
|
+
match_results_structure
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BasicCountingTrackingConfig(BaseConfig):
|
|
26
|
+
"""Configuration for basic counting with tracking."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
category: str = "general",
|
|
31
|
+
usecase: str = "basic_counting_tracking",
|
|
32
|
+
confidence_threshold: float = 0.5,
|
|
33
|
+
target_categories: List[str] = None,
|
|
34
|
+
zones: Optional[Dict[str, List[List[float]]]] = None,
|
|
35
|
+
enable_tracking: bool = True,
|
|
36
|
+
tracking_method: str = "kalman",
|
|
37
|
+
max_age: int = 30,
|
|
38
|
+
min_hits: int = 3,
|
|
39
|
+
count_thresholds: Optional[Dict[str, int]] = None,
|
|
40
|
+
zone_thresholds: Optional[Dict[str, int]] = None,
|
|
41
|
+
alert_cooldown: float = 60.0,
|
|
42
|
+
enable_unique_counting: bool = True,
|
|
43
|
+
index_to_category: Optional[Dict[int, str]] = None,
|
|
44
|
+
**kwargs
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize basic counting tracking configuration.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
category: Use case category
|
|
51
|
+
usecase: Use case name
|
|
52
|
+
confidence_threshold: Minimum confidence for detections
|
|
53
|
+
target_categories: List of categories to count
|
|
54
|
+
zones: Zone definitions for spatial analysis
|
|
55
|
+
enable_tracking: Whether to enable tracking
|
|
56
|
+
tracking_method: Tracking algorithm to use
|
|
57
|
+
max_age: Maximum age for tracks in frames
|
|
58
|
+
min_hits: Minimum hits before confirming track
|
|
59
|
+
count_thresholds: Count thresholds for alerts
|
|
60
|
+
zone_thresholds: Zone occupancy thresholds for alerts
|
|
61
|
+
alert_cooldown: Alert cooldown time in seconds
|
|
62
|
+
enable_unique_counting: Enable unique object counting
|
|
63
|
+
index_to_category: Optional mapping from class indices to category names
|
|
64
|
+
**kwargs: Additional parameters
|
|
65
|
+
"""
|
|
66
|
+
super().__init__(
|
|
67
|
+
category=category,
|
|
68
|
+
usecase=usecase,
|
|
69
|
+
confidence_threshold=confidence_threshold,
|
|
70
|
+
enable_tracking=enable_tracking,
|
|
71
|
+
enable_analytics=True,
|
|
72
|
+
**kwargs
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Target categories
|
|
76
|
+
self.target_categories = target_categories or ["person", "people", "object"]
|
|
77
|
+
|
|
78
|
+
# Zone configuration
|
|
79
|
+
self.zones = zones or {}
|
|
80
|
+
|
|
81
|
+
# Category mapping
|
|
82
|
+
self.index_to_category = index_to_category
|
|
83
|
+
|
|
84
|
+
# Tracking configuration
|
|
85
|
+
self.tracking_config = None
|
|
86
|
+
if enable_tracking:
|
|
87
|
+
self.tracking_config = TrackingConfig(
|
|
88
|
+
tracking_method=tracking_method,
|
|
89
|
+
max_age=max_age,
|
|
90
|
+
min_hits=min_hits,
|
|
91
|
+
iou_threshold=0.3,
|
|
92
|
+
target_classes=self.target_categories
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Alert configuration
|
|
96
|
+
self.alert_config = None
|
|
97
|
+
if count_thresholds or zone_thresholds:
|
|
98
|
+
self.alert_config = AlertConfig(
|
|
99
|
+
count_thresholds=count_thresholds or {},
|
|
100
|
+
occupancy_thresholds=zone_thresholds or {},
|
|
101
|
+
alert_cooldown=alert_cooldown
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Counting settings
|
|
105
|
+
self.enable_unique_counting = enable_unique_counting
|
|
106
|
+
|
|
107
|
+
def validate(self) -> List[str]:
|
|
108
|
+
"""Validate configuration."""
|
|
109
|
+
errors = super().validate()
|
|
110
|
+
|
|
111
|
+
if not self.target_categories:
|
|
112
|
+
errors.append("target_categories cannot be empty")
|
|
113
|
+
|
|
114
|
+
# Validate zones
|
|
115
|
+
for zone_name, polygon in self.zones.items():
|
|
116
|
+
if len(polygon) < 3:
|
|
117
|
+
errors.append(f"Zone '{zone_name}' must have at least 3 points")
|
|
118
|
+
|
|
119
|
+
for i, point in enumerate(polygon):
|
|
120
|
+
if len(point) != 2:
|
|
121
|
+
errors.append(f"Zone '{zone_name}' point {i} must have exactly 2 coordinates")
|
|
122
|
+
|
|
123
|
+
# Validate nested configurations
|
|
124
|
+
if self.tracking_config:
|
|
125
|
+
errors.extend(self.tracking_config.validate())
|
|
126
|
+
|
|
127
|
+
if self.alert_config:
|
|
128
|
+
errors.extend(self.alert_config.validate())
|
|
129
|
+
|
|
130
|
+
return errors
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BasicCountingTrackingUseCase(BaseProcessor):
|
|
134
|
+
"""Basic counting with tracking use case."""
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
"""Initialize basic counting tracking use case."""
|
|
138
|
+
super().__init__("basic_counting_tracking")
|
|
139
|
+
self.category = "general"
|
|
140
|
+
|
|
141
|
+
def get_config_schema(self) -> Dict[str, Any]:
|
|
142
|
+
"""Get configuration schema for basic counting tracking."""
|
|
143
|
+
return {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"confidence_threshold": {
|
|
147
|
+
"type": "number",
|
|
148
|
+
"minimum": 0.0,
|
|
149
|
+
"maximum": 1.0,
|
|
150
|
+
"default": 0.5,
|
|
151
|
+
"description": "Minimum confidence threshold for detections"
|
|
152
|
+
},
|
|
153
|
+
"target_categories": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"items": {"type": "string"},
|
|
156
|
+
"default": ["person", "people", "object"],
|
|
157
|
+
"description": "Categories to count and track"
|
|
158
|
+
},
|
|
159
|
+
"zones": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"additionalProperties": {
|
|
162
|
+
"type": "array",
|
|
163
|
+
"items": {
|
|
164
|
+
"type": "array",
|
|
165
|
+
"items": {"type": "number"},
|
|
166
|
+
"minItems": 2,
|
|
167
|
+
"maxItems": 2
|
|
168
|
+
},
|
|
169
|
+
"minItems": 3
|
|
170
|
+
},
|
|
171
|
+
"description": "Zone definitions as polygons"
|
|
172
|
+
},
|
|
173
|
+
"enable_tracking": {
|
|
174
|
+
"type": "boolean",
|
|
175
|
+
"default": True,
|
|
176
|
+
"description": "Enable tracking for unique counting"
|
|
177
|
+
},
|
|
178
|
+
"tracking_method": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"enum": ["kalman", "sort", "deepsort", "bytetrack"],
|
|
181
|
+
"default": "kalman",
|
|
182
|
+
"description": "Tracking algorithm to use"
|
|
183
|
+
},
|
|
184
|
+
"max_age": {
|
|
185
|
+
"type": "integer",
|
|
186
|
+
"minimum": 1,
|
|
187
|
+
"default": 30,
|
|
188
|
+
"description": "Maximum age for tracks in frames"
|
|
189
|
+
},
|
|
190
|
+
"min_hits": {
|
|
191
|
+
"type": "integer",
|
|
192
|
+
"minimum": 1,
|
|
193
|
+
"default": 3,
|
|
194
|
+
"description": "Minimum hits before confirming track"
|
|
195
|
+
},
|
|
196
|
+
"count_thresholds": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"additionalProperties": {"type": "integer", "minimum": 1},
|
|
199
|
+
"description": "Count thresholds for alerts"
|
|
200
|
+
},
|
|
201
|
+
"zone_thresholds": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"additionalProperties": {"type": "integer", "minimum": 1},
|
|
204
|
+
"description": "Zone occupancy thresholds for alerts"
|
|
205
|
+
},
|
|
206
|
+
"alert_cooldown": {
|
|
207
|
+
"type": "number",
|
|
208
|
+
"minimum": 0.0,
|
|
209
|
+
"default": 60.0,
|
|
210
|
+
"description": "Alert cooldown time in seconds"
|
|
211
|
+
},
|
|
212
|
+
"enable_unique_counting": {
|
|
213
|
+
"type": "boolean",
|
|
214
|
+
"default": True,
|
|
215
|
+
"description": "Enable unique object counting using tracking"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"required": ["confidence_threshold"],
|
|
219
|
+
"additionalProperties": False
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def create_default_config(self, **overrides) -> BasicCountingTrackingConfig:
|
|
223
|
+
"""Create default configuration with optional overrides."""
|
|
224
|
+
defaults = {
|
|
225
|
+
"category": self.category,
|
|
226
|
+
"usecase": self.name,
|
|
227
|
+
"confidence_threshold": 0.5,
|
|
228
|
+
"enable_tracking": True,
|
|
229
|
+
"enable_unique_counting": True,
|
|
230
|
+
"tracking_method": "kalman",
|
|
231
|
+
"max_age": 30,
|
|
232
|
+
"min_hits": 3,
|
|
233
|
+
"alert_cooldown": 60.0
|
|
234
|
+
}
|
|
235
|
+
defaults.update(overrides)
|
|
236
|
+
return BasicCountingTrackingConfig(**defaults)
|
|
237
|
+
|
|
238
|
+
def validate_config(self, config: ConfigProtocol) -> bool:
|
|
239
|
+
"""Validate configuration for this use case."""
|
|
240
|
+
if not isinstance(config, BasicCountingTrackingConfig):
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
errors = config.validate()
|
|
244
|
+
if errors:
|
|
245
|
+
self.logger.warning(f"Configuration validation errors: {errors}")
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
def process(self, data: Any, config: ConfigProtocol,
|
|
251
|
+
context: Optional[ProcessingContext] = None) -> ProcessingResult:
|
|
252
|
+
"""
|
|
253
|
+
Process basic counting with tracking.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
data: Raw model output (detection or tracking format)
|
|
257
|
+
config: Basic counting tracking configuration
|
|
258
|
+
context: Processing context
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ProcessingResult: Processing result with counting and tracking analytics
|
|
262
|
+
"""
|
|
263
|
+
start_time = time.time()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Ensure we have the right config type
|
|
267
|
+
if not isinstance(config, BasicCountingTrackingConfig):
|
|
268
|
+
return self.create_error_result(
|
|
269
|
+
"Invalid configuration type for basic counting tracking",
|
|
270
|
+
usecase=self.name,
|
|
271
|
+
category=self.category,
|
|
272
|
+
context=context
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Initialize processing context if not provided
|
|
276
|
+
if context is None:
|
|
277
|
+
context = ProcessingContext()
|
|
278
|
+
|
|
279
|
+
# Detect input format
|
|
280
|
+
input_format = match_results_structure(data)
|
|
281
|
+
context.input_format = input_format
|
|
282
|
+
context.confidence_threshold = config.confidence_threshold
|
|
283
|
+
|
|
284
|
+
self.logger.info(f"Processing basic counting tracking with format: {input_format.value}")
|
|
285
|
+
|
|
286
|
+
# Step 1: Apply confidence filtering
|
|
287
|
+
processed_data = data
|
|
288
|
+
if config.confidence_threshold is not None:
|
|
289
|
+
processed_data = filter_by_confidence(processed_data, config.confidence_threshold)
|
|
290
|
+
self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
|
|
291
|
+
|
|
292
|
+
# Step 2: Apply category mapping if provided
|
|
293
|
+
if config.index_to_category:
|
|
294
|
+
processed_data = apply_category_mapping(processed_data, config.index_to_category)
|
|
295
|
+
self.logger.debug("Applied category mapping")
|
|
296
|
+
|
|
297
|
+
# Step 3: Calculate basic counting summary
|
|
298
|
+
counting_summary = calculate_counting_summary(
|
|
299
|
+
processed_data,
|
|
300
|
+
zones=config.zones
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Step 4: Zone-based analysis if zones are configured
|
|
304
|
+
zone_analysis = {}
|
|
305
|
+
if config.zones:
|
|
306
|
+
zone_analysis = count_objects_in_zones(processed_data, config.zones)
|
|
307
|
+
self.logger.debug(f"Analyzed {len(config.zones)} zones")
|
|
308
|
+
|
|
309
|
+
# Step 5: Unique tracking analysis if enabled
|
|
310
|
+
tracking_analysis = {}
|
|
311
|
+
if config.enable_tracking and config.enable_unique_counting:
|
|
312
|
+
tracking_analysis = self._analyze_tracking(processed_data, config)
|
|
313
|
+
self.logger.debug("Performed tracking analysis")
|
|
314
|
+
|
|
315
|
+
# Step 6: Generate insights and alerts
|
|
316
|
+
insights = self._generate_insights(counting_summary, zone_analysis, tracking_analysis, config)
|
|
317
|
+
alerts = self._check_alerts(counting_summary, zone_analysis, config)
|
|
318
|
+
|
|
319
|
+
# Step 7: Calculate metrics
|
|
320
|
+
metrics = self._calculate_metrics(counting_summary, zone_analysis, tracking_analysis, config, context)
|
|
321
|
+
|
|
322
|
+
# Step 8: Extract predictions
|
|
323
|
+
predictions = self._extract_predictions(processed_data)
|
|
324
|
+
|
|
325
|
+
# Step 9: Generate summary
|
|
326
|
+
summary = self._generate_summary(counting_summary, zone_analysis, tracking_analysis, alerts)
|
|
327
|
+
|
|
328
|
+
# Mark processing as completed
|
|
329
|
+
context.mark_completed()
|
|
330
|
+
|
|
331
|
+
# Create successful result
|
|
332
|
+
result = self.create_result(
|
|
333
|
+
data={
|
|
334
|
+
"counting_summary": counting_summary,
|
|
335
|
+
"zone_analysis": zone_analysis,
|
|
336
|
+
"tracking_analysis": tracking_analysis,
|
|
337
|
+
"alerts": alerts,
|
|
338
|
+
"total_objects": counting_summary.get("total_objects", 0),
|
|
339
|
+
"zones_count": len(config.zones) if config.zones else 0,
|
|
340
|
+
"tracking_enabled": config.enable_tracking
|
|
341
|
+
},
|
|
342
|
+
usecase=self.name,
|
|
343
|
+
category=self.category,
|
|
344
|
+
context=context
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Add human-readable information
|
|
348
|
+
result.summary = summary
|
|
349
|
+
result.insights = insights
|
|
350
|
+
result.predictions = predictions
|
|
351
|
+
result.metrics = metrics
|
|
352
|
+
|
|
353
|
+
# Add warnings
|
|
354
|
+
if config.confidence_threshold and config.confidence_threshold < 0.3:
|
|
355
|
+
result.add_warning(f"Low confidence threshold ({config.confidence_threshold}) may result in false positives")
|
|
356
|
+
|
|
357
|
+
if config.enable_tracking and not any(item.get("track_id") for item in self._flatten_data(processed_data)):
|
|
358
|
+
result.add_warning("Tracking enabled but no track IDs found in input data")
|
|
359
|
+
|
|
360
|
+
self.logger.info(f"Basic counting tracking completed successfully in {result.processing_time:.2f}s")
|
|
361
|
+
return result
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
self.logger.error(f"Basic counting tracking failed: {str(e)}", exc_info=True)
|
|
365
|
+
|
|
366
|
+
if context:
|
|
367
|
+
context.mark_completed()
|
|
368
|
+
|
|
369
|
+
return self.create_error_result(
|
|
370
|
+
str(e),
|
|
371
|
+
type(e).__name__,
|
|
372
|
+
usecase=self.name,
|
|
373
|
+
category=self.category,
|
|
374
|
+
context=context
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _analyze_tracking(self, data: Any, config: BasicCountingTrackingConfig) -> Dict[str, Any]:
|
|
378
|
+
"""Analyze tracking data for unique counting."""
|
|
379
|
+
tracking_analysis = {
|
|
380
|
+
"unique_tracks": 0,
|
|
381
|
+
"active_tracks": 0,
|
|
382
|
+
"track_categories": {},
|
|
383
|
+
"track_zones": {}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
if isinstance(data, dict):
|
|
388
|
+
# Frame-based tracking data
|
|
389
|
+
unique_tracks = count_unique_tracks(data)
|
|
390
|
+
tracking_analysis["unique_tracks"] = sum(unique_tracks.values())
|
|
391
|
+
tracking_analysis["track_categories"] = unique_tracks
|
|
392
|
+
|
|
393
|
+
# Count active tracks in current frame
|
|
394
|
+
latest_frame = max(data.keys()) if data else None
|
|
395
|
+
if latest_frame and isinstance(data[latest_frame], list):
|
|
396
|
+
active_track_ids = set()
|
|
397
|
+
for item in data[latest_frame]:
|
|
398
|
+
track_id = item.get("track_id")
|
|
399
|
+
if track_id is not None:
|
|
400
|
+
active_track_ids.add(track_id)
|
|
401
|
+
tracking_analysis["active_tracks"] = len(active_track_ids)
|
|
402
|
+
|
|
403
|
+
elif isinstance(data, list):
|
|
404
|
+
# Detection format with track IDs
|
|
405
|
+
unique_track_ids = set()
|
|
406
|
+
track_categories = {}
|
|
407
|
+
|
|
408
|
+
for item in data:
|
|
409
|
+
track_id = item.get("track_id")
|
|
410
|
+
category = item.get("category", "unknown")
|
|
411
|
+
|
|
412
|
+
if track_id is not None:
|
|
413
|
+
unique_track_ids.add(track_id)
|
|
414
|
+
if category not in track_categories:
|
|
415
|
+
track_categories[category] = set()
|
|
416
|
+
track_categories[category].add(track_id)
|
|
417
|
+
|
|
418
|
+
tracking_analysis["unique_tracks"] = len(unique_track_ids)
|
|
419
|
+
tracking_analysis["active_tracks"] = len(unique_track_ids)
|
|
420
|
+
tracking_analysis["track_categories"] = {
|
|
421
|
+
cat: len(tracks) for cat, tracks in track_categories.items()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Zone-based tracking analysis
|
|
425
|
+
if config.zones:
|
|
426
|
+
zone_tracks = {}
|
|
427
|
+
for zone_name in config.zones:
|
|
428
|
+
zone_tracks[zone_name] = 0
|
|
429
|
+
|
|
430
|
+
# This would require more complex zone intersection logic
|
|
431
|
+
# For now, we'll keep it simple
|
|
432
|
+
tracking_analysis["track_zones"] = zone_tracks
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
self.logger.warning(f"Tracking analysis failed: {str(e)}")
|
|
436
|
+
|
|
437
|
+
return tracking_analysis
|
|
438
|
+
|
|
439
|
+
def _generate_insights(self, counting_summary: Dict, zone_analysis: Dict,
|
|
440
|
+
tracking_analysis: Dict, config: BasicCountingTrackingConfig) -> List[str]:
|
|
441
|
+
"""Generate human-readable insights."""
|
|
442
|
+
insights = []
|
|
443
|
+
|
|
444
|
+
total_objects = counting_summary.get("total_objects", 0)
|
|
445
|
+
|
|
446
|
+
if total_objects == 0:
|
|
447
|
+
insights.append("No objects detected in the scene")
|
|
448
|
+
return insights
|
|
449
|
+
|
|
450
|
+
# Basic counting insights
|
|
451
|
+
insights.append(f"Detected {total_objects} objects in total")
|
|
452
|
+
|
|
453
|
+
# Category breakdown
|
|
454
|
+
category_counts = counting_summary.get("by_category", {})
|
|
455
|
+
for category, count in category_counts.items():
|
|
456
|
+
if count > 0 and category in config.target_categories:
|
|
457
|
+
percentage = (count / total_objects) * 100
|
|
458
|
+
insights.append(f"Category '{category}': {count} objects ({percentage:.1f}% of total)")
|
|
459
|
+
|
|
460
|
+
# Zone insights
|
|
461
|
+
if zone_analysis:
|
|
462
|
+
zones_with_objects = sum(1 for zone_counts in zone_analysis.values()
|
|
463
|
+
if (sum(zone_counts.values()) if isinstance(zone_counts, dict) else zone_counts) > 0)
|
|
464
|
+
insights.append(f"Objects detected in {zones_with_objects}/{len(zone_analysis)} zones")
|
|
465
|
+
|
|
466
|
+
for zone_name, zone_counts in zone_analysis.items():
|
|
467
|
+
zone_total = sum(zone_counts.values()) if isinstance(zone_counts, dict) else zone_counts
|
|
468
|
+
if zone_total > 0:
|
|
469
|
+
percentage = (zone_total / total_objects) * 100
|
|
470
|
+
insights.append(f"Zone '{zone_name}': {zone_total} objects ({percentage:.1f}% of total)")
|
|
471
|
+
|
|
472
|
+
# Tracking insights
|
|
473
|
+
if config.enable_tracking and tracking_analysis:
|
|
474
|
+
unique_tracks = tracking_analysis.get("unique_tracks", 0)
|
|
475
|
+
active_tracks = tracking_analysis.get("active_tracks", 0)
|
|
476
|
+
|
|
477
|
+
if unique_tracks > 0:
|
|
478
|
+
insights.append(f"Tracking: {unique_tracks} unique objects, {active_tracks} currently active")
|
|
479
|
+
|
|
480
|
+
if unique_tracks != total_objects:
|
|
481
|
+
efficiency = (unique_tracks / total_objects) * 100 if total_objects > 0 else 0
|
|
482
|
+
insights.append(f"Tracking efficiency: {efficiency:.1f}% ({unique_tracks}/{total_objects} tracked)")
|
|
483
|
+
|
|
484
|
+
# Track category breakdown
|
|
485
|
+
track_categories = tracking_analysis.get("track_categories", {})
|
|
486
|
+
for category, count in track_categories.items():
|
|
487
|
+
if count > 0:
|
|
488
|
+
insights.append(f"Tracked '{category}': {count} unique objects")
|
|
489
|
+
|
|
490
|
+
return insights
|
|
491
|
+
|
|
492
|
+
def _check_alerts(self, counting_summary: Dict, zone_analysis: Dict,
|
|
493
|
+
config: BasicCountingTrackingConfig) -> List[Dict]:
|
|
494
|
+
"""Check for alert conditions."""
|
|
495
|
+
alerts = []
|
|
496
|
+
|
|
497
|
+
if not config.alert_config:
|
|
498
|
+
return alerts
|
|
499
|
+
|
|
500
|
+
total_objects = counting_summary.get("total_objects", 0)
|
|
501
|
+
|
|
502
|
+
# Count threshold alerts
|
|
503
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
|
504
|
+
if category == "all" and total_objects >= threshold:
|
|
505
|
+
alerts.append({
|
|
506
|
+
"type": "count_threshold",
|
|
507
|
+
"severity": "warning",
|
|
508
|
+
"message": f"Total object count ({total_objects}) exceeds threshold ({threshold})",
|
|
509
|
+
"category": category,
|
|
510
|
+
"current_count": total_objects,
|
|
511
|
+
"threshold": threshold,
|
|
512
|
+
"timestamp": time.time()
|
|
513
|
+
})
|
|
514
|
+
elif category in counting_summary.get("by_category", {}):
|
|
515
|
+
count = counting_summary["by_category"][category]
|
|
516
|
+
if count >= threshold:
|
|
517
|
+
alerts.append({
|
|
518
|
+
"type": "count_threshold",
|
|
519
|
+
"severity": "warning",
|
|
520
|
+
"message": f"{category} count ({count}) exceeds threshold ({threshold})",
|
|
521
|
+
"category": category,
|
|
522
|
+
"current_count": count,
|
|
523
|
+
"threshold": threshold,
|
|
524
|
+
"timestamp": time.time()
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
# Zone occupancy alerts
|
|
528
|
+
for zone_name, threshold in config.alert_config.occupancy_thresholds.items():
|
|
529
|
+
if zone_name in zone_analysis:
|
|
530
|
+
zone_count = sum(zone_analysis[zone_name].values()) if isinstance(zone_analysis[zone_name], dict) else zone_analysis[zone_name]
|
|
531
|
+
if zone_count >= threshold:
|
|
532
|
+
alerts.append({
|
|
533
|
+
"type": "zone_occupancy",
|
|
534
|
+
"severity": "warning",
|
|
535
|
+
"message": f"Zone '{zone_name}' occupancy ({zone_count}) exceeds threshold ({threshold})",
|
|
536
|
+
"zone": zone_name,
|
|
537
|
+
"current_occupancy": zone_count,
|
|
538
|
+
"threshold": threshold,
|
|
539
|
+
"timestamp": time.time()
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
return alerts
|
|
543
|
+
|
|
544
|
+
def _calculate_metrics(self, counting_summary: Dict, zone_analysis: Dict,
|
|
545
|
+
tracking_analysis: Dict, config: BasicCountingTrackingConfig,
|
|
546
|
+
context: ProcessingContext) -> Dict[str, Any]:
|
|
547
|
+
"""Calculate detailed metrics."""
|
|
548
|
+
total_objects = counting_summary.get("total_objects", 0)
|
|
549
|
+
|
|
550
|
+
metrics = {
|
|
551
|
+
"total_objects": total_objects,
|
|
552
|
+
"processing_time": context.processing_time or 0.0,
|
|
553
|
+
"input_format": context.input_format.value,
|
|
554
|
+
"confidence_threshold": config.confidence_threshold,
|
|
555
|
+
"zones_analyzed": len(zone_analysis),
|
|
556
|
+
"tracking_enabled": config.enable_tracking,
|
|
557
|
+
"unique_counting_enabled": config.enable_unique_counting
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# Zone metrics
|
|
561
|
+
if zone_analysis:
|
|
562
|
+
zone_metrics = {}
|
|
563
|
+
for zone_name, zone_counts in zone_analysis.items():
|
|
564
|
+
zone_total = sum(zone_counts.values()) if isinstance(zone_counts, dict) else zone_counts
|
|
565
|
+
zone_metrics[zone_name] = {
|
|
566
|
+
"count": zone_total,
|
|
567
|
+
"percentage": (zone_total / total_objects) * 100 if total_objects > 0 else 0
|
|
568
|
+
}
|
|
569
|
+
metrics["zone_metrics"] = zone_metrics
|
|
570
|
+
|
|
571
|
+
# Tracking metrics
|
|
572
|
+
if config.enable_tracking and tracking_analysis:
|
|
573
|
+
unique_tracks = tracking_analysis.get("unique_tracks", 0)
|
|
574
|
+
active_tracks = tracking_analysis.get("active_tracks", 0)
|
|
575
|
+
|
|
576
|
+
metrics.update({
|
|
577
|
+
"unique_tracks": unique_tracks,
|
|
578
|
+
"active_tracks": active_tracks,
|
|
579
|
+
"tracking_efficiency": (unique_tracks / total_objects) * 100 if total_objects > 0 else 0,
|
|
580
|
+
"track_categories": tracking_analysis.get("track_categories", {})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
# Category metrics
|
|
584
|
+
category_counts = counting_summary.get("by_category", {})
|
|
585
|
+
category_metrics = {}
|
|
586
|
+
for category, count in category_counts.items():
|
|
587
|
+
if category in config.target_categories:
|
|
588
|
+
category_metrics[category] = {
|
|
589
|
+
"count": count,
|
|
590
|
+
"percentage": (count / total_objects) * 100 if total_objects > 0 else 0
|
|
591
|
+
}
|
|
592
|
+
metrics["category_metrics"] = category_metrics
|
|
593
|
+
|
|
594
|
+
return metrics
|
|
595
|
+
|
|
596
|
+
def _extract_predictions(self, data: Any) -> List[Dict[str, Any]]:
|
|
597
|
+
"""Extract predictions from processed data."""
|
|
598
|
+
predictions = []
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
flattened_data = self._flatten_data(data)
|
|
602
|
+
for item in flattened_data:
|
|
603
|
+
prediction = self._normalize_prediction(item)
|
|
604
|
+
if prediction:
|
|
605
|
+
predictions.append(prediction)
|
|
606
|
+
except Exception as e:
|
|
607
|
+
self.logger.warning(f"Failed to extract predictions: {str(e)}")
|
|
608
|
+
|
|
609
|
+
return predictions
|
|
610
|
+
|
|
611
|
+
def _flatten_data(self, data: Any) -> List[Dict[str, Any]]:
|
|
612
|
+
"""Flatten data structure to list of items."""
|
|
613
|
+
items = []
|
|
614
|
+
|
|
615
|
+
if isinstance(data, list):
|
|
616
|
+
items.extend(data)
|
|
617
|
+
elif isinstance(data, dict):
|
|
618
|
+
for frame_id, frame_data in data.items():
|
|
619
|
+
if isinstance(frame_data, list):
|
|
620
|
+
for item in frame_data:
|
|
621
|
+
if isinstance(item, dict):
|
|
622
|
+
item["frame_id"] = frame_id
|
|
623
|
+
items.append(item)
|
|
624
|
+
|
|
625
|
+
return items
|
|
626
|
+
|
|
627
|
+
def _normalize_prediction(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
|
628
|
+
"""Normalize a single prediction item."""
|
|
629
|
+
if not isinstance(item, dict):
|
|
630
|
+
return {}
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
"category": item.get("category", item.get("class", "unknown")),
|
|
634
|
+
"confidence": item.get("confidence", item.get("score", 0.0)),
|
|
635
|
+
"bounding_box": item.get("bounding_box", item.get("bbox", {})),
|
|
636
|
+
"track_id": item.get("track_id"),
|
|
637
|
+
"frame_id": item.get("frame_id")
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
def _generate_summary(self, counting_summary: Dict, zone_analysis: Dict,
|
|
641
|
+
tracking_analysis: Dict, alerts: List) -> str:
|
|
642
|
+
"""Generate human-readable summary."""
|
|
643
|
+
total_objects = counting_summary.get("total_objects", 0)
|
|
644
|
+
|
|
645
|
+
if total_objects == 0:
|
|
646
|
+
return "No objects detected"
|
|
647
|
+
|
|
648
|
+
summary_parts = [f"{total_objects} objects detected"]
|
|
649
|
+
|
|
650
|
+
# Add tracking info
|
|
651
|
+
if tracking_analysis:
|
|
652
|
+
unique_tracks = tracking_analysis.get("unique_tracks", 0)
|
|
653
|
+
if unique_tracks > 0:
|
|
654
|
+
summary_parts.append(f"{unique_tracks} unique tracks")
|
|
655
|
+
|
|
656
|
+
# Add zone info
|
|
657
|
+
if zone_analysis:
|
|
658
|
+
zones_with_objects = sum(1 for zone_counts in zone_analysis.values()
|
|
659
|
+
if (sum(zone_counts.values()) if isinstance(zone_counts, dict) else zone_counts) > 0)
|
|
660
|
+
summary_parts.append(f"in {zones_with_objects}/{len(zone_analysis)} zones")
|
|
661
|
+
|
|
662
|
+
# Add alert info
|
|
663
|
+
if alerts:
|
|
664
|
+
alert_count = len(alerts)
|
|
665
|
+
summary_parts.append(f"with {alert_count} alert{'s' if alert_count != 1 else ''}")
|
|
666
|
+
|
|
667
|
+
return ", ".join(summary_parts)
|