matrice-analytics 0.1.34__py3-none-any.whl → 0.1.36__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.

@@ -711,7 +711,8 @@ class PostProcessor:
711
711
 
712
712
  # Async use cases
713
713
  async_use_cases = {
714
- FaceRecognitionEmbeddingUseCase
714
+ FaceRecognitionEmbeddingUseCase,
715
+ LicensePlateMonitorUseCase
715
716
  }
716
717
 
717
718
  # Determine the appropriate method signature and call
@@ -25,6 +25,7 @@ import numpy as np
25
25
  import re
26
26
  from collections import Counter, defaultdict
27
27
  import sys
28
+ import subprocess
28
29
  import logging
29
30
  import asyncio
30
31
  import urllib
@@ -37,33 +38,96 @@ print(f"Python version: {major_version}.{minor_version}")
37
38
  os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
38
39
 
39
40
 
40
- # Try to import LicensePlateRecognizer from local repo first, then installed package
41
+ # Lazy import mechanism for LicensePlateRecognizer
41
42
  _OCR_IMPORT_SOURCE = None
42
- try:
43
- from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
44
- _OCR_IMPORT_SOURCE = "local_repo"
45
- except ImportError:
43
+ _LicensePlateRecognizerClass = None
44
+
45
+ def _get_license_plate_recognizer_class():
46
+ """Lazy load LicensePlateRecognizer with automatic installation fallback."""
47
+ global _OCR_IMPORT_SOURCE, _LicensePlateRecognizerClass
48
+
49
+ if _LicensePlateRecognizerClass is not None:
50
+ return _LicensePlateRecognizerClass
51
+
52
+ # Try to import from local repo first
53
+ try:
54
+ from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
55
+ _OCR_IMPORT_SOURCE = "local_repo"
56
+ _LicensePlateRecognizerClass = LicensePlateRecognizer
57
+ logging.info("Successfully imported LicensePlateRecognizer from local repo")
58
+ return _LicensePlateRecognizerClass
59
+ except ImportError as e:
60
+ logging.debug(f"Could not import from local repo: {e}")
61
+
62
+ # Try to import from installed package
46
63
  try:
47
64
  from fast_plate_ocr import LicensePlateRecognizer # type: ignore
48
65
  _OCR_IMPORT_SOURCE = "installed_package"
49
- except ImportError:
50
- # Use stub class if neither import works
51
- _OCR_IMPORT_SOURCE = "stub"
52
- class LicensePlateRecognizer: # type: ignore
53
- """Stub fallback when fast_plate_ocr is not available."""
54
- def __init__(self, *args, **kwargs):
55
- pass # Silent stub - error will be logged once during initialization
66
+ _LicensePlateRecognizerClass = LicensePlateRecognizer
67
+ logging.info("Successfully imported LicensePlateRecognizer from installed package")
68
+ return _LicensePlateRecognizerClass
69
+ except ImportError as e:
70
+ logging.warning(f"Could not import from installed package: {e}")
71
+
72
+ # Try to install with GPU support first
73
+ logging.info("Attempting to install fast-plate-ocr with GPU support...")
74
+ try:
75
+ import subprocess
76
+ result = subprocess.run(
77
+ [sys.executable, "-m", "pip", "install", "fast-plate-ocr[onnx-gpu]", "--no-cache-dir"],
78
+ capture_output=True,
79
+ text=True,
80
+ timeout=300
81
+ )
82
+ if result.returncode == 0:
83
+ logging.info("Successfully installed fast-plate-ocr[onnx-gpu]")
84
+ try:
85
+ from fast_plate_ocr import LicensePlateRecognizer # type: ignore
86
+ _OCR_IMPORT_SOURCE = "installed_package_gpu"
87
+ _LicensePlateRecognizerClass = LicensePlateRecognizer
88
+ logging.info("Successfully imported LicensePlateRecognizer after GPU installation")
89
+ return _LicensePlateRecognizerClass
90
+ except ImportError as e:
91
+ logging.warning(f"Installation succeeded but import failed: {e}")
92
+ else:
93
+ logging.warning(f"GPU installation failed: {result.stderr}")
94
+ except Exception as e:
95
+ logging.warning(f"Error during GPU installation: {e}")
96
+
97
+ # Try to install with CPU support as fallback
98
+ logging.info("Attempting to install fast-plate-ocr with CPU support...")
99
+ try:
100
+ import subprocess
101
+ result = subprocess.run(
102
+ [sys.executable, "-m", "pip", "install", "fast-plate-ocr[onnx]", "--no-cache-dir"],
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=300
106
+ )
107
+ if result.returncode == 0:
108
+ logging.info("Successfully installed fast-plate-ocr[onnx]")
109
+ try:
110
+ from fast_plate_ocr import LicensePlateRecognizer # type: ignore
111
+ _OCR_IMPORT_SOURCE = "installed_package_cpu"
112
+ _LicensePlateRecognizerClass = LicensePlateRecognizer
113
+ logging.info("Successfully imported LicensePlateRecognizer after CPU installation")
114
+ return _LicensePlateRecognizerClass
115
+ except ImportError as e:
116
+ logging.error(f"Installation succeeded but import failed: {e}")
117
+ else:
118
+ logging.error(f"CPU installation failed: {result.stderr}")
119
+ except Exception as e:
120
+ logging.error(f"Error during CPU installation: {e}")
121
+
122
+ # Return None if all attempts failed
123
+ logging.error("All attempts to load or install LicensePlateRecognizer failed")
124
+ _OCR_IMPORT_SOURCE = "unavailable"
125
+ return None
56
126
 
