matrice-analytics 0.1.3__py3-none-any.whl → 0.1.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of matrice-analytics might be problematic. Click here for more details.
- matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
- matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
- matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
- matrice_analytics/post_processing/face_reg/embedding_manager.py +14 -7
- matrice_analytics/post_processing/face_reg/face_recognition.py +123 -34
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +332 -82
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +29 -22
- 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 +0 -1
- matrice_analytics/post_processing/post_processor.py +19 -5
- matrice_analytics/post_processing/usecases/color/clip.py +42 -8
- matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
- matrice_analytics/post_processing/usecases/color_detection.py +21 -98
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
- matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
- matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +252 -11
- matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
- matrice_analytics/post_processing/utils/__init__.py +8 -8
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/RECORD +59 -24
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
People counting use case implementation.
|
|
3
|
-
|
|
4
|
-
This module provides a clean implementation of people counting functionality
|
|
5
|
-
with zone-based analysis, tracking, and alerting capabilities.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import Any, Dict, List, Optional, Set
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
9
2
|
from dataclasses import asdict
|
|
10
3
|
import time
|
|
11
4
|
from datetime import datetime, timezone
|
|
12
5
|
|
|
13
6
|
from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
|
|
14
|
-
from ..core.config import PeopleCountingConfig, ZoneConfig, AlertConfig
|
|
15
7
|
from ..utils import (
|
|
16
8
|
filter_by_confidence,
|
|
17
9
|
filter_by_categories,
|
|
@@ -22,229 +14,132 @@ from ..utils import (
|
|
|
22
14
|
match_results_structure,
|
|
23
15
|
bbox_smoothing,
|
|
24
16
|
BBoxSmoothingConfig,
|
|
25
|
-
BBoxSmoothingTracker
|
|
26
|
-
calculate_iou
|
|
17
|
+
BBoxSmoothingTracker
|
|
27
18
|
)
|
|
28
|
-
from
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from ..core.config import PeopleCountingConfig, BaseConfig, AlertConfig, ZoneConfig
|
|
29
21
|
|
|
30
22
|
|
|
31
23
|
class PeopleCountingUseCase(BaseProcessor):
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
CATEGORY_DISPLAY = {
|
|
25
|
+
"person": "Person",
|
|
26
|
+
"people": "People",
|
|
27
|
+
"human": "Human",
|
|
28
|
+
"man": "Man",
|
|
29
|
+
"woman": "Woman",
|
|
30
|
+
"male": "Male",
|
|
31
|
+
"female": "Female"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
34
|
def __init__(self):
|
|
35
|
-
"""Initialize people counting use case."""
|
|
36
35
|
super().__init__("people_counting")
|
|
37
36
|
self.category = "general"
|
|
38
|
-
self.CASE_TYPE: Optional[str] = '
|
|
39
|
-
self.CASE_VERSION: Optional[str] = '1.
|
|
40
|
-
|
|
41
|
-
# Track ID storage for total count calculation
|
|
42
|
-
self._total_track_ids = set() # Store all unique track IDs seen across calls
|
|
43
|
-
self._current_frame_track_ids = set() # Store track IDs from current frame
|
|
44
|
-
self._total_count = 0 # Cached total count
|
|
45
|
-
self._last_update_time = time.time() # Track when last updated
|
|
46
|
-
|
|
47
|
-
# Zone-based tracking storage
|
|
48
|
-
self._zone_current_track_ids = {} # zone_name -> set of current track IDs in zone
|
|
49
|
-
self._zone_total_track_ids = {} # zone_name -> set of all track IDs that have been in zone
|
|
50
|
-
self._zone_current_counts = {} # zone_name -> current count in zone
|
|
51
|
-
self._zone_total_counts = {} # zone_name -> total count that have been in zone
|
|
52
|
-
|
|
53
|
-
# Frame counter for tracking total frames processed
|
|
54
|
-
self._total_frame_counter = 0 # Total frames processed across all calls
|
|
55
|
-
|
|
56
|
-
# Global frame offset for video chunk processing
|
|
57
|
-
self._global_frame_offset = 0 # Offset to add to local frame IDs for global frame numbering
|
|
58
|
-
self._frames_in_current_chunk = 0 # Number of frames in current chunk
|
|
59
|
-
|
|
60
|
-
# Initialize smoothing tracker
|
|
37
|
+
self.CASE_TYPE: Optional[str] = 'people_counting'
|
|
38
|
+
self.CASE_VERSION: Optional[str] = '1.4'
|
|
39
|
+
self.target_categories = ['person', 'people','human','man','woman','male','female']
|
|
61
40
|
self.smoothing_tracker = None
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
self.tracker = None
|
|
42
|
+
self._total_frame_counter = 0
|
|
43
|
+
self._global_frame_offset = 0
|
|
64
44
|
self._tracking_start_time = None
|
|
65
|
-
|
|
66
|
-
# --------------------------------------------------------------------- #
|
|
67
|
-
# Tracking aliasing structures to merge fragmented IDs #
|
|
68
|
-
# --------------------------------------------------------------------- #
|
|
69
|
-
# Maps raw tracker IDs generated by ByteTrack to a stable canonical ID
|
|
70
|
-
# that represents a real-world person. This helps avoid double counting
|
|
71
|
-
# when the tracker loses a target temporarily and assigns a new ID.
|
|
72
45
|
self._track_aliases: Dict[Any, Any] = {}
|
|
73
|
-
|
|
74
|
-
# Stores metadata about each canonical track such as its last seen
|
|
75
|
-
# bounding box, last update timestamp and all raw IDs that have been
|
|
76
|
-
# merged into it.
|
|
77
46
|
self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# person for alias merging. Tuned for people (robust CCTV scenarios).
|
|
81
|
-
# Using a moderate IoU to handle jitter and perspective changes.
|
|
82
|
-
self._track_merge_iou_threshold: float = 0.3
|
|
83
|
-
|
|
84
|
-
# Merge window in seconds (people typically move slowly; shorter window
|
|
85
|
-
# reduces accidental merges across cuts).
|
|
86
|
-
self._track_merge_time_window: float = 3.0
|
|
87
|
-
|
|
47
|
+
self._track_merge_iou_threshold: float = 0.05
|
|
48
|
+
self._track_merge_time_window: float = 7.0
|
|
88
49
|
self._ascending_alert_list: List[int] = []
|
|
89
50
|
self.current_incident_end_timestamp: str = "N/A"
|
|
90
|
-
|
|
91
51
|
self.start_timer = None
|
|
92
52
|
|
|
93
|
-
|
|
94
|
-
|
|
53
|
+
def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
|
|
54
|
+
stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
|
|
55
|
+
processing_start = time.time()
|
|
56
|
+
if not isinstance(config, PeopleCountingConfig):
|
|
57
|
+
return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
|
|
58
|
+
if context is None:
|
|
59
|
+
context = ProcessingContext()
|
|
95
60
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
61
|
+
input_format = match_results_structure(data)
|
|
62
|
+
context.input_format = input_format
|
|
63
|
+
context.confidence_threshold = config.confidence_threshold
|
|
64
|
+
|
|
65
|
+
if config.confidence_threshold is not None:
|
|
66
|
+
processed_data = filter_by_confidence(data, config.confidence_threshold)
|
|
67
|
+
self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
|
|
68
|
+
else:
|
|
69
|
+
processed_data = data
|
|
70
|
+
self.logger.debug("Did not apply confidence filtering since no threshold provided")
|
|
100
71
|
|
|
72
|
+
if config.index_to_category:
|
|
73
|
+
processed_data = apply_category_mapping(processed_data, config.index_to_category)
|
|
74
|
+
self.logger.debug("Applied category mapping")
|
|
75
|
+
|
|
76
|
+
if config.target_categories:
|
|
77
|
+
processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
|
|
78
|
+
self.logger.debug("Applied category filtering")
|
|
79
|
+
|
|
80
|
+
# if config.enable_smoothing:
|
|
81
|
+
# if self.smoothing_tracker is None:
|
|
82
|
+
# smoothing_config = BBoxSmoothingConfig(
|
|
83
|
+
# smoothing_algorithm=config.smoothing_algorithm,
|
|
84
|
+
# window_size=config.smoothing_window_size,
|
|
85
|
+
# cooldown_frames=config.smoothing_cooldown_frames,
|
|
86
|
+
# confidence_threshold=config.confidence_threshold,
|
|
87
|
+
# confidence_range_factor=config.smoothing_confidence_range_factor,
|
|
88
|
+
# enable_smoothing=True
|
|
89
|
+
# )
|
|
90
|
+
# self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
|
|
91
|
+
# processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
|
|
101
92
|
|
|
102
|
-
def process(self, data: Any, config: ConfigProtocol,
|
|
103
|
-
context: Optional[ProcessingContext] = None, stream_info: Optional[Any] = None) -> ProcessingResult:
|
|
104
|
-
"""
|
|
105
|
-
Process people counting use case - automatically detects single or multi-frame structure.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
data: Raw model output (detection or tracking format)
|
|
109
|
-
config: People counting configuration
|
|
110
|
-
context: Processing context
|
|
111
|
-
stream_info: Stream information containing frame details (optional)
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
ProcessingResult: Processing result with standardized agg_summary structure
|
|
115
|
-
"""
|
|
116
|
-
start_time = time.time()
|
|
117
|
-
|
|
118
93
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
context = ProcessingContext()
|
|
131
|
-
|
|
132
|
-
# Detect input format and frame structure
|
|
133
|
-
input_format = match_results_structure(data)
|
|
134
|
-
context.input_format = input_format
|
|
135
|
-
context.confidence_threshold = config.confidence_threshold
|
|
136
|
-
|
|
137
|
-
is_multi_frame = self.detect_frame_structure(data)
|
|
138
|
-
|
|
139
|
-
# Apply smoothing if enabled
|
|
140
|
-
if config.enable_smoothing and input_format == ResultFormat.OBJECT_TRACKING:
|
|
141
|
-
data = self._apply_smoothing(data, config)
|
|
142
|
-
|
|
143
|
-
# Process based on frame structure
|
|
144
|
-
if is_multi_frame:
|
|
145
|
-
|
|
146
|
-
return self._process_multi_frame(data, config, context, stream_info)
|
|
147
|
-
else:
|
|
148
|
-
return self._process_single_frame(data, config, context, stream_info)
|
|
149
|
-
|
|
94
|
+
from ..advanced_tracker import AdvancedTracker
|
|
95
|
+
from ..advanced_tracker.config import TrackerConfig
|
|
96
|
+
if self.tracker is None:
|
|
97
|
+
tracker_config = TrackerConfig(
|
|
98
|
+
track_high_thresh=0.4,
|
|
99
|
+
track_low_thresh=0.05,
|
|
100
|
+
new_track_thresh=0.3,
|
|
101
|
+
match_thresh=0.8)
|
|
102
|
+
self.tracker = AdvancedTracker(tracker_config)
|
|
103
|
+
self.logger.info("Initialized AdvancedTracker for People Counting")
|
|
104
|
+
processed_data = self.tracker.update(processed_data)
|
|
150
105
|
except Exception as e:
|
|
151
|
-
self.logger.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
176
|
-
|
|
177
|
-
# Process each frame individually
|
|
178
|
-
for frame_key, frame_detections in data.items():
|
|
179
|
-
# Extract frame ID from tracking data
|
|
180
|
-
frame_id = self._extract_frame_id_from_tracking(frame_detections, frame_key)
|
|
181
|
-
global_frame_id = self.get_global_frame_id(frame_id)
|
|
182
|
-
|
|
183
|
-
# Process this single frame's detections
|
|
184
|
-
alerts, incidents_list, tracking_stats_list, business_analytics_list, summary_list = self._process_frame_detections(
|
|
185
|
-
frame_detections, config, global_frame_id, stream_info
|
|
186
|
-
)
|
|
187
|
-
incidents = incidents_list[0] if incidents_list else {}
|
|
188
|
-
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
189
|
-
business_analytics = business_analytics_list[0] if business_analytics_list else {}
|
|
190
|
-
summary = summary_list[0] if summary_list else {}
|
|
191
|
-
|
|
192
|
-
# Store frame-wise results
|
|
193
|
-
if incidents:
|
|
194
|
-
frame_incidents[global_frame_id] = incidents
|
|
195
|
-
if tracking_stats:
|
|
196
|
-
frame_tracking_stats[global_frame_id] = tracking_stats
|
|
197
|
-
if business_analytics:
|
|
198
|
-
frame_business_analytics[global_frame_id] = business_analytics
|
|
199
|
-
if summary:
|
|
200
|
-
frame_human_text[global_frame_id] = summary
|
|
201
|
-
if alerts:
|
|
202
|
-
frame_alerts[global_frame_id] = alerts
|
|
203
|
-
|
|
204
|
-
# Update global frame offset after processing this chunk
|
|
205
|
-
self.update_global_frame_offset(frames_in_this_call)
|
|
206
|
-
|
|
207
|
-
# Create frame-wise agg_summary
|
|
208
|
-
agg_summary = self.create_frame_wise_agg_summary(
|
|
209
|
-
frame_incidents, frame_tracking_stats, frame_business_analytics, frame_alerts,
|
|
210
|
-
frame_human_text=frame_human_text
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
# Mark processing as completed
|
|
214
|
-
context.mark_completed()
|
|
215
|
-
|
|
216
|
-
# Create result with standardized agg_summary
|
|
217
|
-
return self.create_result(
|
|
218
|
-
data={"agg_summary": agg_summary},
|
|
219
|
-
usecase=self.name,
|
|
220
|
-
category=self.category,
|
|
221
|
-
context=context
|
|
222
|
-
)
|
|
106
|
+
self.logger.warning(f"AdvancedTracker failed: {e}")
|
|
107
|
+
|
|
108
|
+
self._update_tracking_state(processed_data)
|
|
109
|
+
self._total_frame_counter += 1
|
|
110
|
+
|
|
111
|
+
frame_number = None
|
|
112
|
+
if stream_info:
|
|
113
|
+
input_settings = stream_info.get("input_settings", {})
|
|
114
|
+
start_frame = input_settings.get("start_frame")
|
|
115
|
+
end_frame = input_settings.get("end_frame")
|
|
116
|
+
if start_frame is not None and end_frame is not None and start_frame == end_frame:
|
|
117
|
+
frame_number = start_frame
|
|
118
|
+
|
|
119
|
+
general_counting_summary = calculate_counting_summary(data)
|
|
120
|
+
counting_summary = self._count_categories(processed_data, config)
|
|
121
|
+
total_counts = self.get_total_counts()
|
|
122
|
+
counting_summary['total_counts'] = total_counts
|
|
123
|
+
|
|
124
|
+
alerts = self._check_alerts(counting_summary, frame_number, config)
|
|
125
|
+
predictions = self._extract_predictions(processed_data)
|
|
126
|
+
|
|
127
|
+
incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
|
|
128
|
+
tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
|
|
129
|
+
business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
|
|
130
|
+
summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
|
|
223
131
|
|
|
224
|
-
def _process_single_frame(self, data: Any, config: PeopleCountingConfig, context: ProcessingContext, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
|
|
225
|
-
"""Process single frame data and return standardized agg_summary."""
|
|
226
|
-
|
|
227
|
-
current_frame = stream_info.get("input_settings", {}).get("start_frame", "current_frame")
|
|
228
|
-
# Process frame data
|
|
229
|
-
alerts, incidents_list, tracking_stats_list, business_analytics_list, summary_list = self._process_frame_detections(
|
|
230
|
-
data, config, current_frame, stream_info
|
|
231
|
-
)
|
|
232
132
|
incidents = incidents_list[0] if incidents_list else {}
|
|
233
133
|
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
234
134
|
business_analytics = business_analytics_list[0] if business_analytics_list else {}
|
|
235
135
|
summary = summary_list[0] if summary_list else {}
|
|
236
|
-
|
|
237
|
-
# Create single-frame agg_summary
|
|
238
|
-
# agg_summary = self.create_agg_summary(
|
|
239
|
-
# current_frame, incidents, tracking_stats, business_analytics, alerts, human_text=summary
|
|
240
|
-
# )
|
|
241
|
-
agg_summary = {str(current_frame): {
|
|
136
|
+
agg_summary = {str(frame_number): {
|
|
242
137
|
"incidents": incidents,
|
|
243
138
|
"tracking_stats": tracking_stats,
|
|
244
139
|
"business_analytics": business_analytics,
|
|
245
140
|
"alerts": alerts,
|
|
246
|
-
"human_text": summary
|
|
247
|
-
}
|
|
141
|
+
"human_text": summary}
|
|
142
|
+
}
|
|
248
143
|
|
|
249
144
|
context.mark_completed()
|
|
250
145
|
result = self.create_result(
|
|
@@ -253,154 +148,84 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
253
148
|
category=self.category,
|
|
254
149
|
context=context
|
|
255
150
|
)
|
|
256
|
-
|
|
151
|
+
proc_time = time.time() - processing_start
|
|
152
|
+
processing_latency_ms = proc_time * 1000.0
|
|
153
|
+
processing_fps = (1.0 / proc_time) if proc_time > 0 else None
|
|
154
|
+
# Log the performance metrics using the module-level logger
|
|
155
|
+
print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
|
|
257
156
|
return result
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if config.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
309
|
-
# Keep defaults for confidence thresholds; AdvancedTracker handles activation
|
|
310
|
-
self.tracker = AdvancedTracker(tracker_config)
|
|
311
|
-
self.logger.info("Initialized AdvancedTracker for People Counting (single-frame)")
|
|
312
|
-
except Exception as e:
|
|
313
|
-
self.logger.warning(f"AdvancedTracker init failed, falling back to IoU aliasing: {e}")
|
|
314
|
-
|
|
315
|
-
tracked_detections = frame_detections
|
|
316
|
-
if self.tracker is not None and needs_tracking:
|
|
317
|
-
try:
|
|
318
|
-
tracked_detections = self.tracker.update(frame_detections)
|
|
319
|
-
except Exception as e:
|
|
320
|
-
self.logger.warning(f"AdvancedTracker update failed, using raw detections: {e}")
|
|
321
|
-
tracked_detections = frame_detections
|
|
322
|
-
|
|
323
|
-
# Step 4: Create counting summary for this frame
|
|
324
|
-
counting_summary = {
|
|
325
|
-
"total_objects": len(tracked_detections),
|
|
326
|
-
"detections": tracked_detections,
|
|
327
|
-
"categories": {}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# Count by category
|
|
331
|
-
for detection in tracked_detections:
|
|
332
|
-
category = detection.get("category", "unknown")
|
|
333
|
-
counting_summary["categories"][category] = counting_summary["categories"].get(category, 0) + 1
|
|
334
|
-
|
|
335
|
-
# Step 4.5: Always update tracking state BEFORE zone enhancements so detections have track_ids
|
|
336
|
-
self._update_tracking_state(counting_summary)
|
|
337
|
-
|
|
338
|
-
# Step 5: Zone analysis for this frame
|
|
339
|
-
zone_analysis = {}
|
|
340
|
-
if config.zone_config and config.zone_config.zones:
|
|
341
|
-
# Convert single frame to format expected by count_objects_in_zones
|
|
342
|
-
frame_data = frame_detections #[frame_detections]
|
|
343
|
-
zone_analysis = count_objects_in_zones(frame_data, config.zone_config.zones)
|
|
344
|
-
|
|
345
|
-
# Update zone tracking with current frame data (now detections have canonical track_ids)
|
|
346
|
-
if zone_analysis and config.enable_tracking:
|
|
347
|
-
enhanced_zone_analysis = self._update_zone_tracking(zone_analysis, frame_detections, config)
|
|
348
|
-
# Merge enhanced zone analysis with original zone analysis
|
|
349
|
-
for zone_name, enhanced_data in enhanced_zone_analysis.items():
|
|
350
|
-
zone_analysis[zone_name] = enhanced_data
|
|
351
|
-
|
|
352
|
-
# Step 5: Generate insights and alerts for this frame
|
|
353
|
-
alerts = self._check_alerts(counting_summary, zone_analysis, config, frame_id)
|
|
354
|
-
|
|
355
|
-
# Step 6: Generate summary and standardized agg_summary components for this frame
|
|
356
|
-
incidents = self._generate_incidents(counting_summary, zone_analysis, alerts, config, frame_id, stream_info)
|
|
157
|
+
|
|
158
|
+
def _check_alerts(self, summary: dict, frame_number: Any, config: PeopleCountingConfig) -> List[Dict]:
|
|
159
|
+
def get_trend(data, lookback=900, threshold=0.6):
|
|
160
|
+
window = data[-lookback:] if len(data) >= lookback else data
|
|
161
|
+
if len(window) < 2:
|
|
162
|
+
return True
|
|
163
|
+
increasing = 0
|
|
164
|
+
total = 0
|
|
165
|
+
for i in range(1, len(window)):
|
|
166
|
+
if window[i] >= window[i - 1]:
|
|
167
|
+
increasing += 1
|
|
168
|
+
total += 1
|
|
169
|
+
ratio = increasing / total
|
|
170
|
+
return ratio >= threshold
|
|
171
|
+
|
|
172
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
173
|
+
alerts = []
|
|
174
|
+
total_detections = summary.get("total_count", 0)
|
|
175
|
+
total_counts_dict = summary.get("total_counts", {})
|
|
176
|
+
per_category_count = summary.get("per_category_count", {})
|
|
177
|
+
|
|
178
|
+
if not config.alert_config:
|
|
179
|
+
return alerts
|
|
180
|
+
|
|
181
|
+
if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
|
|
182
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
|
183
|
+
if category == "all" and total_detections > threshold:
|
|
184
|
+
alerts.append({
|
|
185
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
186
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
187
|
+
"incident_category": self.CASE_TYPE,
|
|
188
|
+
"threshold_level": threshold,
|
|
189
|
+
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
|
190
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
|
|
191
|
+
getattr(config.alert_config, 'alert_value', ['JSON']))}
|
|
192
|
+
})
|
|
193
|
+
elif category in per_category_count and per_category_count[category] > threshold:
|
|
194
|
+
alerts.append({
|
|
195
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
196
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
197
|
+
"incident_category": self.CASE_TYPE,
|
|
198
|
+
"threshold_level": threshold,
|
|
199
|
+
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
|
200
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
|
|
201
|
+
getattr(config.alert_config, 'alert_value', ['JSON']))}
|
|
202
|
+
})
|
|
203
|
+
return alerts
|
|
204
|
+
|
|
205
|
+
def _generate_incidents(self, counting_summary: Dict, alerts: List, config: PeopleCountingConfig,
|
|
206
|
+
frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
|
357
207
|
incidents = []
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
summary = self._generate_summary(counting_summary, incidents, tracking_stats, business_analytics, alerts)
|
|
361
|
-
|
|
362
|
-
# Return standardized components as tuple
|
|
363
|
-
return alerts, incidents, tracking_stats, business_analytics, summary
|
|
364
|
-
|
|
365
|
-
def _generate_incidents(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: PeopleCountingConfig, frame_id: str, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
|
366
|
-
"""Generate standardized incidents for the agg_summary structure."""
|
|
367
|
-
|
|
208
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
209
|
+
current_timestamp = self._get_current_timestamp_str(stream_info)
|
|
368
210
|
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
369
|
-
incidents = []
|
|
370
|
-
total_people = counting_summary.get("total_objects", 0)
|
|
371
|
-
current_timestamp = self._get_current_timestamp_str(stream_info, frame_id=frame_id)
|
|
372
|
-
self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
|
|
373
211
|
|
|
374
|
-
|
|
375
|
-
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
|
376
|
-
alert_settings.append({
|
|
377
|
-
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
378
|
-
"incident_category": self.CASE_TYPE,
|
|
379
|
-
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
|
380
|
-
"ascending": True,
|
|
381
|
-
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
382
|
-
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
|
383
|
-
}
|
|
384
|
-
})
|
|
212
|
+
self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
|
|
385
213
|
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
level = "info"
|
|
214
|
+
if total_detections > 0:
|
|
215
|
+
level = "low"
|
|
390
216
|
intensity = 5.0
|
|
391
217
|
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
392
|
-
if start_timestamp and self.current_incident_end_timestamp=='N/A':
|
|
218
|
+
if start_timestamp and self.current_incident_end_timestamp == 'N/A':
|
|
393
219
|
self.current_incident_end_timestamp = 'Incident still active'
|
|
394
|
-
elif start_timestamp and self.current_incident_end_timestamp=='Incident still active':
|
|
395
|
-
if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
|
|
220
|
+
elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
|
|
221
|
+
if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
|
|
396
222
|
self.current_incident_end_timestamp = current_timestamp
|
|
397
|
-
elif self.current_incident_end_timestamp!='Incident still active' and self.current_incident_end_timestamp!='N/A':
|
|
223
|
+
elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
|
|
398
224
|
self.current_incident_end_timestamp = 'N/A'
|
|
399
|
-
|
|
400
|
-
if config.alert_config and
|
|
401
|
-
threshold = config.alert_config.count_thresholds.get("all",
|
|
402
|
-
intensity = min(10.0, (
|
|
403
|
-
|
|
225
|
+
|
|
226
|
+
if config.alert_config and config.alert_config.count_thresholds:
|
|
227
|
+
threshold = config.alert_config.count_thresholds.get("all", 15)
|
|
228
|
+
intensity = min(10.0, (total_detections / threshold) * 10)
|
|
404
229
|
if intensity >= 9:
|
|
405
230
|
level = "critical"
|
|
406
231
|
self._ascending_alert_list.append(3)
|
|
@@ -414,373 +239,136 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
414
239
|
level = "low"
|
|
415
240
|
self._ascending_alert_list.append(0)
|
|
416
241
|
else:
|
|
417
|
-
if
|
|
242
|
+
if total_detections > 30:
|
|
418
243
|
level = "critical"
|
|
419
244
|
intensity = 10.0
|
|
420
245
|
self._ascending_alert_list.append(3)
|
|
421
|
-
elif
|
|
246
|
+
elif total_detections > 25:
|
|
422
247
|
level = "significant"
|
|
423
248
|
intensity = 9.0
|
|
424
249
|
self._ascending_alert_list.append(2)
|
|
425
|
-
elif
|
|
250
|
+
elif total_detections > 15:
|
|
426
251
|
level = "medium"
|
|
427
252
|
intensity = 7.0
|
|
428
253
|
self._ascending_alert_list.append(1)
|
|
429
254
|
else:
|
|
430
255
|
level = "low"
|
|
431
|
-
intensity = min(10.0,
|
|
256
|
+
intensity = min(10.0, total_detections / 3.0)
|
|
432
257
|
self._ascending_alert_list.append(0)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
human_text_lines
|
|
436
|
-
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
|
|
258
|
+
|
|
259
|
+
human_text_lines = [f"COUNTING INCIDENTS DETECTED @ {current_timestamp}:"]
|
|
260
|
+
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
|
|
437
261
|
human_text = "\n".join(human_text_lines)
|
|
438
262
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
263
|
+
alert_settings = []
|
|
264
|
+
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
|
265
|
+
alert_settings.append({
|
|
266
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
267
|
+
"incident_category": self.CASE_TYPE,
|
|
268
|
+
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
|
269
|
+
"ascending": True,
|
|
270
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
|
|
271
|
+
getattr(config.alert_config, 'alert_value', ['JSON']))}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
event = self.create_incident(
|
|
275
|
+
incident_id=f"{self.CASE_TYPE}_{frame_number}",
|
|
276
|
+
incident_type=self.CASE_TYPE,
|
|
277
|
+
severity_level=level,
|
|
278
|
+
human_text=human_text,
|
|
279
|
+
camera_info=camera_info,
|
|
280
|
+
alerts=alerts,
|
|
281
|
+
alert_settings=alert_settings,
|
|
282
|
+
start_time=start_timestamp,
|
|
283
|
+
end_time=self.current_incident_end_timestamp,
|
|
284
|
+
level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
|
|
285
|
+
)
|
|
444
286
|
incidents.append(event)
|
|
445
287
|
else:
|
|
446
288
|
self._ascending_alert_list.append(0)
|
|
447
289
|
incidents.append({})
|
|
448
|
-
|
|
449
|
-
# Add zone-specific events if applicable
|
|
450
|
-
if zone_analysis:
|
|
451
|
-
human_text_lines.append(f"\t- ZONE EVENTS:")
|
|
452
|
-
for zone_name, zone_count in zone_analysis.items():
|
|
453
|
-
zone_total = self._robust_zone_total(zone_count)
|
|
454
|
-
if zone_total > 0:
|
|
455
|
-
zone_intensity = min(10.0, zone_total / 5.0)
|
|
456
|
-
zone_level = "info"
|
|
457
|
-
if intensity >= 9:
|
|
458
|
-
level = "critical"
|
|
459
|
-
self._ascending_alert_list.append(3)
|
|
460
|
-
elif intensity >= 7:
|
|
461
|
-
level = "significant"
|
|
462
|
-
self._ascending_alert_list.append(2)
|
|
463
|
-
elif intensity >= 5:
|
|
464
|
-
level = "medium"
|
|
465
|
-
self._ascending_alert_list.append(1)
|
|
466
|
-
else:
|
|
467
|
-
level = "low"
|
|
468
|
-
self._ascending_alert_list.append(0)
|
|
469
|
-
|
|
470
|
-
if zone_total > 0:
|
|
471
|
-
human_text_lines.append(f"\t\t- Zone name: {zone_name}")
|
|
472
|
-
human_text_lines.append(f"\t\t\t- Total people in zone: {zone_total}")
|
|
473
|
-
# Main people counting incident
|
|
474
|
-
event= self.create_incident(incident_id=self.CASE_TYPE+'_'+'zone_'+zone_name+str(frame_id), incident_type=self.CASE_TYPE,
|
|
475
|
-
severity_level=zone_level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
|
476
|
-
start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
|
|
477
|
-
level_settings= {"low": 1, "medium": 3, "significant":4, "critical": 7})
|
|
478
|
-
incidents.append(event)
|
|
479
290
|
return incidents
|
|
480
291
|
|
|
481
|
-
def _generate_tracking_stats(self, counting_summary: Dict,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
total_people = counting_summary.get("total_objects", 0)
|
|
485
|
-
|
|
486
|
-
# Get total count from cached tracking state
|
|
487
|
-
total_unique_count = self.get_total_count()
|
|
488
|
-
current_frame_count = self.get_current_frame_count()
|
|
489
|
-
|
|
490
|
-
# Get camera info using standardized method
|
|
292
|
+
def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: PeopleCountingConfig,
|
|
293
|
+
frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
|
491
294
|
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
elif isinstance(zone_data, dict):
|
|
505
|
-
# Sum up zone counts
|
|
506
|
-
for v in zone_data.values():
|
|
507
|
-
if isinstance(v, int):
|
|
508
|
-
category_total_count += v
|
|
509
|
-
elif isinstance(v, list):
|
|
510
|
-
category_total_count += len(v)
|
|
511
|
-
elif isinstance(zone_data, (int, list)):
|
|
512
|
-
category_total_count += len(zone_data) if isinstance(zone_data, list) else zone_data
|
|
513
|
-
else:
|
|
514
|
-
# Use total unique count from tracking state
|
|
515
|
-
category_total_count = total_unique_count
|
|
516
|
-
|
|
517
|
-
if category_total_count > 0:
|
|
518
|
-
total_counts.append(self.create_count_object(category, category_total_count))
|
|
519
|
-
per_category_total[category] = category_total_count
|
|
520
|
-
|
|
521
|
-
# Build current_counts using standardized method
|
|
522
|
-
current_counts = []
|
|
523
|
-
per_category_current = {}
|
|
524
|
-
|
|
525
|
-
for category in config.person_categories or ["person"]:
|
|
526
|
-
# Get current count for this category
|
|
527
|
-
category_current_count = 0
|
|
528
|
-
if zone_analysis:
|
|
529
|
-
for zone_data in zone_analysis.values():
|
|
530
|
-
if isinstance(zone_data, dict) and "current_count" in zone_data:
|
|
531
|
-
category_current_count += zone_data.get("current_count", 0)
|
|
532
|
-
elif isinstance(zone_data, dict):
|
|
533
|
-
# For current frame, look at detections count
|
|
534
|
-
for v in zone_data.values():
|
|
535
|
-
if isinstance(v, int):
|
|
536
|
-
category_current_count += v
|
|
537
|
-
elif isinstance(v, list):
|
|
538
|
-
category_current_count += len(v)
|
|
539
|
-
elif isinstance(zone_data, (int, list)):
|
|
540
|
-
category_current_count += len(zone_data) if isinstance(zone_data, list) else zone_data
|
|
541
|
-
else:
|
|
542
|
-
# Count detections in current frame for this category
|
|
543
|
-
detections = counting_summary.get("detections", [])
|
|
544
|
-
category_current_count = sum(1 for d in detections if d.get("category") == category)
|
|
545
|
-
|
|
546
|
-
if category_current_count > 0 or total_people > 0: # Include even if 0 when there are people
|
|
547
|
-
current_counts.append(self.create_count_object(category, category_current_count))
|
|
548
|
-
per_category_current[category] = category_current_count
|
|
549
|
-
|
|
550
|
-
# Prepare detections using standardized method (without confidence and track_id)
|
|
295
|
+
tracking_stats = []
|
|
296
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
297
|
+
total_counts_dict = counting_summary.get("total_counts", {})
|
|
298
|
+
per_category_count = counting_summary.get("per_category_count", {})
|
|
299
|
+
current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
|
|
300
|
+
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
301
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
|
|
302
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
|
303
|
+
|
|
304
|
+
total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
|
|
305
|
+
current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
|
|
306
|
+
|
|
551
307
|
detections = []
|
|
552
308
|
for detection in counting_summary.get("detections", []):
|
|
553
309
|
bbox = detection.get("bounding_box", {})
|
|
554
310
|
category = detection.get("category", "person")
|
|
555
|
-
# Include segmentation if available (like in eg.json)
|
|
556
311
|
if detection.get("masks"):
|
|
557
|
-
segmentation= detection.get("masks", [])
|
|
312
|
+
segmentation = detection.get("masks", [])
|
|
558
313
|
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
|
559
314
|
elif detection.get("segmentation"):
|
|
560
|
-
segmentation= detection.get("segmentation")
|
|
315
|
+
segmentation = detection.get("segmentation")
|
|
561
316
|
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
|
562
317
|
elif detection.get("mask"):
|
|
563
|
-
segmentation= detection.get("mask")
|
|
318
|
+
segmentation = detection.get("mask")
|
|
564
319
|
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
|
565
320
|
else:
|
|
566
321
|
detection_obj = self.create_detection_object(category, bbox)
|
|
567
322
|
detections.append(detection_obj)
|
|
568
|
-
|
|
569
|
-
# Build alerts and alert_settings arrays
|
|
323
|
+
|
|
570
324
|
alert_settings = []
|
|
571
325
|
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
|
572
326
|
alert_settings.append({
|
|
573
|
-
"alert_type": getattr(config.alert_config, 'alert_type', ['Default'])
|
|
327
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
574
328
|
"incident_category": self.CASE_TYPE,
|
|
575
329
|
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
|
576
330
|
"ascending": True,
|
|
577
|
-
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default'])
|
|
578
|
-
|
|
579
|
-
}
|
|
331
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
|
|
332
|
+
getattr(config.alert_config, 'alert_value', ['JSON']))}
|
|
580
333
|
})
|
|
581
|
-
if zone_analysis:
|
|
582
|
-
human_text_lines=[]
|
|
583
|
-
current_timestamp = self._get_current_timestamp_str(stream_info, frame_id=frame_id)
|
|
584
|
-
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
585
|
-
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
|
|
586
|
-
def robust_zone_total(zone_count):
|
|
587
|
-
if isinstance(zone_count, dict):
|
|
588
|
-
total = 0
|
|
589
|
-
for v in zone_count.values():
|
|
590
|
-
if isinstance(v, int):
|
|
591
|
-
total += v
|
|
592
|
-
elif isinstance(v, list) and total==0:
|
|
593
|
-
total += len(v)
|
|
594
|
-
return total
|
|
595
|
-
elif isinstance(zone_count, list):
|
|
596
|
-
return len(zone_count)
|
|
597
|
-
elif isinstance(zone_count, int):
|
|
598
|
-
return zone_count
|
|
599
|
-
else:
|
|
600
|
-
return 0
|
|
601
|
-
human_text_lines.append(f"\t- People Detected: {total_people}")
|
|
602
|
-
human_text_lines.append("")
|
|
603
|
-
human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
|
|
604
|
-
|
|
605
|
-
for zone_name, zone_count in zone_analysis.items():
|
|
606
|
-
zone_total = robust_zone_total(zone_count)
|
|
607
|
-
human_text_lines.append(f"\t- Zone name: {zone_name}")
|
|
608
|
-
human_text_lines.append(f"\t\t- Total count in zone: {zone_total-1}")
|
|
609
|
-
|
|
610
|
-
if total_unique_count > 0:
|
|
611
|
-
human_text_lines.append(f"\t- Total unique people in the scene: {total_unique_count}")
|
|
612
|
-
if alerts:
|
|
613
|
-
for alert in alerts:
|
|
614
|
-
human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
|
|
615
|
-
else:
|
|
616
|
-
human_text_lines.append("Alerts: None")
|
|
617
|
-
human_text = "\n".join(human_text_lines)
|
|
618
|
-
else:
|
|
619
|
-
human_text = self._generate_human_text_for_tracking(total_people, total_unique_count, config, frame_id, alerts, stream_info)
|
|
620
|
-
|
|
621
|
-
# Create high precision timestamps for input_timestamp and reset_timestamp
|
|
622
|
-
high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True, frame_id=frame_id)
|
|
623
|
-
high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
|
624
|
-
# Create tracking_stat using standardized method
|
|
625
|
-
tracking_stat = self.create_tracking_stats(
|
|
626
|
-
total_counts, current_counts, detections, human_text, camera_info, alerts, alert_settings, start_time=high_precision_start_timestamp, reset_time=high_precision_reset_timestamp
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
return [tracking_stat]
|
|
630
|
-
|
|
631
|
-
def _generate_human_text_for_tracking(self, total_people: int, total_unique_count: int, config: PeopleCountingConfig, frame_id: str, alerts:Any=[], stream_info: Optional[Dict[str, Any]] = None) -> str:
|
|
632
|
-
"""Generate human-readable text for tracking stats in old format."""
|
|
633
|
-
from datetime import datetime, timezone
|
|
634
|
-
|
|
635
|
-
human_text_lines=[]
|
|
636
|
-
current_timestamp = self._get_current_timestamp_str(stream_info, precision=True, frame_id=frame_id)
|
|
637
|
-
start_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
|
638
|
-
|
|
639
|
-
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
|
|
640
|
-
human_text_lines.append(f"\t- People Detected: {total_people}")
|
|
641
334
|
|
|
335
|
+
human_text_lines = []
|
|
336
|
+
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
|
|
337
|
+
for cat, count in per_category_count.items():
|
|
338
|
+
human_text_lines.append(f"\t- People Detected: {count}")
|
|
642
339
|
human_text_lines.append("")
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
print(human_text_lines)
|
|
649
|
-
print('------------------HUMANNTEXTTT-------------------------')
|
|
650
|
-
|
|
340
|
+
human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
|
|
341
|
+
for cat, count in total_counts_dict.items():
|
|
342
|
+
if count > 0:
|
|
343
|
+
human_text_lines.append("")
|
|
344
|
+
human_text_lines.append(f"\t- Total unique people count: {count}")
|
|
651
345
|
if alerts:
|
|
652
346
|
for alert in alerts:
|
|
653
347
|
human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
|
|
654
348
|
else:
|
|
655
349
|
human_text_lines.append("Alerts: None")
|
|
656
|
-
|
|
657
|
-
return "\n".join(human_text_lines)
|
|
658
|
-
|
|
659
|
-
def _check_alerts(self, counting_summary: Dict, zone_analysis: Dict,
|
|
660
|
-
config: PeopleCountingConfig, frame_id: str) -> List[Dict]:
|
|
661
|
-
"""Check for alert conditions and generate alerts."""
|
|
662
|
-
def get_trend(data, lookback=900, threshold=0.6):
|
|
663
|
-
'''
|
|
664
|
-
Determine if the trend is ascending or descending based on actual value progression.
|
|
665
|
-
Now works with values 0,1,2,3 (not just binary).
|
|
666
|
-
'''
|
|
667
|
-
window = data[-lookback:] if len(data) >= lookback else data
|
|
668
|
-
if len(window) < 2:
|
|
669
|
-
return True # not enough data to determine trend
|
|
670
|
-
increasing = 0
|
|
671
|
-
total = 0
|
|
672
|
-
for i in range(1, len(window)):
|
|
673
|
-
if window[i] >= window[i - 1]:
|
|
674
|
-
increasing += 1
|
|
675
|
-
total += 1
|
|
676
|
-
ratio = increasing / total
|
|
677
|
-
if ratio >= threshold:
|
|
678
|
-
return True
|
|
679
|
-
elif ratio <= (1 - threshold):
|
|
680
|
-
return False
|
|
681
|
-
alerts = []
|
|
682
|
-
|
|
683
|
-
if not config.alert_config:
|
|
684
|
-
return alerts
|
|
685
|
-
|
|
686
|
-
total_people = counting_summary.get("total_objects", 0)
|
|
687
|
-
|
|
688
|
-
# Count threshold alerts
|
|
689
|
-
if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
|
|
690
|
-
|
|
691
|
-
for category, threshold in config.alert_config.count_thresholds.items():
|
|
692
|
-
if category == "all" and total_people >= threshold:
|
|
693
|
-
|
|
694
|
-
alerts.append({
|
|
695
|
-
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
696
|
-
"alert_id": "alert_"+category+'_'+frame_id,
|
|
697
|
-
"incident_category": self.CASE_TYPE,
|
|
698
|
-
"threshold_level": threshold,
|
|
699
|
-
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
|
700
|
-
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
701
|
-
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
|
702
|
-
}
|
|
703
|
-
})
|
|
704
|
-
elif category in counting_summary.get("by_category", {}):
|
|
705
|
-
count = counting_summary["by_category"][category]
|
|
350
|
+
human_text = "\n".join(human_text_lines)
|
|
706
351
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if hasattr(config.alert_config, 'occupancy_thresholds') and config.alert_config.occupancy_thresholds:
|
|
723
|
-
for zone_name, threshold in config.alert_config.occupancy_thresholds.items():
|
|
724
|
-
if zone_name in zone_analysis:
|
|
725
|
-
# Calculate zone_count robustly (supports int, list, dict values)
|
|
726
|
-
print('ZONEEE',zone_name, zone_analysis[zone_name])
|
|
727
|
-
zone_count = self._robust_zone_total(zone_analysis[zone_name])
|
|
728
|
-
if zone_count >= threshold:
|
|
729
|
-
alerts.append({
|
|
730
|
-
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
731
|
-
"alert_id": f"alert_zone_{zone_name}_{frame_id}",
|
|
732
|
-
"incident_category": f"{self.CASE_TYPE}_{zone_name}",
|
|
733
|
-
"threshold_level": threshold,
|
|
734
|
-
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
|
735
|
-
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
|
736
|
-
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
|
737
|
-
}
|
|
738
|
-
})
|
|
739
|
-
|
|
740
|
-
return alerts
|
|
352
|
+
reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
|
|
353
|
+
tracking_stat = self.create_tracking_stats(
|
|
354
|
+
total_counts=total_counts,
|
|
355
|
+
current_counts=current_counts,
|
|
356
|
+
detections=detections,
|
|
357
|
+
human_text=human_text,
|
|
358
|
+
camera_info=camera_info,
|
|
359
|
+
alerts=alerts,
|
|
360
|
+
alert_settings=alert_settings,
|
|
361
|
+
reset_settings=reset_settings,
|
|
362
|
+
start_time=high_precision_start_timestamp,
|
|
363
|
+
reset_time=high_precision_reset_timestamp
|
|
364
|
+
)
|
|
365
|
+
tracking_stats.append(tracking_stat)
|
|
366
|
+
return tracking_stats
|
|
741
367
|
|
|
742
|
-
def _generate_business_analytics(self, counting_summary: Dict,
|
|
743
|
-
|
|
368
|
+
def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: PeopleCountingConfig,
|
|
369
|
+
stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
|
|
744
370
|
if is_empty:
|
|
745
371
|
return []
|
|
746
|
-
business_analytics = []
|
|
747
|
-
|
|
748
|
-
total_people = counting_summary.get("total_objects", 0)
|
|
749
|
-
|
|
750
|
-
# Get camera info using standardized method
|
|
751
|
-
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
752
|
-
|
|
753
|
-
if total_people > 0 or config.enable_analytics:
|
|
754
|
-
# Calculate analytics statistics
|
|
755
|
-
analytics_stats = {
|
|
756
|
-
"people_count": total_people,
|
|
757
|
-
"unique_people_count": self.get_total_count(),
|
|
758
|
-
"current_frame_count": self.get_current_frame_count()
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
# Add zone analytics if available
|
|
762
|
-
if zone_analysis:
|
|
763
|
-
zone_stats = {}
|
|
764
|
-
for zone_name, zone_count in zone_analysis.items():
|
|
765
|
-
zone_total = self._robust_zone_total(zone_count)
|
|
766
|
-
zone_stats[f"{zone_name}_occupancy"] = zone_total
|
|
767
|
-
analytics_stats.update(zone_stats)
|
|
768
|
-
|
|
769
|
-
# Generate human text for analytics
|
|
770
|
-
current_timestamp = self._get_current_timestamp_str(stream_info, frame_id=frame_id)
|
|
771
|
-
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
772
|
-
|
|
773
|
-
analytics_human_text = self.generate_analytics_human_text(
|
|
774
|
-
"people_counting_analytics", analytics_stats, current_timestamp, start_timestamp
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
# Create business analytics using standardized method
|
|
778
|
-
analytics = self.create_business_analytics(
|
|
779
|
-
"people_counting_analytics", analytics_stats, analytics_human_text, camera_info
|
|
780
|
-
)
|
|
781
|
-
business_analytics.append(analytics)
|
|
782
|
-
|
|
783
|
-
return business_analytics
|
|
784
372
|
|
|
785
373
|
def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
|
|
786
374
|
"""
|
|
@@ -789,8 +377,8 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
789
377
|
lines = []
|
|
790
378
|
lines.append("Application Name: "+self.CASE_TYPE)
|
|
791
379
|
lines.append("Application Version: "+self.CASE_VERSION)
|
|
792
|
-
if len(incidents) > 0:
|
|
793
|
-
|
|
380
|
+
# if len(incidents) > 0:
|
|
381
|
+
# lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
|
|
794
382
|
if len(tracking_stats) > 0:
|
|
795
383
|
lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
|
|
796
384
|
if len(business_analytics) > 0:
|
|
@@ -800,523 +388,100 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
800
388
|
lines.append("Summary: "+"No Summary Data")
|
|
801
389
|
|
|
802
390
|
return ["\n".join(lines)]
|
|
803
|
-
|
|
804
|
-
def
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
"confidence_threshold": config.confidence_threshold,
|
|
814
|
-
"zones_analyzed": len(zone_analysis),
|
|
815
|
-
"detection_rate": 0.0,
|
|
816
|
-
"coverage_percentage": 0.0
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
# Calculate detection rate
|
|
820
|
-
if config.time_window_minutes and config.time_window_minutes > 0:
|
|
821
|
-
metrics["detection_rate"] = (total_people / config.time_window_minutes) * 60
|
|
822
|
-
|
|
823
|
-
# Calculate zone coverage
|
|
824
|
-
if zone_analysis and total_people > 0:
|
|
825
|
-
people_in_zones = 0
|
|
826
|
-
for zone_counts in zone_analysis.values():
|
|
827
|
-
if isinstance(zone_counts, dict):
|
|
828
|
-
for v in zone_counts.values():
|
|
829
|
-
if isinstance(v, int):
|
|
830
|
-
people_in_zones += v
|
|
831
|
-
elif isinstance(v, list):
|
|
832
|
-
people_in_zones += len(v)
|
|
833
|
-
elif isinstance(zone_counts, list):
|
|
834
|
-
people_in_zones += len(zone_counts)
|
|
835
|
-
elif isinstance(zone_counts, int):
|
|
836
|
-
people_in_zones += zone_counts
|
|
837
|
-
metrics["coverage_percentage"] = (people_in_zones / total_people) * 100
|
|
838
|
-
|
|
839
|
-
# Unique tracking metrics
|
|
840
|
-
if config.enable_unique_counting:
|
|
841
|
-
unique_count = self._count_unique_tracks(counting_summary, config)
|
|
842
|
-
if unique_count is not None:
|
|
843
|
-
metrics["unique_people"] = unique_count
|
|
844
|
-
metrics["tracking_efficiency"] = (unique_count / total_people) * 100 if total_people > 0 else 0
|
|
845
|
-
|
|
846
|
-
# Per-zone metrics
|
|
847
|
-
if zone_analysis:
|
|
848
|
-
zone_metrics = {}
|
|
849
|
-
for zone_name, zone_counts in zone_analysis.items():
|
|
850
|
-
# Robustly sum counts, handling dicts with int or list values
|
|
851
|
-
if isinstance(zone_counts, dict):
|
|
852
|
-
zone_total = 0
|
|
853
|
-
for v in zone_counts.values():
|
|
854
|
-
if isinstance(v, int):
|
|
855
|
-
zone_total += v
|
|
856
|
-
elif isinstance(v, list):
|
|
857
|
-
zone_total += len(v)
|
|
858
|
-
elif isinstance(zone_counts, list):
|
|
859
|
-
zone_total = len(zone_counts)
|
|
860
|
-
elif isinstance(zone_counts, int):
|
|
861
|
-
zone_total = zone_counts
|
|
862
|
-
else:
|
|
863
|
-
zone_total = 0
|
|
864
|
-
zone_metrics[zone_name] = {
|
|
865
|
-
"count": zone_total,
|
|
866
|
-
"percentage": (zone_total / total_people) * 100 if total_people > 0 else 0
|
|
867
|
-
}
|
|
868
|
-
metrics["zone_metrics"] = zone_metrics
|
|
869
|
-
|
|
870
|
-
return metrics
|
|
871
|
-
|
|
872
|
-
def _extract_predictions(self, data: Any) -> List[Dict[str, Any]]:
|
|
873
|
-
"""Extract predictions from processed data for API compatibility."""
|
|
874
|
-
predictions = []
|
|
875
|
-
|
|
876
|
-
try:
|
|
877
|
-
if isinstance(data, list):
|
|
878
|
-
# Detection format
|
|
879
|
-
for item in data:
|
|
880
|
-
prediction = self._normalize_prediction(item)
|
|
881
|
-
if prediction:
|
|
882
|
-
predictions.append(prediction)
|
|
883
|
-
|
|
884
|
-
elif isinstance(data, dict):
|
|
885
|
-
# Frame-based or tracking format
|
|
886
|
-
for frame_id, items in data.items():
|
|
887
|
-
if isinstance(items, list):
|
|
888
|
-
for item in items:
|
|
889
|
-
prediction = self._normalize_prediction(item)
|
|
890
|
-
if prediction:
|
|
891
|
-
prediction["frame_id"] = frame_id
|
|
892
|
-
predictions.append(prediction)
|
|
893
|
-
|
|
894
|
-
except Exception as e:
|
|
895
|
-
self.logger.warning(f"Failed to extract predictions: {str(e)}")
|
|
896
|
-
|
|
897
|
-
return predictions
|
|
898
|
-
|
|
899
|
-
def _normalize_prediction(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
|
900
|
-
"""Normalize a single prediction item."""
|
|
901
|
-
if not isinstance(item, dict):
|
|
902
|
-
return {}
|
|
903
|
-
|
|
391
|
+
|
|
392
|
+
def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
|
|
393
|
+
frame_track_ids = set()
|
|
394
|
+
for det in detections:
|
|
395
|
+
tid = det.get('track_id')
|
|
396
|
+
if tid is not None:
|
|
397
|
+
frame_track_ids.add(tid)
|
|
398
|
+
total_track_ids = set()
|
|
399
|
+
for s in getattr(self, '_per_category_total_track_ids', {}).values():
|
|
400
|
+
total_track_ids.update(s)
|
|
904
401
|
return {
|
|
905
|
-
"
|
|
906
|
-
"
|
|
907
|
-
"
|
|
908
|
-
"
|
|
402
|
+
"total_count": len(total_track_ids),
|
|
403
|
+
"current_frame_count": len(frame_track_ids),
|
|
404
|
+
"total_unique_track_ids": len(total_track_ids),
|
|
405
|
+
"current_frame_track_ids": list(frame_track_ids),
|
|
406
|
+
"last_update_time": time.time(),
|
|
407
|
+
"total_frames_processed": getattr(self, '_total_frame_counter', 0)
|
|
909
408
|
}
|
|
910
|
-
|
|
911
|
-
def _get_detections_with_confidence(self, counting_summary: Dict) -> List[Dict]:
|
|
912
|
-
"""Extract detection items with confidence scores."""
|
|
913
|
-
return counting_summary.get("detections", [])
|
|
914
|
-
|
|
915
|
-
def _count_unique_tracks(self, counting_summary: Dict, config: PeopleCountingConfig = None) -> Optional[int]:
|
|
916
|
-
"""Count unique tracks if tracking is enabled."""
|
|
917
|
-
# Always update tracking state regardless of enable_unique_counting setting
|
|
918
|
-
self._update_tracking_state(counting_summary)
|
|
919
|
-
|
|
920
|
-
# Only return the count if unique counting is enabled
|
|
921
|
-
if config and config.enable_unique_counting:
|
|
922
|
-
return self._total_count if self._total_count > 0 else None
|
|
923
|
-
else:
|
|
924
|
-
return None
|
|
925
|
-
|
|
926
|
-
def _update_tracking_state(self, counting_summary: Dict) -> None:
|
|
927
|
-
"""Update tracking state with current frame data with 3-frame confirmation.
|
|
928
|
-
|
|
929
|
-
Behavior:
|
|
930
|
-
- Prefer tracker-provided track_id when available (from AdvancedTracker).
|
|
931
|
-
- Otherwise use IoU-based canonical aliasing with tight person-specific thresholds.
|
|
932
|
-
- Only add a canonical_id to cumulative total after it appears in 3 consecutive frames.
|
|
933
|
-
- Cumulative totals never decrease.
|
|
934
|
-
"""
|
|
935
|
-
detections = self._get_detections_with_confidence(counting_summary)
|
|
936
|
-
|
|
937
|
-
if not detections:
|
|
938
|
-
# If no detections this frame, decay consecutive counters softly rather than clearing,
|
|
939
|
-
# so brief detector dropouts don't reset confirmation progress.
|
|
940
|
-
for tid in list(self._consecutive_track_frames.keys()):
|
|
941
|
-
self._consecutive_track_frames[tid] = max(0, self._consecutive_track_frames[tid] - 1)
|
|
942
|
-
self._current_frame_track_ids = set()
|
|
943
|
-
self._last_update_time = time.time()
|
|
944
|
-
return
|
|
945
|
-
|
|
946
|
-
current_frame_tracks: Set[Any] = set()
|
|
947
|
-
|
|
948
|
-
ephemeral_seq = 0
|
|
949
|
-
for detection in detections:
|
|
950
|
-
raw_track_id = detection.get("track_id")
|
|
951
|
-
bbox = detection.get("bounding_box", detection.get("bbox"))
|
|
952
|
-
if not bbox:
|
|
953
|
-
continue
|
|
954
409
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
410
|
+
def _update_tracking_state(self, detections: list):
|
|
411
|
+
if not hasattr(self, "_per_category_total_track_ids"):
|
|
412
|
+
self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
|
|
413
|
+
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
959
414
|
|
|
415
|
+
for det in detections:
|
|
416
|
+
cat = det.get("category")
|
|
417
|
+
raw_track_id = det.get("track_id")
|
|
418
|
+
if cat not in self.target_categories or raw_track_id is None:
|
|
419
|
+
continue
|
|
420
|
+
bbox = det.get("bounding_box", det.get("bbox"))
|
|
960
421
|
canonical_id = self._merge_or_register_track(raw_track_id, bbox)
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
# Update consecutive presence counts for confirmation
|
|
965
|
-
updated_consecutive: Dict[Any, int] = {}
|
|
966
|
-
for tid in current_frame_tracks:
|
|
967
|
-
prev = self._consecutive_track_frames.get(tid, 0)
|
|
968
|
-
updated_consecutive[tid] = min(self._min_confirm_frames, prev + 1)
|
|
969
|
-
# carry over decayed counts for those not seen this frame (bounded by 0)
|
|
970
|
-
for tid, prev in self._consecutive_track_frames.items():
|
|
971
|
-
if tid not in updated_consecutive:
|
|
972
|
-
updated_consecutive[tid] = max(0, prev - 1)
|
|
973
|
-
self._consecutive_track_frames = updated_consecutive
|
|
974
|
-
|
|
975
|
-
# Promote confirmed tracks to cumulative unique set
|
|
976
|
-
for tid, count in self._consecutive_track_frames.items():
|
|
977
|
-
if count >= self._min_confirm_frames:
|
|
978
|
-
if tid not in self._total_track_ids:
|
|
979
|
-
self._total_track_ids.add(tid)
|
|
980
|
-
|
|
981
|
-
# Overwrite current-frame set
|
|
982
|
-
self._current_frame_track_ids = current_frame_tracks
|
|
983
|
-
self._last_update_time = time.time()
|
|
984
|
-
|
|
985
|
-
# Cumulative total never decreases
|
|
986
|
-
self._total_count = len(self._total_track_ids)
|
|
987
|
-
|
|
988
|
-
def _generate_ephemeral_track_id(self, bbox: Any, seq: int) -> str:
|
|
989
|
-
"""Create a short-lived raw track id for detections without a track_id.
|
|
990
|
-
|
|
991
|
-
Combines a coarse hash of the bbox geometry with a per-call sequence and
|
|
992
|
-
a millisecond timestamp, so the same person across adjacent frames will
|
|
993
|
-
still be merged to the same canonical track via IoU and time window,
|
|
994
|
-
while avoiding long-lived ID collisions across distant calls.
|
|
995
|
-
"""
|
|
996
|
-
try:
|
|
997
|
-
# Normalize bbox to xyxy list for hashing
|
|
998
|
-
if isinstance(bbox, dict):
|
|
999
|
-
if "x1" in bbox:
|
|
1000
|
-
xyxy = [bbox.get("x1"), bbox.get("y1"), bbox.get("x2"), bbox.get("y2")]
|
|
1001
|
-
elif "xmin" in bbox:
|
|
1002
|
-
xyxy = [bbox.get("xmin"), bbox.get("ymin"), bbox.get("xmax"), bbox.get("ymax")]
|
|
1003
|
-
else:
|
|
1004
|
-
values = list(bbox.values())
|
|
1005
|
-
xyxy = values[:4] if len(values) >= 4 else []
|
|
1006
|
-
elif isinstance(bbox, list):
|
|
1007
|
-
xyxy = bbox[:4]
|
|
1008
|
-
else:
|
|
1009
|
-
xyxy = []
|
|
1010
|
-
|
|
1011
|
-
if len(xyxy) < 4:
|
|
1012
|
-
xyxy = [0, 0, 0, 0]
|
|
1013
|
-
|
|
1014
|
-
x1, y1, x2, y2 = xyxy
|
|
1015
|
-
# Coarse-quantize geometry to stabilize hash across minor jitter
|
|
1016
|
-
cx = int(round((float(x1) + float(x2)) / 2.0))
|
|
1017
|
-
cy = int(round((float(y1) + float(y2)) / 2.0))
|
|
1018
|
-
w = int(round(abs(float(x2) - float(x1))))
|
|
1019
|
-
h = int(round(abs(float(y2) - float(y1))))
|
|
1020
|
-
geom_token = f"{cx}_{cy}_{w}_{h}"
|
|
1021
|
-
except Exception:
|
|
1022
|
-
geom_token = "0_0_0_0"
|
|
1023
|
-
|
|
1024
|
-
ms = int(time.time() * 1000)
|
|
1025
|
-
return f"tmp_{ms}_{seq}_{abs(hash(geom_token)) % 1000003}"
|
|
1026
|
-
|
|
1027
|
-
def get_total_count(self) -> int:
|
|
1028
|
-
"""Get the total count of unique people tracked across all calls."""
|
|
1029
|
-
return self._total_count
|
|
1030
|
-
|
|
1031
|
-
def get_current_frame_count(self) -> int:
|
|
1032
|
-
"""Get the count of people in the current frame."""
|
|
1033
|
-
return len(self._current_frame_track_ids)
|
|
1034
|
-
|
|
1035
|
-
def get_total_frames_processed(self) -> int:
|
|
1036
|
-
"""Get the total number of frames processed across all calls."""
|
|
1037
|
-
return self._total_frame_counter
|
|
1038
|
-
|
|
1039
|
-
def set_global_frame_offset(self, offset: int) -> None:
|
|
1040
|
-
"""Set the global frame offset for video chunk processing."""
|
|
1041
|
-
self._global_frame_offset = offset
|
|
1042
|
-
self.logger.info(f"Global frame offset set to: {offset}")
|
|
1043
|
-
|
|
1044
|
-
def get_global_frame_offset(self) -> int:
|
|
1045
|
-
"""Get the current global frame offset."""
|
|
1046
|
-
return self._global_frame_offset
|
|
1047
|
-
|
|
1048
|
-
def update_global_frame_offset(self, frames_in_chunk: int) -> None:
|
|
1049
|
-
"""Update global frame offset after processing a chunk."""
|
|
1050
|
-
old_offset = self._global_frame_offset
|
|
1051
|
-
self._global_frame_offset += frames_in_chunk
|
|
1052
|
-
self.logger.info(f"Global frame offset updated: {old_offset} -> {self._global_frame_offset} (added {frames_in_chunk} frames)")
|
|
1053
|
-
|
|
1054
|
-
def get_global_frame_id(self, local_frame_id: str) -> str:
|
|
1055
|
-
"""Convert local frame ID to global frame ID."""
|
|
1056
|
-
try:
|
|
1057
|
-
# Try to convert local_frame_id to integer
|
|
1058
|
-
local_frame_num = int(local_frame_id)
|
|
1059
|
-
global_frame_num = local_frame_num #+ self._global_frame_offset
|
|
1060
|
-
return str(global_frame_num)
|
|
1061
|
-
except (ValueError, TypeError):
|
|
1062
|
-
# If local_frame_id is not a number (e.g., timestamp), return as is
|
|
1063
|
-
return local_frame_id
|
|
1064
|
-
|
|
1065
|
-
def get_track_ids_info(self) -> Dict[str, Any]:
|
|
1066
|
-
"""Get detailed information about track IDs."""
|
|
1067
|
-
return {
|
|
1068
|
-
"total_count": self._total_count,
|
|
1069
|
-
"current_frame_count": len(self._current_frame_track_ids),
|
|
1070
|
-
"total_unique_track_ids": len(self._total_track_ids),
|
|
1071
|
-
"current_frame_track_ids": list(self._current_frame_track_ids),
|
|
1072
|
-
"last_update_time": self._last_update_time,
|
|
1073
|
-
"total_frames_processed": self._total_frame_counter
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
def get_tracking_debug_info(self) -> Dict[str, Any]:
|
|
1077
|
-
"""Get detailed debugging information about tracking state."""
|
|
1078
|
-
return {
|
|
1079
|
-
"total_track_ids": list(self._total_track_ids),
|
|
1080
|
-
"current_frame_track_ids": list(self._current_frame_track_ids),
|
|
1081
|
-
"total_count": self._total_count,
|
|
1082
|
-
"current_frame_count": len(self._current_frame_track_ids),
|
|
1083
|
-
"total_frames_processed": self._total_frame_counter,
|
|
1084
|
-
"last_update_time": self._last_update_time,
|
|
1085
|
-
"zone_current_track_ids": {zone: list(tracks) for zone, tracks in self._zone_current_track_ids.items()},
|
|
1086
|
-
"zone_total_track_ids": {zone: list(tracks) for zone, tracks in self._zone_total_track_ids.items()},
|
|
1087
|
-
"zone_current_counts": self._zone_current_counts.copy(),
|
|
1088
|
-
"zone_total_counts": self._zone_total_counts.copy(),
|
|
1089
|
-
"global_frame_offset": self._global_frame_offset,
|
|
1090
|
-
"frames_in_current_chunk": self._frames_in_current_chunk
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
def get_frame_info(self) -> Dict[str, Any]:
|
|
1094
|
-
"""Get detailed information about frame processing and global frame offset."""
|
|
1095
|
-
return {
|
|
1096
|
-
"global_frame_offset": self._global_frame_offset,
|
|
1097
|
-
"total_frames_processed": self._total_frame_counter,
|
|
1098
|
-
"frames_in_current_chunk": self._frames_in_current_chunk,
|
|
1099
|
-
"next_global_frame": self._global_frame_offset + self._frames_in_current_chunk
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
def reset_tracking_state(self) -> None:
|
|
1103
|
-
"""
|
|
1104
|
-
WARNING: This completely resets ALL tracking data including cumulative totals!
|
|
1105
|
-
|
|
1106
|
-
This should ONLY be used when:
|
|
1107
|
-
- Starting a completely new tracking session
|
|
1108
|
-
- Switching to a different video/stream
|
|
1109
|
-
- Manual reset requested by user
|
|
1110
|
-
|
|
1111
|
-
For clearing expired/stale tracks, use clear_current_frame_tracking() instead.
|
|
1112
|
-
"""
|
|
1113
|
-
self._total_track_ids.clear()
|
|
1114
|
-
self._current_frame_track_ids.clear()
|
|
1115
|
-
self._total_count = 0
|
|
1116
|
-
self._last_update_time = time.time()
|
|
1117
|
-
|
|
1118
|
-
# Clear zone tracking data
|
|
1119
|
-
self._zone_current_track_ids.clear()
|
|
1120
|
-
self._zone_total_track_ids.clear()
|
|
1121
|
-
self._zone_current_counts.clear()
|
|
1122
|
-
self._zone_total_counts.clear()
|
|
1123
|
-
|
|
1124
|
-
# Reset frame counter and global frame offset
|
|
1125
|
-
self._total_frame_counter = 0
|
|
1126
|
-
self._global_frame_offset = 0
|
|
1127
|
-
self._frames_in_current_chunk = 0
|
|
422
|
+
det["track_id"] = canonical_id
|
|
423
|
+
self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
|
|
424
|
+
self._current_frame_track_ids[cat].add(canonical_id)
|
|
1128
425
|
|
|
1129
|
-
|
|
1130
|
-
self.
|
|
1131
|
-
self._track_aliases.clear()
|
|
1132
|
-
self._tracking_start_time = None
|
|
1133
|
-
|
|
1134
|
-
self.logger.warning(" FULL tracking state reset - all track IDs, zone data, frame counter, and global frame offset cleared. Cumulative totals lost!")
|
|
1135
|
-
|
|
1136
|
-
def clear_current_frame_tracking(self) -> int:
|
|
1137
|
-
"""
|
|
1138
|
-
MANUAL USE ONLY: Clear only current frame tracking data while preserving cumulative totals.
|
|
1139
|
-
|
|
1140
|
-
This method is NOT called automatically anywhere in the code.
|
|
1141
|
-
|
|
1142
|
-
This is the SAFE method to use for manual clearing of stale/expired current frame data.
|
|
1143
|
-
The cumulative total (self._total_count) is always preserved.
|
|
1144
|
-
|
|
1145
|
-
In streaming scenarios, you typically don't need to call this at all.
|
|
1146
|
-
|
|
1147
|
-
Returns:
|
|
1148
|
-
Number of current frame tracks cleared
|
|
1149
|
-
"""
|
|
1150
|
-
old_current_count = len(self._current_frame_track_ids)
|
|
1151
|
-
self._current_frame_track_ids.clear()
|
|
1152
|
-
|
|
1153
|
-
# Clear current zone tracking (but keep total zone tracking)
|
|
1154
|
-
cleared_zone_tracks = 0
|
|
1155
|
-
for zone_name in list(self._zone_current_track_ids.keys()):
|
|
1156
|
-
cleared_zone_tracks += len(self._zone_current_track_ids[zone_name])
|
|
1157
|
-
self._zone_current_track_ids[zone_name].clear()
|
|
1158
|
-
self._zone_current_counts[zone_name] = 0
|
|
1159
|
-
|
|
1160
|
-
# Update timestamp
|
|
1161
|
-
self._last_update_time = time.time()
|
|
1162
|
-
|
|
1163
|
-
self.logger.info(f"Cleared {old_current_count} current frame tracks and {cleared_zone_tracks} zone current tracks. Cumulative total preserved: {self._total_count}")
|
|
1164
|
-
return old_current_count
|
|
1165
|
-
|
|
1166
|
-
def reset_frame_counter(self) -> None:
|
|
1167
|
-
"""Reset only the frame counter."""
|
|
1168
|
-
old_count = self._total_frame_counter
|
|
1169
|
-
self._total_frame_counter = 0
|
|
1170
|
-
self.logger.info(f"Frame counter reset from {old_count} to 0")
|
|
1171
|
-
|
|
1172
|
-
def clear_expired_tracks(self, max_age_seconds: float = 300.0) -> int:
|
|
1173
|
-
"""
|
|
1174
|
-
MANUAL USE ONLY: Clear current frame tracking data if no updates for a while.
|
|
1175
|
-
|
|
1176
|
-
This method is NOT called automatically anywhere in the code.
|
|
1177
|
-
It's provided as a utility function for manual cleanup if needed.
|
|
1178
|
-
|
|
1179
|
-
In streaming scenarios, you typically don't need to call this at all.
|
|
1180
|
-
The cumulative total should keep growing as new unique people are detected.
|
|
1181
|
-
|
|
1182
|
-
This method only clears current frame tracking data while preserving
|
|
1183
|
-
the cumulative total count. The cumulative total should never decrease.
|
|
1184
|
-
|
|
1185
|
-
Args:
|
|
1186
|
-
max_age_seconds: Maximum age in seconds before clearing current frame tracks
|
|
1187
|
-
|
|
1188
|
-
Returns:
|
|
1189
|
-
Number of current frame tracks cleared
|
|
1190
|
-
"""
|
|
1191
|
-
current_time = time.time()
|
|
1192
|
-
if current_time - self._last_update_time > max_age_seconds:
|
|
1193
|
-
# Use the safe method that preserves cumulative totals
|
|
1194
|
-
cleared_count = self.clear_current_frame_tracking()
|
|
1195
|
-
self.logger.info(f"Manual cleanup: cleared {cleared_count} expired current frame tracks (age > {max_age_seconds}s)")
|
|
1196
|
-
return cleared_count
|
|
1197
|
-
return 0
|
|
1198
|
-
|
|
1199
|
-
def _update_zone_tracking(self, zone_analysis: Dict[str, Dict[str, int]], detections: List[Dict], config: PeopleCountingConfig) -> Dict[str, Dict[str, Any]]:
|
|
1200
|
-
"""
|
|
1201
|
-
Update zone tracking with current frame data.
|
|
1202
|
-
|
|
1203
|
-
Args:
|
|
1204
|
-
zone_analysis: Current zone analysis results
|
|
1205
|
-
detections: List of detections with track IDs
|
|
1206
|
-
config: People counting configuration with zone polygons
|
|
1207
|
-
|
|
1208
|
-
Returns:
|
|
1209
|
-
Enhanced zone analysis with tracking information
|
|
1210
|
-
"""
|
|
1211
|
-
if not zone_analysis or not config.zone_config or not config.zone_config.zones:
|
|
1212
|
-
return {}
|
|
1213
|
-
|
|
1214
|
-
enhanced_zone_analysis = {}
|
|
1215
|
-
zones = config.zone_config.zones
|
|
1216
|
-
|
|
1217
|
-
# Get current frame track IDs in each zone
|
|
1218
|
-
current_frame_zone_tracks = {}
|
|
1219
|
-
|
|
1220
|
-
# Initialize zone tracking for all zones
|
|
1221
|
-
for zone_name in zones.keys():
|
|
1222
|
-
current_frame_zone_tracks[zone_name] = set()
|
|
1223
|
-
if zone_name not in self._zone_current_track_ids:
|
|
1224
|
-
self._zone_current_track_ids[zone_name] = set()
|
|
1225
|
-
if zone_name not in self._zone_total_track_ids:
|
|
1226
|
-
self._zone_total_track_ids[zone_name] = set()
|
|
1227
|
-
|
|
1228
|
-
# Check each detection against each zone
|
|
1229
|
-
for detection in detections:
|
|
1230
|
-
track_id = detection.get("track_id")
|
|
1231
|
-
if track_id is None:
|
|
1232
|
-
continue
|
|
1233
|
-
|
|
1234
|
-
# Get detection bbox
|
|
1235
|
-
bbox = detection.get("bounding_box", detection.get("bbox"))
|
|
1236
|
-
if not bbox:
|
|
1237
|
-
continue
|
|
1238
|
-
|
|
1239
|
-
# Get detection center point
|
|
1240
|
-
center_point = get_bbox_bottom25_center(bbox) #get_bbox_center(bbox)
|
|
1241
|
-
|
|
1242
|
-
# Check which zone this detection is in using actual zone polygons
|
|
1243
|
-
for zone_name, zone_polygon in zones.items():
|
|
1244
|
-
# Convert polygon points to tuples for point_in_polygon function
|
|
1245
|
-
# zone_polygon format: [[x1, y1], [x2, y2], [x3, y3], ...]
|
|
1246
|
-
polygon_points = [(point[0], point[1]) for point in zone_polygon]
|
|
1247
|
-
|
|
1248
|
-
# Check if detection center is inside the zone polygon using ray casting algorithm
|
|
1249
|
-
if point_in_polygon(center_point, polygon_points):
|
|
1250
|
-
current_frame_zone_tracks[zone_name].add(track_id)
|
|
1251
|
-
|
|
1252
|
-
# Update zone tracking for each zone
|
|
1253
|
-
for zone_name, zone_counts in zone_analysis.items():
|
|
1254
|
-
# Get current frame tracks for this zone
|
|
1255
|
-
current_tracks = current_frame_zone_tracks.get(zone_name, set())
|
|
1256
|
-
|
|
1257
|
-
# Update current zone tracks
|
|
1258
|
-
self._zone_current_track_ids[zone_name] = current_tracks
|
|
1259
|
-
|
|
1260
|
-
# Update total zone tracks (accumulate all track IDs that have been in this zone)
|
|
1261
|
-
self._zone_total_track_ids[zone_name].update(current_tracks)
|
|
1262
|
-
|
|
1263
|
-
# Update counts
|
|
1264
|
-
self._zone_current_counts[zone_name] = len(current_tracks)
|
|
1265
|
-
self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
|
|
1266
|
-
|
|
1267
|
-
# Create enhanced zone analysis
|
|
1268
|
-
enhanced_zone_analysis[zone_name] = {
|
|
1269
|
-
"current_count": self._zone_current_counts[zone_name],
|
|
1270
|
-
"total_count": self._zone_total_counts[zone_name],
|
|
1271
|
-
"current_track_ids": list(current_tracks),
|
|
1272
|
-
"total_track_ids": list(self._zone_total_track_ids[zone_name]),
|
|
1273
|
-
"original_counts": zone_counts # Preserve original zone counts
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return enhanced_zone_analysis
|
|
1277
|
-
|
|
1278
|
-
def get_zone_tracking_info(self) -> Dict[str, Dict[str, Any]]:
|
|
1279
|
-
"""Get detailed zone tracking information."""
|
|
1280
|
-
return {
|
|
1281
|
-
zone_name: {
|
|
1282
|
-
"current_count": self._zone_current_counts.get(zone_name, 0),
|
|
1283
|
-
"total_count": self._zone_total_counts.get(zone_name, 0),
|
|
1284
|
-
"current_track_ids": list(self._zone_current_track_ids.get(zone_name, set())),
|
|
1285
|
-
"total_track_ids": list(self._zone_total_track_ids.get(zone_name, set()))
|
|
1286
|
-
}
|
|
1287
|
-
for zone_name in set(self._zone_current_counts.keys()) | set(self._zone_total_counts.keys())
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
def get_zone_current_count(self, zone_name: str) -> int:
|
|
1291
|
-
"""Get current count of people in a specific zone."""
|
|
1292
|
-
return self._zone_current_counts.get(zone_name, 0)
|
|
1293
|
-
|
|
1294
|
-
def get_zone_total_count(self, zone_name: str) -> int:
|
|
1295
|
-
"""Get total count of people who have been in a specific zone."""
|
|
1296
|
-
return self._zone_total_counts.get(zone_name, 0)
|
|
1297
|
-
|
|
1298
|
-
def get_all_zone_counts(self) -> Dict[str, Dict[str, int]]:
|
|
1299
|
-
"""Get current and total counts for all zones."""
|
|
1300
|
-
return {
|
|
1301
|
-
zone_name: {
|
|
1302
|
-
"current": self._zone_current_counts.get(zone_name, 0),
|
|
1303
|
-
"total": self._zone_total_counts.get(zone_name, 0)
|
|
1304
|
-
}
|
|
1305
|
-
for zone_name in set(self._zone_current_counts.keys()) | set(self._zone_total_counts.keys())
|
|
1306
|
-
}
|
|
426
|
+
def get_total_counts(self):
|
|
427
|
+
return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
|
|
1307
428
|
|
|
1308
429
|
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
|
1309
|
-
|
|
1310
|
-
dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
|
|
430
|
+
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
1311
431
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1312
432
|
|
|
1313
433
|
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
|
1314
|
-
"""Format timestamp for video chunks (HH:MM:SS.ms format)."""
|
|
1315
434
|
hours = int(timestamp // 3600)
|
|
1316
435
|
minutes = int((timestamp % 3600) // 60)
|
|
1317
|
-
seconds = round(float(timestamp % 60),2)
|
|
436
|
+
seconds = round(float(timestamp % 60), 2)
|
|
1318
437
|
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
1319
438
|
|
|
439
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
440
|
+
"""Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
|
|
441
|
+
|
|
442
|
+
The input can be either:
|
|
443
|
+
1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
|
|
444
|
+
string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
445
|
+
2. A string already following the same layout.
|
|
446
|
+
|
|
447
|
+
The returned value preserves the overall format of the input but truncates or pads
|
|
448
|
+
the fractional seconds portion to **exactly two digits**.
|
|
449
|
+
|
|
450
|
+
Example
|
|
451
|
+
-------
|
|
452
|
+
>>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
|
|
453
|
+
'2025-08-19-04:22:47.18 UTC'
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
# Convert numeric timestamps to the expected string representation first
|
|
457
|
+
if isinstance(timestamp, (int, float)):
|
|
458
|
+
timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
459
|
+
'%Y-%m-%d-%H:%M:%S.%f UTC'
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Ensure we are working with a string from here on
|
|
463
|
+
if not isinstance(timestamp, str):
|
|
464
|
+
return str(timestamp)
|
|
465
|
+
|
|
466
|
+
# If there is no fractional component, simply return the original string
|
|
467
|
+
if '.' not in timestamp:
|
|
468
|
+
return timestamp
|
|
469
|
+
|
|
470
|
+
# Split out the main portion (up to the decimal point)
|
|
471
|
+
main_part, fractional_and_suffix = timestamp.split('.', 1)
|
|
472
|
+
|
|
473
|
+
# Separate fractional digits from the suffix (typically ' UTC')
|
|
474
|
+
if ' ' in fractional_and_suffix:
|
|
475
|
+
fractional_part, suffix = fractional_and_suffix.split(' ', 1)
|
|
476
|
+
suffix = ' ' + suffix # Re-attach the space removed by split
|
|
477
|
+
else:
|
|
478
|
+
fractional_part, suffix = fractional_and_suffix, ''
|
|
479
|
+
|
|
480
|
+
# Guarantee exactly two digits for the fractional part
|
|
481
|
+
fractional_part = (fractional_part + '00')[:2]
|
|
482
|
+
|
|
483
|
+
return f"{main_part}.{fractional_part}{suffix}"
|
|
484
|
+
|
|
1320
485
|
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
1321
486
|
"""Get formatted current timestamp based on stream type."""
|
|
1322
487
|
|
|
@@ -1330,7 +495,6 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1330
495
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
1331
496
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
1332
497
|
|
|
1333
|
-
|
|
1334
498
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
1335
499
|
else:
|
|
1336
500
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
@@ -1342,7 +506,8 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1342
506
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
1343
507
|
|
|
1344
508
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
1345
|
-
|
|
509
|
+
|
|
510
|
+
|
|
1346
511
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
1347
512
|
else:
|
|
1348
513
|
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
@@ -1361,26 +526,60 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1361
526
|
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
1362
527
|
if not stream_info:
|
|
1363
528
|
return "00:00:00"
|
|
1364
|
-
|
|
529
|
+
|
|
1365
530
|
if precision:
|
|
1366
531
|
if self.start_timer is None:
|
|
1367
|
-
|
|
532
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
533
|
+
if not candidate or candidate == "NA":
|
|
534
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
535
|
+
self.start_timer = candidate
|
|
1368
536
|
return self._format_timestamp(self.start_timer)
|
|
1369
537
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1370
|
-
|
|
538
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
539
|
+
if not candidate or candidate == "NA":
|
|
540
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
541
|
+
self.start_timer = candidate
|
|
1371
542
|
return self._format_timestamp(self.start_timer)
|
|
1372
543
|
else:
|
|
1373
544
|
return self._format_timestamp(self.start_timer)
|
|
1374
545
|
|
|
1375
546
|
if self.start_timer is None:
|
|
1376
|
-
|
|
547
|
+
# Prefer direct input_settings.stream_time if available and not NA
|
|
548
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
549
|
+
if not candidate or candidate == "NA":
|
|
550
|
+
# Fallback to nested stream_info.stream_time used by current timestamp path
|
|
551
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
552
|
+
if stream_time_str:
|
|
553
|
+
try:
|
|
554
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
555
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
556
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
557
|
+
candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
558
|
+
except:
|
|
559
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
560
|
+
else:
|
|
561
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
562
|
+
self.start_timer = candidate
|
|
1377
563
|
return self._format_timestamp(self.start_timer)
|
|
1378
564
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1379
|
-
|
|
565
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
566
|
+
if not candidate or candidate == "NA":
|
|
567
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
568
|
+
if stream_time_str:
|
|
569
|
+
try:
|
|
570
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
571
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
572
|
+
ts = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
573
|
+
candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
574
|
+
except:
|
|
575
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
576
|
+
else:
|
|
577
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
578
|
+
self.start_timer = candidate
|
|
1380
579
|
return self._format_timestamp(self.start_timer)
|
|
1381
580
|
|
|
1382
581
|
else:
|
|
1383
|
-
if self.start_timer is not None:
|
|
582
|
+
if self.start_timer is not None and self.start_timer != "NA":
|
|
1384
583
|
return self._format_timestamp(self.start_timer)
|
|
1385
584
|
|
|
1386
585
|
if self._tracking_start_time is None:
|
|
@@ -1398,49 +597,38 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1398
597
|
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
|
1399
598
|
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
1400
599
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1401
|
-
|
|
1402
|
-
def _extract_frame_id_from_tracking(self, frame_detections: List[Dict], frame_key: str) -> str:
|
|
1403
|
-
"""Extract frame ID from tracking data."""
|
|
1404
|
-
# Priority 1: Check if detections have frame information
|
|
1405
|
-
if frame_detections and len(frame_detections) > 0:
|
|
1406
|
-
first_detection = frame_detections[0]
|
|
1407
|
-
if "frame" in first_detection:
|
|
1408
|
-
return str(first_detection["frame"])
|
|
1409
|
-
elif "frame_id" in first_detection:
|
|
1410
|
-
return str(first_detection["frame_id"])
|
|
1411
|
-
# Priority 2: Use frame_key from input data
|
|
1412
|
-
return str(frame_key)
|
|
1413
|
-
|
|
1414
|
-
def _robust_zone_total(self, zone_count):
|
|
1415
|
-
"""Helper method to robustly calculate zone total."""
|
|
1416
|
-
if isinstance(zone_count, dict):
|
|
1417
|
-
total = 0
|
|
1418
|
-
for v in zone_count.values():
|
|
1419
|
-
if isinstance(v, int):
|
|
1420
|
-
total += v
|
|
1421
|
-
elif isinstance(v, list):
|
|
1422
|
-
total += len(v)
|
|
1423
|
-
return total
|
|
1424
|
-
elif isinstance(zone_count, list):
|
|
1425
|
-
return len(zone_count)
|
|
1426
|
-
elif isinstance(zone_count, int):
|
|
1427
|
-
return zone_count
|
|
1428
|
-
else:
|
|
1429
|
-
return 0
|
|
1430
|
-
|
|
1431
|
-
# --------------------------------------------------------------------- #
|
|
1432
|
-
# Private helpers for canonical track aliasing #
|
|
1433
|
-
# --------------------------------------------------------------------- #
|
|
1434
600
|
|
|
1435
|
-
def
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
601
|
+
def _count_categories(self, detections: list, config: PeopleCountingConfig) -> dict:
|
|
602
|
+
counts = {}
|
|
603
|
+
for det in detections:
|
|
604
|
+
cat = det.get('category', 'unknown')
|
|
605
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
606
|
+
return {
|
|
607
|
+
"total_count": sum(counts.values()),
|
|
608
|
+
"per_category_count": counts,
|
|
609
|
+
"detections": [
|
|
610
|
+
{
|
|
611
|
+
"bounding_box": det.get("bounding_box"),
|
|
612
|
+
"category": det.get("category"),
|
|
613
|
+
"confidence": det.get("confidence"),
|
|
614
|
+
"track_id": det.get("track_id"),
|
|
615
|
+
"frame_id": det.get("frame_id")
|
|
616
|
+
}
|
|
617
|
+
for det in detections
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
|
|
622
|
+
return [
|
|
623
|
+
{
|
|
624
|
+
"category": det.get("category", "unknown"),
|
|
625
|
+
"confidence": det.get("confidence", 0.0),
|
|
626
|
+
"bounding_box": det.get("bounding_box", {})
|
|
627
|
+
}
|
|
628
|
+
for det in detections
|
|
629
|
+
]
|
|
1442
630
|
|
|
1443
|
-
|
|
631
|
+
def _compute_iou(self, box1: Any, box2: Any) -> float:
|
|
1444
632
|
def _bbox_to_list(bbox):
|
|
1445
633
|
if bbox is None:
|
|
1446
634
|
return []
|
|
@@ -1451,54 +639,36 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1451
639
|
return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
|
|
1452
640
|
if "x1" in bbox:
|
|
1453
641
|
return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
|
|
1454
|
-
|
|
1455
|
-
values = list(bbox.values())
|
|
642
|
+
values = [v for v in bbox.values() if isinstance(v, (int, float))]
|
|
1456
643
|
return values[:4] if len(values) >= 4 else []
|
|
1457
|
-
# Unsupported type
|
|
1458
644
|
return []
|
|
1459
645
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if len(list1) < 4 or len(list2) < 4:
|
|
646
|
+
l1 = _bbox_to_list(box1)
|
|
647
|
+
l2 = _bbox_to_list(box2)
|
|
648
|
+
if len(l1) < 4 or len(l2) < 4:
|
|
1464
649
|
return 0.0
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
x2_min, y2_min, x2_max, y2_max = list2
|
|
1468
|
-
|
|
1469
|
-
# Ensure correct ordering of coordinates
|
|
650
|
+
x1_min, y1_min, x1_max, y1_max = l1
|
|
651
|
+
x2_min, y2_min, x2_max, y2_max = l2
|
|
1470
652
|
x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
|
|
1471
653
|
y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
|
|
1472
654
|
x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
|
|
1473
655
|
y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
|
|
1474
|
-
|
|
1475
656
|
inter_x_min = max(x1_min, x2_min)
|
|
1476
657
|
inter_y_min = max(y1_min, y2_min)
|
|
1477
658
|
inter_x_max = min(x1_max, x2_max)
|
|
1478
659
|
inter_y_max = min(y1_max, y2_max)
|
|
1479
|
-
|
|
1480
660
|
inter_w = max(0.0, inter_x_max - inter_x_min)
|
|
1481
661
|
inter_h = max(0.0, inter_y_max - inter_y_min)
|
|
1482
662
|
inter_area = inter_w * inter_h
|
|
1483
|
-
|
|
1484
663
|
area1 = (x1_max - x1_min) * (y1_max - y1_min)
|
|
1485
664
|
area2 = (x2_max - x2_min) * (y2_max - y2_min)
|
|
1486
665
|
union_area = area1 + area2 - inter_area
|
|
1487
|
-
|
|
1488
666
|
return (inter_area / union_area) if union_area > 0 else 0.0
|
|
1489
667
|
|
|
1490
|
-
def
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
def _merge_or_register_track(self, raw_id: Any, bbox: List[float]) -> Any:
|
|
1495
|
-
"""Merge the raw track into an existing canonical track if possible,
|
|
1496
|
-
otherwise register it as a new canonical track. Returns the canonical
|
|
1497
|
-
ID to use for counting.
|
|
1498
|
-
"""
|
|
668
|
+
def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
|
|
669
|
+
if raw_id is None or bbox is None:
|
|
670
|
+
return raw_id
|
|
1499
671
|
now = time.time()
|
|
1500
|
-
|
|
1501
|
-
# Fast path: raw_id already mapped
|
|
1502
672
|
if raw_id in self._track_aliases:
|
|
1503
673
|
canonical_id = self._track_aliases[raw_id]
|
|
1504
674
|
track_info = self._canonical_tracks.get(canonical_id)
|
|
@@ -1507,25 +677,16 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1507
677
|
track_info["last_update"] = now
|
|
1508
678
|
track_info["raw_ids"].add(raw_id)
|
|
1509
679
|
return canonical_id
|
|
1510
|
-
|
|
1511
|
-
# Attempt to merge with an existing canonical track
|
|
1512
680
|
for canonical_id, info in self._canonical_tracks.items():
|
|
1513
|
-
# Only consider recently updated tracks to avoid stale matches
|
|
1514
681
|
if now - info["last_update"] > self._track_merge_time_window:
|
|
1515
682
|
continue
|
|
1516
|
-
|
|
1517
683
|
iou = self._compute_iou(bbox, info["last_bbox"])
|
|
1518
684
|
if iou >= self._track_merge_iou_threshold:
|
|
1519
|
-
# Merge raw_id into canonical track
|
|
1520
685
|
self._track_aliases[raw_id] = canonical_id
|
|
1521
686
|
info["last_bbox"] = bbox
|
|
1522
687
|
info["last_update"] = now
|
|
1523
688
|
info["raw_ids"].add(raw_id)
|
|
1524
|
-
self.logger.debug(
|
|
1525
|
-
f"Merged raw track {raw_id} into canonical track {canonical_id} (IoU={iou:.2f})")
|
|
1526
689
|
return canonical_id
|
|
1527
|
-
|
|
1528
|
-
# No match found – create a new canonical track
|
|
1529
690
|
canonical_id = raw_id
|
|
1530
691
|
self._track_aliases[raw_id] = canonical_id
|
|
1531
692
|
self._canonical_tracks[canonical_id] = {
|
|
@@ -1533,196 +694,12 @@ class PeopleCountingUseCase(BaseProcessor):
|
|
|
1533
694
|
"last_update": now,
|
|
1534
695
|
"raw_ids": {raw_id},
|
|
1535
696
|
}
|
|
1536
|
-
|
|
1537
|
-
return canonical_id
|
|
1538
|
-
|
|
1539
|
-
def _format_timestamp(self, timestamp: Any) -> str:
|
|
1540
|
-
"""Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
|
|
1541
|
-
|
|
1542
|
-
The input can be either:
|
|
1543
|
-
1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
|
|
1544
|
-
string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
1545
|
-
2. A string already following the same layout.
|
|
1546
|
-
|
|
1547
|
-
The returned value preserves the overall format of the input but truncates or pads
|
|
1548
|
-
the fractional seconds portion to **exactly two digits**.
|
|
1549
|
-
|
|
1550
|
-
Example
|
|
1551
|
-
-------
|
|
1552
|
-
>>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
|
|
1553
|
-
'2025-08-19-04:22:47.18 UTC'
|
|
1554
|
-
"""
|
|
1555
|
-
|
|
1556
|
-
# Convert numeric timestamps to the expected string representation first
|
|
1557
|
-
if isinstance(timestamp, (int, float)):
|
|
1558
|
-
timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
1559
|
-
'%Y-%m-%d-%H:%M:%S.%f UTC'
|
|
1560
|
-
)
|
|
1561
|
-
|
|
1562
|
-
# Ensure we are working with a string from here on
|
|
1563
|
-
if not isinstance(timestamp, str):
|
|
1564
|
-
return str(timestamp)
|
|
1565
|
-
|
|
1566
|
-
# If there is no fractional component, simply return the original string
|
|
1567
|
-
if '.' not in timestamp:
|
|
1568
|
-
return timestamp
|
|
1569
|
-
|
|
1570
|
-
# Split out the main portion (up to the decimal point)
|
|
1571
|
-
main_part, fractional_and_suffix = timestamp.split('.', 1)
|
|
1572
|
-
|
|
1573
|
-
# Separate fractional digits from the suffix (typically ' UTC')
|
|
1574
|
-
if ' ' in fractional_and_suffix:
|
|
1575
|
-
fractional_part, suffix = fractional_and_suffix.split(' ', 1)
|
|
1576
|
-
suffix = ' ' + suffix # Re-attach the space removed by split
|
|
1577
|
-
else:
|
|
1578
|
-
fractional_part, suffix = fractional_and_suffix, ''
|
|
1579
|
-
|
|
1580
|
-
# Guarantee exactly two digits for the fractional part
|
|
1581
|
-
fractional_part = (fractional_part + '00')[:2]
|
|
1582
|
-
|
|
1583
|
-
return f"{main_part}.{fractional_part}{suffix}"
|
|
697
|
+
return canonical_id
|
|
1584
698
|
|
|
1585
699
|
def _get_tracking_start_time(self) -> str:
|
|
1586
|
-
"""Get the tracking start time, formatted as a string."""
|
|
1587
700
|
if self._tracking_start_time is None:
|
|
1588
701
|
return "N/A"
|
|
1589
702
|
return self._format_timestamp(self._tracking_start_time)
|
|
1590
703
|
|
|
1591
704
|
def _set_tracking_start_time(self) -> None:
|
|
1592
|
-
|
|
1593
|
-
self._tracking_start_time = time.time()
|
|
1594
|
-
|
|
1595
|
-
def get_config_schema(self) -> Dict[str, Any]:
|
|
1596
|
-
"""Get configuration schema for people counting."""
|
|
1597
|
-
return {
|
|
1598
|
-
"type": "object",
|
|
1599
|
-
"properties": {
|
|
1600
|
-
"confidence_threshold": {
|
|
1601
|
-
"type": "number",
|
|
1602
|
-
"minimum": 0.0,
|
|
1603
|
-
"maximum": 1.0,
|
|
1604
|
-
"default": 0.5,
|
|
1605
|
-
"description": "Minimum confidence threshold for detections"
|
|
1606
|
-
},
|
|
1607
|
-
"enable_tracking": {
|
|
1608
|
-
"type": "boolean",
|
|
1609
|
-
"default": False,
|
|
1610
|
-
"description": "Enable tracking for unique counting"
|
|
1611
|
-
},
|
|
1612
|
-
"zone_config": {
|
|
1613
|
-
"type": "object",
|
|
1614
|
-
"properties": {
|
|
1615
|
-
"zones": {
|
|
1616
|
-
"type": "object",
|
|
1617
|
-
"additionalProperties": {
|
|
1618
|
-
"type": "array",
|
|
1619
|
-
"items": {
|
|
1620
|
-
"type": "array",
|
|
1621
|
-
"items": {"type": "number"},
|
|
1622
|
-
"minItems": 2,
|
|
1623
|
-
"maxItems": 2
|
|
1624
|
-
},
|
|
1625
|
-
"minItems": 3
|
|
1626
|
-
},
|
|
1627
|
-
"description": "Zone definitions as polygons"
|
|
1628
|
-
},
|
|
1629
|
-
"zone_confidence_thresholds": {
|
|
1630
|
-
"type": "object",
|
|
1631
|
-
"additionalProperties": {"type": "number", "minimum": 0.0, "maximum": 1.0},
|
|
1632
|
-
"description": "Per-zone confidence thresholds"
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
},
|
|
1636
|
-
"person_categories": {
|
|
1637
|
-
"type": "array",
|
|
1638
|
-
"items": {"type": "string"},
|
|
1639
|
-
"default": ["person", "people"],
|
|
1640
|
-
"description": "Category names that represent people"
|
|
1641
|
-
},
|
|
1642
|
-
"target_categories": {
|
|
1643
|
-
"type": "array",
|
|
1644
|
-
"items": {"type": "string"},
|
|
1645
|
-
"default": ["person", "people"],
|
|
1646
|
-
"description": "Category names that represent people"
|
|
1647
|
-
},
|
|
1648
|
-
"enable_unique_counting": {
|
|
1649
|
-
"type": "boolean",
|
|
1650
|
-
"default": True,
|
|
1651
|
-
"description": "Enable unique people counting using tracking"
|
|
1652
|
-
},
|
|
1653
|
-
"time_window_minutes": {
|
|
1654
|
-
"type": "integer",
|
|
1655
|
-
"minimum": 1,
|
|
1656
|
-
"default": 60,
|
|
1657
|
-
"description": "Time window for counting analysis in minutes"
|
|
1658
|
-
},
|
|
1659
|
-
"alert_config": {
|
|
1660
|
-
"type": "object",
|
|
1661
|
-
"properties": {
|
|
1662
|
-
"count_thresholds": {
|
|
1663
|
-
"type": "object",
|
|
1664
|
-
"additionalProperties": {"type": "integer", "minimum": 1},
|
|
1665
|
-
"description": "Count thresholds for alerts"
|
|
1666
|
-
},
|
|
1667
|
-
"occupancy_thresholds": {
|
|
1668
|
-
"type": "object",
|
|
1669
|
-
"additionalProperties": {"type": "integer", "minimum": 1},
|
|
1670
|
-
"description": "Zone occupancy thresholds for alerts"
|
|
1671
|
-
},
|
|
1672
|
-
"alert_type": {
|
|
1673
|
-
"type": "array",
|
|
1674
|
-
"items": {"type": "string"},
|
|
1675
|
-
"default": ["Default"],
|
|
1676
|
-
"description": "To pass the type of alert. EG: email, sms, etc."
|
|
1677
|
-
},
|
|
1678
|
-
"alert_value": {
|
|
1679
|
-
"type": "array",
|
|
1680
|
-
"items": {"type": "string"},
|
|
1681
|
-
"default": ["JSON"],
|
|
1682
|
-
"description": "Alert value to pass the value based on type. EG: email id if type is email."
|
|
1683
|
-
},
|
|
1684
|
-
"alert_incident_category": {
|
|
1685
|
-
"type": "array",
|
|
1686
|
-
"items": {"type": "string"},
|
|
1687
|
-
"default": ["Incident Detection Alert"],
|
|
1688
|
-
"description": "Group and name the Alert category Type"
|
|
1689
|
-
},
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
},
|
|
1693
|
-
"required": ["confidence_threshold"],
|
|
1694
|
-
"additionalProperties": False
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
def create_default_config(self, **overrides) -> PeopleCountingConfig:
|
|
1698
|
-
"""Create default configuration with optional overrides."""
|
|
1699
|
-
defaults = {
|
|
1700
|
-
"category": self.category,
|
|
1701
|
-
"usecase": self.name,
|
|
1702
|
-
"confidence_threshold": 0.5,
|
|
1703
|
-
"enable_tracking": False,
|
|
1704
|
-
"enable_analytics": True,
|
|
1705
|
-
"enable_unique_counting": True,
|
|
1706
|
-
"time_window_minutes": 60,
|
|
1707
|
-
"person_categories": ["person", "people"],
|
|
1708
|
-
"target_categories": ["person", "people", "human", "man", "woman", "male", "female"]
|
|
1709
|
-
}
|
|
1710
|
-
defaults.update(overrides)
|
|
1711
|
-
return PeopleCountingConfig(**defaults)
|
|
1712
|
-
|
|
1713
|
-
def _apply_smoothing(self, data: Any, config: PeopleCountingConfig) -> Any:
|
|
1714
|
-
"""Apply smoothing to tracking data if enabled."""
|
|
1715
|
-
if self.smoothing_tracker is None:
|
|
1716
|
-
smoothing_config = BBoxSmoothingConfig(
|
|
1717
|
-
smoothing_algorithm=config.smoothing_algorithm,
|
|
1718
|
-
window_size=config.smoothing_window_size,
|
|
1719
|
-
cooldown_frames=config.smoothing_cooldown_frames,
|
|
1720
|
-
confidence_threshold=config.confidence_threshold or 0.5,
|
|
1721
|
-
confidence_range_factor=config.smoothing_confidence_range_factor,
|
|
1722
|
-
enable_smoothing=True
|
|
1723
|
-
)
|
|
1724
|
-
self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
|
|
1725
|
-
|
|
1726
|
-
smoothed_data = bbox_smoothing(data, self.smoothing_tracker.config, self.smoothing_tracker)
|
|
1727
|
-
self.logger.debug(f"Applied bbox smoothing to tracking results")
|
|
1728
|
-
return smoothed_data
|
|
705
|
+
self._tracking_start_time = time.time()
|