matrice-analytics 0.1.31__py3-none-any.whl → 0.1.32__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 +128 -44
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +14 -14
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.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,
|
|
@@ -36,24 +36,22 @@ print(f"Python version: {major_version}.{minor_version}")
|
|
|
36
36
|
os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
|
|
37
37
|
|
|
38
38
|
|
|
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:
|
|
39
|
+
# Try to import LicensePlateRecognizer from local repo first, then installed package
|
|
40
|
+
_OCR_IMPORT_SOURCE = None
|
|
41
|
+
try:
|
|
42
|
+
from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
|
|
43
|
+
_OCR_IMPORT_SOURCE = "local_repo"
|
|
44
|
+
except ImportError:
|
|
49
45
|
try:
|
|
50
46
|
from fast_plate_ocr import LicensePlateRecognizer # type: ignore
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
_OCR_IMPORT_SOURCE = "installed_package"
|
|
48
|
+
except ImportError:
|
|
49
|
+
# Use stub class if neither import works
|
|
50
|
+
_OCR_IMPORT_SOURCE = "stub"
|
|
53
51
|
class LicensePlateRecognizer: # type: ignore
|
|
54
|
-
"""Stub fallback when fast_plate_ocr is not
|
|
52
|
+
"""Stub fallback when fast_plate_ocr is not available."""
|
|
55
53
|
def __init__(self, *args, **kwargs):
|
|
56
|
-
|
|
54
|
+
pass # Silent stub - error will be logged once during initialization
|
|
57
55
|
|
|
58
56
|
# Internal utilities that are still required
|
|
59
57
|
from ..ocr.preprocessing import ImagePreprocessor
|
|
@@ -124,23 +122,29 @@ class LicensePlateMonitorLogger:
|
|
|
124
122
|
|
|
125
123
|
def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
|
|
126
124
|
"""Initialize session and fetch server connection info if lpr_server_id is provided."""
|
|
125
|
+
self.logger.info("Initializing LicensePlateMonitorLogger session...")
|
|
126
|
+
|
|
127
127
|
# Use existing session if provided, otherwise create new one
|
|
128
128
|
if self.session:
|
|
129
|
+
self.logger.info("Session already initialized, skipping initialization")
|
|
129
130
|
return
|
|
130
131
|
if config.session:
|
|
131
132
|
self.session = config.session
|
|
133
|
+
self.logger.info("Using provided session from config")
|
|
132
134
|
if not self.session:
|
|
133
135
|
# Initialize Matrice session
|
|
134
136
|
if not HAS_MATRICE_SESSION:
|
|
137
|
+
self.logger.error("Matrice session module not available")
|
|
135
138
|
raise ImportError("Matrice session is required for License Plate Monitoring")
|
|
136
139
|
try:
|
|
140
|
+
self.logger.info("Creating new Matrice session...")
|
|
137
141
|
self.session = Session(
|
|
138
142
|
account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
|
|
139
143
|
access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
|
|
140
144
|
secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
|
|
141
145
|
project_id=os.getenv("MATRICE_PROJECT_ID", ""),
|
|
142
146
|
)
|
|
143
|
-
self.logger.info("
|
|
147
|
+
self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
|
|
144
148
|
except Exception as e:
|
|
145
149
|
self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
|
|
146
150
|
raise
|
|
@@ -148,6 +152,7 @@ class LicensePlateMonitorLogger:
|
|
|
148
152
|
# Fetch server connection info if lpr_server_id is provided
|
|
149
153
|
if config.lpr_server_id:
|
|
150
154
|
self.lpr_server_id = config.lpr_server_id
|
|
155
|
+
self.logger.info(f"Fetching LPR server connection info for server ID: {self.lpr_server_id}")
|
|
151
156
|
try:
|
|
152
157
|
self.server_info = self.get_server_connection_info()
|
|
153
158
|
if self.server_info:
|
|
@@ -158,23 +163,26 @@ class LicensePlateMonitorLogger:
|
|
|
158
163
|
|
|
159
164
|
if server_host == self.public_ip:
|
|
160
165
|
self.server_base_url = f"http://localhost:{server_port}"
|
|
161
|
-
self.logger.
|
|
166
|
+
self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
|
|
162
167
|
else:
|
|
163
168
|
self.server_base_url = f"https://{server_host}:{server_port}"
|
|
164
|
-
self.logger.
|
|
169
|
+
self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
|
|
165
170
|
|
|
166
171
|
self.session.update(self.server_info.get('projectID', ''))
|
|
167
172
|
self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
|
|
168
173
|
else:
|
|
169
|
-
self.logger.warning("Failed to fetch LPR server connection info")
|
|
174
|
+
self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
|
|
170
175
|
except Exception as e:
|
|
171
176
|
self.logger.error(f"Error fetching LPR server connection info: {e}", exc_info=True)
|
|
177
|
+
else:
|
|
178
|
+
self.logger.info("No lpr_server_id provided in config, skipping server connection info fetch")
|
|
172
179
|
|
|
173
180
|
def _get_public_ip(self) -> str:
|
|
174
181
|
"""Get the public IP address of this machine."""
|
|
182
|
+
self.logger.info("Fetching public IP address...")
|
|
175
183
|
try:
|
|
176
184
|
public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
|
|
177
|
-
self.logger.
|
|
185
|
+
self.logger.info(f"Successfully fetched external IP: {public_ip}")
|
|
178
186
|
return public_ip
|
|
179
187
|
except Exception as e:
|
|
180
188
|
self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
|
|
@@ -183,10 +191,15 @@ class LicensePlateMonitorLogger:
|
|
|
183
191
|
def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
|
|
184
192
|
"""Fetch server connection info from RPC."""
|
|
185
193
|
if not self.lpr_server_id:
|
|
194
|
+
self.logger.warning("No lpr_server_id set, cannot fetch server connection info")
|
|
186
195
|
return None
|
|
187
196
|
|
|
188
197
|
try:
|
|
189
|
-
|
|
198
|
+
endpoint = f"/v1/actions/lpr_servers/{self.lpr_server_id}"
|
|
199
|
+
self.logger.info(f"Sending GET request to: {endpoint}")
|
|
200
|
+
response = self.session.rpc.get(endpoint)
|
|
201
|
+
self.logger.info(f"Received response: success={response.get('success')}, code={response.get('code')}, message={response.get('message')}")
|
|
202
|
+
|
|
190
203
|
if response.get("success", False) and response.get("code") == 200:
|
|
191
204
|
# Response format:
|
|
192
205
|
# {'success': True,
|
|
@@ -202,7 +215,9 @@ class LicensePlateMonitorLogger:
|
|
|
202
215
|
# 'projectID': '68ca6372ab79ba13ef699ba6',
|
|
203
216
|
# 'region': 'United States',
|
|
204
217
|
# 'isShared': False}}
|
|
205
|
-
|
|
218
|
+
data = response.get("data", {})
|
|
219
|
+
self.logger.info(f"Server connection info retrieved: name={data.get('name')}, host={data.get('host')}, port={data.get('port')}, status={data.get('status')}")
|
|
220
|
+
return data
|
|
206
221
|
else:
|
|
207
222
|
self.logger.warning(f"Failed to fetch server info: {response.get('message', 'Unknown error')}")
|
|
208
223
|
return None
|
|
@@ -214,20 +229,69 @@ class LicensePlateMonitorLogger:
|
|
|
214
229
|
"""Check if enough time has passed since last log for this plate."""
|
|
215
230
|
current_time = time.time()
|
|
216
231
|
last_log_time = self.plate_log_timestamps.get(plate_text, 0)
|
|
232
|
+
time_since_last_log = current_time - last_log_time
|
|
217
233
|
|
|
218
|
-
if
|
|
234
|
+
if time_since_last_log >= cooldown:
|
|
235
|
+
self.logger.debug(f"Plate {plate_text} ready to log (last logged {time_since_last_log:.1f}s ago, cooldown={cooldown}s)")
|
|
219
236
|
return True
|
|
220
|
-
|
|
237
|
+
else:
|
|
238
|
+
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)")
|
|
239
|
+
return False
|
|
221
240
|
|
|
222
241
|
def update_log_timestamp(self, plate_text: str) -> None:
|
|
223
242
|
"""Update the last log timestamp for a plate."""
|
|
224
243
|
self.plate_log_timestamps[plate_text] = time.time()
|
|
244
|
+
self.logger.debug(f"Updated log timestamp for plate: {plate_text}")
|
|
245
|
+
|
|
246
|
+
def _format_timestamp_rfc3339(self, timestamp: str) -> str:
|
|
247
|
+
"""Convert timestamp to RFC3339 format (2006-01-02T15:04:05Z).
|
|
248
|
+
|
|
249
|
+
Handles various input formats:
|
|
250
|
+
- "YYYY-MM-DD-HH:MM:SS.ffffff UTC"
|
|
251
|
+
- "YYYY:MM:DD HH:MM:SS"
|
|
252
|
+
- Unix timestamp (float/int)
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
# If already in RFC3339 format, return as is
|
|
256
|
+
if 'T' in timestamp and timestamp.endswith('Z'):
|
|
257
|
+
return timestamp
|
|
258
|
+
|
|
259
|
+
# Try to parse common formats
|
|
260
|
+
dt = None
|
|
261
|
+
|
|
262
|
+
# Format: "2025-08-19-04:22:47.187574 UTC"
|
|
263
|
+
if '-' in timestamp and 'UTC' in timestamp:
|
|
264
|
+
timestamp_clean = timestamp.replace(' UTC', '')
|
|
265
|
+
dt = datetime.strptime(timestamp_clean, '%Y-%m-%d-%H:%M:%S.%f')
|
|
266
|
+
# Format: "2025:10:23 14:30:45"
|
|
267
|
+
elif ':' in timestamp and ' ' in timestamp:
|
|
268
|
+
dt = datetime.strptime(timestamp, '%Y:%m:%d %H:%M:%S')
|
|
269
|
+
# Format: numeric timestamp
|
|
270
|
+
elif timestamp.replace('.', '').isdigit():
|
|
271
|
+
dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
|
|
272
|
+
|
|
273
|
+
if dt is None:
|
|
274
|
+
# Fallback to current time
|
|
275
|
+
dt = datetime.now(timezone.utc)
|
|
276
|
+
else:
|
|
277
|
+
# Ensure timezone is UTC
|
|
278
|
+
if dt.tzinfo is None:
|
|
279
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
280
|
+
|
|
281
|
+
# Format to RFC3339: 2006-01-02T15:04:05Z
|
|
282
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
self.logger.warning(f"Failed to parse timestamp '{timestamp}': {e}. Using current time.")
|
|
286
|
+
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
225
287
|
|
|
226
288
|
async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any], cooldown: float = 30.0) -> bool:
|
|
227
289
|
"""Log plate to RPC server with cooldown period."""
|
|
290
|
+
self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
|
|
291
|
+
|
|
228
292
|
# Check cooldown
|
|
229
293
|
if not self.should_log_plate(plate_text, cooldown):
|
|
230
|
-
self.logger.
|
|
294
|
+
self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
|
|
231
295
|
return False
|
|
232
296
|
|
|
233
297
|
try:
|
|
@@ -236,23 +300,36 @@ class LicensePlateMonitorLogger:
|
|
|
236
300
|
location = camera_info.get("location", "")
|
|
237
301
|
frame_id = stream_info.get("frame_id", "")
|
|
238
302
|
|
|
303
|
+
# Get project ID from server_info
|
|
304
|
+
project_id = self.server_info.get('projectID', '') if self.server_info else ''
|
|
305
|
+
|
|
306
|
+
# Format timestamp to RFC3339 format (2006-01-02T15:04:05Z)
|
|
307
|
+
rfc3339_timestamp = self._format_timestamp_rfc3339(timestamp)
|
|
308
|
+
|
|
239
309
|
payload = {
|
|
240
310
|
'licensePlate': plate_text,
|
|
241
311
|
'frameId': frame_id,
|
|
242
312
|
'location': location,
|
|
243
313
|
'camera': camera_name,
|
|
244
|
-
'captureTimestamp':
|
|
314
|
+
'captureTimestamp': rfc3339_timestamp,
|
|
315
|
+
'projectId': project_id
|
|
245
316
|
}
|
|
246
317
|
|
|
247
|
-
|
|
318
|
+
# Add projectId as query parameter
|
|
319
|
+
endpoint = f'/v1/lpr-server/detections?projectId={project_id}'
|
|
320
|
+
self.logger.info(f"Sending POST request to {self.server_base_url}{endpoint} with payload: {payload}")
|
|
321
|
+
|
|
322
|
+
response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
|
|
323
|
+
|
|
324
|
+
self.logger.info(f"API Response received for plate {plate_text}: {response}")
|
|
248
325
|
|
|
249
326
|
# Update timestamp after successful log
|
|
250
327
|
self.update_log_timestamp(plate_text)
|
|
251
|
-
self.logger.info(f"
|
|
328
|
+
self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
|
|
252
329
|
return True
|
|
253
330
|
|
|
254
331
|
except Exception as e:
|
|
255
|
-
self.logger.error(f"Failed to log
|
|
332
|
+
self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
|
|
256
333
|
return False
|
|
257
334
|
|
|
258
335
|
class LicensePlateMonitorUseCase(BaseProcessor):
|
|
@@ -296,11 +373,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
296
373
|
self.image_preprocessor = ImagePreprocessor()
|
|
297
374
|
# Fast OCR model (shared across instances)
|
|
298
375
|
if LicensePlateMonitorUseCase._ocr_model is None:
|
|
299
|
-
|
|
376
|
+
if _OCR_IMPORT_SOURCE == "stub":
|
|
377
|
+
# Using stub - log warning once
|
|
378
|
+
self.logger.error("OCR module not available. LicensePlateRecognizer will not function. Install: pip install fast-plate-ocr[onnx]")
|
|
300
379
|
LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
380
|
+
else:
|
|
381
|
+
# Try to load real OCR model
|
|
382
|
+
try:
|
|
383
|
+
LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
|
|
384
|
+
source_msg = "from local repo" if _OCR_IMPORT_SOURCE == "local_repo" else "from installed package"
|
|
385
|
+
self.logger.info(f"LicensePlateRecognizer loaded successfully {source_msg}")
|
|
386
|
+
except Exception as e:
|
|
387
|
+
self.logger.error(f"Failed to initialize LicensePlateRecognizer: {e}", exc_info=True)
|
|
388
|
+
LicensePlateMonitorUseCase._ocr_model = None
|
|
304
389
|
self.ocr_model = LicensePlateMonitorUseCase._ocr_model
|
|
305
390
|
# OCR text history for stability checks (text → consecutive frame count)
|
|
306
391
|
self._text_history: Dict[str, int] = {}
|
|
@@ -419,11 +504,6 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
419
504
|
if not input_bytes:
|
|
420
505
|
return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
|
|
421
506
|
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
507
|
|
|
428
508
|
# Initialize plate logger if lpr_server_id is provided (optional flow)
|
|
429
509
|
if config.lpr_server_id and self._logging_enabled:
|
|
@@ -704,15 +784,17 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
704
784
|
|
|
705
785
|
def _run_ocr(self, crop: np.ndarray) -> str:
|
|
706
786
|
"""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
787
|
if crop is None or crop.size == 0 or self.ocr_model is None:
|
|
711
788
|
return ""
|
|
789
|
+
|
|
790
|
+
# Check if we have a valid OCR model (not the stub) - silently return empty if stub
|
|
791
|
+
if not hasattr(self.ocr_model, 'run'):
|
|
792
|
+
return ""
|
|
793
|
+
|
|
712
794
|
try:
|
|
713
|
-
# fast_plate_ocr
|
|
714
|
-
#rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
|
|
795
|
+
# fast_plate_ocr LicensePlateRecognizer has a run() method
|
|
715
796
|
res = self.ocr_model.run(crop)
|
|
797
|
+
|
|
716
798
|
if isinstance(res, list):
|
|
717
799
|
res = res[0] if res else ""
|
|
718
800
|
cleaned_text = self._clean_text(str(res))
|
|
@@ -723,12 +805,14 @@ class LicensePlateMonitorUseCase(BaseProcessor):
|
|
|
723
805
|
response = all(ch.isalpha() for ch in cleaned_text)
|
|
724
806
|
elif self._ocr_mode == "alphanumeric":
|
|
725
807
|
response = True
|
|
808
|
+
else:
|
|
809
|
+
response = False
|
|
726
810
|
|
|
727
811
|
if response:
|
|
728
812
|
return cleaned_text
|
|
729
|
-
|
|
730
|
-
return ""
|
|
813
|
+
return ""
|
|
731
814
|
except Exception as exc:
|
|
815
|
+
# Only log at debug level to avoid spam
|
|
732
816
|
self.logger.warning(f"OCR failed: {exc}")
|
|
733
817
|
return ""
|
|
734
818
|
|
|
@@ -12,8 +12,8 @@ matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py,sh
|
|
|
12
12
|
matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py,sha256=jHPriRLorLuiC8km0MFNS96w121tKxd7t5GQl7I5kKE,3494
|
|
13
13
|
matrice_analytics/post_processing/README.md,sha256=bDszazvqV5xbGhMM6hDaMctIyk5gox9bADo2IZZ9Goo,13368
|
|
14
14
|
matrice_analytics/post_processing/__init__.py,sha256=dxGBUQaRCGndQmXpYAWqUhDeUZAcxU-_6HFnm3GRDRA,29417
|
|
15
|
-
matrice_analytics/post_processing/config.py,sha256=
|
|
16
|
-
matrice_analytics/post_processing/post_processor.py,sha256=
|
|
15
|
+
matrice_analytics/post_processing/config.py,sha256=V0s86qNapyDE6Q81ZS_1uzNqAjz-vc5L-9Tb33XaLEo,6771
|
|
16
|
+
matrice_analytics/post_processing/post_processor.py,sha256=ql4WuT1qVusXXYP36PNICQYPgvWOwBBErAb4UBMXlm8,44344
|
|
17
17
|
matrice_analytics/post_processing/advanced_tracker/README.md,sha256=RM8dynVoUWKn_hTbw9c6jHAbnQj-8hEAXnmuRZr2w1M,22485
|
|
18
18
|
matrice_analytics/post_processing/advanced_tracker/__init__.py,sha256=tAPFzI_Yep5TLX60FDwKqBqppc-EbxSr0wNsQ9DGI1o,423
|
|
19
19
|
matrice_analytics/post_processing/advanced_tracker/base.py,sha256=VqWy4dd5th5LK-JfueTt2_GSEoOi5QQfQxjTNhmQoLc,3580
|
|
@@ -24,14 +24,14 @@ matrice_analytics/post_processing/advanced_tracker/strack.py,sha256=OSai-SSpC9_u
|
|
|
24
24
|
matrice_analytics/post_processing/advanced_tracker/tracker.py,sha256=yN_tUzDZ-8M4NSoZrKf0OW0JA0JxOvz2oYKgbm-os88,15687
|
|
25
25
|
matrice_analytics/post_processing/core/__init__.py,sha256=QlgoJwjTU-3UYTEmFRN6wFWpOr7zNSnrohoqLBF5bNY,1434
|
|
26
26
|
matrice_analytics/post_processing/core/base.py,sha256=V6iF2_u8APBKIeudNBUP-_cHL4FAYrgEpM7W7fbtT74,29112
|
|
27
|
-
matrice_analytics/post_processing/core/config.py,sha256=
|
|
27
|
+
matrice_analytics/post_processing/core/config.py,sha256=uyxWndO-DE9PeGD_h5K3TeB0AUgGa5JOpgXNAMEKj6s,131528
|
|
28
28
|
matrice_analytics/post_processing/core/config_utils.py,sha256=QuAS-_JKSoNOtfUWgr7Alf_wsqODzN2rHlQu-cHRK0s,34311
|
|
29
29
|
matrice_analytics/post_processing/face_reg/__init__.py,sha256=yntaiGlW9vdjBpPZQXNuovALihJPzRlFyUE88l3MhBA,1364
|
|
30
30
|
matrice_analytics/post_processing/face_reg/compare_similarity.py,sha256=NlFc8b2a74k0PqSFAbuM_fUbA1BT3pr3VUgvSqRpJzQ,23396
|
|
31
|
-
matrice_analytics/post_processing/face_reg/embedding_manager.py,sha256=
|
|
32
|
-
matrice_analytics/post_processing/face_reg/face_recognition.py,sha256=
|
|
33
|
-
matrice_analytics/post_processing/face_reg/face_recognition_client.py,sha256=
|
|
34
|
-
matrice_analytics/post_processing/face_reg/people_activity_logging.py,sha256=
|
|
31
|
+
matrice_analytics/post_processing/face_reg/embedding_manager.py,sha256=qbh0df3-YbE0qvFDQvjpCg-JrsCZRJ5capjQ2LPOj1k,35619
|
|
32
|
+
matrice_analytics/post_processing/face_reg/face_recognition.py,sha256=17RVs_5BtGw2ruksL2JLbpXljbs4-96tP-yKGX3hrOI,90232
|
|
33
|
+
matrice_analytics/post_processing/face_reg/face_recognition_client.py,sha256=jPssMC_UuKn8M-9IaTirP-aswpcTiYV5vdTImt6lt4Q,27576
|
|
34
|
+
matrice_analytics/post_processing/face_reg/people_activity_logging.py,sha256=NhJXy9jCy_mlZiDXv8IeGyrRN_TL0kKCe3ZOlaNbZUw,13676
|
|
35
35
|
matrice_analytics/post_processing/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
matrice_analytics/post_processing/ocr/easyocr_extractor.py,sha256=FwVoUATYdiZtfhSAoiyCo_9dgA786pFZfONx6tsQOfE,11403
|
|
37
37
|
matrice_analytics/post_processing/ocr/postprocessing.py,sha256=RILArp8I9WRH7bALVZ9wPGc-aR7YMdqV1ndOOIcOnGQ,12309
|
|
@@ -100,7 +100,7 @@ matrice_analytics/post_processing/usecases/cardiomegaly_classification.py,sha256
|
|
|
100
100
|
matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py,sha256=eQ_s5u3Vnvja6-FmI6ZPxlNkaZtG-pVjTu8NuLjZJ5M,43714
|
|
101
101
|
matrice_analytics/post_processing/usecases/chicken_pose_detection.py,sha256=-e8di7Am-E-FCQFrSY8qJTO1aWtdRAVJoE-VKBgcyyI,29291
|
|
102
102
|
matrice_analytics/post_processing/usecases/child_monitoring.py,sha256=z3oymoqq4hDGwA8MkdEONZW_Vx5CAZmvzZaNLsqmCfw,39380
|
|
103
|
-
matrice_analytics/post_processing/usecases/color_detection.py,sha256=
|
|
103
|
+
matrice_analytics/post_processing/usecases/color_detection.py,sha256=K3jemPgPs5AzYDKeI_3dRmH_nhqN96tvEE9wsw5bPIs,90498
|
|
104
104
|
matrice_analytics/post_processing/usecases/color_map_utils.py,sha256=SP-AEVcjLmL8rxblu-ixqUJC2fqlcr7ab4hWo4Fcr_k,2677
|
|
105
105
|
matrice_analytics/post_processing/usecases/concrete_crack_detection.py,sha256=pxhOH_hG4hq9yytNepbGMdk2W_lTG8D1_2RAagaPBkg,40252
|
|
106
106
|
matrice_analytics/post_processing/usecases/crop_weed_detection.py,sha256=Ao1k5fJDYU_f6yZ8VO-jW8-esECV0-zY5Q570c_fako,35674
|
|
@@ -126,7 +126,7 @@ matrice_analytics/post_processing/usecases/leaf.py,sha256=cwgB1ZNxkQFtkk-thSJrkX
|
|
|
126
126
|
matrice_analytics/post_processing/usecases/leaf_disease.py,sha256=bkiLccTdf4KUq3he4eCpBlKXb5exr-WBhQ_oWQ7os68,36225
|
|
127
127
|
matrice_analytics/post_processing/usecases/leak_detection.py,sha256=oOCLLVMuXVeXPHyN8FUrD3U9JYJJwIz-5fcEMgvLdls,40531
|
|
128
128
|
matrice_analytics/post_processing/usecases/license_plate_detection.py,sha256=dsavd92-wnyXCNrCzaRj24zH7BVvLSa09HkYsrOXYDM,50806
|
|
129
|
-
matrice_analytics/post_processing/usecases/license_plate_monitoring.py,sha256=
|
|
129
|
+
matrice_analytics/post_processing/usecases/license_plate_monitoring.py,sha256=h4RHX9HXR6W0eQfFE1G6FRygvlmZQAWN3H323FWqvlQ,75268
|
|
130
130
|
matrice_analytics/post_processing/usecases/litter_monitoring.py,sha256=XaHAUGRBDJg_iVbu8hRMjTR-5TqrLj6ZNCRkInbzZTY,33255
|
|
131
131
|
matrice_analytics/post_processing/usecases/mask_detection.py,sha256=L_s6ZiT5zeXG-BsFcskb3HEG98DhLgqeMSDmCuwOteU,41501
|
|
132
132
|
matrice_analytics/post_processing/usecases/natural_disaster.py,sha256=ehxdPBoYcZWGVDOVn_mHFoz4lIE8LrveAkuXQj0n9XE,44253
|
|
@@ -188,8 +188,8 @@ matrice_analytics/post_processing/utils/format_utils.py,sha256=UTF7A5h9j0_S12xH9
|
|
|
188
188
|
matrice_analytics/post_processing/utils/geometry_utils.py,sha256=BWfdM6RsdJTTLR1GqkWfdwpjMEjTCJyuBxA4zVGKdfk,9623
|
|
189
189
|
matrice_analytics/post_processing/utils/smoothing_utils.py,sha256=78U-yucAcjUiZ0NIAc9NOUSIT0PWP1cqyIPA_Fdrjp0,14699
|
|
190
190
|
matrice_analytics/post_processing/utils/tracking_utils.py,sha256=rWxuotnJ3VLMHIBOud2KLcu4yZfDp7hVPWUtNAq_2xw,8288
|
|
191
|
-
matrice_analytics-0.1.
|
|
192
|
-
matrice_analytics-0.1.
|
|
193
|
-
matrice_analytics-0.1.
|
|
194
|
-
matrice_analytics-0.1.
|
|
195
|
-
matrice_analytics-0.1.
|
|
191
|
+
matrice_analytics-0.1.32.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
|
|
192
|
+
matrice_analytics-0.1.32.dist-info/METADATA,sha256=zSaINlI-8xwx56Oc1aLW6qnbechmTBUIp1ZLZ0mKqJQ,14378
|
|
193
|
+
matrice_analytics-0.1.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
194
|
+
matrice_analytics-0.1.32.dist-info/top_level.txt,sha256=STAPEU-e-rWTerXaspdi76T_eVRSrEfFpURSP7_Dt8E,18
|
|
195
|
+
matrice_analytics-0.1.32.dist-info/RECORD,,
|