57
127
  # Internal utilities that are still required
58
128
  from ..ocr.preprocessing import ImagePreprocessor
59
129
  from ..core.config import BaseConfig, AlertConfig, ZoneConfig
60
130
 
61
- # (Catch import errors early in the logs)
62
- try:
63
- _ = LicensePlateRecognizer # noqa: B018 – reference to quiet linters
64
- except Exception as _e:
65
- print(f"Warning: fast_plate_ocr could not be imported ⇒ {_e}")
66
-
67
131
  try:
68
132
  from matrice_common.session import Session
69
133
  HAS_MATRICE_SESSION = True
@@ -123,60 +187,91 @@ class LicensePlateMonitorLogger:
123
187
 
124
188
  def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
125
189
  """Initialize session and fetch server connection info if lpr_server_id is provided."""
126
- self.logger.info("Initializing LicensePlateMonitorLogger session...")
190
+ print("[LP_LOGGING] ===== INITIALIZING LP LOGGER SESSION =====")
191
+ print(f"[LP_LOGGING] Config lpr_server_id: {config.lpr_server_id}")
192
+ self.logger.info("[LP_LOGGING] ===== INITIALIZING LP LOGGER SESSION =====")
193
+ self.logger.info(f"[LP_LOGGING] Config lpr_server_id: {config.lpr_server_id}")
127
194
 
128
195
  # Use existing session if provided, otherwise create new one
129
- if self.session:
130
- self.logger.info("Session already initialized, skipping initialization")
196
+ if self.session and self.server_info and self.server_base_url:
197
+ self.logger.info("[LP_LOGGING] Session already initialized with server info, skipping re-initialization")
198
+ self.logger.info(f"[LP_LOGGING] Using existing server: {self.server_base_url}")
131
199
  return
200
+ elif self.session:
201
+ self.logger.info("[LP_LOGGING] Session exists but server info missing, continuing initialization...")
202
+ else:
203
+ self.logger.info("[LP_LOGGING] No existing session, initializing from scratch...")
204
+
132
205
  if config.session:
133
206
  self.session = config.session
134
- self.logger.info("Using provided session from config")
207
+ self.logger.info("[LP_LOGGING] Using provided session from config")
208
+
135
209
  if not self.session:
136
210
  # Initialize Matrice session
137
211
  if not HAS_MATRICE_SESSION:
138
- self.logger.error("Matrice session module not available")
212
+ self.logger.error("[LP_LOGGING] Matrice session module not available")
139
213
  raise ImportError("Matrice session is required for License Plate Monitoring")
140
214
  try:
141
- self.logger.info("Creating new Matrice session...")
215
+ self.logger.info("[LP_LOGGING] Creating new Matrice session from environment variables...")
216
+ account_number = os.getenv("MATRICE_ACCOUNT_NUMBER", "")
217
+ access_key_id = os.getenv("MATRICE_ACCESS_KEY_ID", "")
218
+ secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY", "")
219
+ project_id = os.getenv("MATRICE_PROJECT_ID", "")
220
+
221
+ self.logger.info(f"[LP_LOGGING] Account Number: {'SET' if account_number else 'NOT SET'}")
222
+ self.logger.info(f"[LP_LOGGING] Access Key ID: {'SET' if access_key_id else 'NOT SET'}")
223
+ self.logger.info(f"[LP_LOGGING] Secret Key: {'SET' if secret_key else 'NOT SET'}")
224
+ self.logger.info(f"[LP_LOGGING] Project ID: {'SET' if project_id else 'NOT SET'}")
225
+
142
226
  self.session = Session(
143
- account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
144
- access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
145
- secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
146
- project_id=os.getenv("MATRICE_PROJECT_ID", ""),
227
+ account_number=account_number,
228
+ access_key=access_key_id,
229
+ secret_key=secret_key,
230
+ project_id=project_id,
147
231
  )
148
- self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
232
+ self.logger.info("[LP_LOGGING] Successfully initialized new Matrice session")
149
233
  except Exception as e:
150
- self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
234
+ self.logger.error(f"[LP_LOGGING] Failed to initialize Matrice session: {e}", exc_info=True)
151
235
  raise
152
236
 
153
237
  # Fetch server connection info if lpr_server_id is provided
154
238
  if config.lpr_server_id:
155
239
  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}")
240
+ self.logger.info(f"[LP_LOGGING] Fetching LPR server connection info for server ID: {self.lpr_server_id}")
157
241
  try:
158
242
  self.server_info = self.get_server_connection_info()
159
243
  if self.server_info:
160
- self.logger.info(f"Successfully fetched LPR server info: {self.server_info.get('name', 'Unknown')}")
244
+ self.logger.info(f"[LP_LOGGING] Successfully fetched LPR server info")
245
+ self.logger.info(f"[LP_LOGGING] - Name: {self.server_info.get('name', 'Unknown')}")
246
+ self.logger.info(f"[LP_LOGGING] - Host: {self.server_info.get('host', 'Unknown')}")
247
+ self.logger.info(f"[LP_LOGGING] - Port: {self.server_info.get('port', 'Unknown')}")
248
+ self.logger.info(f"[LP_LOGGING] - Status: {self.server_info.get('status', 'Unknown')}")
249
+ self.logger.info(f"[LP_LOGGING] - Project ID: {self.server_info.get('projectID', 'Unknown')}")
250
+
161
251
  # Compare server host with public IP to determine if it's localhost
162
252
  server_host = self.server_info.get('host', 'localhost')
163
253
  server_port = self.server_info.get('port', 8200)
164
254
 
165
255
  if server_host == self.public_ip:
166
256
  self.server_base_url = f"http://localhost:{server_port}"
167
- self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
257
+ self.logger.info(f"[LP_LOGGING] Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
168
258
  else:
169
- self.server_base_url = f"https://{server_host}:{server_port}"
170
- self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
259
+ self.server_base_url = f"http://{server_host}:{server_port}"
260
+ self.logger.info(f"[LP_LOGGING] LPR server base URL configured: {self.server_base_url}")
171
261
 
172
262
  self.session.update(self.server_info.get('projectID', ''))
173
- self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
263
+ self.logger.info(f"[LP_LOGGING] Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
174
264
  else:
175
- self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
265
+ self.logger.error("[LP_LOGGING] Failed to fetch LPR server connection info - server_info is None")
266
+ self.logger.error("[LP_LOGGING] This will prevent plate logging from working!")
176
267
  except Exception as e:
177
- self.logger.error(f"Error fetching LPR server connection info: {e}", exc_info=True)
268
+ self.logger.error(f"[LP_LOGGING] Error fetching LPR server connection info: {e}", exc_info=True)
269
+ self.logger.error("[LP_LOGGING] This will prevent plate logging from working!")
178
270
  else:
179
- self.logger.info("No lpr_server_id provided in config, skipping server connection info fetch")
271
+ self.logger.warning("[LP_LOGGING] No lpr_server_id provided in config, skipping server connection info fetch")
272
+
273
+ print("[LP_LOGGING] ===== LP LOGGER SESSION INITIALIZATION COMPLETE =====")
274
+ self.logger.info("[LP_LOGGING] ===== LP LOGGER SESSION INITIALIZATION COMPLETE =====")
180
275
 
181
276
  def _get_public_ip(self) -> str:
182
277
  """Get the public IP address of this machine."""
@@ -233,10 +328,12 @@ class LicensePlateMonitorLogger:
233
328
  time_since_last_log = current_time - last_log_time
234
329
 
235
330
  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)")
331
+ print(f"[LP_LOGGING] ✓ Plate '{plate_text}' ready to log ({time_since_last_log:.1f}s since last)")
332
+ self.logger.info(f"[LP_LOGGING] OK - Plate '{plate_text}' ready to log (last logged {time_since_last_log:.1f}s ago, cooldown={cooldown}s)")
237
333
  return True
238
334
  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)")
335
+ print(f"[LP_LOGGING] ⊗ Plate '{plate_text}' in cooldown ({cooldown - time_since_last_log:.1f}s remaining)")
336
+ self.logger.info(f"[LP_LOGGING] SKIP - Plate '{plate_text}' in cooldown period ({time_since_last_log:.1f}s elapsed, {cooldown - time_since_last_log:.1f}s remaining)")
240
337
  return False
241
338
 
242
339
  def update_log_timestamp(self, plate_text: str) -> None:
@@ -297,11 +394,16 @@ class LicensePlateMonitorLogger:
297
394
  image_data: Base64-encoded JPEG image of the license plate crop
298
395
  cooldown: Cooldown period in seconds
