matrice-analytics 0.1.106__py3-none-any.whl → 0.1.124__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/post_processing/__init__.py +22 -0
- matrice_analytics/post_processing/config.py +15 -0
- matrice_analytics/post_processing/core/config.py +107 -1
- matrice_analytics/post_processing/face_reg/face_recognition.py +2 -2
- matrice_analytics/post_processing/post_processor.py +16 -0
- matrice_analytics/post_processing/usecases/__init__.py +9 -0
- matrice_analytics/post_processing/usecases/crowdflow.py +1088 -0
- matrice_analytics/post_processing/usecases/footfall.py +103 -62
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +2 -1
- matrice_analytics/post_processing/usecases/parking_lot_analytics.py +1137 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +30 -4
- matrice_analytics/post_processing/usecases/vehicle_monitoring_drone_view.py +33 -6
- matrice_analytics/post_processing/usecases/vehicle_monitoring_parking_lot.py +18 -2
- matrice_analytics/post_processing/usecases/vehicle_monitoring_wrong_way.py +1021 -0
- matrice_analytics/post_processing/utils/alert_instance_utils.py +18 -5
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +25 -2
- matrice_analytics/post_processing/utils/incident_manager_utils.py +12 -1
- matrice_analytics/post_processing/utils/parking_analytics_tracker.py +359 -0
- matrice_analytics/post_processing/utils/wrong_way_tracker.py +670 -0
- {matrice_analytics-0.1.106.dist-info → matrice_analytics-0.1.124.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.106.dist-info → matrice_analytics-0.1.124.dist-info}/RECORD +24 -19
- {matrice_analytics-0.1.106.dist-info → matrice_analytics-0.1.124.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.106.dist-info → matrice_analytics-0.1.124.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.106.dist-info → matrice_analytics-0.1.124.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
|
|
7
|
+
from ..utils import (
|
|
8
|
+
filter_by_confidence,
|
|
9
|
+
filter_by_categories,
|
|
10
|
+
apply_category_mapping,
|
|
11
|
+
count_objects_by_category,
|
|
12
|
+
count_objects_in_zones,
|
|
13
|
+
calculate_counting_summary,
|
|
14
|
+
match_results_structure,
|
|
15
|
+
bbox_smoothing,
|
|
16
|
+
BBoxSmoothingConfig,
|
|
17
|
+
BBoxSmoothingTracker
|
|
18
|
+
)
|
|
19
|
+
from ..core.config import BaseConfig, AlertConfig, ZoneConfig
|
|
20
|
+
from ..utils.geometry_utils import get_bbox_center, point_in_polygon, get_bbox_bottom25_center
|
|
21
|
+
from ..utils.wrong_way_tracker import WrongWayDetectionTracker
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class VehicleMonitoringWrongWayConfig(BaseConfig):
|
|
25
|
+
"""Configuration for wrong-way vehicle detection use case."""
|
|
26
|
+
enable_smoothing: bool = True
|
|
27
|
+
smoothing_algorithm: str = "observability"
|
|
28
|
+
smoothing_window_size: int = 20
|
|
29
|
+
smoothing_cooldown_frames: int = 5
|
|
30
|
+
smoothing_confidence_range_factor: float = 0.5
|
|
31
|
+
confidence_threshold: float = 0.6
|
|
32
|
+
|
|
33
|
+
# Class Aggregation: Configuration parameters
|
|
34
|
+
enable_class_aggregation: bool = True
|
|
35
|
+
class_aggregation_window_size: int = 30 # 30 frames ≈ 1 second at 30 FPS
|
|
36
|
+
|
|
37
|
+
# Wrong-Way Detection Settings (Trajectory-Based)
|
|
38
|
+
enable_wrong_way_detection: bool = True
|
|
39
|
+
|
|
40
|
+
wrong_way_confidence_suspect: float = 0.3 # Threshold to enter SUSPECT state
|
|
41
|
+
wrong_way_confidence_confirm: float = 0.7 # Threshold to confirm WRONG_WAY
|
|
42
|
+
wrong_way_min_velocity: float = 2.0 # Min velocity (pixels/frame) to consider motion
|
|
43
|
+
auto_ref_min_tracks: int = 5 # Min tracks needed for auto-estimation
|
|
44
|
+
stale_track_frames: int = 30
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#JBK_720_GATE POLYGON = [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]]
|
|
48
|
+
# zone_config: Optional[Dict[str, List[List[float]]]] = None #field(
|
|
49
|
+
# default_factory=lambda: {
|
|
50
|
+
# "zones": {
|
|
51
|
+
# "Interest_Region": [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]],
|
|
52
|
+
# }
|
|
53
|
+
# }
|
|
54
|
+
# )
|
|
55
|
+
# NOTE : Remove this hard-coded zone after Testing (TODO)
|
|
56
|
+
# Motorcyclists wrong way sample test video polygon
|
|
57
|
+
# USER_REFERENCE_POLYLINE = [[296,401], [293,338], [292,263]]
|
|
58
|
+
zone_config: Optional[Dict[str, Any]] = field(
|
|
59
|
+
default_factory=lambda: {
|
|
60
|
+
"zones": {
|
|
61
|
+
"Interest_Region": [[296,401], [293,338], [292,263]],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
usecase_categories: List[str] = field(
|
|
67
|
+
default_factory=lambda: [
|
|
68
|
+
'bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck'
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
target_categories: List[str] = field(
|
|
73
|
+
default_factory=lambda: [
|
|
74
|
+
'bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck'
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
alert_config: Optional[AlertConfig] = None
|
|
78
|
+
index_to_category: Optional[Dict[int, str]] = field(
|
|
79
|
+
default_factory=lambda: {
|
|
80
|
+
0: "bicycle",
|
|
81
|
+
1: "motorcycle",
|
|
82
|
+
2: "car",
|
|
83
|
+
3: "van",
|
|
84
|
+
4: "bus",
|
|
85
|
+
5: "truck"
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VehicleMonitoringWrongWayUseCase(BaseProcessor):
|
|
91
|
+
CATEGORY_DISPLAY = {
|
|
92
|
+
"bicycle": "Bicycle",
|
|
93
|
+
"motorcycle": "Motorcycle",
|
|
94
|
+
"car": "Car",
|
|
95
|
+
"van": "Van",
|
|
96
|
+
"bus": "Bus",
|
|
97
|
+
"truck": "Truck",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
super().__init__("vehicle_monitoring_wrong_way")
|
|
102
|
+
self.category = "traffic"
|
|
103
|
+
self.CASE_TYPE: Optional[str] = 'vehicle_monitoring_wrong_way'
|
|
104
|
+
self.CASE_VERSION: Optional[str] = '1.0'
|
|
105
|
+
self.target_categories = ['bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck']
|
|
106
|
+
self.smoothing_tracker = None
|
|
107
|
+
self.tracker = None
|
|
108
|
+
self._total_frame_counter = 0
|
|
109
|
+
self._global_frame_offset = 0
|
|
110
|
+
self._tracking_start_time = None
|
|
111
|
+
self._track_aliases: Dict[Any, Any] = {}
|
|
112
|
+
self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
|
|
113
|
+
self._track_merge_iou_threshold: float = 0.05
|
|
114
|
+
self._track_merge_time_window: float = 7.0
|
|
115
|
+
self._ascending_alert_list: List[int] = []
|
|
116
|
+
self.current_incident_end_timestamp: str = "N/A"
|
|
117
|
+
self.start_timer = None
|
|
118
|
+
|
|
119
|
+
# Wrong-way detection tracker (NEW)
|
|
120
|
+
self.wrong_way_tracker = None
|
|
121
|
+
# Reference direction tracking (for zone-based reference)
|
|
122
|
+
self._reference_zone_name: Optional[str] = None
|
|
123
|
+
self._reference_zone_polygon: Optional[List[List[float]]] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Track ID storage for total count calculation
|
|
127
|
+
self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
|
|
128
|
+
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
129
|
+
self._tracked_in_zones = set() # New: Unique track IDs that have entered any zone
|
|
130
|
+
self._total_count = 0 # Cached total count
|
|
131
|
+
self._last_update_time = time.time() # Track when last updated
|
|
132
|
+
self._total_count_list = []
|
|
133
|
+
|
|
134
|
+
# Zone-based tracking storage
|
|
135
|
+
self._zone_current_track_ids = {} # zone_name -> set of current track IDs in zone
|
|
136
|
+
self._zone_total_track_ids = {} # zone_name -> set of all track IDs that have been in zone
|
|
137
|
+
self._zone_current_counts = {} # zone_name -> current count in zone
|
|
138
|
+
self._zone_total_counts = {} # zone_name -> total count that have been in zone
|
|
139
|
+
|
|
140
|
+
def process(
|
|
141
|
+
self,
|
|
142
|
+
data: Any,
|
|
143
|
+
config: ConfigProtocol,
|
|
144
|
+
context: Optional[ProcessingContext] = None,
|
|
145
|
+
stream_info: Optional[Dict[str, Any]] = None
|
|
146
|
+
) -> ProcessingResult:
|
|
147
|
+
processing_start = time.time()
|
|
148
|
+
|
|
149
|
+
# Config validation
|
|
150
|
+
is_valid_config = (
|
|
151
|
+
isinstance(config, VehicleMonitoringWrongWayConfig) or
|
|
152
|
+
(hasattr(config, 'usecase') and hasattr(config, 'category'))
|
|
153
|
+
)
|
|
154
|
+
if not is_valid_config:
|
|
155
|
+
self.logger.error(
|
|
156
|
+
f"Config validation failed in vehicle_monitoring_wrong_way. "
|
|
157
|
+
f"Got type={type(config).__name__}, module={type(config).__module__}, "
|
|
158
|
+
f"usecase={getattr(config, 'usecase', 'N/A')}, category={getattr(config, 'category', 'N/A')}"
|
|
159
|
+
)
|
|
160
|
+
return self.create_error_result(
|
|
161
|
+
f"Invalid config type: expected VehicleMonitoringWrongWayConfig or config with usecase='vehicle_monitoring_wrong_way', "
|
|
162
|
+
f"got {type(config).__name__} with usecase={getattr(config, 'usecase', 'N/A')}",
|
|
163
|
+
usecase=self.name, category=self.category, context=context
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if context is None:
|
|
167
|
+
context = ProcessingContext()
|
|
168
|
+
|
|
169
|
+
# Determine if zones are configured
|
|
170
|
+
has_zones = bool(config.zone_config and config.zone_config.get('zones')) and not self.enable_wrong_way_detection
|
|
171
|
+
# Disable zones if wrong-way detection is enabled
|
|
172
|
+
|
|
173
|
+
# Normalize YOLO outputs to internal schema
|
|
174
|
+
data = self._normalize_yolo_results(data, getattr(config, 'index_to_category', None))
|
|
175
|
+
|
|
176
|
+
input_format = match_results_structure(data)
|
|
177
|
+
context.input_format = input_format
|
|
178
|
+
context.confidence_threshold = config.confidence_threshold
|
|
179
|
+
config.confidence_threshold = 0.25
|
|
180
|
+
# TODO: param to be updated
|
|
181
|
+
|
|
182
|
+
if config.confidence_threshold is not None:
|
|
183
|
+
processed_data = filter_by_confidence(data, config.confidence_threshold)
|
|
184
|
+
self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
|
|
185
|
+
else:
|
|
186
|
+
processed_data = data
|
|
187
|
+
|
|
188
|
+
if config.index_to_category:
|
|
189
|
+
processed_data = apply_category_mapping(processed_data, config.index_to_category)
|
|
190
|
+
self.logger.debug("Applied category mapping")
|
|
191
|
+
|
|
192
|
+
processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
|
|
193
|
+
self.logger.debug("Applied category filtering")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if config.enable_smoothing:
|
|
197
|
+
if self.smoothing_tracker is None:
|
|
198
|
+
smoothing_config = BBoxSmoothingConfig(
|
|
199
|
+
smoothing_algorithm=config.smoothing_algorithm,
|
|
200
|
+
window_size=config.smoothing_window_size,
|
|
201
|
+
cooldown_frames=config.smoothing_cooldown_frames,
|
|
202
|
+
confidence_threshold=config.confidence_threshold,
|
|
203
|
+
confidence_range_factor=config.smoothing_confidence_range_factor,
|
|
204
|
+
enable_smoothing=True
|
|
205
|
+
)
|
|
206
|
+
self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
|
|
207
|
+
processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
from ..advanced_tracker import AdvancedTracker
|
|
211
|
+
from ..advanced_tracker.config import TrackerConfig
|
|
212
|
+
|
|
213
|
+
if self.tracker is None:
|
|
214
|
+
tracker_config = TrackerConfig(
|
|
215
|
+
enable_class_aggregation=config.enable_class_aggregation,
|
|
216
|
+
class_aggregation_window_size=config.class_aggregation_window_size
|
|
217
|
+
)
|
|
218
|
+
self.tracker = AdvancedTracker(tracker_config)
|
|
219
|
+
self.logger.info(
|
|
220
|
+
f"Initialized AdvancedTracker for wrong-way detection use case "
|
|
221
|
+
f"(class_aggregation={config.enable_class_aggregation})"
|
|
222
|
+
)
|
|
223
|
+
processed_data = self.tracker.update(processed_data)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.logger.warning(f"AdvancedTracker failed: {e}")
|
|
226
|
+
|
|
227
|
+
# WRONG-WAY DETECTION
|
|
228
|
+
wrong_way_analytics = None
|
|
229
|
+
if config.enable_wrong_way_detection and processed_data:
|
|
230
|
+
|
|
231
|
+
if self.wrong_way_tracker is None:
|
|
232
|
+
self.wrong_way_tracker = WrongWayDetectionTracker(
|
|
233
|
+
v_min=config.wrong_way_min_velocity,
|
|
234
|
+
c_suspect=config.wrong_way_confidence_suspect,
|
|
235
|
+
c_confirm=config.wrong_way_confidence_confirm,
|
|
236
|
+
stale_track_frames=config.stale_track_frames,
|
|
237
|
+
auto_ref_min_tracks=config.auto_ref_min_tracks
|
|
238
|
+
)
|
|
239
|
+
self.logger.info(
|
|
240
|
+
f"Initialized WrongWayDetectionTracker v2: "
|
|
241
|
+
f"v_min={config.wrong_way_min_velocity}, "
|
|
242
|
+
f"c_suspect={config.wrong_way_confidence_suspect}, "
|
|
243
|
+
f"c_confirm={config.wrong_way_confidence_confirm}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self._setup_reference_from_zone(config)
|
|
247
|
+
|
|
248
|
+
wrong_way_analytics = self.wrong_way_tracker.update(
|
|
249
|
+
detections=processed_data,
|
|
250
|
+
current_frame=self._total_frame_counter
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
ww_count = wrong_way_analytics.get('current_wrong_way_count', 0)
|
|
254
|
+
suspect_count = wrong_way_analytics.get('current_suspect_count', 0)
|
|
255
|
+
ref_status = wrong_way_analytics.get('reference_status', 'NONE')
|
|
256
|
+
|
|
257
|
+
if ww_count > 0 or suspect_count > 0:
|
|
258
|
+
self.logger.info(
|
|
259
|
+
f"[Frame {self._total_frame_counter}] Wrong-Way: "
|
|
260
|
+
f"ref={ref_status}, wrong_way={ww_count}, suspect={suspect_count}, "
|
|
261
|
+
f"total={wrong_way_analytics.get('total_wrong_way_count', 0)}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Update tracking state
|
|
265
|
+
self._update_tracking_state(processed_data, has_zones=has_zones)
|
|
266
|
+
self._total_frame_counter += 1
|
|
267
|
+
|
|
268
|
+
# Extract frame number
|
|
269
|
+
frame_number = None
|
|
270
|
+
if stream_info:
|
|
271
|
+
input_settings = stream_info.get("input_settings", {})
|
|
272
|
+
start_frame = input_settings.get("start_frame")
|
|
273
|
+
end_frame = input_settings.get("end_frame")
|
|
274
|
+
if start_frame is not None and end_frame is not None and start_frame == end_frame:
|
|
275
|
+
frame_number = start_frame
|
|
276
|
+
|
|
277
|
+
# Calculate summaries
|
|
278
|
+
general_counting_summary = calculate_counting_summary(data)
|
|
279
|
+
counting_summary = self._count_categories(processed_data, config)
|
|
280
|
+
total_counts = self.get_total_counts()
|
|
281
|
+
counting_summary['total_counts'] = total_counts
|
|
282
|
+
counting_summary['categories'] = {}
|
|
283
|
+
for detection in processed_data:
|
|
284
|
+
category = detection.get("category", "unknown")
|
|
285
|
+
counting_summary["categories"][category] = counting_summary["categories"].get(category, 0) + 1
|
|
286
|
+
|
|
287
|
+
# Zone analysis (if configured)
|
|
288
|
+
zone_analysis = {}
|
|
289
|
+
if has_zones:
|
|
290
|
+
frame_data = processed_data
|
|
291
|
+
zone_analysis = count_objects_in_zones(frame_data, config.zone_config['zones'], stream_info)
|
|
292
|
+
if zone_analysis:
|
|
293
|
+
enhanced_zone_analysis = self._update_zone_tracking(zone_analysis, processed_data, config)
|
|
294
|
+
for zone_name, enhanced_data in enhanced_zone_analysis.items():
|
|
295
|
+
zone_analysis[zone_name] = enhanced_data
|
|
296
|
+
per_category_count = {cat: len(self._current_frame_track_ids.get(cat, set())) for cat in self.target_categories}
|
|
297
|
+
counting_summary['per_category_count'] = {k: v for k, v in per_category_count.items() if v > 0}
|
|
298
|
+
counting_summary['total_count'] = sum(per_category_count.values())
|
|
299
|
+
|
|
300
|
+
# Generate outputs
|
|
301
|
+
alerts = self._check_alerts(counting_summary, zone_analysis, frame_number, config)
|
|
302
|
+
predictions = self._extract_predictions(processed_data)
|
|
303
|
+
|
|
304
|
+
incidents_list = [] # Not generating incidents for wrong-way (analytics only)
|
|
305
|
+
|
|
306
|
+
tracking_stats_list = self._generate_tracking_stats(
|
|
307
|
+
counting_summary, zone_analysis, alerts, config,
|
|
308
|
+
frame_number, stream_info, wrong_way_analytics
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
business_analytics_list = self._generate_business_analytics(
|
|
312
|
+
counting_summary, zone_analysis, alerts, config, stream_info, is_empty=True
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
summary_list = self._generate_summary(
|
|
316
|
+
counting_summary, zone_analysis, incidents_list,
|
|
317
|
+
tracking_stats_list, business_analytics_list, alerts
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Assemble output
|
|
321
|
+
incidents = incidents_list[0] if incidents_list else {}
|
|
322
|
+
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
323
|
+
business_analytics = business_analytics_list[0] if business_analytics_list else {}
|
|
324
|
+
summary = summary_list[0] if summary_list else {}
|
|
325
|
+
|
|
326
|
+
agg_summary = {
|
|
327
|
+
str(frame_number): {
|
|
328
|
+
"incidents": incidents,
|
|
329
|
+
"tracking_stats": tracking_stats,
|
|
330
|
+
"business_analytics": business_analytics,
|
|
331
|
+
"alerts": alerts,
|
|
332
|
+
"zone_analysis": zone_analysis,
|
|
333
|
+
"human_text": summary
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
context.mark_completed()
|
|
338
|
+
result = self.create_result(
|
|
339
|
+
data={"agg_summary": agg_summary},
|
|
340
|
+
usecase=self.name,
|
|
341
|
+
category=self.category,
|
|
342
|
+
context=context
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Log performance
|
|
346
|
+
proc_time = time.time() - processing_start
|
|
347
|
+
processing_latency_ms = proc_time * 1000.0
|
|
348
|
+
processing_fps = (1.0 / proc_time) if proc_time > 0 else None
|
|
349
|
+
print(f"latency in ms: {processing_latency_ms} | Throughput fps: {processing_fps} | Frame_Number: {self._total_frame_counter}")
|
|
350
|
+
|
|
351
|
+
return result
|
|
352
|
+
|
|
353
|
+
def _generate_tracking_stats(
|
|
354
|
+
self,
|
|
355
|
+
counting_summary: Dict,
|
|
356
|
+
zone_analysis: Dict,
|
|
357
|
+
alerts: List,
|
|
358
|
+
config: VehicleMonitoringWrongWayConfig,
|
|
359
|
+
frame_number: Optional[int] = None,
|
|
360
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
361
|
+
wrong_way_analytics: Optional[Dict] = None
|
|
362
|
+
) -> List[Dict]:
|
|
363
|
+
"""Generate tracking statistics including wrong-way analytics."""
|
|
364
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
365
|
+
tracking_stats = []
|
|
366
|
+
|
|
367
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
368
|
+
total_counts_dict = counting_summary.get("total_counts", {})
|
|
369
|
+
per_category_count = counting_summary.get("per_category_count", {})
|
|
370
|
+
|
|
371
|
+
current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
|
|
372
|
+
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
373
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
|
|
374
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
|
375
|
+
|
|
376
|
+
total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
|
|
377
|
+
current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0]
|
|
378
|
+
|
|
379
|
+
# Build detections list
|
|
380
|
+
detections = []
|
|
381
|
+
for detection in counting_summary.get("detections", []):
|
|
382
|
+
bbox = detection.get("bounding_box", {})
|
|
383
|
+
category = detection.get("category", "vehicle")
|
|
384
|
+
if detection.get("masks"):
|
|
385
|
+
segmentation = detection.get("masks", [])
|
|
386
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
|
387
|
+
elif detection.get("segmentation"):
|
|
388
|
+
segmentation = detection.get("segmentation")
|
|
389
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
|
390
|
+
else:
|
|
391
|
+
detection_obj = self.create_detection_object(category, bbox)
|
|
392
|
+
detections.append(detection_obj)
|
|
393
|
+
|
|
394
|
+
# Alert settings
|
|
395
|
+
alert_settings = []
|
|
396
|
+
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
|
397
|
+
alert_settings.append({
|
|
398
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
399
|
+
"incident_category": self.CASE_TYPE,
|
|
400
|
+
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
|
401
|
+
"ascending": True,
|
|
402
|
+
"settings": {t: v for t, v in zip(
|
|
403
|
+
getattr(config.alert_config, 'alert_type', ['Default']),
|
|
404
|
+
getattr(config.alert_config, 'alert_value', ['JSON'])
|
|
405
|
+
)}
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
# === BUILD HUMAN TEXT ===
|
|
409
|
+
human_text_lines = []
|
|
410
|
+
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
|
|
411
|
+
|
|
412
|
+
# Display current counts
|
|
413
|
+
if zone_analysis:
|
|
414
|
+
human_text_lines.append("\t- Vehicles Detected by Zone:")
|
|
415
|
+
for zone_name, zone_data in zone_analysis.items():
|
|
416
|
+
current_count = 0
|
|
417
|
+
if isinstance(zone_data, dict):
|
|
418
|
+
if "current_count" in zone_data:
|
|
419
|
+
current_count = zone_data.get("current_count", 0)
|
|
420
|
+
else:
|
|
421
|
+
counts_dict = zone_data.get("original_counts") if isinstance(zone_data.get("original_counts"), dict) else zone_data
|
|
422
|
+
current_count = counts_dict.get("total", sum(v for v in counts_dict.values() if isinstance(v, (int, float))))
|
|
423
|
+
human_text_lines.append(f"\t\t- {zone_name}: {int(current_count)}")
|
|
424
|
+
else:
|
|
425
|
+
human_text_lines.append(f"\t- Vehicles Detected: {total_detections}")
|
|
426
|
+
if per_category_count:
|
|
427
|
+
for cat, count in per_category_count.items():
|
|
428
|
+
if count > 0:
|
|
429
|
+
human_text_lines.append(f"\t\t- {cat}: {count}")
|
|
430
|
+
|
|
431
|
+
# === WRONG-WAY ANALYTICS IN HUMAN TEXT ===
|
|
432
|
+
if wrong_way_analytics:
|
|
433
|
+
ref_source = wrong_way_analytics.get('reference_source', 'NONE')
|
|
434
|
+
ref_status = wrong_way_analytics.get('reference_status', 'NONE')
|
|
435
|
+
current_wrong_way = wrong_way_analytics.get('current_wrong_way_count', 0)
|
|
436
|
+
total_wrong_way = wrong_way_analytics.get('total_wrong_way_count', 0)
|
|
437
|
+
current_suspect = wrong_way_analytics.get('current_suspect_count', 0)
|
|
438
|
+
|
|
439
|
+
human_text_lines.append("")
|
|
440
|
+
human_text_lines.append("WRONG-WAY DETECTION:")
|
|
441
|
+
human_text_lines.append(f"\t- Reference: {ref_source} ({ref_status})")
|
|
442
|
+
|
|
443
|
+
if ref_status == "LEARNING":
|
|
444
|
+
human_text_lines.append("\t- Status: Learning traffic pattern...")
|
|
445
|
+
else:
|
|
446
|
+
human_text_lines.append(f"\t- Current Wrong-Way: {current_wrong_way}")
|
|
447
|
+
human_text_lines.append(f"\t- Total Wrong-Way Events: {total_wrong_way}")
|
|
448
|
+
human_text_lines.append(f"\t- Current Suspects: {current_suspect}")
|
|
449
|
+
|
|
450
|
+
# List wrong-way vehicles with confidence
|
|
451
|
+
for det in wrong_way_analytics.get('current_wrong_way_detections', []):
|
|
452
|
+
human_text_lines.append(
|
|
453
|
+
f"\t\t- [WRONG-WAY] {det['category']} (ID:{det['track_id']}, "
|
|
454
|
+
f"conf:{det.get('wrong_way_confidence', 0):.2f})"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# List suspect vehicles
|
|
458
|
+
for det in wrong_way_analytics.get('current_suspect_detections', []):
|
|
459
|
+
human_text_lines.append(
|
|
460
|
+
f"\t\t- [SUSPECT] {det['category']} (ID:{det['track_id']}, "
|
|
461
|
+
f"conf:{det.get('wrong_way_confidence', 0):.2f})"
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
human_text_lines.append("")
|
|
466
|
+
human_text = "\n".join(human_text_lines)
|
|
467
|
+
|
|
468
|
+
# Build tracking stat
|
|
469
|
+
reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
|
|
470
|
+
tracking_stat = self.create_tracking_stats(
|
|
471
|
+
total_counts=total_counts,
|
|
472
|
+
current_counts=current_counts,
|
|
473
|
+
detections=detections,
|
|
474
|
+
human_text=human_text,
|
|
475
|
+
camera_info=camera_info,
|
|
476
|
+
alerts=alerts,
|
|
477
|
+
alert_settings=alert_settings,
|
|
478
|
+
reset_settings=reset_settings,
|
|
479
|
+
start_time=high_precision_start_timestamp,
|
|
480
|
+
reset_time=high_precision_reset_timestamp
|
|
481
|
+
)
|
|
482
|
+
tracking_stat['target_categories'] = self.target_categories
|
|
483
|
+
|
|
484
|
+
# NOTE : Add wrong-way analytics to tracking stats
|
|
485
|
+
if wrong_way_analytics:
|
|
486
|
+
tracking_stat["wrong_way_analytics"] = {
|
|
487
|
+
"reference_source": wrong_way_analytics.get("reference_source", "NONE"),
|
|
488
|
+
"reference_status": wrong_way_analytics.get("reference_status", "NONE"),
|
|
489
|
+
"current_wrong_way_count": wrong_way_analytics.get("current_wrong_way_count", 0),
|
|
490
|
+
"total_wrong_way_count": wrong_way_analytics.get("total_wrong_way_count", 0),
|
|
491
|
+
"current_wrong_way_detections": wrong_way_analytics.get("current_wrong_way_detections", []),
|
|
492
|
+
"current_suspect_count": wrong_way_analytics.get("current_suspect_count", 0),
|
|
493
|
+
"current_suspect_detections": wrong_way_analytics.get("current_suspect_detections", [])
|
|
494
|
+
}
|
|
495
|
+
self.logger.debug(
|
|
496
|
+
f"Wrong-way analytics: ref={wrong_way_analytics.get('reference_status')}, "
|
|
497
|
+
f"wrong_way={wrong_way_analytics.get('current_wrong_way_count', 0)}, "
|
|
498
|
+
f"suspect={wrong_way_analytics.get('current_suspect_count', 0)}"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
tracking_stats.append(tracking_stat)
|
|
502
|
+
return tracking_stats
|
|
503
|
+
|
|
504
|
+
def _setup_reference_from_zone(self, config: VehicleMonitoringWrongWayConfig) -> None:
|
|
505
|
+
"""Extract reference direction from zone_config (first point → last point)."""
|
|
506
|
+
|
|
507
|
+
if not config.zone_config or not config.zone_config.get('zones'):
|
|
508
|
+
self.logger.info("No zone_config provided — using auto-reference estimation")
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
zones = config.zone_config['zones']
|
|
512
|
+
|
|
513
|
+
# Use the first zone as reference direction source
|
|
514
|
+
for zone_name, zone_polygon in zones.items():
|
|
515
|
+
if zone_polygon and len(zone_polygon) >= 2:
|
|
516
|
+
self._reference_zone_name = zone_name
|
|
517
|
+
self._reference_zone_polygon = zone_polygon
|
|
518
|
+
|
|
519
|
+
success = self.wrong_way_tracker.set_reference_from_zone(zone_polygon)
|
|
520
|
+
|
|
521
|
+
if success:
|
|
522
|
+
self.logger.info(
|
|
523
|
+
f"Reference direction set from zone '{zone_name}': "
|
|
524
|
+
f"first={zone_polygon[0]} → last={zone_polygon[-1]}"
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
self.logger.warning(f"Failed to set reference from zone '{zone_name}'")
|
|
528
|
+
|
|
529
|
+
break
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _update_zone_tracking(
|
|
533
|
+
self,
|
|
534
|
+
zone_analysis: Dict[str, Dict[str, int]],
|
|
535
|
+
detections: List[Dict],
|
|
536
|
+
config: VehicleMonitoringWrongWayConfig
|
|
537
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
538
|
+
"""Update zone tracking with current frame data."""
|
|
539
|
+
if not zone_analysis or not config.zone_config or not config.zone_config['zones']:
|
|
540
|
+
return {}
|
|
541
|
+
|
|
542
|
+
enhanced_zone_analysis = {}
|
|
543
|
+
zones = config.zone_config['zones']
|
|
544
|
+
|
|
545
|
+
track_to_cat = {det.get('track_id'): det.get('category') for det in detections if det.get('track_id') is not None}
|
|
546
|
+
current_frame_zone_tracks = {}
|
|
547
|
+
|
|
548
|
+
for zone_name in zones.keys():
|
|
549
|
+
current_frame_zone_tracks[zone_name] = set()
|
|
550
|
+
if zone_name not in self._zone_current_track_ids:
|
|
551
|
+
self._zone_current_track_ids[zone_name] = set()
|
|
552
|
+
if zone_name not in self._zone_total_track_ids:
|
|
553
|
+
self._zone_total_track_ids[zone_name] = set()
|
|
554
|
+
|
|
555
|
+
for detection in detections:
|
|
556
|
+
track_id = detection.get("track_id")
|
|
557
|
+
if track_id is None:
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
bbox = detection.get("bounding_box", detection.get("bbox"))
|
|
561
|
+
if not bbox:
|
|
562
|
+
continue
|
|
563
|
+
|
|
564
|
+
center_point = get_bbox_bottom25_center(bbox)
|
|
565
|
+
in_any_zone = False
|
|
566
|
+
|
|
567
|
+
for zone_name, zone_polygon in zones.items():
|
|
568
|
+
polygon_points = [(point[0], point[1]) for point in zone_polygon]
|
|
569
|
+
if point_in_polygon(center_point, polygon_points):
|
|
570
|
+
current_frame_zone_tracks[zone_name].add(track_id)
|
|
571
|
+
in_any_zone = True
|
|
572
|
+
if track_id not in self._total_count_list:
|
|
573
|
+
self._total_count_list.append(track_id)
|
|
574
|
+
|
|
575
|
+
if in_any_zone:
|
|
576
|
+
cat = track_to_cat.get(track_id)
|
|
577
|
+
if cat:
|
|
578
|
+
self._current_frame_track_ids.setdefault(cat, set()).add(track_id)
|
|
579
|
+
if track_id not in self._tracked_in_zones:
|
|
580
|
+
self._tracked_in_zones.add(track_id)
|
|
581
|
+
self._per_category_total_track_ids.setdefault(cat, set()).add(track_id)
|
|
582
|
+
|
|
583
|
+
for zone_name, zone_counts in zone_analysis.items():
|
|
584
|
+
current_tracks = current_frame_zone_tracks.get(zone_name, set())
|
|
585
|
+
self._zone_current_track_ids[zone_name] = current_tracks
|
|
586
|
+
self._zone_total_track_ids[zone_name].update(current_tracks)
|
|
587
|
+
self._zone_current_counts[zone_name] = len(current_tracks)
|
|
588
|
+
self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
|
|
589
|
+
|
|
590
|
+
enhanced_zone_analysis[zone_name] = {
|
|
591
|
+
"current_count": self._zone_current_counts[zone_name],
|
|
592
|
+
"total_count": self._zone_total_counts[zone_name],
|
|
593
|
+
"current_track_ids": list(current_tracks),
|
|
594
|
+
"total_track_ids": list(self._zone_total_track_ids[zone_name]),
|
|
595
|
+
"original_counts": zone_counts
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return enhanced_zone_analysis
|
|
599
|
+
|
|
600
|
+
def _normalize_yolo_results(self, data: Any, index_to_category: Optional[Dict[int, str]] = None) -> Any:
|
|
601
|
+
"""Normalize YOLO-style outputs to internal detection schema."""
|
|
602
|
+
def to_bbox_dict(d: Dict[str, Any]) -> Dict[str, Any]:
|
|
603
|
+
if "bounding_box" in d and isinstance(d["bounding_box"], dict):
|
|
604
|
+
return d["bounding_box"]
|
|
605
|
+
if "bbox" in d:
|
|
606
|
+
bbox = d["bbox"]
|
|
607
|
+
if isinstance(bbox, dict):
|
|
608
|
+
return bbox
|
|
609
|
+
if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
|
|
610
|
+
return {"x1": bbox[0], "y1": bbox[1], "x2": bbox[2], "y2": bbox[3]}
|
|
611
|
+
if "xyxy" in d and isinstance(d["xyxy"], (list, tuple)) and len(d["xyxy"]) >= 4:
|
|
612
|
+
return {"x1": d["xyxy"][0], "y1": d["xyxy"][1], "x2": d["xyxy"][2], "y2": d["xyxy"][3]}
|
|
613
|
+
if "xywh" in d and isinstance(d["xywh"], (list, tuple)) and len(d["xywh"]) >= 4:
|
|
614
|
+
cx, cy, w, h = d["xywh"][:4]
|
|
615
|
+
return {"x1": cx - w/2, "y1": cy - h/2, "x2": cx + w/2, "y2": cy + h/2}
|
|
616
|
+
return {}
|
|
617
|
+
|
|
618
|
+
def resolve_category(d: Dict[str, Any]) -> Tuple[str, Optional[int]]:
|
|
619
|
+
raw_cls = d.get("category", d.get("category_id", d.get("class", d.get("cls"))))
|
|
620
|
+
label_name = d.get("name")
|
|
621
|
+
if isinstance(raw_cls, int):
|
|
622
|
+
if index_to_category and raw_cls in index_to_category:
|
|
623
|
+
return index_to_category[raw_cls], raw_cls
|
|
624
|
+
return str(raw_cls), raw_cls
|
|
625
|
+
if isinstance(raw_cls, str):
|
|
626
|
+
return raw_cls, None
|
|
627
|
+
if label_name:
|
|
628
|
+
return str(label_name), None
|
|
629
|
+
return "unknown", None
|
|
630
|
+
|
|
631
|
+
def normalize_det(det: Dict[str, Any]) -> Dict[str, Any]:
|
|
632
|
+
category_name, category_id = resolve_category(det)
|
|
633
|
+
confidence = det.get("confidence", det.get("conf", det.get("score", 0.0)))
|
|
634
|
+
bbox = to_bbox_dict(det)
|
|
635
|
+
normalized = {"category": category_name, "confidence": confidence, "bounding_box": bbox}
|
|
636
|
+
if category_id is not None:
|
|
637
|
+
normalized["category_id"] = category_id
|
|
638
|
+
for key in ("track_id", "frame_id", "masks", "segmentation"):
|
|
639
|
+
if key in det:
|
|
640
|
+
normalized[key] = det[key]
|
|
641
|
+
return normalized
|
|
642
|
+
|
|
643
|
+
if isinstance(data, list):
|
|
644
|
+
return [normalize_det(d) if isinstance(d, dict) else d for d in data]
|
|
645
|
+
if isinstance(data, dict):
|
|
646
|
+
normalized_dict: Dict[str, Any] = {}
|
|
647
|
+
for k, v in data.items():
|
|
648
|
+
if isinstance(v, list):
|
|
649
|
+
normalized_dict[k] = [normalize_det(d) if isinstance(d, dict) else d for d in v]
|
|
650
|
+
elif isinstance(v, dict):
|
|
651
|
+
normalized_dict[k] = normalize_det(v)
|
|
652
|
+
else:
|
|
653
|
+
normalized_dict[k] = v
|
|
654
|
+
return normalized_dict
|
|
655
|
+
return data
|
|
656
|
+
|
|
657
|
+
def _check_alerts(self, summary: dict, zone_analysis: Dict, frame_number: Any, config: VehicleMonitoringWrongWayConfig) -> List[Dict]:
|
|
658
|
+
"""Check for alert conditions."""
|
|
659
|
+
alerts = []
|
|
660
|
+
if not config.alert_config:
|
|
661
|
+
return alerts
|
|
662
|
+
|
|
663
|
+
total_detections = summary.get("total_count", 0)
|
|
664
|
+
per_category_count = summary.get("per_category_count", {})
|
|
665
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
666
|
+
|
|
667
|
+
if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
|
|
668
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
|
669
|
+
if category == "all" and total_detections > threshold:
|
|
670
|
+
alerts.append({
|
|
671
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
672
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
673
|
+
"incident_category": self.CASE_TYPE,
|
|
674
|
+
"threshold_level": threshold,
|
|
675
|
+
"settings": {}
|
|
676
|
+
})
|
|
677
|
+
elif category in per_category_count and per_category_count[category] > threshold:
|
|
678
|
+
alerts.append({
|
|
679
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
680
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
681
|
+
"incident_category": self.CASE_TYPE,
|
|
682
|
+
"threshold_level": threshold,
|
|
683
|
+
"settings": {}
|
|
684
|
+
})
|
|
685
|
+
return alerts
|
|
686
|
+
|
|
687
|
+
def _generate_business_analytics(self, counting_summary: Dict, zone_analysis: Dict, alerts: Any,
|
|
688
|
+
config: VehicleMonitoringWrongWayConfig, stream_info: Optional[Dict[str, Any]] = None,
|
|
689
|
+
is_empty=False) -> List[Dict]:
|
|
690
|
+
"""Generate business analytics (placeholder)."""
|
|
691
|
+
if is_empty:
|
|
692
|
+
return []
|
|
693
|
+
return []
|
|
694
|
+
|
|
695
|
+
def _generate_summary(self, summary: dict, zone_analysis: Dict, incidents: List, tracking_stats: List,
|
|
696
|
+
business_analytics: List, alerts: List) -> List[str]:
|
|
697
|
+
"""Generate human-readable summary."""
|
|
698
|
+
lines = []
|
|
699
|
+
lines.append(f"Application Name: {self.CASE_TYPE}")
|
|
700
|
+
lines.append(f"Application Version: {self.CASE_VERSION}")
|
|
701
|
+
if len(incidents) > 0:
|
|
702
|
+
lines.append(f"Incidents: \n\t{incidents[0].get('human_text', 'No incidents detected')}")
|
|
703
|
+
if len(tracking_stats) > 0:
|
|
704
|
+
lines.append(f"Tracking Statistics: \t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
|
|
705
|
+
if len(business_analytics) > 0:
|
|
706
|
+
lines.append(f"Business Analytics: \t{business_analytics[0].get('human_text', 'No business analytics detected')}")
|
|
707
|
+
if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
|
|
708
|
+
lines.append("Summary: No Summary Data")
|
|
709
|
+
return ["\n".join(lines)]
|
|
710
|
+
|
|
711
|
+
def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
|
|
712
|
+
"""Get track ID information."""
|
|
713
|
+
frame_track_ids = set()
|
|
714
|
+
for det in detections:
|
|
715
|
+
tid = det.get('track_id')
|
|
716
|
+
if tid is not None:
|
|
717
|
+
frame_track_ids.add(tid)
|
|
718
|
+
total_track_ids = set()
|
|
719
|
+
for s in getattr(self, '_per_category_total_track_ids', {}).values():
|
|
720
|
+
total_track_ids.update(s)
|
|
721
|
+
return {
|
|
722
|
+
"total_count": len(total_track_ids),
|
|
723
|
+
"current_frame_count": len(frame_track_ids),
|
|
724
|
+
"total_unique_track_ids": len(total_track_ids),
|
|
725
|
+
"current_frame_track_ids": list(frame_track_ids),
|
|
726
|
+
"last_update_time": time.time(),
|
|
727
|
+
"total_frames_processed": getattr(self, '_total_frame_counter', 0)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
def _update_tracking_state(self, detections: list, has_zones: bool = False):
|
|
731
|
+
"""Update tracking state with canonical ID merging."""
|
|
732
|
+
if not hasattr(self, "_per_category_total_track_ids"):
|
|
733
|
+
self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
|
|
734
|
+
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
735
|
+
|
|
736
|
+
for det in detections:
|
|
737
|
+
cat = det.get("category")
|
|
738
|
+
raw_track_id = det.get("track_id")
|
|
739
|
+
if cat not in self.target_categories or raw_track_id is None:
|
|
740
|
+
continue
|
|
741
|
+
bbox = det.get("bounding_box", det.get("bbox"))
|
|
742
|
+
canonical_id = self._merge_or_register_track(raw_track_id, bbox)
|
|
743
|
+
det["track_id"] = canonical_id
|
|
744
|
+
if not has_zones:
|
|
745
|
+
self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
|
|
746
|
+
self._current_frame_track_ids.setdefault(cat, set()).add(canonical_id)
|
|
747
|
+
|
|
748
|
+
def get_total_counts(self):
|
|
749
|
+
"""Get total counts per category."""
|
|
750
|
+
return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
|
|
751
|
+
|
|
752
|
+
def _count_categories(self, detections: list, config: VehicleMonitoringWrongWayConfig) -> dict:
|
|
753
|
+
"""Count detections per category."""
|
|
754
|
+
counts = {}
|
|
755
|
+
for det in detections:
|
|
756
|
+
cat = det.get('category', 'unknown')
|
|
757
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
758
|
+
return {
|
|
759
|
+
"total_count": sum(counts.values()),
|
|
760
|
+
"per_category_count": counts,
|
|
761
|
+
"detections": [
|
|
762
|
+
{
|
|
763
|
+
"bounding_box": det.get("bounding_box"),
|
|
764
|
+
"category": det.get("category"),
|
|
765
|
+
"confidence": det.get("confidence"),
|
|
766
|
+
"track_id": det.get("track_id"),
|
|
767
|
+
"frame_id": det.get("frame_id")
|
|
768
|
+
}
|
|
769
|
+
for det in detections
|
|
770
|
+
]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
|
|
774
|
+
"""Extract predictions from detections."""
|
|
775
|
+
return [
|
|
776
|
+
{
|
|
777
|
+
"category": det.get("category", "unknown"),
|
|
778
|
+
"confidence": det.get("confidence", 0.0),
|
|
779
|
+
"bounding_box": det.get("bounding_box", {})
|
|
780
|
+
}
|
|
781
|
+
for det in detections
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
|
785
|
+
"""Format timestamp for stream output."""
|
|
786
|
+
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
787
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
788
|
+
|
|
789
|
+
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
|
790
|
+
"""Format timestamp for video output."""
|
|
791
|
+
hours = int(timestamp // 3600)
|
|
792
|
+
minutes = int((timestamp % 3600) // 60)
|
|
793
|
+
seconds = round(float(timestamp % 60), 2)
|
|
794
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
795
|
+
|
|
796
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
797
|
+
"""Format timestamp to standard format."""
|
|
798
|
+
if isinstance(timestamp, (int, float)):
|
|
799
|
+
dt = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
800
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
801
|
+
if not isinstance(timestamp, str):
|
|
802
|
+
return str(timestamp)
|
|
803
|
+
timestamp_clean = timestamp.replace(' UTC', '').strip()
|
|
804
|
+
if '.' in timestamp_clean:
|
|
805
|
+
timestamp_clean = timestamp_clean.split('.')[0]
|
|
806
|
+
try:
|
|
807
|
+
if timestamp_clean.count('-') >= 2:
|
|
808
|
+
parts = timestamp_clean.split('-')
|
|
809
|
+
if len(parts) >= 4:
|
|
810
|
+
return f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
|
|
811
|
+
except Exception:
|
|
812
|
+
pass
|
|
813
|
+
return timestamp_clean
|
|
814
|
+
|
|
815
|
+
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
816
|
+
"""Get formatted current timestamp based on stream type."""
|
|
817
|
+
if not stream_info:
|
|
818
|
+
return "00:00:00.00"
|
|
819
|
+
|
|
820
|
+
if precision:
|
|
821
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
822
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
823
|
+
else:
|
|
824
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
825
|
+
|
|
826
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
827
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
828
|
+
else:
|
|
829
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
830
|
+
if stream_time_str:
|
|
831
|
+
try:
|
|
832
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
833
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
834
|
+
timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
835
|
+
return self._format_timestamp_for_stream(timestamp)
|
|
836
|
+
except:
|
|
837
|
+
return self._format_timestamp_for_stream(time.time())
|
|
838
|
+
else:
|
|
839
|
+
return self._format_timestamp_for_stream(time.time())
|
|
840
|
+
|
|
841
|
+
def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
|
|
842
|
+
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
843
|
+
if not stream_info:
|
|
844
|
+
return "00:00:00"
|
|
845
|
+
|
|
846
|
+
if precision:
|
|
847
|
+
if self.start_timer is None:
|
|
848
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
849
|
+
if not candidate or candidate == "NA":
|
|
850
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
851
|
+
self.start_timer = candidate
|
|
852
|
+
return self._format_timestamp(self.start_timer)
|
|
853
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
854
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
855
|
+
if not candidate or candidate == "NA":
|
|
856
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
857
|
+
self.start_timer = candidate
|
|
858
|
+
return self._format_timestamp(self.start_timer)
|
|
859
|
+
else:
|
|
860
|
+
return self._format_timestamp(self.start_timer)
|
|
861
|
+
|
|
862
|
+
if self.start_timer is None:
|
|
863
|
+
# Prefer direct input_settings.stream_time if available and not NA
|
|
864
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
865
|
+
if not candidate or candidate == "NA":
|
|
866
|
+
# Fallback to nested stream_info.stream_time used by current timestamp path
|
|
867
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
868
|
+
if stream_time_str:
|
|
869
|
+
try:
|
|
870
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
871
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
872
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
873
|
+
candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
874
|
+
except:
|
|
875
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
876
|
+
else:
|
|
877
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
878
|
+
self.start_timer = candidate
|
|
879
|
+
return self._format_timestamp(self.start_timer)
|
|
880
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
881
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
882
|
+
if not candidate or candidate == "NA":
|
|
883
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
884
|
+
if stream_time_str:
|
|
885
|
+
try:
|
|
886
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
887
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
888
|
+
ts = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
889
|
+
candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
890
|
+
except:
|
|
891
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
892
|
+
else:
|
|
893
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
894
|
+
self.start_timer = candidate
|
|
895
|
+
return self._format_timestamp(self.start_timer)
|
|
896
|
+
|
|
897
|
+
else:
|
|
898
|
+
if self.start_timer is not None and self.start_timer != "NA":
|
|
899
|
+
return self._format_timestamp(self.start_timer)
|
|
900
|
+
|
|
901
|
+
if self._tracking_start_time is None:
|
|
902
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
903
|
+
if stream_time_str:
|
|
904
|
+
try:
|
|
905
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
906
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
907
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
908
|
+
except:
|
|
909
|
+
self._tracking_start_time = time.time()
|
|
910
|
+
else:
|
|
911
|
+
self._tracking_start_time = time.time()
|
|
912
|
+
|
|
913
|
+
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
|
914
|
+
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
915
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
916
|
+
|
|
917
|
+
def _count_categories(self, detections: list, config: VehicleMonitoringWrongWayConfig) -> dict:
|
|
918
|
+
counts = {}
|
|
919
|
+
for det in detections:
|
|
920
|
+
cat = det.get('category', 'unknown')
|
|
921
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
922
|
+
return {
|
|
923
|
+
"total_count": sum(counts.values()),
|
|
924
|
+
"per_category_count": counts,
|
|
925
|
+
"detections": [
|
|
926
|
+
{
|
|
927
|
+
"bounding_box": det.get("bounding_box"),
|
|
928
|
+
"category": det.get("category"),
|
|
929
|
+
"confidence": det.get("confidence"),
|
|
930
|
+
"track_id": det.get("track_id"),
|
|
931
|
+
"frame_id": det.get("frame_id")
|
|
932
|
+
}
|
|
933
|
+
for det in detections
|
|
934
|
+
]
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
|
|
938
|
+
return [
|
|
939
|
+
{
|
|
940
|
+
"category": det.get("category", "unknown"),
|
|
941
|
+
"confidence": det.get("confidence", 0.0),
|
|
942
|
+
"bounding_box": det.get("bounding_box", {})
|
|
943
|
+
}
|
|
944
|
+
for det in detections
|
|
945
|
+
]
|
|
946
|
+
|
|
947
|
+
def _compute_iou(self, box1: Any, box2: Any) -> float:
|
|
948
|
+
def _bbox_to_list(bbox):
|
|
949
|
+
if bbox is None:
|
|
950
|
+
return []
|
|
951
|
+
if isinstance(bbox, list):
|
|
952
|
+
return bbox[:4] if len(bbox) >= 4 else []
|
|
953
|
+
if isinstance(bbox, dict):
|
|
954
|
+
if "xmin" in bbox:
|
|
955
|
+
return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
|
|
956
|
+
if "x1" in bbox:
|
|
957
|
+
return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
|
|
958
|
+
values = [v for v in bbox.values() if isinstance(v, (int, float))]
|
|
959
|
+
return values[:4] if len(values) >= 4 else []
|
|
960
|
+
return []
|
|
961
|
+
|
|
962
|
+
l1 = _bbox_to_list(box1)
|
|
963
|
+
l2 = _bbox_to_list(box2)
|
|
964
|
+
if len(l1) < 4 or len(l2) < 4:
|
|
965
|
+
return 0.0
|
|
966
|
+
x1_min, y1_min, x1_max, y1_max = l1
|
|
967
|
+
x2_min, y2_min, x2_max, y2_max = l2
|
|
968
|
+
x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
|
|
969
|
+
y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
|
|
970
|
+
x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
|
|
971
|
+
y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
|
|
972
|
+
inter_x_min = max(x1_min, x2_min)
|
|
973
|
+
inter_y_min = max(y1_min, y2_min)
|
|
974
|
+
inter_x_max = min(x1_max, x2_max)
|
|
975
|
+
inter_y_max = min(y1_max, y2_max)
|
|
976
|
+
inter_w = max(0.0, inter_x_max - inter_x_min)
|
|
977
|
+
inter_h = max(0.0, inter_y_max - inter_y_min)
|
|
978
|
+
inter_area = inter_w * inter_h
|
|
979
|
+
area1 = (x1_max - x1_min) * (y1_max - y1_min)
|
|
980
|
+
area2 = (x2_max - x2_min) * (y2_max - y2_min)
|
|
981
|
+
union_area = area1 + area2 - inter_area
|
|
982
|
+
return (inter_area / union_area) if union_area > 0 else 0.0
|
|
983
|
+
|
|
984
|
+
def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
|
|
985
|
+
if raw_id is None or bbox is None:
|
|
986
|
+
return raw_id
|
|
987
|
+
now = time.time()
|
|
988
|
+
if raw_id in self._track_aliases:
|
|
989
|
+
canonical_id = self._track_aliases[raw_id]
|
|
990
|
+
track_info = self._canonical_tracks.get(canonical_id)
|
|
991
|
+
if track_info is not None:
|
|
992
|
+
track_info["last_bbox"] = bbox
|
|
993
|
+
track_info["last_update"] = now
|
|
994
|
+
track_info["raw_ids"].add(raw_id)
|
|
995
|
+
return canonical_id
|
|
996
|
+
for canonical_id, info in self._canonical_tracks.items():
|
|
997
|
+
if now - info["last_update"] > self._track_merge_time_window:
|
|
998
|
+
continue
|
|
999
|
+
iou = self._compute_iou(bbox, info["last_bbox"])
|
|
1000
|
+
if iou >= self._track_merge_iou_threshold:
|
|
1001
|
+
self._track_aliases[raw_id] = canonical_id
|
|
1002
|
+
info["last_bbox"] = bbox
|
|
1003
|
+
info["last_update"] = now
|
|
1004
|
+
info["raw_ids"].add(raw_id)
|
|
1005
|
+
return canonical_id
|
|
1006
|
+
canonical_id = raw_id
|
|
1007
|
+
self._track_aliases[raw_id] = canonical_id
|
|
1008
|
+
self._canonical_tracks[canonical_id] = {
|
|
1009
|
+
"last_bbox": bbox,
|
|
1010
|
+
"last_update": now,
|
|
1011
|
+
"raw_ids": {raw_id},
|
|
1012
|
+
}
|
|
1013
|
+
return canonical_id
|
|
1014
|
+
|
|
1015
|
+
def _get_tracking_start_time(self) -> str:
|
|
1016
|
+
if self._tracking_start_time is None:
|
|
1017
|
+
return "N/A"
|
|
1018
|
+
return self._format_timestamp(self._tracking_start_time)
|
|
1019
|
+
|
|
1020
|
+
def _set_tracking_start_time(self) -> None:
|
|
1021
|
+
self._tracking_start_time = time.time()
|