matrice-analytics 0.1.3__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.

Files changed (61) hide show
  1. matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
  2. matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
  3. matrice_analytics/post_processing/config.py +4 -0
  4. matrice_analytics/post_processing/core/config.py +115 -12
  5. matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
  6. matrice_analytics/post_processing/face_reg/embedding_manager.py +109 -8
  7. matrice_analytics/post_processing/face_reg/face_recognition.py +157 -61
  8. matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -88
  9. matrice_analytics/post_processing/face_reg/people_activity_logging.py +67 -29
  10. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  11. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  12. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  13. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  14. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  15. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  16. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  17. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  18. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  19. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  20. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  21. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  22. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  23. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  24. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  25. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  26. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  27. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  28. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  29. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  30. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  31. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  32. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  33. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  34. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  44. matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
  45. matrice_analytics/post_processing/post_processor.py +32 -11
  46. matrice_analytics/post_processing/usecases/color/clip.py +42 -8
  47. matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
  48. matrice_analytics/post_processing/usecases/color_detection.py +50 -129
  49. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
  50. matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
  51. matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
  52. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +351 -26
  53. matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
  54. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  55. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
  56. matrice_analytics/post_processing/utils/__init__.py +8 -8
  57. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
  58. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +61 -26
  59. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
  60. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
  61. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/top_level.txt +0 -0
@@ -24,17 +24,34 @@ import numpy as np
24
24
  #import torch
25
25
  import re
26
26
  from collections import Counter, defaultdict
27
- #from turbojpeg import TurboJPEG, TJPF_RGB
27
+ import sys
28
+ import logging
29
+ import asyncio
30
+ import urllib
31
+ import urllib.request
32
+ # Get the major and minor version numbers
33
+ major_version = sys.version_info.major
34
+ minor_version = sys.version_info.minor
35
+ print(f"Python version: {major_version}.{minor_version}")
28
36
  os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
29
- # Fast license-plate OCR (replaces EasyOCR)
30
- # Attempt to import fast_plate_ocr; fall back to a stub if unavailable
37
+
38
+
39
+ # Try to import LicensePlateRecognizer from local repo first, then installed package
40
+ _OCR_IMPORT_SOURCE = None
31
41
  try:
32
- from fast_plate_ocr import LicensePlateRecognizer # type: ignore
33
- except: # pragma: no cover – optional dependency may be absent
34
- class LicensePlateRecognizer: # type: ignore
35
- """Stub fallback when fast_plate_ocr is not installed."""
36
- def __init__(self, *args, **kwargs):
37
- print("fast_plate_ocr is required for LicensePlateMonitorUseCase but is not installed.")
42
+ from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
43
+ _OCR_IMPORT_SOURCE = "local_repo"
44
+ except ImportError:
45
+ try:
46
+ from fast_plate_ocr import LicensePlateRecognizer # type: ignore
47
+ _OCR_IMPORT_SOURCE = "installed_package"
48
+ except ImportError:
49
+ # Use stub class if neither import works
50
+ _OCR_IMPORT_SOURCE = "stub"
51
+ class LicensePlateRecognizer: # type: ignore
52
+ """Stub fallback when fast_plate_ocr is not available."""
53
+ def __init__(self, *args, **kwargs):
54
+ pass # Silent stub - error will be logged once during initialization
38
55
 
39
56
  # Internal utilities that are still required
40
57
  from ..ocr.preprocessing import ImagePreprocessor
@@ -46,6 +63,12 @@ try:
46
63
  except Exception as _e:
47
64
  print(f"Warning: fast_plate_ocr could not be imported ⇒ {_e}")
48
65
 
66
+ try:
67
+ from matrice_common.session import Session
68
+ HAS_MATRICE_SESSION = True
69
+ except ImportError:
70
+ HAS_MATRICE_SESSION = False
71
+ logging.warning("Matrice session not available")
49
72
 
50
73
  @dataclass
51
74
  class LicensePlateMonitorConfig(BaseConfig):
@@ -66,7 +89,10 @@ class LicensePlateMonitorConfig(BaseConfig):
66
89
  language: List[str] = field(default_factory=lambda: ['en'])
67
90
  country: str = field(default_factory=lambda: 'us')
68
91
  ocr_mode:str = field(default_factory=lambda: "numeric") # "alphanumeric" or "numeric" or "alphabetic"
69
-
92
+ session: Optional[Session] = None
93
+ lpr_server_id: Optional[str] = None # Optional LPR server ID for remote logging
94
+ plate_log_cooldown: float = 30.0 # Cooldown period in seconds for logging same plate
95
+
70
96
  def validate(self) -> List[str]:
71
97
  """Validate configuration parameters."""
72
98
  errors = super().validate()
@@ -84,6 +110,228 @@ class LicensePlateMonitorConfig(BaseConfig):
84
110
  errors.append("smoothing_confidence_range_factor must be positive")
85
111
  return errors
86
112
 
113
+ class LicensePlateMonitorLogger:
114
+ def __init__(self):
115
+ self.session = None
116
+ self.logger = logging.getLogger(__name__)
117
+ self.lpr_server_id = None
118
+ self.server_info = None
119
+ self.plate_log_timestamps: Dict[str, float] = {} # Track last log time per plate
120
+ self.server_base_url = None
121
+ self.public_ip = self._get_public_ip()
122
+
123
+ def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
124
+ """Initialize session and fetch server connection info if lpr_server_id is provided."""
125
+ self.logger.info("Initializing LicensePlateMonitorLogger session...")
126
+
127
+ # Use existing session if provided, otherwise create new one
128
+ if self.session:
129
+ self.logger.info("Session already initialized, skipping initialization")
130
+ return
131
+ if config.session:
132
+ self.session = config.session
133
+ self.logger.info("Using provided session from config")
134
+ if not self.session:
135
+ # Initialize Matrice session
136
+ if not HAS_MATRICE_SESSION:
137
+ self.logger.error("Matrice session module not available")
138
+ raise ImportError("Matrice session is required for License Plate Monitoring")
139
+ try:
140
+ self.logger.info("Creating new Matrice session...")
141
+ self.session = Session(
142
+ account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
143
+ access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
144
+ secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
145
+ project_id=os.getenv("MATRICE_PROJECT_ID", ""),
146
+ )
147
+ self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
148
+ except Exception as e:
149
+ self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
150
+ raise
151
+
152
+ # Fetch server connection info if lpr_server_id is provided
153
+ if config.lpr_server_id:
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}")
156
+ try:
157
+ self.server_info = self.get_server_connection_info()
158
+ if self.server_info:
159
+ self.logger.info(f"Successfully fetched LPR server info: {self.server_info.get('name', 'Unknown')}")
160
+ # Compare server host with public IP to determine if it's localhost
161
+ server_host = self.server_info.get('host', 'localhost')
162
+ server_port = self.server_info.get('port', 8200)
163
+
164
+ if server_host == self.public_ip:
165
+ self.server_base_url = f"http://localhost:{server_port}"
166
+ self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
167
+ else:
168
+ self.server_base_url = f"https://{server_host}:{server_port}"
169
+ self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
170
+
171
+ self.session.update(self.server_info.get('projectID', ''))
172
+ self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
173
+ else:
174
+ self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
175
+ except Exception as e:
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")
179
+
180
+ def _get_public_ip(self) -> str:
181
+ """Get the public IP address of this machine."""
182
+ self.logger.info("Fetching public IP address...")
183
+ try:
184
+ public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
185
+ self.logger.info(f"Successfully fetched external IP: {public_ip}")
186
+ return public_ip
187
+ except Exception as e:
188
+ self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
189
+ return "localhost"
190
+
191
+ def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
192
+ """Fetch server connection info from RPC."""
193
+ if not self.lpr_server_id:
194
+ self.logger.warning("No lpr_server_id set, cannot fetch server connection info")
195
+ return None
196
+
197
+ try:
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
+
203
+ if response.get("success", False) and response.get("code") == 200:
204
+ # Response format:
205
+ # {'success': True,
206
+ # 'code': 200,
207
+ # 'message': 'Success',
208
+ # 'serverTime': '2025-10-19T04:58:04Z',
209
+ # 'data': {'id': '68f07e515cd5c6134a075384',
210
+ # 'name': 'lpr-server-1',
211
+ # 'host': '106.219.122.19',
212
+ # 'port': 8200,
213
+ # 'status': 'created',
214
+ # 'accountNumber': '3823255831182978487149732',
215
+ # 'projectID': '68ca6372ab79ba13ef699ba6',
216
+ # 'region': 'United States',
217
+ # 'isShared': False}}
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
221
+ else:
222
+ self.logger.warning(f"Failed to fetch server info: {response.get('message', 'Unknown error')}")
223
+ return None
224
+ except Exception as e:
225
+ self.logger.error(f"Exception while fetching server connection info: {e}", exc_info=True)
226
+ return None
227
+
228
+ def should_log_plate(self, plate_text: str, cooldown: float) -> bool:
229
+ """Check if enough time has passed since last log for this plate."""
230
+ current_time = time.time()
231
+ last_log_time = self.plate_log_timestamps.get(plate_text, 0)
232
+ time_since_last_log = current_time - last_log_time
233
+
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)")
236
+ return True
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
240
+
241
+ def update_log_timestamp(self, plate_text: str) -> None:
242
+ """Update the last log timestamp for a plate."""
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')
287
+
288
+ async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any], cooldown: float = 30.0) -> bool:
289
+ """Log plate to RPC server with cooldown period."""
290
+ self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
291
+
292
+ # Check cooldown
293
+ if not self.should_log_plate(plate_text, cooldown):
294
+ self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
295
+ return False
296
+
297
+ try:
298
+ camera_info = stream_info.get("camera_info", {})
299
+ camera_name = camera_info.get("camera_name", "")
300
+ location = camera_info.get("location", "")
301
+ frame_id = stream_info.get("frame_id", "")
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
+
309
+ payload = {
310
+ 'licensePlate': plate_text,
311
+ 'frameId': frame_id,
312
+ 'location': location,
313
+ 'camera': camera_name,
314
+ 'captureTimestamp': rfc3339_timestamp,
315
+ 'projectId': project_id
316
+ }
317
+
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}")
325
+
326
+ # Update timestamp after successful log
327
+ self.update_log_timestamp(plate_text)
328
+ self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
329
+ return True
330
+
331
+ except Exception as e:
332
+ self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
333
+ return False
334
+
87
335
  class LicensePlateMonitorUseCase(BaseProcessor):
