matrice-analytics 0.1.31__py3-none-any.whl → 0.1.33__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matrice-analytics might be problematic. Click here for more details.

@@ -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 and uploads"""
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
- # Store activity data
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
- upload_url = await self.face_client.store_people_activity(
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 upload_url:
203
- self.logger.info(f"Activity log stored successfully, upload URL received for employee_id={employee_id}")
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
- self.logger.warning(f"Failed to store activity log for employee_id={employee_id} - no upload URL returned")
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 upload_url
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 isinstance(use_case_class, FaceRecognitionEmbeddingUseCase):
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
- parsed_config = self._parse_config(config)
763
- else:
764
- parsed_config = self.post_processing_config
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
- detector = True
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
- # 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
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
- #self.detector = None
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("ERROR: Detector is None after initialization attempt!")
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 = config.detector.process_color_in_frame(
318
+ curr_frame_color = self.detector.process_color_in_frame(
321
319
  color_processed_data,
322
320
  input_bytes,
323
321
  config.zone_config,
@@ -29,6 +29,7 @@ import logging
29
29
  import asyncio
30
30
  import urllib
31
31
  import urllib.request
32
+ import base64
32
33
  # Get the major and minor version numbers
33
34
  major_version = sys.version_info.major
34
35
  minor_version = sys.version_info.minor
@@ -36,24 +37,22 @@ print(f"Python version: {major_version}.{minor_version}")
36
37
  os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
37
38
 
38
39
 
39
- _ENABLE_OCR_BOOTSTRAP = os.getenv("MATRICE_ENABLE_OCR_BOOTSTRAP", "0")
40
- if _ENABLE_OCR_BOOTSTRAP == "1":
41
- try:
42
- from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
43
- except:
44
- class LicensePlateRecognizer:
45
- """Stub fallback when fast_plate_ocr is not installed."""
46
- def __init__(self, *args, **kwargs):
47
- print("fast_plate_ocr from the local library py_inferenceis required for LicensePlateMonitorUseCase as platform is determined as python3.8 Jetson")
48
- else:
40
+ # Try to import LicensePlateRecognizer from local repo first, then installed package
41
+ _OCR_IMPORT_SOURCE = None
42
+ try:
43
+ from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
44
+ _OCR_IMPORT_SOURCE = "local_repo"
45
+ except ImportError:
49
46
  try:
50
47
  from fast_plate_ocr import LicensePlateRecognizer # type: ignore
51
- except: # pragma: no cover – optional dependency may be absent
52
- logging.error("fast_plate_ocr is required for LicensePlateMonitorUseCase but is not installed.")
48
+ _OCR_IMPORT_SOURCE = "installed_package"
49
+ except ImportError:
50
+ # Use stub class if neither import works
51
+ _OCR_IMPORT_SOURCE = "stub"
53
52
  class LicensePlateRecognizer: # type: ignore
54
- """Stub fallback when fast_plate_ocr is not installed."""
53
+ """Stub fallback when fast_plate_ocr is not available."""
55
54
  def __init__(self, *args, **kwargs):
56
- print("fast_plate_ocr is required for LicensePlateMonitorUseCase but is not installed.")
55
+ pass # Silent stub - error will be logged once during initialization
57
56
 
58
57
  # Internal utilities that are still required
59
58
  from ..ocr.preprocessing import ImagePreprocessor
@@ -124,23 +123,29 @@ class LicensePlateMonitorLogger:
124
123
 
125
124
  def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
126
125
  """Initialize session and fetch server connection info if lpr_server_id is provided."""
126
+ self.logger.info("Initializing LicensePlateMonitorLogger session...")
127
+
127
128
  # Use existing session if provided, otherwise create new one
128
129
  if self.session:
130
+ self.logger.info("Session already initialized, skipping initialization")
129
131
  return
130
132
  if config.session:
131
133
  self.session = config.session
134
+ self.logger.info("Using provided session from config")
132
135
  if not self.session:
133
136
  # Initialize Matrice session
134
137
  if not HAS_MATRICE_SESSION:
138
+ self.logger.error("Matrice session module not available")
135
139
  raise ImportError("Matrice session is required for License Plate Monitoring")
136
140
  try:
141
+ self.logger.info("Creating new Matrice session...")
137
142
  self.session = Session(
138
143
  account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
139
144
  access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
140
145
  secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
141
146
  project_id=os.getenv("MATRICE_PROJECT_ID", ""),
142
147
  )
143
- self.logger.info("Initialized new Matrice session for License Plate Monitoring")
148
+ self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
144
149
  except Exception as e:
145
150
  self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
146
151
  raise
@@ -148,6 +153,7 @@ class LicensePlateMonitorLogger:
148
153
  # Fetch server connection info if lpr_server_id is provided
149
154
  if config.lpr_server_id:
150
155
  self.lpr_server_id = config.lpr_server_id
156
+ self.logger.info(f"Fetching LPR server connection info for server ID: {self.lpr_server_id}")
151
157
  try:
152
158
  self.server_info = self.get_server_connection_info()
153
159
  if self.server_info:
@@ -158,23 +164,26 @@ class LicensePlateMonitorLogger:
158
164
 
159
165
  if server_host == self.public_ip:
160
166
  self.server_base_url = f"http://localhost:{server_port}"
161
- self.logger.warning(f"Server host matches public IP, using localhost: {self.server_base_url}")
167
+ self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
162
168
  else:
163
169
  self.server_base_url = f"https://{server_host}:{server_port}"
164
- self.logger.warning(f"LPR server base URL: {self.server_base_url}")
170
+ self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
165
171
 
166
172
  self.session.update(self.server_info.get('projectID', ''))
167
173
  self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
168
174
  else:
169
- self.logger.warning("Failed to fetch LPR server connection info")
175
+ self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
170
176
  except Exception as e:
171
177
  self.logger.error(f"Error fetching LPR server connection info: {e}", exc_info=True)
178
+ else:
179
+ self.logger.info("No lpr_server_id provided in config, skipping server connection info fetch")
172
180
 
173
181
  def _get_public_ip(self) -> str:
174
182
  """Get the public IP address of this machine."""
183
+ self.logger.info("Fetching public IP address...")
175
184
  try:
176
185
  public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
177
- self.logger.warning(f"Successfully fetched external IP: {public_ip}")
186
+ self.logger.info(f"Successfully fetched external IP: {public_ip}")
178
187
  return public_ip
179
188
  except Exception as e:
180
189
  self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
@@ -183,10 +192,15 @@ class LicensePlateMonitorLogger:
183
192
  def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
184
193
  """Fetch server connection info from RPC."""
185
194
  if not self.lpr_server_id:
195
+ self.logger.warning("No lpr_server_id set, cannot fetch server connection info")
186
196
  return None
187
197
 
188
198
  try:
189
- response = self.session.rpc.get(f"/v1/actions/lpr_servers/{self.lpr_server_id}")
199
+ endpoint = f"/v1/actions/lpr_servers/{self.lpr_server_id}"
200
+ self.logger.info(f"Sending GET request to: {endpoint}")
201
+ response = self.session.rpc.get(endpoint)
202
+ self.logger.info(f"Received response: success={response.get('success')}, code={response.get('code')}, message={response.get('message')}")
203
+
190
204
  if response.get("success", False) and response.get("code") == 200:
191
205
  # Response format:
192
206
  # {'success': True,
@@ -202,7 +216,9 @@ class LicensePlateMonitorLogger:
202
216
  # 'projectID': '68ca6372ab79ba13ef699ba6',
203
217
  # 'region': 'United States',
204
218
  # 'isShared': False}}
205
- return response.get("data", {})
219
+ data = response.get("data", {})
220
+ self.logger.info(f"Server connection info retrieved: name={data.get('name')}, host={data.get('host')}, port={data.get('port')}, status={data.get('status')}")
221
+ return data
206
222
  else:
207
223
  self.logger.warning(f"Failed to fetch server info: {response.get('message', 'Unknown error')}")
208
224
  return None
@@ -214,20 +230,78 @@ class LicensePlateMonitorLogger:
214
230
  """Check if enough time has passed since last log for this plate."""
215
231
  current_time = time.time()
216
232
  last_log_time = self.plate_log_timestamps.get(plate_text, 0)
233
+ time_since_last_log = current_time - last_log_time
217
234
 
218
- if current_time - last_log_time >= cooldown:
235
+ if time_since_last_log >= cooldown:
236
+ self.logger.debug(f"Plate {plate_text} ready to log (last logged {time_since_last_log:.1f}s ago, cooldown={cooldown}s)")
219
237
  return True
220
- return False
238
+ else:
239
+ self.logger.debug(f"Plate {plate_text} in cooldown period ({time_since_last_log:.1f}s elapsed, {cooldown - time_since_last_log:.1f}s remaining)")
240
+ return False
221
241
 
222
242
  def update_log_timestamp(self, plate_text: str) -> None:
223
243
  """Update the last log timestamp for a plate."""
224
244
  self.plate_log_timestamps[plate_text] = time.time()
245
+ self.logger.debug(f"Updated log timestamp for plate: {plate_text}")
246
+
247
+ def _format_timestamp_rfc3339(self, timestamp: str) -> str:
248
+ """Convert timestamp to RFC3339 format (2006-01-02T15:04:05Z).
249
+
250
+ Handles various input formats:
251
+ - "YYYY-MM-DD-HH:MM:SS.ffffff UTC"
252
+ - "YYYY:MM:DD HH:MM:SS"
253
+ - Unix timestamp (float/int)
254
+ """
255
+ try:
256
+ # If already in RFC3339 format, return as is
257
+ if 'T' in timestamp and timestamp.endswith('Z'):
258
+ return timestamp
259
+
260
+ # Try to parse common formats
261
+ dt = None
262
+
263
+ # Format: "2025-08-19-04:22:47.187574 UTC"
264
+ if '-' in timestamp and 'UTC' in timestamp:
265
+ timestamp_clean = timestamp.replace(' UTC', '')
266
+ dt = datetime.strptime(timestamp_clean, '%Y-%m-%d-%H:%M:%S.%f')
267
+ # Format: "2025:10:23 14:30:45"
268
+ elif ':' in timestamp and ' ' in timestamp:
269
+ dt = datetime.strptime(timestamp, '%Y:%m:%d %H:%M:%S')
270
+ # Format: numeric timestamp
271
+ elif timestamp.replace('.', '').isdigit():
272
+ dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
273
+
274
+ if dt is None:
275
+ # Fallback to current time
276
+ dt = datetime.now(timezone.utc)
277
+ else:
278
+ # Ensure timezone is UTC
279
+ if dt.tzinfo is None:
280
+ dt = dt.replace(tzinfo=timezone.utc)
281
+
282
+ # Format to RFC3339: 2006-01-02T15:04:05Z
283
+ return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
284
+
285
+ except Exception as e:
286
+ self.logger.warning(f"Failed to parse timestamp '{timestamp}': {e}. Using current time.")
287
+ return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
225
288
 
226
- async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any], cooldown: float = 30.0) -> bool:
227
- """Log plate to RPC server with cooldown period."""
289
+ async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any],
290
+ image_data: Optional[str] = None, cooldown: float = 30.0) -> bool:
291
+ """Log plate to RPC server with cooldown period.
292
+
293
+ Args:
294
+ plate_text: The license plate text
295
+ timestamp: Capture timestamp
296
+ stream_info: Stream information dict
297
+ image_data: Base64-encoded JPEG image of the license plate crop
298
+ cooldown: Cooldown period in seconds
299
+ """
300
+ self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
301
+
228
302
  # Check cooldown
229
303
  if not self.should_log_plate(plate_text, cooldown):
230
- self.logger.debug(f"Plate {plate_text} skipped due to cooldown period")
304
+ self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
231
305
  return False
232
306
 
233
307
  try:
@@ -236,23 +310,37 @@ class LicensePlateMonitorLogger:
236
310
  location = camera_info.get("location", "")
237
311
  frame_id = stream_info.get("frame_id", "")
238
312
 
313
+ # Get project ID from server_info
314
+ project_id = self.server_info.get('projectID', '') if self.server_info else ''
315
+
316
+ # Format timestamp to RFC3339 format (2006-01-02T15:04:05Z)
317
+ rfc3339_timestamp = self._format_timestamp_rfc3339(timestamp)
318
+
239
319
  payload = {
240
320
  'licensePlate': plate_text,
241
321
  'frameId': frame_id,
242
322
  'location': location,
243
323
  'camera': camera_name,
244
- 'captureTimestamp': timestamp
324
+ 'captureTimestamp': rfc3339_timestamp,
325
+ 'projectId': project_id,
326
+ 'imageData': image_data if image_data else ""
245
327
  }
246
328
 
247
- await self.session.rpc.post_async('/v1/lpr-server/detections', payload=payload, base_url=self.server_base_url)
329
+ # Add projectId as query parameter
330
+ endpoint = f'/v1/lpr-server/detections?projectId={project_id}'
331
+ self.logger.info(f"Sending POST request to {self.server_base_url}{endpoint} with plate: {plate_text}, imageData length: {len(image_data) if image_data else 0}")
332
+
333
+ response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
334
+
335
+ self.logger.info(f"API Response received for plate {plate_text}: {response}")
248
336
 
249
337
  # Update timestamp after successful log
250
338
  self.update_log_timestamp(plate_text)
251
- self.logger.info(f"Successfully logged plate: {plate_text} at {timestamp}")
339
+ self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
252
340
  return True
253
341
 
254
342
  except Exception as e:
255
- self.logger.error(f"Failed to log plate {plate_text}: {e}", exc_info=True)
343
+ self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
256
344
  return False
257
345
 
258
346
  class LicensePlateMonitorUseCase(BaseProcessor):
@@ -296,11 +384,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
296
384
  self.image_preprocessor = ImagePreprocessor()
297
385
  # Fast OCR model (shared across instances)
298
386
  if LicensePlateMonitorUseCase._ocr_model is None:
299
- try:
387
+ if _OCR_IMPORT_SOURCE == "stub":
388
+ # Using stub - log warning once
389
+ self.logger.error("OCR module not available. LicensePlateRecognizer will not function. Install: pip install fast-plate-ocr[onnx]")
300
390
  LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
301
- self.logger.info("LicensePlateRecognizer loaded successfully")
302
- except Exception as e:
303
- self.logger.warning(f"Failed to initialise LicensePlateRecognizer: {e}")
391
+ else:
392
+ # Try to load real OCR model
393
+ try:
394
+ LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
395
+ source_msg = "from local repo" if _OCR_IMPORT_SOURCE == "local_repo" else "from installed package"
396
+ self.logger.info(f"LicensePlateRecognizer loaded successfully {source_msg}")
397
+ except Exception as e:
398
+ self.logger.error(f"Failed to initialize LicensePlateRecognizer: {e}", exc_info=True)
399
+ LicensePlateMonitorUseCase._ocr_model = None
304
400
  self.ocr_model = LicensePlateMonitorUseCase._ocr_model
305
401
  # OCR text history for stability checks (text → consecutive frame count)
306
402
  self._text_history: Dict[str, int] = {}
@@ -365,7 +461,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
365
461
  self._logging_enabled = False
366
462
 
367
463
  def _log_detected_plates(self, detections: List[Dict[str, Any]], config: LicensePlateMonitorConfig,
368
- stream_info: Optional[Dict[str, Any]]) -> None:
464
+ stream_info: Optional[Dict[str, Any]], image_bytes: Optional[bytes] = None) -> None:
369
465
  """Log all detected plates to RPC server with cooldown."""
370
466
  if not self._logging_enabled or not self.plate_logger or not stream_info:
371
467
  return
@@ -373,6 +469,24 @@ class LicensePlateMonitorUseCase(BaseProcessor):
373
469
  # Get current timestamp
374
470
  current_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
375
471
 
472
+ # Encode the full frame image as base64 JPEG
473
+ image_data = ""
474
+ if image_bytes:
475
+ try:
476
+ # Decode image bytes
477
+ image_array = np.frombuffer(image_bytes, np.uint8)
478
+ image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
479
+
480
+ if image is not None:
481
+ # Encode as JPEG with 85% quality
482
+ success, jpeg_buffer = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, 99])
483
+ if success:
484
+ # Convert to base64
485
+ image_data = base64.b64encode(jpeg_buffer.tobytes()).decode('utf-8')
486
+ self.logger.debug(f"Encoded frame image as base64, length: {len(image_data)}")
487
+ except Exception as e:
488
+ self.logger.error(f"Failed to encode frame image: {e}")
489
+
376
490
  # Collect all unique plates from current detections
377
491
  plates_to_log = set()
378
492
  for det in detections:
@@ -394,6 +508,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
394
508
  plate_text=plate_text,
395
509
  timestamp=current_timestamp,
396
510
  stream_info=stream_info,
511
+ image_data=image_data,
397
512
  cooldown=config.plate_log_cooldown
398
513
  )
399
514
  tasks.append(task)
@@ -419,11 +534,6 @@ class LicensePlateMonitorUseCase(BaseProcessor):
419
534
  if not input_bytes:
420
535
  return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
421
536
  usecase=self.name, category=self.category, context=context)
422
-
423
- print("--------------------------------------")
424
- print("config.alert_config",config.alert_config)
425
- print(config)
426
- print("--------------------------------------")
427
537
 
428
538
  # Initialize plate logger if lpr_server_id is provided (optional flow)
429
539
  if config.lpr_server_id and self._logging_enabled:
@@ -526,7 +636,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
526
636
  self._update_plate_texts(processed_data)
527
637
 
528
638
  # Step 9.5: Log detected plates to RPC (optional, only if lpr_server_id is provided)
529
- self._log_detected_plates(processed_data, config, stream_info)
639
+ self._log_detected_plates(processed_data, config, stream_info, input_bytes)
530
640
 
531
641
  # Step 10: Update frame counter
532
642
  self._total_frame_counter += 1
@@ -704,15 +814,17 @@ class LicensePlateMonitorUseCase(BaseProcessor):
704
814
 
705
815
  def _run_ocr(self, crop: np.ndarray) -> str:
706
816
  """Run OCR on a cropped plate image and return cleaned text or empty string."""
707
- # print("---------OCR CROP22",crop)
708
- # print("---------OCR CROP SIZE22",crop.size)
709
-
710
817
  if crop is None or crop.size == 0 or self.ocr_model is None:
711
818
  return ""
819
+
820
+ # Check if we have a valid OCR model (not the stub) - silently return empty if stub
821
+ if not hasattr(self.ocr_model, 'run'):
822
+ return ""
823
+
712
824
  try:
713
- # fast_plate_ocr expects RGB
714
- #rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
825
+ # fast_plate_ocr LicensePlateRecognizer has a run() method
715
826
  res = self.ocr_model.run(crop)
827
+
716
828
  if isinstance(res, list):
717
829
  res = res[0] if res else ""
718
830
  cleaned_text = self._clean_text(str(res))
@@ -723,12 +835,14 @@ class LicensePlateMonitorUseCase(BaseProcessor):
723
835
  response = all(ch.isalpha() for ch in cleaned_text)
724
836
  elif self._ocr_mode == "alphanumeric":
725
837
  response = True
838
+ else:
839
+ response = False
726
840
 
727
841
  if response:
728
842
  return cleaned_text
729
- else:
730
- return ""
843
+ return ""
731
844
  except Exception as exc:
845
+ # Only log at debug level to avoid spam
732
846
  self.logger.warning(f"OCR failed: {exc}")
733
847
  return ""
734
848
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_analytics
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT