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.
- matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
- matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
- matrice_analytics/post_processing/config.py +4 -0
- matrice_analytics/post_processing/core/config.py +115 -12
- matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
- matrice_analytics/post_processing/face_reg/embedding_manager.py +109 -8
- matrice_analytics/post_processing/face_reg/face_recognition.py +157 -61
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -88
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +67 -29
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
- matrice_analytics/post_processing/post_processor.py +32 -11
- matrice_analytics/post_processing/usecases/color/clip.py +42 -8
- matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
- matrice_analytics/post_processing/usecases/color_detection.py +50 -129
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
- matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
- matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +351 -26
- matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
- matrice_analytics/post_processing/utils/__init__.py +8 -8
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +61 -26
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Try to import LicensePlateRecognizer from local repo first, then installed package
|
|
40
|
+
_OCR_IMPORT_SOURCE = None
|
|
31
41
|
try:
|
|
32
|
-
from
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|