88
336
  CATEGORY_DISPLAY = {"license_plate": "license_plate"}
89
337
 
@@ -125,11 +373,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
125
373
  self.image_preprocessor = ImagePreprocessor()
126
374
  # Fast OCR model (shared across instances)
127
375
  if LicensePlateMonitorUseCase._ocr_model is None:
128
- try:
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]")
129
379
  LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
130
- self.logger.info("LicensePlateRecognizer loaded successfully")
131
- except Exception as e:
132
- self.logger.warning(f"Failed to initialise LicensePlateRecognizer: {e}")
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
133
389
  self.ocr_model = LicensePlateMonitorUseCase._ocr_model
134
390
  # OCR text history for stability checks (text → consecutive frame count)
135
391
  self._text_history: Dict[str, int] = {}
@@ -145,6 +401,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
145
401
  self._ocr_mode = None
146
402
  #self.jpeg = TurboJPEG()
147
403
 
404
+ # Initialize plate logger (optional, only used if lpr_server_id is provided)
405
+ self.plate_logger: Optional[LicensePlateMonitorLogger] = None
406
+ self._logging_enabled = True
407
+
148
408
 
149
409
  def reset_tracker(self) -> None:
150
410
  """Reset the advanced tracker instance."""
@@ -170,6 +430,65 @@ class LicensePlateMonitorUseCase(BaseProcessor):
170
430
  self.reset_tracker()
171
431
  self.reset_plate_tracking()
172
432
  self.logger.info("All plate tracking state reset")
