matrice-analytics 0.1.96__py3-none-any.whl → 0.1.106__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 +14 -1
- matrice_analytics/post_processing/advanced_tracker/config.py +8 -4
- matrice_analytics/post_processing/advanced_tracker/track_class_aggregator.py +128 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +22 -1
- matrice_analytics/post_processing/config.py +6 -2
- matrice_analytics/post_processing/core/config.py +62 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +706 -73
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +25 -14
- matrice_analytics/post_processing/post_processor.py +8 -0
- matrice_analytics/post_processing/usecases/__init__.py +7 -1
- matrice_analytics/post_processing/usecases/footfall.py +109 -2
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +55 -37
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +14 -32
- matrice_analytics/post_processing/usecases/vehicle_monitoring_drone_view.py +1223 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring_parking_lot.py +1028 -0
- matrice_analytics/post_processing/utils/__init__.py +5 -0
- matrice_analytics/post_processing/utils/agnostic_nms.py +759 -0
- matrice_analytics/post_processing/utils/alert_instance_utils.py +37 -2
- {matrice_analytics-0.1.96.dist-info → matrice_analytics-0.1.106.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.96.dist-info → matrice_analytics-0.1.106.dist-info}/RECORD +23 -19
- {matrice_analytics-0.1.96.dist-info → matrice_analytics-0.1.106.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.96.dist-info → matrice_analytics-0.1.106.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.96.dist-info → matrice_analytics-0.1.106.dist-info}/top_level.txt +0 -0
|
@@ -154,33 +154,40 @@ class PeopleActivityLogging:
|
|
|
154
154
|
except Exception as e:
|
|
155
155
|
self.logger.error(f"Error enqueueing detection: {e}", exc_info=True)
|
|
156
156
|
|
|
157
|
-
def _should_log_detection(self, employee_id: str) -> bool:
|
|
157
|
+
def _should_log_detection(self, employee_id: str, camera_id: str = "") -> bool:
|
|
158
158
|
"""
|
|
159
|
-
Check if detection should be logged based on employee ID and time threshold.
|
|
160
|
-
Only log if employee_id was not detected in the past
|
|
159
|
+
Check if detection should be logged based on employee ID (+ camera ID) and time threshold.
|
|
160
|
+
Only log if the same (employee_id, camera_id) was not detected in the past N seconds.
|
|
161
|
+
If camera_id is empty, falls back to global employee_id de-duplication (backward compatible).
|
|
161
162
|
|
|
162
163
|
TODO: Make this use track_id or similarity check instead of just employee_id in 10 secs window
|
|
163
164
|
for better deduplication across different detection sessions.
|
|
164
165
|
"""
|
|
165
166
|
current_time = time.time()
|
|
167
|
+
dedupe_key = f"{employee_id}::{camera_id}" if camera_id else employee_id
|
|
166
168
|
|
|
167
169
|
# Clean up old entries (older than threshold)
|
|
168
170
|
expired_keys = [
|
|
169
|
-
|
|
171
|
+
key for key, timestamp in self.recent_employee_detections.items()
|
|
170
172
|
if current_time - timestamp > self.employee_detection_threshold
|
|
171
173
|
]
|
|
172
|
-
for
|
|
173
|
-
del self.recent_employee_detections[
|
|
174
|
+
for key in expired_keys:
|
|
175
|
+
del self.recent_employee_detections[key]
|
|
174
176
|
|
|
175
|
-
# Check if employee was recently detected
|
|
176
|
-
if
|
|
177
|
-
last_detection = self.recent_employee_detections[
|
|
177
|
+
# Check if employee was recently detected (per camera_id)
|
|
178
|
+
if dedupe_key in self.recent_employee_detections:
|
|
179
|
+
last_detection = self.recent_employee_detections[dedupe_key]
|
|
178
180
|
if current_time - last_detection < self.employee_detection_threshold:
|
|
179
|
-
self.logger.debug(
|
|
181
|
+
self.logger.debug(
|
|
182
|
+
"Skipping logging for employee %s (camera_id=%s) - detected %.1fs ago",
|
|
183
|
+
employee_id,
|
|
184
|
+
camera_id,
|
|
185
|
+
current_time - last_detection,
|
|
186
|
+
)
|
|
180
187
|
return False
|
|
181
188
|
|
|
182
|
-
# Update detection time for this employee
|
|
183
|
-
self.recent_employee_detections[
|
|
189
|
+
# Update detection time for this (employee, camera)
|
|
190
|
+
self.recent_employee_detections[dedupe_key] = current_time
|
|
184
191
|
return True
|
|
185
192
|
|
|
186
193
|
async def _process_activity(self, activity_data: Dict):
|
|
@@ -202,8 +209,12 @@ class PeopleActivityLogging:
|
|
|
202
209
|
return
|
|
203
210
|
|
|
204
211
|
# Check if we should log this detection (avoid duplicates within time window)
|
|
205
|
-
if not self._should_log_detection(employee_id):
|
|
206
|
-
self.logger.debug(
|
|
212
|
+
if not self._should_log_detection(employee_id, camera_id=camera_id):
|
|
213
|
+
self.logger.debug(
|
|
214
|
+
"Skipping activity log for employee_id=%s (camera_id=%s) (within cooldown period)",
|
|
215
|
+
employee_id,
|
|
216
|
+
camera_id,
|
|
217
|
+
)
|
|
207
218
|
return None
|
|
208
219
|
|
|
209
220
|
# Encode frame as base64 JPEG
|
|
@@ -100,6 +100,8 @@ from .usecases import (
|
|
|
100
100
|
SusActivityUseCase,
|
|
101
101
|
NaturalDisasterUseCase,
|
|
102
102
|
FootFallUseCase,
|
|
103
|
+
VehicleMonitoringParkingLotUseCase,
|
|
104
|
+
VehicleMonitoringDroneViewUseCase,
|
|
103
105
|
# Put all IMAGE based usecases here
|
|
104
106
|
BloodCancerDetectionUseCase,
|
|
105
107
|
SkinCancerClassificationUseCase,
|
|
@@ -573,6 +575,12 @@ class PostProcessor:
|
|
|
573
575
|
registry.register_use_case(
|
|
574
576
|
"retail", "footfall", FootFallUseCase
|
|
575
577
|
)
|
|
578
|
+
registry.register_use_case(
|
|
579
|
+
"traffic", "vehicle_monitoring_parking_lot", VehicleMonitoringParkingLotUseCase
|
|
580
|
+
)
|
|
581
|
+
registry.register_use_case(
|
|
582
|
+
"traffic", "vehicle_monitoring_drone_view", VehicleMonitoringDroneViewUseCase
|
|
583
|
+
)
|
|
576
584
|
|
|
577
585
|
# Put all IMAGE based usecases here
|
|
578
586
|
registry.register_use_case(
|
|
@@ -86,7 +86,8 @@ from .underground_pipeline_defect_detection import UndergroundPipelineDefectConf
|
|
|
86
86
|
from .suspicious_activity_detection import SusActivityConfig, SusActivityUseCase
|
|
87
87
|
from .natural_disaster import NaturalDisasterConfig, NaturalDisasterUseCase
|
|
88
88
|
from .footfall import FootFallConfig, FootFallUseCase
|
|
89
|
-
|
|
89
|
+
from .vehicle_monitoring_parking_lot import VehicleMonitoringParkingLotUseCase, VehicleMonitoringParkingLotConfig
|
|
90
|
+
from .vehicle_monitoring_drone_view import VehicleMonitoringDroneViewUseCase, VehicleMonitoringDroneViewConfig
|
|
90
91
|
|
|
91
92
|
#Put all IMAGE based usecases here
|
|
92
93
|
from .blood_cancer_detection_img import BloodCancerDetectionConfig, BloodCancerDetectionUseCase
|
|
@@ -175,6 +176,8 @@ __all__ = [
|
|
|
175
176
|
'SusActivityUseCase',
|
|
176
177
|
'NaturalDisasterUseCase',
|
|
177
178
|
'FootFallUseCase',
|
|
179
|
+
'VehicleMonitoringParkingLotUseCase',
|
|
180
|
+
'VehicleMonitoringDroneViewUseCase',
|
|
178
181
|
|
|
179
182
|
#Put all IMAGE based usecases here
|
|
180
183
|
'BloodCancerDetectionUseCase',
|
|
@@ -258,6 +261,9 @@ __all__ = [
|
|
|
258
261
|
'SusActivityConfig',
|
|
259
262
|
'NaturalDisasterConfig',
|
|
260
263
|
'FootFallConfig',
|
|
264
|
+
'VehicleMonitoringParkingLotConfig',
|
|
265
|
+
'VehicleMonitoringDroneViewConfig',
|
|
266
|
+
|
|
261
267
|
#Put all IMAGE based usecase CONFIGS here
|
|
262
268
|
'BloodCancerDetectionConfig',
|
|
263
269
|
'SkinCancerClassificationConfig',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
|
2
2
|
from dataclasses import asdict
|
|
3
3
|
import time
|
|
4
|
+
import math
|
|
5
|
+
import numpy as np
|
|
4
6
|
from datetime import datetime, timezone
|
|
7
|
+
from collections import defaultdict, deque
|
|
5
8
|
|
|
6
9
|
from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
|
|
7
10
|
from ..utils import (
|
|
@@ -19,6 +22,76 @@ from ..utils import (
|
|
|
19
22
|
from dataclasses import dataclass, field
|
|
20
23
|
from ..core.config import BaseConfig, AlertConfig, ZoneConfig
|
|
21
24
|
|
|
25
|
+
class TrajectoryCorrector:
|
|
26
|
+
"""
|
|
27
|
+
Handles Velocity-Fusion logic to correct model orientation errors.
|
|
28
|
+
Stores history of track centers and applies EMA smoothing.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self):
|
|
31
|
+
# track_id -> { "centers": deque, "smooth_angle": float }
|
|
32
|
+
self.history = defaultdict(lambda: {
|
|
33
|
+
"centers": deque(maxlen=10), # Lookback for velocity
|
|
34
|
+
"smooth_angle": None # For EMA smoothing
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
def update_and_correct(self, track_id, center, raw_angle_deg):
|
|
38
|
+
"""
|
|
39
|
+
Returns the corrected angle based on velocity fusion.
|
|
40
|
+
"""
|
|
41
|
+
state = self.history[track_id]
|
|
42
|
+
state["centers"].append(center)
|
|
43
|
+
|
|
44
|
+
# 1. Calculate Velocity Angle
|
|
45
|
+
velocity_angle = self._compute_velocity_angle(state["centers"])
|
|
46
|
+
|
|
47
|
+
# 2. Apply +90 Fix to Raw Model Angle (Matches your successful tests)
|
|
48
|
+
# Note: raw_angle_deg comes from predict.py
|
|
49
|
+
if raw_angle_deg is None: raw_angle_deg = 0.0
|
|
50
|
+
model_angle = (raw_angle_deg + 90) % 360
|
|
51
|
+
|
|
52
|
+
# 3. Determine Target (Velocity vs Model)
|
|
53
|
+
# Hybrid Logic: If moving (velocity valid), use Physics. Else, use Visuals.
|
|
54
|
+
target_angle = velocity_angle if velocity_angle is not None else model_angle
|
|
55
|
+
|
|
56
|
+
# 4. Apply EMA Smoothing (The Jitter Killer)
|
|
57
|
+
# alpha=0.2 means we trust new data 20%, old history 80%
|
|
58
|
+
state["smooth_angle"] = self._apply_ema(state["smooth_angle"], target_angle, alpha=0.2)
|
|
59
|
+
|
|
60
|
+
return state["smooth_angle"]
|
|
61
|
+
|
|
62
|
+
def _compute_velocity_angle(self, centers):
|
|
63
|
+
if len(centers) < 2:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# Look back 5 frames for stability
|
|
67
|
+
lookback = min(len(centers), 5)
|
|
68
|
+
(x_past, y_past) = centers[-lookback]
|
|
69
|
+
(x_now, y_now) = centers[-1]
|
|
70
|
+
|
|
71
|
+
dx = x_now - x_past
|
|
72
|
+
dy = y_now - y_past
|
|
73
|
+
|
|
74
|
+
# THRESHOLD: 2.5 pixels (Validated in your tests)
|
|
75
|
+
# If moving less than this, velocity is noise.
|
|
76
|
+
if math.hypot(dx, dy) < 2.5:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Angle calculation (0-360)
|
|
80
|
+
return math.degrees(math.atan2(-dy, dx)) % 360
|
|
81
|
+
|
|
82
|
+
def _apply_ema(self, current_smooth, new_target, alpha=0.2):
|
|
83
|
+
if current_smooth is None:
|
|
84
|
+
return new_target
|
|
85
|
+
|
|
86
|
+
# Vector smoothing to handle 0/360 wrap-around correctly
|
|
87
|
+
prev_rad = math.radians(current_smooth)
|
|
88
|
+
curr_rad = math.radians(new_target)
|
|
89
|
+
|
|
90
|
+
new_sin = (1 - alpha) * math.sin(prev_rad) + alpha * math.sin(curr_rad)
|
|
91
|
+
new_cos = (1 - alpha) * math.cos(prev_rad) + alpha * math.cos(curr_rad)
|
|
92
|
+
|
|
93
|
+
return math.degrees(math.atan2(new_sin, new_cos)) % 360
|
|
94
|
+
|
|
22
95
|
@dataclass
|
|
23
96
|
class FootFallConfig(BaseConfig):
|
|
24
97
|
"""Configuration for footfall use case."""
|
|
@@ -79,9 +152,12 @@ class FootFallUseCase(BaseProcessor):
|
|
|
79
152
|
self.category = "retail"
|
|
80
153
|
self.CASE_TYPE: Optional[str] = 'footfall'
|
|
81
154
|
self.CASE_VERSION: Optional[str] = '1.1'
|
|
82
|
-
self.target_categories = ['person']
|
|
155
|
+
self.target_categories = ['person']
|
|
83
156
|
self.smoothing_tracker = None
|
|
84
157
|
self.tracker = None
|
|
158
|
+
|
|
159
|
+
# Initialize the Velocity Logic
|
|
160
|
+
self.trajectory_corrector = TrajectoryCorrector()
|
|
85
161
|
self._total_frame_counter = 0
|
|
86
162
|
self._global_frame_offset = 0
|
|
87
163
|
self._tracking_start_time = None
|
|
@@ -144,9 +220,40 @@ class FootFallUseCase(BaseProcessor):
|
|
|
144
220
|
match_thresh=0.8)
|
|
145
221
|
self.tracker = AdvancedTracker(tracker_config)
|
|
146
222
|
self.logger.info("Initialized AdvancedTracker for People Counting")
|
|
223
|
+
|
|
224
|
+
# 1. Run Standard Tracker (Assigns IDs)
|
|
147
225
|
processed_data = self.tracker.update(processed_data)
|
|
226
|
+
|
|
227
|
+
# =========================================================
|
|
228
|
+
# NEW: INJECT VELOCITY FUSION LOGIC
|
|
229
|
+
# =========================================================
|
|
230
|
+
for det in processed_data:
|
|
231
|
+
track_id = det.get("track_id")
|
|
232
|
+
bbox = det.get("bounding_box", det.get("bbox"))
|
|
233
|
+
|
|
234
|
+
# Check for 'raw_angle' (from predict.py) or 'orientation'
|
|
235
|
+
raw_angle = det.get("raw_angle", det.get("orientation", 0.0))
|
|
236
|
+
|
|
237
|
+
if track_id is not None and bbox:
|
|
238
|
+
# Calculate Center (cx, cy)
|
|
239
|
+
cx = int((bbox[0] + bbox[2]) / 2)
|
|
240
|
+
cy = int((bbox[1] + bbox[3]) / 2)
|
|
241
|
+
|
|
242
|
+
# Run Correction (Velocity + EMA + 90 Fix)
|
|
243
|
+
final_angle = self.trajectory_corrector.update_and_correct(
|
|
244
|
+
track_id,
|
|
245
|
+
(cx, cy),
|
|
246
|
+
raw_angle
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# OVERWRITE the detection angle
|
|
250
|
+
# This ensures _generate_tracking_stats uses YOUR logic
|
|
251
|
+
det["orientation"] = final_angle # For UI
|
|
252
|
+
det["angle"] = final_angle # For Analytics
|
|
253
|
+
# =========================================================
|
|
254
|
+
|
|
148
255
|
except Exception as e:
|
|
149
|
-
self.logger.warning(f"AdvancedTracker failed: {e}")
|
|
256
|
+
self.logger.warning(f"AdvancedTracker/Velocity failed: {e}")
|
|
150
257
|
|
|
151
258
|
self._update_tracking_state(processed_data)
|
|
152
259
|
self._total_frame_counter += 1
|
|
@@ -535,6 +535,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
535
535
|
self.plate_logger: Optional[LicensePlateMonitorLogger] = None
|
|
536
536
|
self._logging_enabled = True # False //ToDo: DISABLED FOR NOW, ENABLED FOR PRODUCTION. ##
|
|
537
537
|
self._plate_logger_initialized = False # Track if plate logger has been initialized
|
|
538
|
+
|
|
539
|
+
# Track which track_ids have been logged to avoid duplicate logging
|
|
540
|
+
# Only log confirmed/consensus plates, not every OCR prediction
|
|
541
|
+
self._logged_track_ids: set = set()
|
|
538
542
|
|
|
539
543
|
# Initialize instant alert manager (will be lazily initialized on first process() call)
|
|
540
544
|
self.alert_manager: Optional[ALERT_INSTANCE] = None
|
|
@@ -942,6 +946,8 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
942
946
|
self._unique_plate_texts = {}
|
|
943
947
|
self.helper = {}
|
|
944
948
|
self.unique_plate_track = {}
|
|
949
|
+
# Reset logged track_ids to allow fresh logging
|
|
950
|
+
self._logged_track_ids = set()
|
|
945
951
|
self.logger.info("Plate tracking state reset")
|
|
946
952
|
|
|
947
953
|
def reset_all_tracking(self) -> None:
|
|
@@ -1147,13 +1153,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
1147
1153
|
|
|
1148
1154
|
async def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
|
|
1149
1155
|
stream_info: Optional[Dict[str, Any]], image_bytes: Optional[bytes] = None) -> None:
|
|
1150
|
-
"""
|
|
1156
|
+
"""
|
|
1157
|
+
Log confirmed/consensus plates to RPC server.
|
|
1158
|
+
|
|
1159
|
+
Only logs plates that have reached consensus (are in _tracked_plate_texts),
|
|
1160
|
+
and only logs each track_id once to avoid duplicate logging of garbage OCR predictions.
|
|
1161
|
+
Uses the confirmed consensus plate text, not the raw frame-by-frame OCR output.
|
|
1162
|
+
"""
|
|
1151
1163
|
# Enhanced logging for diagnostics
|
|
1152
1164
|
print(f"[LP_LOGGING] Starting plate logging check - detections count: {len(detections)}")
|
|
1153
1165
|
self.logger.info(f"[LP_LOGGING] Starting plate logging check - detections count: {len(detections)}")
|
|
1154
|
-
self.logger.info(f"[LP_LOGGING] Logging enabled: {self._logging_enabled}, Plate logger exists: {self.plate_logger is not None}
|
|
1166
|
+
self.logger.info(f"[LP_LOGGING] Logging enabled: {self._logging_enabled}, Plate logger exists: {self.plate_logger is not None}")
|
|
1167
|
+
self.logger.info(f"[LP_LOGGING] Confirmed plates (tracked): {len(self._tracked_plate_texts)}, Already logged tracks: {len(self._logged_track_ids)}")
|
|
1155
1168
|
|
|
1156
|
-
#self._logging_enabled = False # ToDo: DISABLED FOR NOW, ENABLED FOR PRODUCTION
|
|
1157
1169
|
if not self._logging_enabled:
|
|
1158
1170
|
print("[LP_LOGGING] Plate logging is DISABLED")
|
|
1159
1171
|
self.logger.warning("[LP_LOGGING] Plate logging is DISABLED - logging_enabled flag is False")
|
|
@@ -1164,11 +1176,6 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
1164
1176
|
self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - plate_logger is not initialized (lpr_server_id may not be configured)")
|
|
1165
1177
|
return
|
|
1166
1178
|
|
|
1167
|
-
# if not stream_info:
|
|
1168
|
-
# print("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
|
|
1169
|
-
# self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
|
|
1170
|
-
# return
|
|
1171
|
-
|
|
1172
1179
|
print("[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
|
|
1173
1180
|
self.logger.info(f"[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
|
|
1174
1181
|
|
|
@@ -1195,35 +1202,45 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
1195
1202
|
else:
|
|
1196
1203
|
self.logger.warning(f"[LP_LOGGING] Failed to decode image bytes")
|
|
1197
1204
|
except Exception as e:
|
|
1198
|
-
#pass
|
|
1199
1205
|
self.logger.error(f"[LP_LOGGING] Exception while encoding frame image: {e}", exc_info=True)
|
|
1200
1206
|
else:
|
|
1201
1207
|
self.logger.info(f"[LP_LOGGING] No image_bytes provided, sending without image")
|
|
1202
1208
|
|
|
1203
|
-
#
|
|
1204
|
-
|
|
1205
|
-
|
|
1209
|
+
# Only log CONFIRMED/CONSENSUS plates from _tracked_plate_texts
|
|
1210
|
+
# Avoid logging every raw OCR prediction - only log final confirmed plate per track_id
|
|
1211
|
+
plates_to_log = {} # track_id -> consensus_plate_text
|
|
1212
|
+
|
|
1206
1213
|
for det in detections:
|
|
1207
|
-
|
|
1208
|
-
if
|
|
1209
|
-
detections_without_text += 1
|
|
1214
|
+
track_id = det.get('track_id')
|
|
1215
|
+
if track_id is None:
|
|
1210
1216
|
continue
|
|
1211
|
-
|
|
1217
|
+
|
|
1218
|
+
# Skip if this track_id has already been logged
|
|
1219
|
+
if track_id in self._logged_track_ids:
|
|
1220
|
+
self.logger.debug(f"[LP_LOGGING] Skipping track_id={track_id} - already logged")
|
|
1221
|
+
continue
|
|
1222
|
+
|
|
1223
|
+
# Only log if this track_id has a confirmed/consensus plate
|
|
1224
|
+
if track_id in self._tracked_plate_texts:
|
|
1225
|
+
consensus_plate = self._tracked_plate_texts[track_id]
|
|
1226
|
+
if consensus_plate:
|
|
1227
|
+
plates_to_log[track_id] = consensus_plate
|
|
1228
|
+
self.logger.debug(f"[LP_LOGGING] Found confirmed plate for track_id={track_id}: {consensus_plate}")
|
|
1212
1229
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1230
|
+
confirmed_count = len(plates_to_log)
|
|
1231
|
+
raw_ocr_count = sum(1 for d in detections if d.get('plate_text'))
|
|
1232
|
+
print(f"[LP_LOGGING] Confirmed plates to log: {confirmed_count} (from {raw_ocr_count} raw OCR detections)")
|
|
1233
|
+
self.logger.info(f"[LP_LOGGING] Confirmed plates to log: {confirmed_count}, Raw OCR detections: {raw_ocr_count}")
|
|
1234
|
+
self.logger.info(f"[LP_LOGGING] Plates: {list(plates_to_log.values())}")
|
|
1217
1235
|
|
|
1218
|
-
# Log each
|
|
1236
|
+
# Log each confirmed plate (respecting cooldown)
|
|
1219
1237
|
if plates_to_log:
|
|
1220
|
-
print(f"[LP_LOGGING] Logging {len(plates_to_log)} plates with cooldown={config.plate_log_cooldown}s")
|
|
1221
|
-
self.logger.info(f"[LP_LOGGING] Logging {len(plates_to_log)} plates with cooldown={config.plate_log_cooldown}s")
|
|
1238
|
+
print(f"[LP_LOGGING] Logging {len(plates_to_log)} confirmed plates with cooldown={config.plate_log_cooldown}s")
|
|
1239
|
+
self.logger.info(f"[LP_LOGGING] Logging {len(plates_to_log)} confirmed plates with cooldown={config.plate_log_cooldown}s")
|
|
1222
1240
|
try:
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
self.logger.info(f"[LP_LOGGING] Processing plate: {plate_text}")
|
|
1241
|
+
for track_id, plate_text in plates_to_log.items():
|
|
1242
|
+
print(f"[LP_LOGGING] Processing confirmed plate: {plate_text} (track_id={track_id})")
|
|
1243
|
+
self.logger.info(f"[LP_LOGGING] Processing confirmed plate: {plate_text} (track_id={track_id})")
|
|
1227
1244
|
try:
|
|
1228
1245
|
result = await self.plate_logger.log_plate(
|
|
1229
1246
|
plate_text=plate_text,
|
|
@@ -1232,25 +1249,26 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
1232
1249
|
image_data=image_data,
|
|
1233
1250
|
cooldown=config.plate_log_cooldown
|
|
1234
1251
|
)
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1252
|
+
if result:
|
|
1253
|
+
# Mark this track_id as logged to avoid duplicate logging
|
|
1254
|
+
self._logged_track_ids.add(track_id)
|
|
1255
|
+
print(f"[LP_LOGGING] Plate {plate_text}: SENT (track_id={track_id} marked as logged)")
|
|
1256
|
+
self.logger.info(f"[LP_LOGGING] Plate {plate_text}: SENT (track_id={track_id} marked as logged)")
|
|
1257
|
+
else:
|
|
1258
|
+
print(f"[LP_LOGGING] Plate {plate_text}: SKIPPED (cooldown)")
|
|
1259
|
+
self.logger.info(f"[LP_LOGGING] Plate {plate_text}: SKIPPED (cooldown)")
|
|
1238
1260
|
except Exception as e:
|
|
1239
|
-
#pass
|
|
1240
1261
|
print(f"[LP_LOGGING] ERROR - Plate {plate_text} failed: {e}")
|
|
1241
1262
|
self.logger.error(f"[LP_LOGGING] Plate {plate_text} raised exception: {e}", exc_info=True)
|
|
1242
1263
|
|
|
1243
1264
|
print("[LP_LOGGING] Plate logging complete")
|
|
1244
|
-
self.logger.info(f"[LP_LOGGING] Plate logging complete")
|
|
1265
|
+
self.logger.info(f"[LP_LOGGING] Plate logging complete - {len(self._logged_track_ids)} total tracks logged so far")
|
|
1245
1266
|
except Exception as e:
|
|
1246
|
-
print(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}")
|
|
1247
|
-
|
|
1248
1267
|
print(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}")
|
|
1249
1268
|
self.logger.error(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}", exc_info=True)
|
|
1250
|
-
pass
|
|
1251
1269
|
else:
|
|
1252
|
-
print("[LP_LOGGING] No plates to log")
|
|
1253
|
-
self.logger.info(f"[LP_LOGGING] No plates to log (
|
|
1270
|
+
print("[LP_LOGGING] No confirmed plates to log (plates may still be reaching consensus)")
|
|
1271
|
+
self.logger.info(f"[LP_LOGGING] No confirmed plates to log (waiting for consensus)")
|
|
1254
1272
|
|
|
1255
1273
|
async def process(self, data: Any, config: ConfigProtocol, input_bytes: Optional[bytes] = None,
|
|
1256
1274
|
context: Optional[ProcessingContext] = None, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
|
|
@@ -40,54 +40,35 @@ class VehicleMonitoringConfig(BaseConfig):
|
|
|
40
40
|
# )
|
|
41
41
|
usecase_categories: List[str] = field(
|
|
42
42
|
default_factory=lambda: [
|
|
43
|
-
|
|
44
|
-
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog",
|
|
45
|
-
"horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
|
|
46
|
-
"handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
|
|
47
|
-
"baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle",
|
|
48
|
-
"wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich",
|
|
49
|
-
"orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
|
|
50
|
-
"potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote",
|
|
51
|
-
"keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book",
|
|
52
|
-
"clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
|
|
43
|
+
'bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck'
|
|
53
44
|
]
|
|
54
45
|
)
|
|
55
46
|
target_categories: List[str] = field(
|
|
56
47
|
default_factory=lambda: [
|
|
57
|
-
'car', '
|
|
48
|
+
'bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck'
|
|
49
|
+
]
|
|
58
50
|
)
|
|
59
51
|
alert_config: Optional[AlertConfig] = None
|
|
60
52
|
index_to_category: Optional[Dict[int, str]] = field(
|
|
61
53
|
default_factory=lambda: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
36: "skateboard", 37: "surfboard", 38: "tennis racket", 39: "bottle",
|
|
70
|
-
40: "wine glass", 41: "cup", 42: "fork", 43: "knife", 44: "spoon", 45: "bowl",
|
|
71
|
-
46: "banana", 47: "apple", 48: "sandwich", 49: "orange", 50: "broccoli",
|
|
72
|
-
51: "carrot", 52: "hot dog", 53: "pizza", 54: "donut", 55: "cake", 56: "chair",
|
|
73
|
-
57: "couch", 58: "potted plant", 59: "bed", 60: "dining table", 61: "toilet",
|
|
74
|
-
62: "tv", 63: "laptop", 64: "mouse", 65: "remote", 66: "keyboard",
|
|
75
|
-
67: "cell phone", 68: "microwave", 69: "oven", 70: "toaster", 71: "sink",
|
|
76
|
-
72: "refrigerator", 73: "book", 74: "clock", 75: "vase", 76: "scissors",
|
|
77
|
-
77: "teddy bear", 78: "hair drier", 79: "toothbrush"
|
|
78
|
-
}
|
|
54
|
+
0: "bicycle",
|
|
55
|
+
1: "motorcycle",
|
|
56
|
+
2: "car",
|
|
57
|
+
3: "van",
|
|
58
|
+
4: "bus",
|
|
59
|
+
5: "truck"
|
|
60
|
+
}
|
|
79
61
|
)
|
|
80
62
|
|
|
81
63
|
class VehicleMonitoringUseCase(BaseProcessor):
|
|
82
64
|
CATEGORY_DISPLAY = {
|
|
83
65
|
# Focus on vehicle-related COCO classes
|
|
84
66
|
"bicycle": "Bicycle",
|
|
85
|
-
"car": "Car",
|
|
86
67
|
"motorcycle": "Motorcycle",
|
|
68
|
+
"car": "Car",
|
|
69
|
+
"van": "Van",
|
|
87
70
|
"bus": "Bus",
|
|
88
71
|
"truck": "Truck",
|
|
89
|
-
"train": "Train",
|
|
90
|
-
"boat": "Boat"
|
|
91
72
|
}
|
|
92
73
|
|
|
93
74
|
def __init__(self):
|
|
@@ -95,7 +76,7 @@ class VehicleMonitoringUseCase(BaseProcessor):
|
|
|
95
76
|
self.category = "traffic"
|
|
96
77
|
self.CASE_TYPE: Optional[str] = 'vehicle_monitoring'
|
|
97
78
|
self.CASE_VERSION: Optional[str] = '1.0'
|
|
98
|
-
self.target_categories = ['car', '
|
|
79
|
+
self.target_categories = ['bicycle', 'motorcycle', 'car', 'van', 'bus', 'truck' ]
|
|
99
80
|
self.smoothing_tracker = None
|
|
100
81
|
self.tracker = None
|
|
101
82
|
self._total_frame_counter = 0
|
|
@@ -141,6 +122,7 @@ class VehicleMonitoringUseCase(BaseProcessor):
|
|
|
141
122
|
context.input_format = input_format
|
|
142
123
|
context.confidence_threshold = config.confidence_threshold
|
|
143
124
|
config.confidence_threshold = 0.25
|
|
125
|
+
# param to be updated
|
|
144
126
|
|
|
145
127
|
if config.confidence_threshold is not None:
|
|
146
128
|
processed_data = filter_by_confidence(data, config.confidence_threshold)
|