299
396
  """
300
- self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
397
+ print(f"[LP_LOGGING] ===== PLATE LOG REQUEST START =====")
398
+ print(f"[LP_LOGGING] Plate: '{plate_text}', Timestamp: {timestamp}")
399
+ self.logger.info(f"[LP_LOGGING] ===== PLATE LOG REQUEST START =====")
400
+ self.logger.info(f"[LP_LOGGING] Plate: '{plate_text}', Timestamp: {timestamp}")
301
401
 
302
402
  # Check cooldown
303
403
  if not self.should_log_plate(plate_text, cooldown):
304
- self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
404
+ print(f"[LP_LOGGING] Plate '{plate_text}' NOT SENT - cooldown")
405
+ self.logger.info(f"[LP_LOGGING] Plate '{plate_text}' NOT SENT - skipped due to cooldown period")
406
+ self.logger.info(f"[LP_LOGGING] ===== PLATE LOG REQUEST END (SKIPPED) =====")
305
407
  return False
306
408
 
307
409
  try:
@@ -310,11 +412,16 @@ class LicensePlateMonitorLogger:
310
412
  location = camera_info.get("location", "")
311
413
  frame_id = stream_info.get("frame_id", "")
312
414
 
415
+ print(f"[LP_LOGGING] Camera: '{camera_name}', Location: '{location}'")
416
+ self.logger.info(f"[LP_LOGGING] Stream Info - Camera: '{camera_name}', Location: '{location}', Frame ID: '{frame_id}'")
417
+
313
418
  # Get project ID from server_info
314
419
  project_id = self.server_info.get('projectID', '') if self.server_info else ''
420
+ self.logger.info(f"[LP_LOGGING] Project ID: '{project_id}'")
315
421
 
316
422
  # Format timestamp to RFC3339 format (2006-01-02T15:04:05Z)
317
423
  rfc3339_timestamp = self._format_timestamp_rfc3339(timestamp)
424
+ self.logger.info(f"[LP_LOGGING] Formatted timestamp: {timestamp} -> {rfc3339_timestamp}")
318
425
 
319
426
  payload = {
320
427
  'licensePlate': plate_text,
@@ -328,19 +435,27 @@ class LicensePlateMonitorLogger:
328
435
 
329
436
  # Add projectId as query parameter
330
437
  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}")
438
+ full_url = f"{self.server_base_url}{endpoint}"
439
+ print(f"[LP_LOGGING] Sending POST to: {full_url}")
440
+ self.logger.info(f"[LP_LOGGING] Sending POST request to: {full_url}")
441
+ self.logger.info(f"[LP_LOGGING] Payload: licensePlate='{plate_text}', frameId='{frame_id}', location='{location}', camera='{camera_name}', imageData length={len(image_data) if image_data else 0}")
332
442
 
333
443
  response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
334
444
 
335
- self.logger.info(f"API Response received for plate {plate_text}: {response}")
445
+ print(f"[LP_LOGGING] Response: {response}")
446
+ self.logger.info(f"[LP_LOGGING] API Response received: {response}")
336
447
 
337
448
  # Update timestamp after successful log
338
449
  self.update_log_timestamp(plate_text)
339
- self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
450
+ print(f"[LP_LOGGING] ✓ Plate '{plate_text}' SUCCESSFULLY SENT")
451
+ self.logger.info(f"[LP_LOGGING] Plate '{plate_text}' SUCCESSFULLY SENT at {rfc3339_timestamp}")
452
+ self.logger.info(f"[LP_LOGGING] ===== PLATE LOG REQUEST END (SUCCESS) =====")
340
453
  return True
341
454
 
342
455
  except Exception as e:
343
- self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
456
+ print(f"[LP_LOGGING] ✗ Plate '{plate_text}' FAILED - {e}")
457
+ self.logger.error(f"[LP_LOGGING] Plate '{plate_text}' NOT SENT - Exception occurred: {e}", exc_info=True)
458
+ self.logger.info(f"[LP_LOGGING] ===== PLATE LOG REQUEST END (FAILED) =====")
344
459
  return False
345
460
 
346
461
  class LicensePlateMonitorUseCase(BaseProcessor):
@@ -382,23 +497,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
382
497
  # Map of track_id -> current dominant plate text
383
498
  self.unique_plate_track: Dict[Any, str] = {}
384
499
  self.image_preprocessor = ImagePreprocessor()
385
- # Fast OCR model (shared across instances)
386
- if LicensePlateMonitorUseCase._ocr_model is None:
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]")
390
- LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
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
400
- self.ocr_model = LicensePlateMonitorUseCase._ocr_model
401
- # OCR text history for stability checks (text → consecutive frame count)
500
+ # OCR model will be lazily initialized when first used
501
+ self.ocr_model = None
502
+ self._ocr_initialization_attempted = False
503
+ # OCR text history for stability checks (text consecutive frame count)
402
504
  self._text_history: Dict[str, int] = {}
403
505
 
404
506
  self.start_timer = None
@@ -415,6 +517,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
415
517
  # Initialize plate logger (optional, only used if lpr_server_id is provided)
416
518
  self.plate_logger: Optional[LicensePlateMonitorLogger] = None
417
519
  self._logging_enabled = True
520
+ self._plate_logger_initialized = False # Track if plate logger has been initialized
418
521
 
419
522
 
420
523
  def reset_tracker(self) -> None:
@@ -442,30 +545,62 @@ class LicensePlateMonitorUseCase(BaseProcessor):
442
545
  self.reset_plate_tracking()
443
546
  self.logger.info("All plate tracking state reset")
444
547
 
445
- def _initialize_plate_logger(self, config: LicensePlateMonitorConfig) -> None:
446
- """Initialize the plate logger if lpr_server_id is provided."""
548
+ def _initialize_plate_logger(self, config: LicensePlateMonitorConfig) -> bool:
549
+ """Initialize the plate logger if lpr_server_id is provided. Returns True if successful."""
550
+ self.logger.info(f"[LP_LOGGING] _initialize_plate_logger called with lpr_server_id: {config.lpr_server_id}")
551
+
447
552
  if not config.lpr_server_id:
448
553
  self._logging_enabled = False
449
- self.logger.info("Plate logging disabled: no lpr_server_id provided")
450
- return
554
+ self._plate_logger_initialized = False
555
+ self.logger.warning("[LP_LOGGING] Plate logging disabled: no lpr_server_id provided")
556
+ return False
451
557
 
452
558
  try:
453
559
  if self.plate_logger is None:
560
+ self.logger.info("[LP_LOGGING] Creating new LicensePlateMonitorLogger instance")
454
561
  self.plate_logger = LicensePlateMonitorLogger()
562
+ else:
563
+ self.logger.info("[LP_LOGGING] Using existing LicensePlateMonitorLogger instance")
455
564
 
565
+ self.logger.info("[LP_LOGGING] Initializing session for plate logger")
456
566
  self.plate_logger.initialize_session(config)
457
567
  self._logging_enabled = True
458
- self.logger.info(f"Plate logging enabled with server ID: {config.lpr_server_id}")
568
+ self._plate_logger_initialized = True
569
+ self.logger.info(f"[LP_LOGGING] SUCCESS - Plate logging ENABLED with server ID: {config.lpr_server_id}")
570
+ return True
459
571
  except Exception as e:
460
- self.logger.error(f"Failed to initialize plate logger: {e}", exc_info=True)
572
+ self.logger.error(f"[LP_LOGGING] ERROR - Failed to initialize plate logger: {e}", exc_info=True)
461
573
  self._logging_enabled = False
574
+ self._plate_logger_initialized = False
575
+ self.logger.error(f"[LP_LOGGING] Plate logging has been DISABLED due to initialization failure")
576
+ return False
462
577
 
463
- def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
578
+ async def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
464
579
  stream_info: Optional[Dict[str, Any]], image_bytes: Optional[bytes] = None) -> None:
465
580
  """Log all detected plates to RPC server with cooldown."""
466
- if not self._logging_enabled or not self.plate_logger or not stream_info:
581
+ # Enhanced logging for diagnostics
582
+ print(f"[LP_LOGGING] Starting plate logging check - detections count: {len(detections)}")
583
+ self.logger.info(f"[LP_LOGGING] Starting plate logging check - detections count: {len(detections)}")
584
+ self.logger.info(f"[LP_LOGGING] Logging enabled: {self._logging_enabled}, Plate logger exists: {self.plate_logger is not None}, Stream info exists: {stream_info is not None}")
585
+
586
+ if not self._logging_enabled:
587
+ print("[LP_LOGGING] Plate logging is DISABLED")
588
+ self.logger.warning("[LP_LOGGING] Plate logging is DISABLED - logging_enabled flag is False")
467
589
  return
468
590
 
591
+ if not self.plate_logger:
592
+ print("[LP_LOGGING] Plate logging SKIPPED - plate_logger not initialized")
593
+ self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - plate_logger is not initialized (lpr_server_id may not be configured)")
594
+ return
595
+
596
+ if not stream_info:
597
+ print("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
598
+ self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
599
+ return
600
+
601
+ print("[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
602
+ self.logger.info(f"[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
603
+
469
604
  # Get current timestamp
470
605
  current_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
471
606
 
@@ -483,44 +618,65 @@ class LicensePlateMonitorUseCase(BaseProcessor):
483
618
  if success:
484
619
  # Convert to base64
485
620
  image_data = base64.b64encode(jpeg_buffer.tobytes()).decode('utf-8')
486
- self.logger.debug(f"Encoded frame image as base64, length: {len(image_data)}")
621
+ self.logger.info(f"[LP_LOGGING] Encoded frame image as base64, length: {len(image_data)}")
622
+ else:
623
+ self.logger.warning(f"[LP_LOGGING] Failed to encode JPEG image")
624
+ else:
625
+ self.logger.warning(f"[LP_LOGGING] Failed to decode image bytes")
487
626
  except Exception as e:
488
- self.logger.error(f"Failed to encode frame image: {e}")
627
+ self.logger.error(f"[LP_LOGGING] Exception while encoding frame image: {e}", exc_info=True)
628
+ else:
629
+ self.logger.info(f"[LP_LOGGING] No image_bytes provided, sending without image")
489
630
 
490
631
  # Collect all unique plates from current detections
491
632
  plates_to_log = set()
633
+ detections_without_text = 0
492
634
  for det in detections:
493
635
  plate_text = det.get('plate_text')
494
636
  if not plate_text:
637
+ detections_without_text += 1
495
638
  continue
496
639
  plates_to_log.add(plate_text)
497
640
 
498
- # Log each unique plate (respecting cooldown)
641
+ print(f"[LP_LOGGING] Collected {len(plates_to_log)} unique plates to log: {plates_to_log}")
642
+ self.logger.info(f"[LP_LOGGING] Collected {len(plates_to_log)} unique plates to log: {plates_to_log}")
643
+ if detections_without_text > 0:
644
+ self.logger.warning(f"[LP_LOGGING] {detections_without_text} detections have NO plate_text (OCR may have failed or not run yet)")
645
+
646
+ # Log each unique plate directly with await (respecting cooldown)
499
647
  if plates_to_log:
648
+ print(f"[LP_LOGGING] Logging {len(plates_to_log)} plates with cooldown={config.plate_log_cooldown}s")
649
+ self.logger.info(f"[LP_LOGGING] Logging {len(plates_to_log)} plates with cooldown={config.plate_log_cooldown}s")
500
650
  try:
501
- # Run async logging tasks
502
- loop = asyncio.new_event_loop()
503
- asyncio.set_event_loop(loop)
504
- try:
505
- tasks = []
506
- for plate_text in plates_to_log:
507
- task = self.plate_logger.log_plate(
651
+ # Call log_plate directly with await for each plate
652
+ for plate_text in plates_to_log:
653
+ print(f"[LP_LOGGING] Processing plate: {plate_text}")
654
+ self.logger.info(f"[LP_LOGGING] Processing plate: {plate_text}")
655
+ try:
656
+ result = await self.plate_logger.log_plate(
508
657
  plate_text=plate_text,
509
658
  timestamp=current_timestamp,
510
659
  stream_info=stream_info,
511
660
  image_data=image_data,
512
661
  cooldown=config.plate_log_cooldown
513
662
  )
514
- tasks.append(task)
515
-
516
- # Run all logging tasks concurrently
517
- loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
518
- finally:
519
- loop.close()
663
+ status = "SENT" if result else "SKIPPED (cooldown)"
664
+ print(f"[LP_LOGGING] Plate {plate_text}: {status}")
665
+ self.logger.info(f"[LP_LOGGING] Plate {plate_text}: {status}")
666
+ except Exception as e:
667
+ print(f"[LP_LOGGING] ERROR - Plate {plate_text} failed: {e}")
668
+ self.logger.error(f"[LP_LOGGING] Plate {plate_text} raised exception: {e}", exc_info=True)
669
+
670
+ print("[LP_LOGGING] Plate logging complete")
671
+ self.logger.info(f"[LP_LOGGING] Plate logging complete")
520
672
  except Exception as e:
521
- self.logger.error(f"Error during plate logging: {e}", exc_info=True)
673
+ print(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}")
674
+ self.logger.error(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}", exc_info=True)
675
+ else:
676
+ print("[LP_LOGGING] No plates to log")
677
+ self.logger.info(f"[LP_LOGGING] No plates to log (plates_to_log is empty)")
522
678
 
523
- def process(self, data: Any, config: ConfigProtocol, input_bytes: Optional[bytes] = None,
679
+ async def process(self, data: Any, config: ConfigProtocol, input_bytes: Optional[bytes] = None,
524
680
  context: Optional[ProcessingContext] = None, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
525
681
  processing_start = time.time()
526
682
  try:
@@ -535,9 +691,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
535
691
  return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
536
692
  usecase=self.name, category=self.category, context=context)
537
693
 
538
- # Initialize plate logger if lpr_server_id is provided (optional flow)
539
- if config.lpr_server_id and self._logging_enabled:
540
- self._initialize_plate_logger(config)
694
+ # Initialize plate logger once if lpr_server_id is provided (optional flow)
695
+ if not self._plate_logger_initialized and config.lpr_server_id:
696
+ self.logger.info(f"[LP_LOGGING] First-time initialization - lpr_server_id: {config.lpr_server_id}")
697
+ success = self._initialize_plate_logger(config)
698
+ if success:
699
+ self.logger.info(f"[LP_LOGGING] Plate logger initialized successfully and ready to send plates")
700
+ else:
701
+ self.logger.error(f"[LP_LOGGING] Plate logger initialization FAILED - plates will NOT be sent")
702
+ elif self._plate_logger_initialized:
703
+ self.logger.debug(f"[LP_LOGGING] Plate logger already initialized, skipping re-initialization")
704
+ elif not config.lpr_server_id:
705
+ if self._total_frame_counter == 0: # Only log once at start
706
+ self.logger.warning(f"[LP_LOGGING] Plate logging will be DISABLED - no lpr_server_id provided in config")
541
707
 
542
708
  # Normalize alert_config if provided as a plain dict (JS JSON)
543
709
  if isinstance(getattr(config, 'alert_config', None), dict):
@@ -546,19 +712,8 @@ class LicensePlateMonitorUseCase(BaseProcessor):
546
712
  except Exception:
547
713
  pass
548
714
 
549
- # Initialize OCR extractor if not already done
550
- if self.ocr_model is None:
551
- self.logger.info("Lazy initialisation fallback (should rarely happen)")
552
- try:
553
- LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
554
- self.ocr_model = LicensePlateMonitorUseCase._ocr_model
555
- except Exception as e:
556
- return self.create_error_result(
557
- f"Failed to initialise OCR model: {e}",
558
- usecase=self.name,
559
- category=self.category,
560
- context=context,
561
- )
715
+ # OCR model will be lazily initialized when _run_ocr is first called
716
+ # No need to initialize here
562
717
 
563
718
  input_format = match_results_structure(data)
564
719
  context.input_format = input_format
@@ -627,16 +782,24 @@ class LicensePlateMonitorUseCase(BaseProcessor):
627
782
  #print("---------DATA5--------------",processed_data)
628
783
  # Step 8: Perform OCR on media
629
784
  ocr_analysis = self._analyze_ocr_in_media(processed_data, input_bytes, config)
630
-
631
- #print("ocr_analysis", ocr_analysis)
785
+ self.logger.info(f"[LP_LOGGING] OCR analysis completed, found {len(ocr_analysis)} results")
786
+ ocr_plates_found = [r.get('plate_text') for r in ocr_analysis if r.get('plate_text')]
787
+ if ocr_plates_found:
788
+ self.logger.info(f"[LP_LOGGING] OCR detected plates: {ocr_plates_found}")
789
+ else:
790
+ self.logger.warning(f"[LP_LOGGING] OCR did not detect any valid plate texts")
632
791
 
633
792
  # Step 9: Update plate texts
634
- #print("---------DATA6--------------",processed_data)
635
793
  processed_data = self._update_detections_with_ocr(processed_data, ocr_analysis)
636
794
  self._update_plate_texts(processed_data)
637
795
 
796
+ # Log final detection state before sending
797
+ final_plates = [d.get('plate_text') for d in processed_data if d.get('plate_text')]
798
+ self.logger.info(f"[LP_LOGGING] After OCR update, {len(final_plates)} detections have plate_text: {final_plates}")
799
+
638
800
  # Step 9.5: Log detected plates to RPC (optional, only if lpr_server_id is provided)
639
- self._log_detected_plates(processed_data, config, stream_info, input_bytes)
801
+ # Direct await since process is now async
802
+ await self._log_detected_plates(processed_data, config, stream_info, input_bytes)
640
803
 
641
804
  # Step 10: Update frame counter
642
805
  self._total_frame_counter += 1
@@ -806,6 +969,39 @@ class LicensePlateMonitorUseCase(BaseProcessor):
806
969
  # ------------------------------------------------------------------
807
970
  # Fast OCR helpers
808
971
  # ------------------------------------------------------------------
972
+ def _ensure_ocr_model_loaded(self) -> bool:
973
+ """Lazy initialization of OCR model. Returns True if model is available."""
974
+ if self.ocr_model is not None:
975
+ return True
976
+
977
+ if self._ocr_initialization_attempted:
978
+ return False
979
+
980
+ self._ocr_initialization_attempted = True
981
+
982
+ # Try to get the LicensePlateRecognizer class
983
+ LicensePlateRecognizerClass = _get_license_plate_recognizer_class()
984
+
985
+ if LicensePlateRecognizerClass is None:
986
+ self.logger.error("OCR module not available. LicensePlateRecognizer will not function.")
987
+ return False
988
+
989
+ # Try to initialize the OCR model
990
+ try:
991
+ self.ocr_model = LicensePlateRecognizerClass('cct-s-v1-global-model')
992
+ source_msg = {
993
+ "local_repo": "from local repo",
994
+ "installed_package": "from installed package",
995
+ "installed_package_gpu": "from installed package (GPU)",
996
+ "installed_package_cpu": "from installed package (CPU)"
997
+ }.get(_OCR_IMPORT_SOURCE, "from unknown source")
998
+ self.logger.info(f"LicensePlateRecognizer loaded successfully {source_msg}")
999
+ return True
1000
+ except Exception as e:
1001
+ self.logger.error(f"Failed to initialize LicensePlateRecognizer: {e}", exc_info=True)
1002
+ self.ocr_model = None
1003
+ return False
1004
+
809
1005
  def _clean_text(self, text: str) -> str:
810
1006
  """Sanitise OCR output to keep only alphanumerics and uppercase."""
811
1007
  if not text:
@@ -814,10 +1010,18 @@ class LicensePlateMonitorUseCase(BaseProcessor):
814
1010
 
815
1011
  def _run_ocr(self, crop: np.ndarray) -> str:
816
1012
  """Run OCR on a cropped plate image and return cleaned text or empty string."""
817
- if crop is None or crop.size == 0 or self.ocr_model is None:
1013
+ if crop is None or crop.size == 0:
1014
+ return ""
1015
+
1016
+ # Lazy load OCR model on first use
1017
+ if not self._ensure_ocr_model_loaded():
1018
+ return ""
1019
+
1020
+ # Double-check model is available
1021
+ if self.ocr_model is None:
818
1022
  return ""
819
1023
 
820
- # Check if we have a valid OCR model (not the stub) - silently return empty if stub
1024
+ # Check if we have a valid OCR model with run method
821
1025
  if not hasattr(self.ocr_model, 'run'):
822
1026
  return ""
823
1027
 
@@ -926,7 +1130,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
926
1130
  if not dominant_text:
927
1131
  dominant_text = self.unique_plate_track.get(tid)
928
1132
 
929
- # Enforce length 5–6 and uniqueness per frame
1133
+ # Enforce length 56 and uniqueness per frame
930
1134
  if dominant_text and self._min_plate_len <= len(dominant_text) <= 6:
931
1135
  unique_texts.add(dominant_text)
932
1136
  valid_detections.append({
@@ -1373,7 +1577,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
1373
1577
  """Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