433
+
434
+ def _initialize_plate_logger(self, config: LicensePlateMonitorConfig) -> None:
435
+ """Initialize the plate logger if lpr_server_id is provided."""
436
+ if not config.lpr_server_id:
437
+ self._logging_enabled = False
438
+ self.logger.info("Plate logging disabled: no lpr_server_id provided")
439
+ return
440
+
441
+ try:
442
+ if self.plate_logger is None:
443
+ self.plate_logger = LicensePlateMonitorLogger()
444
+
445
+ self.plate_logger.initialize_session(config)
446
+ self._logging_enabled = True
447
+ self.logger.info(f"Plate logging enabled with server ID: {config.lpr_server_id}")
448
+ except Exception as e:
449
+ self.logger.error(f"Failed to initialize plate logger: {e}", exc_info=True)
450
+ self._logging_enabled = False
451
+
452
+ def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
453
+ stream_info: Optional[Dict[str, Any]]) -> None:
454
+ """Log all detected plates to RPC server with cooldown."""
455
+ if not self._logging_enabled or not self.plate_logger or not stream_info:
456
+ return
457
+
458
+ # Get current timestamp
459
+ current_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
460
+
461
+ # Collect all unique plates from current detections
462
+ plates_to_log = set()
463
+ for det in detections:
464
+ plate_text = det.get('plate_text')
465
+ if not plate_text:
466
+ continue
467
+ plates_to_log.add(plate_text)
468
+
469
+ # Log each unique plate (respecting cooldown)
470
+ if plates_to_log:
471
+ try:
472
+ # Run async logging tasks
473
+ loop = asyncio.new_event_loop()
474
+ asyncio.set_event_loop(loop)
475
+ try:
476
+ tasks = []
477
+ for plate_text in plates_to_log:
478
+ task = self.plate_logger.log_plate(
479
+ plate_text=plate_text,
480
+ timestamp=current_timestamp,
481
+ stream_info=stream_info,
482
+ cooldown=config.plate_log_cooldown
483
+ )
484
+ tasks.append(task)
485
+
486
+ # Run all logging tasks concurrently
487
+ loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
488
+ finally:
489
+ loop.close()
490
+ except Exception as e:
491
+ self.logger.error(f"Error during plate logging: {e}", exc_info=True)
173
492
 
174
493
  def process(self, data: Any, config: ConfigProtocol, input_bytes: Optional[bytes] = None,
175
494
  context: Optional[ProcessingContext] = None, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
@@ -185,11 +504,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
185
504
  if not input_bytes:
186
505
  return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
187
506
  usecase=self.name, category=self.category, context=context)
188
-
189
- print("--------------------------------------")
190
- print("config.alert_config",config.alert_config)
191
- print(config)
192
- print("--------------------------------------")
507
+
508
+ # Initialize plate logger if lpr_server_id is provided (optional flow)
509
+ if config.lpr_server_id and self._logging_enabled:
510
+ self._initialize_plate_logger(config)
193
511
 
194
512
  # Normalize alert_config if provided as a plain dict (JS JSON)
195
513
  if isinstance(getattr(config, 'alert_config', None), dict):
@@ -287,6 +605,9 @@ class LicensePlateMonitorUseCase(BaseProcessor):
287
605
  processed_data = self._update_detections_with_ocr(processed_data, ocr_analysis)
288
606
  self._update_plate_texts(processed_data)
289
607
 
608
+ # Step 9.5: Log detected plates to RPC (optional, only if lpr_server_id is provided)
609
+ self._log_detected_plates(processed_data, config, stream_info)
610
+
290
611
  # Step 10: Update frame counter
291
612
  self._total_frame_counter += 1
292
613
 
@@ -463,15 +784,17 @@ class LicensePlateMonitorUseCase(BaseProcessor):
463
784
 
464
785
  def _run_ocr(self, crop: np.ndarray) -> str:
465
786
  """Run OCR on a cropped plate image and return cleaned text or empty string."""
466
- # print("---------OCR CROP22",crop)
467
- # print("---------OCR CROP SIZE22",crop.size)
468
-
469
787
  if crop is None or crop.size == 0 or self.ocr_model is None:
470
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
+
471
794
  try:
472
- # fast_plate_ocr expects RGB
473
- #rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
795
+ # fast_plate_ocr LicensePlateRecognizer has a run() method
474
796
  res = self.ocr_model.run(crop)
797
+
475
798
  if isinstance(res, list):
476
799
  res = res[0] if res else ""
477
800
  cleaned_text = self._clean_text(str(res))
@@ -482,12 +805,14 @@ class LicensePlateMonitorUseCase(BaseProcessor):
482
805
  response = all(ch.isalpha() for ch in cleaned_text)
483
806
  elif self._ocr_mode == "alphanumeric":
484
807
  response = True
808
+ else:
809
+ response = False
485
810
 
486
811
  if response:
487
812
  return cleaned_text
488
- else:
489
- return ""
813
+ return ""
490
814
  except Exception as exc:
815
+ # Only log at debug level to avoid spam
491
816
  self.logger.warning(f"OCR failed: {exc}")
492
817
  return ""
493
818