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.

Files changed (59) hide show
  1. matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
  2. matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
  3. matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
  4. matrice_analytics/post_processing/face_reg/embedding_manager.py +14 -7
  5. matrice_analytics/post_processing/face_reg/face_recognition.py +123 -34
  6. matrice_analytics/post_processing/face_reg/face_recognition_client.py +332 -82
  7. matrice_analytics/post_processing/face_reg/people_activity_logging.py +29 -22
  8. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  9. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  10. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  11. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  12. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  13. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  14. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  15. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  16. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  17. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  18. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  19. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  20. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  21. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  22. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  23. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  24. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  25. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  26. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  27. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  28. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  29. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  30. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  31. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  32. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  33. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  34. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  42. matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
  43. matrice_analytics/post_processing/post_processor.py +19 -5
  44. matrice_analytics/post_processing/usecases/color/clip.py +42 -8
  45. matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
  46. matrice_analytics/post_processing/usecases/color_detection.py +21 -98
  47. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
  48. matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
  49. matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
  50. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +252 -11
  51. matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
  52. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  53. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
  54. matrice_analytics/post_processing/utils/__init__.py +8 -8
  55. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/METADATA +1 -1
  56. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/RECORD +59 -24
  57. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/WHEEL +0 -0
  58. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.31.dist-info}/licenses/LICENSE.txt +0 -0
  59. {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 ..utils.geometry_utils import get_bbox_center, point_in_polygon, get_bbox_bottom25_center
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
- """People counting use case with zone analysis and alerting."""
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] = 'People_Counting'
39
- self.CASE_VERSION: Optional[str] = '1.3'
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
- # Track start time for "TOTAL SINCE" calculation
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
- # IoU threshold above which two bounding boxes are considered the same
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
- # Maintain last frame presence for consecutive confirmation logic
94
- self._last_frame_track_ids: Set[Any] = set()
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
- # Advanced tracking for single-frame detections
97
- self.tracker = None
98
- self._min_confirm_frames: int = 3 # require 3 consecutive frames before counting as unique
99
- self._consecutive_track_frames: Dict[Any, int] = {}
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
- # Ensure we have the right config type
120
- if not isinstance(config, PeopleCountingConfig):
121
- return self.create_error_result(
122
- "Invalid configuration type for people counting",
123
- usecase=self.name,
124
- category=self.category,
125
- context=context
126
- )
127
-
128
- # Initialize processing context if not provided
129
- if context is None:
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.error(f"People counting failed: {str(e)}", exc_info=True)
152
-
153
- if context:
154
- context.mark_completed()
155
-
156
- return self.create_error_result(
157
- str(e),
158
- type(e).__name__,
159
- usecase=self.name,
160
- category=self.category,
161
- context=context
162
- )
163
-
164
- def _process_multi_frame(self, data: Dict, config: PeopleCountingConfig, context: ProcessingContext, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
165
- """Process multi-frame data to generate frame-wise agg_summary."""
166
-
167
- frame_incidents = {}
168
- frame_tracking_stats = {}
169
- frame_business_analytics = {}
170
- frame_human_text = {}
171
- frame_alerts = {}
172
-
173
- # Increment total frame counter
174
- frames_in_this_call = len(data)
175
- self._total_frame_counter += frames_in_this_call
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
- def _process_frame_detections(self, frame_data: Any, config: PeopleCountingConfig, frame_id: str, stream_info: Optional[Dict[str, Any]] = None) -> tuple:
261
- """Process detections from a single frame and return standardized components."""
262
-
263
- # Convert frame_data to list if it's not already
264
- if isinstance(frame_data, list):
265
- frame_detections = frame_data
266
- else:
267
- # Handle other formats as needed
268
- frame_detections = []
269
-
270
- # Step 1: Apply confidence filtering to this frame
271
- if config.confidence_threshold is not None:
272
- frame_detections = [d for d in frame_detections if d.get("confidence", 0) >= config.confidence_threshold]
273
-
274
- # Step 2: Apply category mapping if provided
275
- if config.index_to_category:
276
- frame_detections = apply_category_mapping(frame_detections, config.index_to_category)
277
-
278
- # Step 3: Filter to person categories
279
- if config.person_categories:
280
- frame_detections = [d for d in frame_detections if d.get("category") in config.person_categories]
281
- if config.target_categories:
282
- frame_detections = [d for d in frame_detections if d.get('category') in config.target_categories]
283
- self.logger.debug("Applied category filtering")
284
-
285
- # Step 4: Track single-frame detections using AdvancedTracker to obtain stable track_ids
286
- # Always apply when tracking is enabled in single-frame path
287
- needs_tracking = bool(config.enable_tracking)
288
- if self.tracker is None and needs_tracking:
289
- try:
290
- from ..advanced_tracker import AdvancedTracker
291
- from ..advanced_tracker.config import TrackerConfig
292
- # Configure tracker thresholds suitable for people
293
- fps = 30
294
- try:
295
- fps = int(stream_info.get("input_settings", {}).get("original_fps", 30)) if stream_info else 30
296
- if fps <= 0:
297
- fps = 30
298
- except Exception:
299
- fps = 30
300
- tracker_config = TrackerConfig(
301
- track_high_thresh=0.4,
302
- track_low_thresh=0.05,
303
- new_track_thresh=0.3,
304
- match_thresh=0.8,
305
- track_buffer=int(3 * fps),
306
- max_time_lost=int(3 * fps),
307
- frame_rate=fps,
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
- tracking_stats = self._generate_tracking_stats(counting_summary, zone_analysis, config, frame_id=frame_id, alerts=alerts, stream_info=stream_info)
359
- business_analytics = self._generate_business_analytics(counting_summary, zone_analysis, config, frame_id, stream_info, is_empty=True)
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
- alert_settings=[]
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 total_people > 0:
387
- # Determine event level based on thresholds
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 hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
401
- threshold = config.alert_config.count_thresholds.get("all", 10)
402
- intensity = min(10.0, (total_people / threshold) * 10)
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 total_people > 30:
242
+ if total_detections > 30:
418
243
  level = "critical"
419
244
  intensity = 10.0
420
245
  self._ascending_alert_list.append(3)
421
- elif total_people > 25:
246
+ elif total_detections > 25:
422
247
  level = "significant"
423
248
  intensity = 9.0
424
249
  self._ascending_alert_list.append(2)
425
- elif total_people > 15:
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, total_people / 3.0)
256
+ intensity = min(10.0, total_detections / 3.0)
432
257
  self._ascending_alert_list.append(0)
433
-
434
- # Generate human text in new format
435
- human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
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
- # Main people counting incident
440
- event= self.create_incident(incident_id=self.CASE_TYPE+'_'+str(frame_id), incident_type=self.CASE_TYPE,
441
- severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
442
- start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
443
- level_settings= {"low": 1, "medium": 3, "significant":4, "critical": 7})
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, zone_analysis: Dict, config: PeopleCountingConfig, frame_id: str, alerts: Any=[], stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
482
- """Generate tracking stats using standardized methods."""
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
- # Build total_counts using standardized method
494
- total_counts = []
495
- per_category_total = {}
496
-
497
- for category in config.person_categories or ["person"]:
498
- # Get count for this category from zone analysis or counting summary
499
- category_total_count = 0
500
- if zone_analysis:
501
- for zone_data in zone_analysis.values():
502
- if isinstance(zone_data, dict) and "total_count" in zone_data:
503
- category_total_count += zone_data.get("total_count", 0)
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']) if hasattr(config.alert_config, 'alert_type') else ['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']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
578
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
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
- #if total_unique_count > 0:
644
- human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
645
- human_text_lines.append(f"\t- Total unique people count: {total_unique_count}")
646
-
647
- print('------------------HUMANNTEXTTT-------------------------')
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
- if count >= threshold:
708
- alerts.append({
709
- "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
710
- "alert_id": "alert_"+category+'_'+frame_id,
711
- "incident_category": self.CASE_TYPE,
712
- "threshold_level": threshold,
713
- "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
714
- "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
715
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
716
- }
717
- })
718
- else:
719
- pass
720
-
721
- # Zone occupancy threshold alerts
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, zone_analysis: Dict, config: PeopleCountingConfig, frame_id: str, stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
743
- """Generate standardized business analytics for the agg_summary structure."""
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
- lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
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 _calculate_metrics(self, counting_summary: Dict, zone_analysis: Dict,
805
- config: PeopleCountingConfig, context: ProcessingContext) -> Dict[str, Any]:
806
- """Calculate detailed metrics for analytics."""
807
- total_people = counting_summary.get("total_objects", 0)
808
-
809
- metrics = {
810
- "total_people": total_people,
811
- "processing_time": context.processing_time or 0.0,
812
- "input_format": context.input_format.value,
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
- "category": item.get("category", item.get("class", "unknown")),
906
- "confidence": item.get("confidence", item.get("score", 0.0)),
907
- "bounding_box": item.get("bounding_box", item.get("bbox", {})),
908
- "track_id": item.get("track_id")
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
- # If no tracker id yet, generate ephemeral then alias-merge by IoU
956
- if raw_track_id is None:
957
- raw_track_id = self._generate_ephemeral_track_id(bbox, ephemeral_seq)
958
- ephemeral_seq += 1
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
- detection["track_id"] = canonical_id
962
- current_frame_tracks.add(canonical_id)
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
- # Clear aliasing information
1130
- self._canonical_tracks.clear()
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
- """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
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
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
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
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
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
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
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
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
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 _compute_iou(self, box1: Any, box2: Any) -> float:
1436
- """Compute IoU between two bounding boxes that may be either list or dict.
1437
- Falls back to geometry_utils.calculate_iou when both boxes are dicts.
1438
- """
1439
- # Handle dict format directly with calculate_iou (supports many keys)
1440
- if isinstance(box1, dict) and isinstance(box2, dict):
1441
- return calculate_iou(box1, box2)
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
- # Helper to convert bbox (dict or list) to a list [x1,y1,x2,y2]
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
- # Fallback: take first four values in insertion order
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
- list1 = _bbox_to_list(box1)
1461
- list2 = _bbox_to_list(box2)
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
- x1_min, y1_min, x1_max, y1_max = list1
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 _get_canonical_id(self, raw_id: Any) -> Any:
1491
- """Return the canonical ID for a raw tracker-generated ID."""
1492
- return self._track_aliases.get(raw_id, raw_id)
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
- self.logger.debug(f"Registered new canonical track {canonical_id}")
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
- """Set the tracking start time to the current time."""
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()