1374
1578
 
1375
1579
  The input can be either:
1376
- 1. A numeric Unix timestamp (``float`` / ``int``)it will first be converted to a
1580
+ 1. A numeric Unix timestamp (``float`` / ``int``) it will first be converted to a
1377
1581
  string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
1378
1582
  2. A string already following the same layout.
1379
1583
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_analytics
3
- Version: 0.1.34
3
+ Version: 0.1.36
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -13,7 +13,7 @@ matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py,sh
13
13
  matrice_analytics/post_processing/README.md,sha256=bDszazvqV5xbGhMM6hDaMctIyk5gox9bADo2IZZ9Goo,13368
14
14
  matrice_analytics/post_processing/__init__.py,sha256=dxGBUQaRCGndQmXpYAWqUhDeUZAcxU-_6HFnm3GRDRA,29417
15
15
  matrice_analytics/post_processing/config.py,sha256=V0s86qNapyDE6Q81ZS_1uzNqAjz-vc5L-9Tb33XaLEo,6771
16
- matrice_analytics/post_processing/post_processor.py,sha256=ql4WuT1qVusXXYP36PNICQYPgvWOwBBErAb4UBMXlm8,44344
16
+ matrice_analytics/post_processing/post_processor.py,sha256=F838_vc7p9tjcp-vTMgTbpHqQcLX94xhL9HM06Wvpo8,44384
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
@@ -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=UaceT9ieQUjj1WRwtXkqmoNloG1Jc055u2AW2LvM8Eo,76747
129
+ matrice_analytics/post_processing/usecases/license_plate_monitoring.py,sha256=5YvqGwvvocyBZkFgSnCGuvCp4XT_YTrPuO-RHOEjRAk,89336
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.34.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
192
- matrice_analytics-0.1.34.dist-info/METADATA,sha256=KZrHth0YkNGwzjdc9O36coURdXHAGlfltJbkZ7PEz5Y,14378
193
- matrice_analytics-0.1.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
194
- matrice_analytics-0.1.34.dist-info/top_level.txt,sha256=STAPEU-e-rWTerXaspdi76T_eVRSrEfFpURSP7_Dt8E,18
195
- matrice_analytics-0.1.34.dist-info/RECORD,,
191
+ matrice_analytics-0.1.36.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
192
+ matrice_analytics-0.1.36.dist-info/METADATA,sha256=SOV2XH7crdPmqvOuZmjQvlkrVM0BoWI1fK2K2xmlR64,14378
193
+ matrice_analytics-0.1.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
194
+ matrice_analytics-0.1.36.dist-info/top_level.txt,sha256=STAPEU-e-rWTerXaspdi76T_eVRSrEfFpURSP7_Dt8E,18
195
+ matrice_analytics-0.1.36.dist-info/RECORD,,