matrice-analytics 0.1.31__py3-none-any.whl → 0.1.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of matrice-analytics might be problematic. Click here for more details.
- matrice_analytics/post_processing/config.py +4 -0
- matrice_analytics/post_processing/core/config.py +115 -12
- matrice_analytics/post_processing/face_reg/embedding_manager.py +95 -1
- matrice_analytics/post_processing/face_reg/face_recognition.py +52 -45
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +13 -12
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +40 -9
- matrice_analytics/post_processing/post_processor.py +14 -7
- matrice_analytics/post_processing/usecases/color_detection.py +38 -40
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +162 -48
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.33.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.33.dist-info}/RECORD +14 -14
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.33.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.33.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.33.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import time
|
|
4
4
|
import threading
|
|
5
5
|
import queue
|
|
6
|
+
import base64
|
|
6
7
|
from typing import Dict, Optional, Set
|
|
7
8
|
import numpy as np
|
|
8
9
|
import cv2
|
|
@@ -169,7 +170,7 @@ class PeopleActivityLogging:
|
|
|
169
170
|
return True
|
|
170
171
|
|
|
171
172
|
async def _process_activity(self, activity_data: Dict):
|
|
172
|
-
"""Process activity data - handle all face detections
|
|
173
|
+
"""Process activity data - handle all face detections with embedded image data"""
|
|
173
174
|
detection_type = activity_data["detection_type"]
|
|
174
175
|
current_frame = activity_data["current_frame"]
|
|
175
176
|
bbox = activity_data["bbox"]
|
|
@@ -188,27 +189,57 @@ class PeopleActivityLogging:
|
|
|
188
189
|
self.logger.debug(f"Skipping activity log for employee_id={employee_id} (within cooldown period)")
|
|
189
190
|
return None
|
|
190
191
|
|
|
191
|
-
#
|
|
192
|
+
# Encode frame as base64 JPEG
|
|
193
|
+
image_data = None
|
|
194
|
+
if current_frame is not None:
|
|
195
|
+
try:
|
|
196
|
+
self.logger.debug(f"Encoding frame as base64 JPEG - employee_id={employee_id}")
|
|
197
|
+
_, buffer = cv2.imencode(".jpg", current_frame)
|
|
198
|
+
frame_bytes = buffer.tobytes()
|
|
199
|
+
image_data = base64.b64encode(frame_bytes).decode('utf-8')
|
|
200
|
+
self.logger.debug(f"Encoded image data - employee_id={employee_id}, size={len(frame_bytes)} bytes")
|
|
201
|
+
except Exception as e:
|
|
202
|
+
self.logger.error(f"Error encoding frame for employee_id={employee_id}: {e}", exc_info=True)
|
|
203
|
+
|
|
204
|
+
# Store activity data with embedded image
|
|
192
205
|
self.logger.info(f"Processing activity log - type={detection_type}, employee_id={employee_id}, staff_id={staff_id}, location={location}")
|
|
193
|
-
|
|
206
|
+
response = await self.face_client.store_people_activity(
|
|
194
207
|
staff_id=staff_id,
|
|
195
208
|
detection_type=detection_type,
|
|
196
209
|
bbox=bbox,
|
|
197
210
|
location=location,
|
|
198
211
|
employee_id=employee_id,
|
|
199
212
|
timestamp=timestamp,
|
|
213
|
+
image_data=image_data,
|
|
200
214
|
)
|
|
201
215
|
|
|
202
|
-
if
|
|
203
|
-
self.logger.info(f"Activity log stored successfully
|
|
204
|
-
await self._upload_frame(current_frame, upload_url, employee_id)
|
|
216
|
+
if response and response.get("success", False):
|
|
217
|
+
self.logger.info(f"Activity log stored successfully for employee_id={employee_id}")
|
|
205
218
|
else:
|
|
206
|
-
|
|
219
|
+
error_msg = response.get("error", "Unknown error") if response else "No response"
|
|
220
|
+
self.logger.warning(f"Failed to store activity log for employee_id={employee_id} - {error_msg}")
|
|
207
221
|
|
|
208
|
-
return
|
|
222
|
+
return response
|
|
209
223
|
except Exception as e:
|
|
210
224
|
self.logger.error(f"Error processing activity log for employee_id={employee_id}: {e}", exc_info=True)
|
|
211
|
-
|
|
225
|
+
|
|
226
|
+
# async def _upload_frame_to_url(self, current_frame: np.ndarray, upload_url: str, employee_id: str):
|
|
227
|
+
# try:
|
|
228
|
+
# self.logger.debug(f"Encoding frame for upload - employee_id={employee_id}")
|
|
229
|
+
# _, buffer = cv2.imencode(".jpg", current_frame)
|
|
230
|
+
# frame_bytes = buffer.tobytes()
|
|
231
|
+
|
|
232
|
+
# self.logger.info(f"Uploading frame to storage - employee_id={employee_id}, size={len(frame_bytes)} bytes")
|
|
233
|
+
# upload_success = await self.face_client.upload_image_to_url(
|
|
234
|
+
# frame_bytes, upload_url
|
|
235
|
+
# )
|
|
236
|
+
|
|
237
|
+
# if upload_success:
|
|
238
|
+
# self.logger.info(f"Frame uploaded successfully for employee_id={employee_id}")
|
|
239
|
+
# else:
|
|
240
|
+
# self.logger.warning(f"Failed to upload frame for employee_id={employee_id}")
|
|
241
|
+
# except Exception as e:
|
|
242
|
+
# self.logger.error(f"Error uploading frame for employee_id={employee_id}: {e}", exc_info=True)
|
|
212
243
|
|
|
213
244
|
async def _upload_frame(self, current_frame: np.ndarray, upload_url: str, employee_id: str):
|
|
214
245
|
try:
|
|
@@ -320,6 +320,7 @@ class PostProcessor:
|
|
|
320
320
|
f"Removing facial_recognition_server_id from {usecase} config"
|
|
321
321
|
)
|
|
322
322
|
config_params.pop("facial_recognition_server_id", None)
|
|
323
|
+
config_params.pop("deployment_id", None)
|
|
323
324
|
|
|
324
325
|
if usecase not in license_plate_monitoring_usecases:
|
|
325
326
|
if "lpr_server_id" in config_params:
|
|
@@ -642,7 +643,7 @@ class PostProcessor:
|
|
|
642
643
|
config_str = json.dumps(cache_data, sort_keys=True, default=str)
|
|
643
644
|
return hashlib.md5(config_str.encode()).hexdigest()[:16] # Shorter hash for readability
|
|
644
645
|
|
|
645
|
-
def _get_use_case_instance(
|
|
646
|
+
async def _get_use_case_instance(
|
|
646
647
|
self, config: BaseConfig, stream_key: Optional[str] = None
|
|
647
648
|
):
|
|
648
649
|
"""
|
|
@@ -669,8 +670,10 @@ class PostProcessor:
|
|
|
669
670
|
raise ValueError(f"Use case '{config.category}/{config.usecase}' not found")
|
|
670
671
|
|
|
671
672
|
|
|
672
|
-
if
|
|
673
|
+
if use_case_class == FaceRecognitionEmbeddingUseCase:
|
|
673
674
|
use_case = use_case_class(config=config)
|
|
675
|
+
# Await async initialization for face recognition use case
|
|
676
|
+
await use_case.initialize(config)
|
|
674
677
|
else:
|
|
675
678
|
use_case = use_case_class()
|
|
676
679
|
logger.info(f"Created use case instance for: {config.category}/{config.usecase}")
|
|
@@ -759,16 +762,20 @@ class PostProcessor:
|
|
|
759
762
|
|
|
760
763
|
try:
|
|
761
764
|
if config:
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
+
try:
|
|
766
|
+
config = self._parse_config(config)
|
|
767
|
+
except Exception as e:
|
|
768
|
+
logger.error(f"Failed to parse config: {e}", exc_info=True)
|
|
769
|
+
raise ValueError(f"Failed to parse config: {e}")
|
|
770
|
+
|
|
771
|
+
parsed_config = config or self.post_processing_config
|
|
765
772
|
|
|
766
773
|
if not parsed_config:
|
|
767
774
|
raise ValueError("No valid configuration found")
|
|
768
775
|
|
|
769
776
|
|
|
770
|
-
# Get cached use case instance
|
|
771
|
-
use_case = self._get_use_case_instance(parsed_config, stream_key)
|
|
777
|
+
# Get cached use case instance (await since it's async now)
|
|
778
|
+
use_case = await self._get_use_case_instance(parsed_config, stream_key)
|
|
772
779
|
|
|
773
780
|
# Create context if not provided
|
|
774
781
|
if context is None:
|
|
@@ -86,7 +86,7 @@ class ColorDetectionConfig(BaseConfig):
|
|
|
86
86
|
smoothing_window_size: int = 20
|
|
87
87
|
smoothing_cooldown_frames: int = 5
|
|
88
88
|
smoothing_confidence_range_factor: float = 0.5
|
|
89
|
-
|
|
89
|
+
enable_detector: bool = True
|
|
90
90
|
|
|
91
91
|
#JBK_720_GATE POLYGON = [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]]
|
|
92
92
|
zone_config: Optional[Dict[str, List[List[float]]]] = None #field(
|
|
@@ -116,25 +116,24 @@ class ColorDetectionConfig(BaseConfig):
|
|
|
116
116
|
errors.append("smoothing_confidence_range_factor must be positive")
|
|
117
117
|
return errors
|
|
118
118
|
|
|
119
|
-
def __post_init__(self):
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
119
|
+
# def __post_init__(self):
|
|
120
|
+
# # Lazy initialization: the ClipProcessor will be created once by the use case
|
|
121
|
+
# # to avoid repeated model downloads and to ensure GPU session reuse.
|
|
122
|
+
# # log_file = open("pip_jetson_bt.log", "w")
|
|
123
|
+
# # cmd = ["pip", "install", "--force-reinstall", "huggingface_hub", "regex", "safetensors"]
|
|
124
|
+
# # subprocess.Popen(
|
|
125
|
+
# # cmd,
|
|
126
|
+
# # stdout=log_file,
|
|
127
|
+
# # stderr=subprocess.STDOUT,
|
|
128
|
+
# # preexec_fn=os.setpgrp
|
|
129
|
+
# # )
|
|
130
|
+
# print("Came to post_init and libraries installed!!!")
|
|
131
|
+
# if self.detector:
|
|
132
|
+
# self.detector = ClipProcessor()
|
|
133
|
+
# print("ClipProcessor Loaded Successfully!!")
|
|
134
|
+
# else:
|
|
135
|
+
# print("Clip color detector disabled by config")
|
|
136
|
+
# self.detector = None
|
|
138
137
|
|
|
139
138
|
|
|
140
139
|
class ColorDetectionUseCase(BaseProcessor):
|
|
@@ -182,7 +181,7 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
182
181
|
self._zone_current_counts = {} # zone_name -> current count in zone
|
|
183
182
|
self._zone_total_counts = {} # zone_name -> total count that have been in zone
|
|
184
183
|
self.logger.info("Initialized ColorDetectionUseCase with tracking")
|
|
185
|
-
|
|
184
|
+
self.detector = None # Will be initialized on first use
|
|
186
185
|
self.all_color_data = {}
|
|
187
186
|
self.all_color_counts = {}
|
|
188
187
|
self.total_category_count = {}
|
|
@@ -294,30 +293,29 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
294
293
|
if config.zone_config:
|
|
295
294
|
color_processed_data = self._is_in_zone_robust(color_processed_data,config.zone_config)
|
|
296
295
|
print(color_processed_data)
|
|
296
|
+
|
|
297
|
+
# Initialize detector lazily on first use if enabled
|
|
297
298
|
try:
|
|
298
299
|
print("About to call process_color_in_frame...")
|
|
299
|
-
|
|
300
|
-
if config.detector is None:
|
|
301
|
-
print("
|
|
300
|
+
|
|
301
|
+
if config.enable_detector and self.detector is None:
|
|
302
|
+
print("Initializing ClipProcessor for color detection...")
|
|
303
|
+
try:
|
|
304
|
+
self.detector = ClipProcessor()
|
|
305
|
+
print("ClipProcessor loaded successfully!")
|
|
306
|
+
logger.info("ClipProcessor loaded successfully!")
|
|
307
|
+
except Exception as init_error:
|
|
308
|
+
print(f"Failed to initialize ClipProcessor: {init_error}")
|
|
309
|
+
logger.error(f"Failed to initialize ClipProcessor: {init_error}")
|
|
310
|
+
self.detector = None
|
|
311
|
+
|
|
312
|
+
if self.detector is None:
|
|
313
|
+
print("Detector is disabled or failed to initialize, skipping color detection")
|
|
314
|
+
logger.warning("Detector is disabled or failed to initialize, skipping color detection")
|
|
302
315
|
curr_frame_color = {}
|
|
303
|
-
|
|
304
|
-
# else:
|
|
305
|
-
# if color_processed_data:
|
|
306
|
-
# t_id = color_processed_data[0].get('track_id')
|
|
307
|
-
# if t_id is not None and t_id not in self.all_color_data:
|
|
308
|
-
# # curr_frame_color = {}
|
|
309
|
-
# curr_frame_color = config.detector.process_color_in_frame(color_processed_data,input_bytes,config.zone_config,stream_info)
|
|
310
|
-
# res_dict[curr_frame_color[t_id]['color']] = curr_frame_color[t_id]['confidence']
|
|
311
|
-
# else:
|
|
312
|
-
# curr_frame_color = {}
|
|
313
|
-
# print("process_color_in_frame completed successfully")
|
|
314
|
-
# else:
|
|
315
|
-
# curr_frame_color = {}
|
|
316
|
-
|
|
317
|
-
#------------------------ORiginal Code to run on all frames-----------------------
|
|
318
316
|
else:
|
|
319
317
|
print(len(color_processed_data))
|
|
320
|
-
curr_frame_color =
|
|
318
|
+
curr_frame_color = self.detector.process_color_in_frame(
|
|
321
319
|
color_processed_data,
|
|
322
320
|
input_bytes,
|
|
323
321
|
config.zone_config,
|
|
@@ -29,6 +29,7 @@ import logging
|
|
|
29
29
|
import asyncio
|
|
30
30
|
import urllib
|
|
31
31
|
import urllib.request
|
|
32
|
+
import base64
|
|
32
33
|
# Get the major and minor version numbers
|
|
33
34
|
major_version = sys.version_info.major
|
|
34
35
|
minor_version = sys.version_info.minor
|
|
@@ -36,24 +37,22 @@ print(f"Python version: {major_version}.{minor_version}")
|
|
|
36
37
|
os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"""Stub fallback when fast_plate_ocr is not installed."""
|
|
46
|
-
def __init__(self, *args, **kwargs):
|
|
47
|
-
print("fast_plate_ocr from the local library py_inferenceis required for LicensePlateMonitorUseCase as platform is determined as python3.8 Jetson")
|
|
48
|
-
else:
|
|
40
|
+
# Try to import LicensePlateRecognizer from local repo first, then installed package
|
|
41
|
+
_OCR_IMPORT_SOURCE = None
|
|
42
|
+
try:
|
|
43
|
+
from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
|
|
44
|
+
_OCR_IMPORT_SOURCE = "local_repo"
|
|
45
|
+
except ImportError:
|
|
49
46
|
try:
|
|
50
47
|
from fast_plate_ocr import LicensePlateRecognizer # type: ignore
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
_OCR_IMPORT_SOURCE = "installed_package"
|
|
49
|
+
except ImportError:
|
|
50
|
+
# Use stub class if neither import works
|
|
51
|
+
_OCR_IMPORT_SOURCE = "stub"
|
|
53
52
|
class LicensePlateRecognizer: # type: ignore
|
|
54
|
-
"""Stub fallback when fast_plate_ocr is not
|
|
53
|
+
"""Stub fallback when fast_plate_ocr is not available."""
|
|
55
54
|
def __init__(self, *args, **kwargs):
|
|
56
|
-
|
|
55
|
+
pass # Silent stub - error will be logged once during initialization
|
|
57
56
|
|
|
58
57
|
# Internal utilities that are still required
|
|
59
58
|
from ..ocr.preprocessing import ImagePreprocessor
|
|
@@ -124,23 +123,29 @@ class LicensePlateMonitorLogger:
|
|
|
124
123
|
|
|
125
124
|
def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
|
|
126
125
|
"""Initialize session and fetch server connection info if lpr_server_id is provided."""
|
|
126
|
+
self.logger.info("Initializing LicensePlateMonitorLogger session...")
|
|
127
|
+
|
|
127
128
|
# Use existing session if provided, otherwise create new one
|
|
128
129
|
if self.session:
|
|
130
|
+
self.logger.info("Session already initialized, skipping initialization")
|
|
129
131
|
return
|
|
130
132
|
if config.session:
|
|
131
133
|
self.session = config.session
|
|
134
|
+
self.logger.info("Using provided session from config")
|
|
132
135
|
if not self.session:
|
|
133
136
|
# Initialize Matrice session
|
|
134
137
|
if not HAS_MATRICE_SESSION:
|
|
138
|
+
self.logger.error("Matrice session module not available")
|
|
135
139
|
raise ImportError("Matrice session is required for License Plate Monitoring")
|
|
136
140
|
try:
|
|
141
|
+
self.logger.info("Creating new Matrice session...")
|
|
137
142
|
self.session = Session(
|
|
138
143
|
account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
|
|
139
144
|
access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
|
|
140
145
|
secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
|
|
141
146
|
project_id=os.getenv("MATRICE_PROJECT_ID", ""),
|
|
142
147
|
)
|
|
143
|
-
self.logger.info("
|
|
148
|
+
self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
|
|
144
149
|
except Exception as e:
|
|
145
150
|
self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
|
|
146
151
|
raise
|
|
@@ -148,6 +153,7 @@ class LicensePlateMonitorLogger:
|
|
|
148
153
|
# Fetch server connection info if lpr_server_id is provided
|
|
149
154
|
if config.lpr_server_id:
|
|
150
155
|
self.lpr_server_id = config.lpr_server_id
|
|
156
|
+
self.logger.info(f"Fetching LPR server connection info for server ID: {self.lpr_server_id}")
|
|
151
157
|
try:
|
|
152
158
|
self.server_info = self.get_server_connection_info()
|
|
153
159
|
if self.server_info:
|
|
@@ -158,23 +164,26 @@ class LicensePlateMonitorLogger:
|
|
|
158
164
|
|
|
159
165
|
if server_host == self.public_ip:
|
|
160
166
|
self.server_base_url = f"http://localhost:{server_port}"
|
|
161
|
-
self.logger.
|
|
167
|
+
self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
|
|
162
168
|
else:
|
|
163
169
|
self.server_base_url = f"https://{server_host}:{server_port}"
|
|
164
|
-
self.logger.
|
|
170
|
+
self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
|
|
165
171
|
|
|
166
172
|
self.session.update(self.server_info.get('projectID', ''))
|
|
167
173
|
self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
|
|
168
174
|
else:
|
|
169
|
-
self.logger.warning("Failed to fetch LPR server connection info")
|
|
175
|
+
self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
|
|
170
176
|
except Exception as e:
|
|
171
177
|
self.logger.error(f"Error fetching LPR server connection info: {e}", exc_info=True)
|
|
178
|
+
else:
|
|
179
|
+
self.logger.info("No lpr_server_id provided in config, skipping server connection info fetch")
|
|
172
180
|
|
|
173
181
|
def _get_public_ip(self) -> str:
|
|
174
182
|
"""Get the public IP address of this machine."""
|
|
183
|
+
self.logger.info("Fetching public IP address...")
|
|
175
184
|
try:
|
|
176
185
|
public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
|
|
177
|
-
self.logger.
|
|
186
|
+
self.logger.info(f"Successfully fetched external IP: {public_ip}")
|
|
178
187
|
return public_ip
|
|
179
188
|
except Exception as e:
|
|
180
189
|
self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
|
|
@@ -183,10 +192,15 @@ class LicensePlateMonitorLogger:
|
|
|
183
192
|
def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
|
|
184
193
|
"""Fetch server connection info from RPC."""
|
|
185
194
|
if not self.lpr_server_id:
|
|
195
|
+
self.logger.warning("No lpr_server_id set, cannot fetch server connection info")
|
|
186
196
|
return None
|
|
187
197
|
|
|
188
198
|
try:
|
|
189
|
-
|
|
199
|
+
endpoint = f"/v1/actions/lpr_servers/{self.lpr_server_id}"
|
|
200
|
+
self.logger.info(f"Sending GET request to: {endpoint}")
|
|
201
|
+
response = self.session.rpc.get(endpoint)
|
|
202
|
+
self.logger.info(f"Received response: success={response.get('success')}, code={response.get('code')}, message={response.get('message')}")
|
|
203
|
+
|
|
190
204
|
if response.get("success", False) and response.get("code") == 200:
|
|
191
205
|
# Response format:
|
|
192
206
|
# {'success': True,
|
|
@@ -202,7 +216,9 @@ class LicensePlateMonitorLogger:
|
|
|
202
216
|
# 'projectID': '68ca6372ab79ba13ef699ba6',
|
|
203
217
|
# 'region': 'United States',
|
|
204
218
|
# 'isShared': False}}
|
|
205
|
-
|
|
219
|
+
data = response.get("data", {})
|
|
220
|
+
self.logger.info(f"Server connection info retrieved: name={data.get('name')}, host={data.get('host')}, port={data.get('port')}, status={data.get('status')}")
|
|
221
|
+
return data
|
|
206
222
|
else:
|
|
207
223
|
self.logger.warning(f"Failed to fetch server info: {response.get('message', 'Unknown error')}")
|
|
208
224
|
return None
|
|
@@ -214,20 +230,78 @@ class LicensePlateMonitorLogger:
|
|
|
214
230
|
"""Check if enough time has passed since last log for this plate."""
|
|
215
231
|
current_time = time.time()
|
|
216
232
|
last_log_time = self.plate_log_timestamps.get(plate_text, 0)
|
|
233
|
+
time_since_last_log = current_time - last_log_time
|
|
217
234
|
|
|
218
|
-
if
|
|
235
|
+
if time_since_last_log >= cooldown:
|
|
236
|
+
self.logger.debug(f"Plate {plate_text} ready to log (last logged {time_since_last_log:.1f}s ago, cooldown={cooldown}s)")
|
|
219
237
|
return True
|
|
220
|
-
|
|
238
|
+
else:
|
|
239
|
+
self.logger.debug(f"Plate {plate_text} in cooldown period ({time_since_last_log:.1f}s elapsed, {cooldown - time_since_last_log:.1f}s remaining)")
|
|
240
|
+
return False
|
|
221
241
|
|
|
222
242
|
def update_log_timestamp(self, plate_text: str) -> None:
|
|
223
243
|
"""Update the last log timestamp for a plate."""
|
|
224
244
|
self.plate_log_timestamps[plate_text] = time.time()
|
|
245
|
+
self.logger.debug(f"Updated log timestamp for plate: {plate_text}")
|
|
246
|
+
|
|
247
|
+
def _format_timestamp_rfc3339(self, timestamp: str) -> str:
|
|
248
|
+
"""Convert timestamp to RFC3339 format (2006-01-02T15:04:05Z).
|
|
249
|
+
|
|
250
|
+
Handles various input formats:
|
|
251
|
+
- "YYYY-MM-DD-HH:MM:SS.ffffff UTC"
|
|
252
|
+
- "YYYY:MM:DD HH:MM:SS"
|
|
253
|
+
- Unix timestamp (float/int)
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
# If already in RFC3339 format, return as is
|
|
257
|
+
if 'T' in timestamp and timestamp.endswith('Z'):
|
|
258
|
+
return timestamp
|
|
259
|
+
|
|
260
|
+
# Try to parse common formats
|
|
261
|
+
dt = None
|
|
262
|
+
|
|
263
|
+
# Format: "2025-08-19-04:22:47.187574 UTC"
|
|
264
|
+
if '-' in timestamp and 'UTC' in timestamp:
|
|
265
|
+
timestamp_clean = timestamp.replace(' UTC', '')
|
|
266
|
+
dt = datetime.strptime(timestamp_clean, '%Y-%m-%d-%H:%M:%S.%f')
|
|
267
|
+
# Format: "2025:10:23 14:30:45"
|
|
268
|
+
elif ':' in timestamp and ' ' in timestamp:
|
|
269
|
+
dt = datetime.strptime(timestamp, '%Y:%m:%d %H:%M:%S')
|
|
270
|
+
# Format: numeric timestamp
|
|
271
|
+
elif timestamp.replace('.', '').isdigit():
|
|
272
|
+
dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
|
|
273
|
+
|
|
274
|
+
if dt is None:
|
|
275
|
+
# Fallback to current time
|
|
276
|
+
dt = datetime.now(timezone.utc)
|
|
277
|
+
else:
|
|
278
|
+
# Ensure timezone is UTC
|
|
279
|
+
if dt.tzinfo is None:
|
|
280
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
281
|
+
|
|
282
|
+
# Format to RFC3339: 2006-01-02T15:04:05Z
|
|
283
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.logger.warning(f"Failed to parse timestamp '{timestamp}': {e}. Using current time.")
|
|
287
|
+
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
225
288
|
|
|
226
|
-
async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any],
|
|
227
|
-
|
|
289
|
+
async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any],
|
|
290
|
+
image_data: Optional[str] = None, cooldown: float = 30.0) -> bool:
|
|
291
|
+
"""Log plate to RPC server with cooldown period.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
plate_text: The license plate text
|
|
295
|
+
timestamp: Capture timestamp
|
|
296
|
+
stream_info: Stream information dict
|
|
297
|
+
image_data: Base64-encoded JPEG image of the license plate crop
|
|
298
|
+
cooldown: Cooldown period in seconds
|
|
299
|
+
"""
|
|
300
|
+
self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
|
|
301
|
+
|
|
228
302
|
# Check cooldown
|
|
229
303
|
if not self.should_log_plate(plate_text, cooldown):
|
|
230
|
-
self.logger.
|
|
304
|
+
self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
|
|
231
305
|
return False
|
|
232
306
|
|
|
233
307
|
try:
|
|
@@ -236,23 +310,37 @@ class LicensePlateMonitorLogger:
|
|
|
236
310
|
location = camera_info.get("location", "")
|
|
237
311
|
frame_id = stream_info.get("frame_id", "")
|
|
238
312
|
|
|
313
|
+
# Get project ID from server_info
|
|
314
|
+
project_id = self.server_info.get('projectID', '') if self.server_info else ''
|
|
315
|
+
|
|
316
|
+
# Format timestamp to RFC3339 format (2006-01-02T15:04:05Z)
|
|
317
|
+
rfc3339_timestamp = self._format_timestamp_rfc3339(timestamp)
|
|
318
|
+
|
|
239
319
|
payload = {
|
|
240
320
|
'licensePlate': plate_text,
|
|
241
321
|
'frameId': frame_id,
|
|
242
322
|
'location': location,
|
|
243
323
|
'camera': camera_name,
|
|
244
|
-
'captureTimestamp':
|
|
324
|
+
'captureTimestamp': rfc3339_timestamp,
|
|
325
|
+
'projectId': project_id,
|
|
326
|
+
'imageData': image_data if image_data else ""
|
|
245
327
|
}
|
|
246
328
|
|
|
247
|
-
|
|
329
|
+
# Add projectId as query parameter
|
|
330
|
+
endpoint = f'/v1/lpr-server/detections?projectId={project_id}'
|
|
331
|
+
self.logger.info(f"Sending POST request to {self.server_base_url}{endpoint} with plate: {plate_text}, imageData length: {len(image_data) if image_data else 0}")
|
|
332
|
+
|
|
333
|
+
response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
|
|
334
|
+
|
|
335
|
+
self.logger.info(f"API Response received for plate {plate_text}: {response}")
|
|
248
336
|
|
|
249
337
|
# Update timestamp after successful log
|
|
250
338
|
self.update_log_timestamp(plate_text)
|
|
251
|
-
self.logger.info(f"
|
|
339
|
+
self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
|
|
252
340
|
return True
|
|
253
341
|
|
|
254
342
|
except Exception as e:
|
|
255
|
-
self.logger.error(f"Failed to log
|
|
343
|
+
self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
|
|
256
344
|
return False
|
|
257
345
|
|
|
258
346
|
class LicensePlateMonitorUseCase(BaseProcessor):
|
|
@@ -296,11 +384,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
296
384
|
self.image_preprocessor = ImagePreprocessor()
|
|
297
385
|
# Fast OCR model (shared across instances)
|
|
298
386
|
if LicensePlateMonitorUseCase._ocr_model is None:
|
|
299
|
-
|
|
387
|
+
if _OCR_IMPORT_SOURCE == "stub":
|
|
388
|
+
# Using stub - log warning once
|
|
389
|
+
self.logger.error("OCR module not available. LicensePlateRecognizer will not function. Install: pip install fast-plate-ocr[onnx]")
|
|
300
390
|
LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
391
|
+
else:
|
|
392
|
+
# Try to load real OCR model
|
|
393
|
+
try:
|
|
394
|
+
LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
|
|
395
|
+
source_msg = "from local repo" if _OCR_IMPORT_SOURCE == "local_repo" else "from installed package"
|
|
396
|
+
self.logger.info(f"LicensePlateRecognizer loaded successfully {source_msg}")
|
|
397
|
+
except Exception as e:
|
|
398
|
+
self.logger.error(f"Failed to initialize LicensePlateRecognizer: {e}", exc_info=True)
|
|
399
|
+
LicensePlateMonitorUseCase._ocr_model = None
|
|
304
400
|
self.ocr_model = LicensePlateMonitorUseCase._ocr_model
|
|
305
401
|
# OCR text history for stability checks (text → consecutive frame count)
|
|
306
402
|
self._text_history: Dict[str, int] = {}
|
|
@@ -365,7 +461,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
365
461
|
self._logging_enabled = False
|
|
366
462
|
|
|
367
463
|
def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
|
|
368
|
-
stream_info: Optional[Dict[str, Any]]) -> None:
|
|
464
|
+
stream_info: Optional[Dict[str, Any]], image_bytes: Optional[bytes] = None) -> None:
|
|
369
465
|
"""Log all detected plates to RPC server with cooldown."""
|
|
370
466
|
if not self._logging_enabled or not self.plate_logger or not stream_info:
|
|
371
467
|
return
|
|
@@ -373,6 +469,24 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
373
469
|
# Get current timestamp
|
|
374
470
|
current_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
|
|
375
471
|
|
|
472
|
+
# Encode the full frame image as base64 JPEG
|
|
473
|
+
image_data = ""
|
|
474
|
+
if image_bytes:
|
|
475
|
+
try:
|
|
476
|
+
# Decode image bytes
|
|
477
|
+
image_array = np.frombuffer(image_bytes, np.uint8)
|
|
478
|
+
image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
|
|
479
|
+
|
|
480
|
+
if image is not None:
|
|
481
|
+
# Encode as JPEG with 85% quality
|
|
482
|
+
success, jpeg_buffer = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, 99])
|
|
483
|
+
if success:
|
|
484
|
+
# Convert to base64
|
|
485
|
+
image_data = base64.b64encode(jpeg_buffer.tobytes()).decode('utf-8')
|
|
486
|
+
self.logger.debug(f"Encoded frame image as base64, length: {len(image_data)}")
|
|
487
|
+
except Exception as e:
|
|
488
|
+
self.logger.error(f"Failed to encode frame image: {e}")
|
|
489
|
+
|
|
376
490
|
# Collect all unique plates from current detections
|
|
377
491
|
plates_to_log = set()
|
|
378
492
|
for det in detections:
|
|
@@ -394,6 +508,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
394
508
|
plate_text=plate_text,
|
|
395
509
|
timestamp=current_timestamp,
|
|
396
510
|
stream_info=stream_info,
|
|
511
|
+
image_data=image_data,
|
|
397
512
|
cooldown=config.plate_log_cooldown
|
|
398
513
|
)
|
|
399
514
|
tasks.append(task)
|
|
@@ -419,11 +534,6 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
419
534
|
if not input_bytes:
|
|
420
535
|
return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
|
|
421
536
|
usecase=self.name, category=self.category, context=context)
|
|
422
|
-
|
|
423
|
-
print("--------------------------------------")
|
|
424
|
-
print("config.alert_config",config.alert_config)
|
|
425
|
-
print(config)
|
|
426
|
-
print("--------------------------------------")
|
|
427
537
|
|
|
428
538
|
# Initialize plate logger if lpr_server_id is provided (optional flow)
|
|
429
539
|
if config.lpr_server_id and self._logging_enabled:
|
|
@@ -526,7 +636,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
526
636
|
self._update_plate_texts(processed_data)
|
|
527
637
|
|
|
528
638
|
# Step 9.5: Log detected plates to RPC (optional, only if lpr_server_id is provided)
|
|
529
|
-
self._log_detected_plates(processed_data, config, stream_info)
|
|
639
|
+
self._log_detected_plates(processed_data, config, stream_info, input_bytes)
|
|
530
640
|
|
|
531
641
|
# Step 10: Update frame counter
|
|
532
642
|
self._total_frame_counter += 1
|
|
@@ -704,15 +814,17 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
704
814
|
|
|
705
815
|
def _run_ocr(self, crop: np.ndarray) -> str:
|
|
706
816
|
"""Run OCR on a cropped plate image and return cleaned text or empty string."""
|
|
707
|
-
# print("---------OCR CROP22",crop)
|
|
708
|
-
# print("---------OCR CROP SIZE22",crop.size)
|
|
709
|
-
|
|
710
817
|
if crop is None or crop.size == 0 or self.ocr_model is None:
|
|
711
818
|
return ""
|
|
819
|
+
|
|
820
|
+
# Check if we have a valid OCR model (not the stub) - silently return empty if stub
|
|
821
|
+
if not hasattr(self.ocr_model, 'run'):
|
|
822
|
+
return ""
|
|
823
|
+
|
|
712
824
|
try:
|
|
713
|
-
# fast_plate_ocr
|
|
714
|
-
#rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
|
|
825
|
+
# fast_plate_ocr LicensePlateRecognizer has a run() method
|
|
715
826
|
res = self.ocr_model.run(crop)
|
|
827
|
+
|
|
716
828
|
if isinstance(res, list):
|
|
717
829
|
res = res[0] if res else ""
|
|
718
830
|
cleaned_text = self._clean_text(str(res))
|
|
@@ -723,12 +835,14 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
723
835
|
response = all(ch.isalpha() for ch in cleaned_text)
|
|
724
836
|
elif self._ocr_mode == "alphanumeric":
|
|
725
837
|
response = True
|
|
838
|
+
else:
|
|
839
|
+
response = False
|
|
726
840
|
|
|
727
841
|
if response:
|
|
728
842
|
return cleaned_text
|
|
729
|
-
|
|
730
|
-
return ""
|
|
843
|
+
return ""
|
|
731
844
|
except Exception as exc:
|
|
845
|
+
# Only log at debug level to avoid spam
|
|
732
846
|
self.logger.warning(f"OCR failed: {exc}")
|
|
733
847
|
return ""
|
|
734
848
|
|