matrice-analytics 0.1.2__py3-none-any.whl → 0.1.31__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/face_reg/compare_similarity.py +5 -5
- matrice_analytics/post_processing/face_reg/embedding_manager.py +14 -7
- matrice_analytics/post_processing/face_reg/face_recognition.py +123 -34
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +332 -82
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +29 -22
- 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 +19 -5
- matrice_analytics/post_processing/usecases/color/clip.py +292 -132
- matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
- matrice_analytics/post_processing/usecases/color_detection.py +429 -355
- 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 +252 -11
- 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.2.dist-info → matrice_analytics-0.1.31.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/RECORD +59 -24
- {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Optional
|
|
2
1
|
from dataclasses import asdict
|
|
3
2
|
import time
|
|
4
3
|
from datetime import datetime, timezone
|
|
5
4
|
import copy # Added for deep copying detections to preserve original masks
|
|
6
|
-
|
|
7
|
-
from ..core.base import
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from ..core.base import (
|
|
7
|
+
BaseProcessor,
|
|
8
|
+
ProcessingContext,
|
|
9
|
+
ProcessingResult,
|
|
10
|
+
ConfigProtocol,
|
|
11
|
+
ResultFormat,
|
|
12
|
+
)
|
|
8
13
|
from ..utils import (
|
|
9
14
|
filter_by_confidence,
|
|
10
15
|
filter_by_categories,
|
|
@@ -15,7 +20,7 @@ from ..utils import (
|
|
|
15
20
|
match_results_structure,
|
|
16
21
|
bbox_smoothing,
|
|
17
22
|
BBoxSmoothingConfig,
|
|
18
|
-
BBoxSmoothingTracker
|
|
23
|
+
BBoxSmoothingTracker,
|
|
19
24
|
)
|
|
20
25
|
from dataclasses import dataclass, field
|
|
21
26
|
from ..core.config import BaseConfig, AlertConfig, ZoneConfig
|
|
@@ -24,6 +29,7 @@ from ..core.config import BaseConfig, AlertConfig, ZoneConfig
|
|
|
24
29
|
@dataclass
|
|
25
30
|
class LicensePlateConfig(BaseConfig):
|
|
26
31
|
"""Configuration for License plate detection use case in License plate monitoring."""
|
|
32
|
+
|
|
27
33
|
# Smoothing configuration
|
|
28
34
|
enable_smoothing: bool = True
|
|
29
35
|
smoothing_algorithm: str = "observability" # "window" or "observability"
|
|
@@ -31,16 +37,12 @@ class LicensePlateConfig(BaseConfig):
|
|
|
31
37
|
smoothing_cooldown_frames: int = 5
|
|
32
38
|
smoothing_confidence_range_factor: float = 0.5
|
|
33
39
|
|
|
34
|
-
#confidence thresholds
|
|
40
|
+
# confidence thresholds
|
|
35
41
|
confidence_threshold: float = 0.6
|
|
36
42
|
|
|
37
|
-
usecase_categories: List[str] = field(
|
|
38
|
-
default_factory=lambda: ['license_plate']
|
|
39
|
-
)
|
|
43
|
+
usecase_categories: List[str] = field(default_factory=lambda: ["license_plate"])
|
|
40
44
|
|
|
41
|
-
target_categories: List[str] = field(
|
|
42
|
-
default_factory=lambda: ['license_plate']
|
|
43
|
-
)
|
|
45
|
+
target_categories: List[str] = field(default_factory=lambda: ["license_plate"])
|
|
44
46
|
|
|
45
47
|
alert_config: Optional[AlertConfig] = None
|
|
46
48
|
|
|
@@ -57,16 +59,16 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
57
59
|
CATEGORY_DISPLAY = {
|
|
58
60
|
"license_plate": "license_plate",
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
def __init__(self):
|
|
62
64
|
super().__init__("license_plate_detection")
|
|
63
65
|
self.category = "license_plate"
|
|
64
66
|
|
|
65
67
|
# List of categories to track
|
|
66
|
-
self.target_categories = [
|
|
68
|
+
self.target_categories = ["license_plate"]
|
|
67
69
|
|
|
68
|
-
self.CASE_TYPE: Optional[str] =
|
|
69
|
-
self.CASE_VERSION: Optional[str] =
|
|
70
|
+
self.CASE_TYPE: Optional[str] = "license_plate_detection"
|
|
71
|
+
self.CASE_VERSION: Optional[str] = "1.3"
|
|
70
72
|
|
|
71
73
|
# Initialize smoothing tracker
|
|
72
74
|
self.smoothing_tracker = None
|
|
@@ -97,8 +99,13 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
97
99
|
self._ascending_alert_list: List[int] = []
|
|
98
100
|
self.current_incident_end_timestamp: str = "N/A"
|
|
99
101
|
|
|
100
|
-
def process(
|
|
101
|
-
|
|
102
|
+
def process(
|
|
103
|
+
self,
|
|
104
|
+
data: Any,
|
|
105
|
+
config: ConfigProtocol,
|
|
106
|
+
context: Optional[ProcessingContext] = None,
|
|
107
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
108
|
+
) -> ProcessingResult:
|
|
102
109
|
"""
|
|
103
110
|
Main entry point for post-processing.
|
|
104
111
|
Applies category mapping, smoothing, counting, alerting, and summary generation.
|
|
@@ -107,8 +114,12 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
107
114
|
start_time = time.time()
|
|
108
115
|
# Ensure config is correct type
|
|
109
116
|
if not isinstance(config, LicensePlateConfig):
|
|
110
|
-
return self.create_error_result(
|
|
111
|
-
|
|
117
|
+
return self.create_error_result(
|
|
118
|
+
"Invalid config type",
|
|
119
|
+
usecase=self.name,
|
|
120
|
+
category=self.category,
|
|
121
|
+
context=context,
|
|
122
|
+
)
|
|
112
123
|
if context is None:
|
|
113
124
|
context = ProcessingContext()
|
|
114
125
|
|
|
@@ -116,21 +127,27 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
116
127
|
input_format = match_results_structure(data)
|
|
117
128
|
context.input_format = input_format
|
|
118
129
|
context.confidence_threshold = config.confidence_threshold
|
|
119
|
-
|
|
130
|
+
|
|
120
131
|
# Step 1: Confidence filtering
|
|
121
132
|
if config.confidence_threshold is not None:
|
|
122
133
|
processed_data = filter_by_confidence(data, config.confidence_threshold)
|
|
123
134
|
else:
|
|
124
135
|
processed_data = data
|
|
125
|
-
self.logger.debug(
|
|
136
|
+
self.logger.debug(
|
|
137
|
+
f"Did not apply confidence filtering with threshold since nothing was provided"
|
|
138
|
+
)
|
|
126
139
|
|
|
127
140
|
# Step 2: Apply category mapping if provided
|
|
128
141
|
if config.index_to_category:
|
|
129
|
-
processed_data = apply_category_mapping(
|
|
142
|
+
processed_data = apply_category_mapping(
|
|
143
|
+
processed_data, config.index_to_category
|
|
144
|
+
)
|
|
130
145
|
|
|
131
146
|
# Step 3: Category filtering
|
|
132
147
|
if config.target_categories:
|
|
133
|
-
processed_data = [
|
|
148
|
+
processed_data = [
|
|
149
|
+
d for d in processed_data if d.get("category") in self.target_categories
|
|
150
|
+
]
|
|
134
151
|
|
|
135
152
|
# Step 4: Apply bbox smoothing if enabled
|
|
136
153
|
# Deep-copy detections so that we preserve the original masks before any
|
|
@@ -144,18 +161,20 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
144
161
|
cooldown_frames=config.smoothing_cooldown_frames,
|
|
145
162
|
confidence_threshold=config.confidence_threshold,
|
|
146
163
|
confidence_range_factor=config.smoothing_confidence_range_factor,
|
|
147
|
-
enable_smoothing=True
|
|
164
|
+
enable_smoothing=True,
|
|
148
165
|
)
|
|
149
166
|
self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
|
|
150
|
-
|
|
151
|
-
processed_data = bbox_smoothing(
|
|
167
|
+
|
|
168
|
+
processed_data = bbox_smoothing(
|
|
169
|
+
processed_data, self.smoothing_tracker.config, self.smoothing_tracker
|
|
170
|
+
)
|
|
152
171
|
# Restore masks after smoothing
|
|
153
172
|
|
|
154
173
|
# Step 5: Advanced tracking (BYTETracker-like)
|
|
155
174
|
try:
|
|
156
175
|
from ..advanced_tracker import AdvancedTracker
|
|
157
176
|
from ..advanced_tracker.config import TrackerConfig
|
|
158
|
-
|
|
177
|
+
|
|
159
178
|
if self.tracker is None:
|
|
160
179
|
# Configure tracker thresholds based on the use-case confidence threshold so that
|
|
161
180
|
# low-confidence detections (e.g. < 0.7) can still be initialised as tracks when
|
|
@@ -164,8 +183,10 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
164
183
|
tracker_config = TrackerConfig(
|
|
165
184
|
track_high_thresh=float(config.confidence_threshold),
|
|
166
185
|
# Allow even lower detections to participate in secondary association
|
|
167
|
-
track_low_thresh=max(
|
|
168
|
-
|
|
186
|
+
track_low_thresh=max(
|
|
187
|
+
0.05, float(config.confidence_threshold) / 2
|
|
188
|
+
),
|
|
189
|
+
new_track_thresh=float(config.confidence_threshold),
|
|
169
190
|
)
|
|
170
191
|
else:
|
|
171
192
|
tracker_config = TrackerConfig()
|
|
@@ -193,7 +214,9 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
193
214
|
# processed detection back to the raw detection with the highest IoU
|
|
194
215
|
# and copy over its "masks" field (if available).
|
|
195
216
|
# ------------------------------------------------------------------ #
|
|
196
|
-
processed_data = self._attach_masks_to_detections(
|
|
217
|
+
processed_data = self._attach_masks_to_detections(
|
|
218
|
+
processed_data, raw_processed_data
|
|
219
|
+
)
|
|
197
220
|
|
|
198
221
|
# Update frame counter
|
|
199
222
|
self._total_frame_counter += 1
|
|
@@ -205,7 +228,11 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
205
228
|
start_frame = input_settings.get("start_frame")
|
|
206
229
|
end_frame = input_settings.get("end_frame")
|
|
207
230
|
# If start and end frame are the same, it's a single frame
|
|
208
|
-
if
|
|
231
|
+
if (
|
|
232
|
+
start_frame is not None
|
|
233
|
+
and end_frame is not None
|
|
234
|
+
and start_frame == end_frame
|
|
235
|
+
):
|
|
209
236
|
frame_number = start_frame
|
|
210
237
|
|
|
211
238
|
# Compute summaries and alerts
|
|
@@ -213,31 +240,45 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
213
240
|
counting_summary = self._count_categories(processed_data, config)
|
|
214
241
|
# Add total unique counts after tracking using only local state
|
|
215
242
|
total_counts = self.get_total_counts()
|
|
216
|
-
counting_summary[
|
|
243
|
+
counting_summary["total_counts"] = total_counts
|
|
217
244
|
|
|
218
245
|
alerts = self._check_alerts(counting_summary, frame_number, config)
|
|
219
246
|
predictions = self._extract_predictions(processed_data)
|
|
220
|
-
|
|
247
|
+
|
|
221
248
|
# Step: Generate structured events and tracking stats with frame-based keys
|
|
222
|
-
incidents_list = self._generate_incidents(
|
|
223
|
-
|
|
249
|
+
incidents_list = self._generate_incidents(
|
|
250
|
+
counting_summary, alerts, config, frame_number, stream_info
|
|
251
|
+
)
|
|
252
|
+
tracking_stats_list = self._generate_tracking_stats(
|
|
253
|
+
counting_summary, alerts, config, frame_number, stream_info
|
|
254
|
+
)
|
|
224
255
|
# business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, frame_number, stream_info, is_empty=False)
|
|
225
256
|
business_analytics_list = []
|
|
226
|
-
summary_list = self._generate_summary(
|
|
257
|
+
summary_list = self._generate_summary(
|
|
258
|
+
counting_summary,
|
|
259
|
+
incidents_list,
|
|
260
|
+
tracking_stats_list,
|
|
261
|
+
business_analytics_list,
|
|
262
|
+
alerts,
|
|
263
|
+
)
|
|
227
264
|
|
|
228
265
|
# Extract frame-based dictionaries from the lists
|
|
229
266
|
incidents = incidents_list[0] if incidents_list else {}
|
|
230
267
|
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
231
|
-
business_analytics =
|
|
268
|
+
business_analytics = (
|
|
269
|
+
business_analytics_list[0] if business_analytics_list else {}
|
|
270
|
+
)
|
|
232
271
|
summary = summary_list[0] if summary_list else {}
|
|
233
|
-
agg_summary = {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
272
|
+
agg_summary = {
|
|
273
|
+
str(frame_number): {
|
|
274
|
+
"incidents": incidents,
|
|
275
|
+
"tracking_stats": tracking_stats,
|
|
276
|
+
"business_analytics": business_analytics,
|
|
277
|
+
"alerts": alerts,
|
|
278
|
+
"human_text": summary,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
241
282
|
context.mark_completed()
|
|
242
283
|
|
|
243
284
|
# Build result object following the new pattern
|
|
@@ -246,20 +287,23 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
246
287
|
data={"agg_summary": agg_summary},
|
|
247
288
|
usecase=self.name,
|
|
248
289
|
category=self.category,
|
|
249
|
-
context=context
|
|
290
|
+
context=context,
|
|
250
291
|
)
|
|
251
|
-
|
|
292
|
+
|
|
252
293
|
return result
|
|
253
294
|
|
|
254
|
-
def _check_alerts(
|
|
295
|
+
def _check_alerts(
|
|
296
|
+
self, summary: dict, frame_number: Any, config: LicensePlateConfig
|
|
297
|
+
) -> List[Dict]:
|
|
255
298
|
"""
|
|
256
299
|
Check if any alert thresholds are exceeded and return alert dicts.
|
|
257
300
|
"""
|
|
301
|
+
|
|
258
302
|
def get_trend(data, lookback=900, threshold=0.6):
|
|
259
|
-
|
|
303
|
+
"""
|
|
260
304
|
Determine if the trend is ascending or descending based on actual value progression.
|
|
261
305
|
Now works with values 0,1,2,3 (not just binary).
|
|
262
|
-
|
|
306
|
+
"""
|
|
263
307
|
window = data[-lookback:] if len(data) >= lookback else data
|
|
264
308
|
if len(window) < 2:
|
|
265
309
|
return True # not enough data to determine trend
|
|
@@ -277,75 +321,166 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
277
321
|
|
|
278
322
|
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
279
323
|
alerts = []
|
|
280
|
-
total_detections = summary.get(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
324
|
+
total_detections = summary.get(
|
|
325
|
+
"total_count", 0
|
|
326
|
+
) # CURRENT combined total count of all classes
|
|
327
|
+
total_counts_dict = summary.get(
|
|
328
|
+
"total_counts", {}
|
|
329
|
+
) # TOTAL cumulative counts per class
|
|
330
|
+
cumulative_total = (
|
|
331
|
+
sum(total_counts_dict.values()) if total_counts_dict else 0
|
|
332
|
+
) # TOTAL combined cumulative count
|
|
333
|
+
per_category_count = summary.get(
|
|
334
|
+
"per_category_count", {}
|
|
335
|
+
) # CURRENT count per class
|
|
284
336
|
|
|
285
337
|
if not config.alert_config:
|
|
286
338
|
return alerts
|
|
287
339
|
|
|
288
340
|
total = summary.get("total_count", 0)
|
|
289
|
-
#self._ascending_alert_list
|
|
290
|
-
if
|
|
341
|
+
# self._ascending_alert_list
|
|
342
|
+
if (
|
|
343
|
+
hasattr(config.alert_config, "count_thresholds")
|
|
344
|
+
and config.alert_config.count_thresholds
|
|
345
|
+
):
|
|
291
346
|
|
|
292
347
|
for category, threshold in config.alert_config.count_thresholds.items():
|
|
293
|
-
if category == "all" and total > threshold:
|
|
294
|
-
|
|
295
|
-
alerts.append(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
if category == "all" and total > threshold:
|
|
349
|
+
|
|
350
|
+
alerts.append(
|
|
351
|
+
{
|
|
352
|
+
"alert_type": (
|
|
353
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
354
|
+
if hasattr(config.alert_config, "alert_type")
|
|
355
|
+
else ["Default"]
|
|
356
|
+
),
|
|
357
|
+
"alert_id": "alert_" + category + "_" + frame_key,
|
|
358
|
+
"incident_category": self.CASE_TYPE,
|
|
359
|
+
"threshold_level": threshold,
|
|
360
|
+
"ascending": get_trend(
|
|
361
|
+
self._ascending_alert_list, lookback=900, threshold=0.8
|
|
362
|
+
),
|
|
363
|
+
"settings": {
|
|
364
|
+
t: v
|
|
365
|
+
for t, v in zip(
|
|
366
|
+
(
|
|
367
|
+
getattr(
|
|
368
|
+
config.alert_config,
|
|
369
|
+
"alert_type",
|
|
370
|
+
["Default"],
|
|
371
|
+
)
|
|
372
|
+
if hasattr(config.alert_config, "alert_type")
|
|
373
|
+
else ["Default"]
|
|
374
|
+
),
|
|
375
|
+
(
|
|
376
|
+
getattr(
|
|
377
|
+
config.alert_config, "alert_value", ["JSON"]
|
|
378
|
+
)
|
|
379
|
+
if hasattr(config.alert_config, "alert_value")
|
|
380
|
+
else ["JSON"]
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
)
|
|
305
386
|
elif category in summary.get("per_category_count", {}):
|
|
306
387
|
count = summary.get("per_category_count", {})[category]
|
|
307
388
|
if count > threshold: # Fixed logic: alert when EXCEEDING threshold
|
|
308
|
-
alerts.append(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
389
|
+
alerts.append(
|
|
390
|
+
{
|
|
391
|
+
"alert_type": (
|
|
392
|
+
getattr(
|
|
393
|
+
config.alert_config, "alert_type", ["Default"]
|
|
394
|
+
)
|
|
395
|
+
if hasattr(config.alert_config, "alert_type")
|
|
396
|
+
else ["Default"]
|
|
397
|
+
),
|
|
398
|
+
"alert_id": "alert_" + category + "_" + frame_key,
|
|
399
|
+
"incident_category": self.CASE_TYPE,
|
|
400
|
+
"threshold_level": threshold,
|
|
401
|
+
"ascending": get_trend(
|
|
402
|
+
self._ascending_alert_list,
|
|
403
|
+
lookback=900,
|
|
404
|
+
threshold=0.8,
|
|
405
|
+
),
|
|
406
|
+
"settings": {
|
|
407
|
+
t: v
|
|
408
|
+
for t, v in zip(
|
|
409
|
+
(
|
|
410
|
+
getattr(
|
|
411
|
+
config.alert_config,
|
|
412
|
+
"alert_type",
|
|
413
|
+
["Default"],
|
|
414
|
+
)
|
|
415
|
+
if hasattr(
|
|
416
|
+
config.alert_config, "alert_type"
|
|
417
|
+
)
|
|
418
|
+
else ["Default"]
|
|
419
|
+
),
|
|
420
|
+
(
|
|
421
|
+
getattr(
|
|
422
|
+
config.alert_config,
|
|
423
|
+
"alert_value",
|
|
424
|
+
["JSON"],
|
|
425
|
+
)
|
|
426
|
+
if hasattr(
|
|
427
|
+
config.alert_config, "alert_value"
|
|
428
|
+
)
|
|
429
|
+
else ["JSON"]
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
)
|
|
318
435
|
else:
|
|
319
436
|
pass
|
|
320
437
|
return alerts
|
|
321
438
|
|
|
322
|
-
def _generate_incidents(
|
|
323
|
-
|
|
324
|
-
Dict
|
|
439
|
+
def _generate_incidents(
|
|
440
|
+
self,
|
|
441
|
+
counting_summary: Dict,
|
|
442
|
+
alerts: List,
|
|
443
|
+
config: LicensePlateConfig,
|
|
444
|
+
frame_number: Optional[int] = None,
|
|
445
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
446
|
+
) -> List[Dict]:
|
|
325
447
|
"""Generate structured events for the output format with frame-based keys."""
|
|
326
448
|
|
|
327
449
|
# Use frame number as key, fallback to 'current_frame' if not available
|
|
328
450
|
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
329
|
-
incidents=[]
|
|
451
|
+
incidents = []
|
|
330
452
|
total_detections = counting_summary.get("total_count", 0)
|
|
331
453
|
current_timestamp = self._get_current_timestamp_str(stream_info)
|
|
332
454
|
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
333
|
-
|
|
334
|
-
self._ascending_alert_list =
|
|
455
|
+
|
|
456
|
+
self._ascending_alert_list = (
|
|
457
|
+
self._ascending_alert_list[-900:]
|
|
458
|
+
if len(self._ascending_alert_list) > 900
|
|
459
|
+
else self._ascending_alert_list
|
|
460
|
+
)
|
|
335
461
|
|
|
336
462
|
if total_detections > 0:
|
|
337
463
|
# Determine event level based on thresholds
|
|
338
464
|
level = "low"
|
|
339
465
|
intensity = 5.0
|
|
340
466
|
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
341
|
-
if start_timestamp and self.current_incident_end_timestamp==
|
|
342
|
-
self.current_incident_end_timestamp =
|
|
343
|
-
elif
|
|
344
|
-
|
|
467
|
+
if start_timestamp and self.current_incident_end_timestamp == "N/A":
|
|
468
|
+
self.current_incident_end_timestamp = "Incident still active"
|
|
469
|
+
elif (
|
|
470
|
+
start_timestamp
|
|
471
|
+
and self.current_incident_end_timestamp == "Incident still active"
|
|
472
|
+
):
|
|
473
|
+
if (
|
|
474
|
+
len(self._ascending_alert_list) >= 15
|
|
475
|
+
and sum(self._ascending_alert_list[-15:]) / 15 < 1.5
|
|
476
|
+
):
|
|
345
477
|
self.current_incident_end_timestamp = current_timestamp
|
|
346
|
-
elif
|
|
347
|
-
self.current_incident_end_timestamp
|
|
348
|
-
|
|
478
|
+
elif (
|
|
479
|
+
self.current_incident_end_timestamp != "Incident still active"
|
|
480
|
+
and self.current_incident_end_timestamp != "N/A"
|
|
481
|
+
):
|
|
482
|
+
self.current_incident_end_timestamp = "N/A"
|
|
483
|
+
|
|
349
484
|
if config.alert_config and config.alert_config.count_thresholds:
|
|
350
485
|
threshold = config.alert_config.count_thresholds.get("all", 15)
|
|
351
486
|
intensity = min(10.0, (total_detections / threshold) * 10)
|
|
@@ -380,27 +515,61 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
380
515
|
intensity = min(10.0, total_detections / 3.0)
|
|
381
516
|
self._ascending_alert_list.append(0)
|
|
382
517
|
|
|
383
|
-
|
|
518
|
+
# Generate human text in new format
|
|
384
519
|
human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
|
|
385
520
|
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
|
|
386
521
|
human_text = "\n".join(human_text_lines)
|
|
387
522
|
|
|
388
|
-
alert_settings=[]
|
|
389
|
-
if config.alert_config and hasattr(config.alert_config,
|
|
390
|
-
alert_settings.append(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
523
|
+
alert_settings = []
|
|
524
|
+
if config.alert_config and hasattr(config.alert_config, "alert_type"):
|
|
525
|
+
alert_settings.append(
|
|
526
|
+
{
|
|
527
|
+
"alert_type": (
|
|
528
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
529
|
+
if hasattr(config.alert_config, "alert_type")
|
|
530
|
+
else ["Default"]
|
|
531
|
+
),
|
|
532
|
+
"incident_category": self.CASE_TYPE,
|
|
533
|
+
"threshold_level": (
|
|
534
|
+
config.alert_config.count_thresholds
|
|
535
|
+
if hasattr(config.alert_config, "count_thresholds")
|
|
536
|
+
else {}
|
|
537
|
+
),
|
|
538
|
+
"ascending": True,
|
|
539
|
+
"settings": {
|
|
540
|
+
t: v
|
|
541
|
+
for t, v in zip(
|
|
542
|
+
(
|
|
543
|
+
getattr(
|
|
544
|
+
config.alert_config, "alert_type", ["Default"]
|
|
545
|
+
)
|
|
546
|
+
if hasattr(config.alert_config, "alert_type")
|
|
547
|
+
else ["Default"]
|
|
548
|
+
),
|
|
549
|
+
(
|
|
550
|
+
getattr(
|
|
551
|
+
config.alert_config, "alert_value", ["JSON"]
|
|
552
|
+
)
|
|
553
|
+
if hasattr(config.alert_config, "alert_value")
|
|
554
|
+
else ["JSON"]
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
event = self.create_incident(
|
|
562
|
+
incident_id=self.CASE_TYPE + "_" + str(frame_number),
|
|
563
|
+
incident_type=self.CASE_TYPE,
|
|
564
|
+
severity_level=level,
|
|
565
|
+
human_text=human_text,
|
|
566
|
+
camera_info=camera_info,
|
|
567
|
+
alerts=alerts,
|
|
568
|
+
alert_settings=alert_settings,
|
|
569
|
+
start_time=start_timestamp,
|
|
570
|
+
end_time=self.current_incident_end_timestamp,
|
|
571
|
+
level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7},
|
|
572
|
+
)
|
|
404
573
|
incidents.append(event)
|
|
405
574
|
|
|
406
575
|
else:
|
|
@@ -410,12 +579,12 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
410
579
|
return incidents
|
|
411
580
|
|
|
412
581
|
def _generate_tracking_stats(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
582
|
+
self,
|
|
583
|
+
counting_summary: Dict,
|
|
584
|
+
alerts: Any,
|
|
585
|
+
config: LicensePlateConfig,
|
|
586
|
+
frame_number: Optional[int] = None,
|
|
587
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
419
588
|
) -> List[Dict]:
|
|
420
589
|
"""Generate structured tracking stats for the output format with frame-based keys, including track_ids_info and detections with masks."""
|
|
421
590
|
# frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
@@ -428,14 +597,22 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
428
597
|
cumulative_total = sum(total_counts.values()) if total_counts else 0
|
|
429
598
|
per_category_count = counting_summary.get("per_category_count", {})
|
|
430
599
|
|
|
431
|
-
track_ids_info = self._get_track_ids_info(
|
|
600
|
+
track_ids_info = self._get_track_ids_info(
|
|
601
|
+
counting_summary.get("detections", [])
|
|
602
|
+
)
|
|
432
603
|
|
|
433
|
-
current_timestamp = self._get_current_timestamp_str(
|
|
604
|
+
current_timestamp = self._get_current_timestamp_str(
|
|
605
|
+
stream_info, precision=False
|
|
606
|
+
)
|
|
434
607
|
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
435
|
-
|
|
608
|
+
|
|
436
609
|
# Create high precision timestamps for input_timestamp and reset_timestamp
|
|
437
|
-
high_precision_start_timestamp = self._get_current_timestamp_str(
|
|
438
|
-
|
|
610
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(
|
|
611
|
+
stream_info, precision=True
|
|
612
|
+
)
|
|
613
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(
|
|
614
|
+
stream_info, precision=True
|
|
615
|
+
)
|
|
439
616
|
|
|
440
617
|
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
441
618
|
human_text_lines = []
|
|
@@ -443,11 +620,15 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
443
620
|
# CURRENT FRAME section
|
|
444
621
|
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
|
|
445
622
|
if total_detections > 0:
|
|
446
|
-
category_counts = [
|
|
623
|
+
category_counts = [
|
|
624
|
+
f"{count} {cat}" for cat, count in per_category_count.items()
|
|
625
|
+
]
|
|
447
626
|
if len(category_counts) == 1:
|
|
448
627
|
detection_text = category_counts[0] + " detected"
|
|
449
628
|
elif len(category_counts) == 2:
|
|
450
|
-
detection_text =
|
|
629
|
+
detection_text = (
|
|
630
|
+
f"{category_counts[0]} and {category_counts[1]} detected"
|
|
631
|
+
)
|
|
451
632
|
else:
|
|
452
633
|
detection_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
|
|
453
634
|
human_text_lines.append(f"\t- {detection_text}")
|
|
@@ -464,14 +645,13 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
464
645
|
for cat, count in total_counts.items():
|
|
465
646
|
if count > 0: # Only include categories with non-zero counts
|
|
466
647
|
human_text_lines.append(f"\t- {cat}: {count}")
|
|
467
|
-
# Build current_counts array in expected format
|
|
648
|
+
# Build current_counts array in expected format
|
|
468
649
|
current_counts = []
|
|
469
650
|
for cat, count in per_category_count.items():
|
|
470
|
-
if
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
})
|
|
651
|
+
if (
|
|
652
|
+
count > 0 or total_detections > 0
|
|
653
|
+
): # Include even if 0 when there are detections
|
|
654
|
+
current_counts.append({"category": cat, "count": count})
|
|
475
655
|
|
|
476
656
|
human_text = "\n".join(human_text_lines)
|
|
477
657
|
|
|
@@ -483,69 +663,115 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
483
663
|
category = detection.get("category", "person")
|
|
484
664
|
# Include segmentation if available (like in eg.json)
|
|
485
665
|
if detection.get("masks"):
|
|
486
|
-
segmentation= detection.get("masks", [])
|
|
487
|
-
detection_obj = self.create_detection_object(
|
|
666
|
+
segmentation = detection.get("masks", [])
|
|
667
|
+
detection_obj = self.create_detection_object(
|
|
668
|
+
category, bbox, segmentation=segmentation
|
|
669
|
+
)
|
|
488
670
|
elif detection.get("segmentation"):
|
|
489
|
-
segmentation= detection.get("segmentation")
|
|
490
|
-
detection_obj = self.create_detection_object(
|
|
671
|
+
segmentation = detection.get("segmentation")
|
|
672
|
+
detection_obj = self.create_detection_object(
|
|
673
|
+
category, bbox, segmentation=segmentation
|
|
674
|
+
)
|
|
491
675
|
elif detection.get("mask"):
|
|
492
|
-
segmentation= detection.get("mask")
|
|
493
|
-
detection_obj = self.create_detection_object(
|
|
676
|
+
segmentation = detection.get("mask")
|
|
677
|
+
detection_obj = self.create_detection_object(
|
|
678
|
+
category, bbox, segmentation=segmentation
|
|
679
|
+
)
|
|
494
680
|
else:
|
|
495
681
|
detection_obj = self.create_detection_object(category, bbox)
|
|
496
682
|
detections.append(detection_obj)
|
|
497
683
|
|
|
498
684
|
# Build alert_settings array in expected format
|
|
499
685
|
alert_settings = []
|
|
500
|
-
if config.alert_config and hasattr(config.alert_config,
|
|
501
|
-
alert_settings.append(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
686
|
+
if config.alert_config and hasattr(config.alert_config, "alert_type"):
|
|
687
|
+
alert_settings.append(
|
|
688
|
+
{
|
|
689
|
+
"alert_type": (
|
|
690
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
691
|
+
if hasattr(config.alert_config, "alert_type")
|
|
692
|
+
else ["Default"]
|
|
693
|
+
),
|
|
694
|
+
"incident_category": self.CASE_TYPE,
|
|
695
|
+
"threshold_level": (
|
|
696
|
+
config.alert_config.count_thresholds
|
|
697
|
+
if hasattr(config.alert_config, "count_thresholds")
|
|
698
|
+
else {}
|
|
699
|
+
),
|
|
700
|
+
"ascending": True,
|
|
701
|
+
"settings": {
|
|
702
|
+
t: v
|
|
703
|
+
for t, v in zip(
|
|
704
|
+
(
|
|
705
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
706
|
+
if hasattr(config.alert_config, "alert_type")
|
|
707
|
+
else ["Default"]
|
|
708
|
+
),
|
|
709
|
+
(
|
|
710
|
+
getattr(config.alert_config, "alert_value", ["JSON"])
|
|
711
|
+
if hasattr(config.alert_config, "alert_value")
|
|
712
|
+
else ["JSON"]
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
)
|
|
510
718
|
|
|
511
719
|
if alerts:
|
|
512
720
|
for alert in alerts:
|
|
513
|
-
human_text_lines.append(
|
|
721
|
+
human_text_lines.append(
|
|
722
|
+
f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}"
|
|
723
|
+
)
|
|
514
724
|
else:
|
|
515
725
|
human_text_lines.append("Alerts: None")
|
|
516
726
|
|
|
517
727
|
human_text = "\n".join(human_text_lines)
|
|
518
728
|
reset_settings = [
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"reset_time": {
|
|
522
|
-
"value": 9,
|
|
523
|
-
"time_unit": "hour"
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
]
|
|
729
|
+
{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}
|
|
730
|
+
]
|
|
527
731
|
|
|
528
|
-
tracking_stat=self.create_tracking_stats(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
732
|
+
tracking_stat = self.create_tracking_stats(
|
|
733
|
+
total_counts=total_counts,
|
|
734
|
+
current_counts=current_counts,
|
|
735
|
+
detections=detections,
|
|
736
|
+
human_text=human_text,
|
|
737
|
+
camera_info=camera_info,
|
|
738
|
+
alerts=alerts,
|
|
739
|
+
alert_settings=alert_settings,
|
|
740
|
+
reset_settings=reset_settings,
|
|
741
|
+
start_time=high_precision_start_timestamp,
|
|
742
|
+
reset_time=high_precision_reset_timestamp,
|
|
743
|
+
)
|
|
532
744
|
|
|
533
745
|
tracking_stats.append(tracking_stat)
|
|
534
746
|
return tracking_stats
|
|
535
747
|
|
|
536
|
-
def _generate_business_analytics(
|
|
748
|
+
def _generate_business_analytics(
|
|
749
|
+
self,
|
|
750
|
+
counting_summary: Dict,
|
|
751
|
+
zone_analysis: Dict,
|
|
752
|
+
config: LicensePlateConfig,
|
|
753
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
754
|
+
is_empty=False,
|
|
755
|
+
) -> List[Dict]:
|
|
537
756
|
"""Generate standardized business analytics for the agg_summary structure."""
|
|
538
757
|
if is_empty:
|
|
539
758
|
return []
|
|
540
759
|
|
|
541
|
-
|
|
542
|
-
#camera_info = self.get_camera_info_from_stream(stream_info)
|
|
760
|
+
# -----IF YOUR USECASE NEEDS BUSINESS ANALYTICS, YOU CAN USE THIS FUNCTION------#
|
|
761
|
+
# camera_info = self.get_camera_info_from_stream(stream_info)
|
|
543
762
|
# business_analytics = self.create_business_analytics(nalysis_name, statistics,
|
|
544
763
|
# human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
|
545
764
|
# reset_settings)
|
|
546
765
|
# return business_analytics
|
|
547
766
|
|
|
548
|
-
def _generate_summary(
|
|
767
|
+
def _generate_summary(
|
|
768
|
+
self,
|
|
769
|
+
summary: dict,
|
|
770
|
+
incidents: List,
|
|
771
|
+
tracking_stats: List,
|
|
772
|
+
business_analytics: List,
|
|
773
|
+
alerts: List,
|
|
774
|
+
) -> List[str]:
|
|
549
775
|
"""
|
|
550
776
|
Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
|
|
551
777
|
"""
|
|
@@ -553,18 +779,27 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
553
779
|
lines["Application Name"] = self.CASE_TYPE
|
|
554
780
|
lines["Application Version"] = self.CASE_VERSION
|
|
555
781
|
if len(incidents) > 0:
|
|
556
|
-
lines["Incidents:"]=
|
|
782
|
+
lines["Incidents:"] = (
|
|
783
|
+
f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
|
|
784
|
+
)
|
|
557
785
|
if len(tracking_stats) > 0:
|
|
558
|
-
lines["Tracking Statistics:"]=
|
|
786
|
+
lines["Tracking Statistics:"] = (
|
|
787
|
+
f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
|
|
788
|
+
)
|
|
559
789
|
if len(business_analytics) > 0:
|
|
560
|
-
lines["Business Analytics:"]=
|
|
561
|
-
|
|
562
|
-
|
|
790
|
+
lines["Business Analytics:"] = (
|
|
791
|
+
f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
if (
|
|
795
|
+
len(incidents) == 0
|
|
796
|
+
and len(tracking_stats) == 0
|
|
797
|
+
and len(business_analytics) == 0
|
|
798
|
+
):
|
|
563
799
|
lines["Summary"] = "No Summary Data"
|
|
564
800
|
|
|
565
801
|
return [lines]
|
|
566
802
|
|
|
567
|
-
|
|
568
803
|
def _count_categories(self, detections: list, config: LicensePlateConfig) -> dict:
|
|
569
804
|
"""
|
|
570
805
|
Count the number of detections per category and return a summary dict.
|
|
@@ -574,24 +809,30 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
574
809
|
counts = {}
|
|
575
810
|
valid_detections = []
|
|
576
811
|
for det in detections:
|
|
577
|
-
cat = det.get(
|
|
578
|
-
if not all(
|
|
812
|
+
cat = det.get("category", "unknown")
|
|
813
|
+
if not all(
|
|
814
|
+
k in det for k in ["category", "confidence", "bounding_box"]
|
|
815
|
+
): # Validate required fields
|
|
579
816
|
self.logger.warning(f"Skipping invalid detection: {det}")
|
|
580
817
|
continue
|
|
581
818
|
counts[cat] = counts.get(cat, 0) + 1
|
|
582
|
-
valid_detections.append(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
819
|
+
valid_detections.append(
|
|
820
|
+
{
|
|
821
|
+
"bounding_box": det.get("bounding_box"),
|
|
822
|
+
"category": det.get("category"),
|
|
823
|
+
"confidence": det.get("confidence"),
|
|
824
|
+
"track_id": det.get("track_id"),
|
|
825
|
+
"frame_id": det.get("frame_id"),
|
|
826
|
+
"masks": det.get(
|
|
827
|
+
"masks", det.get("mask", [])
|
|
828
|
+
), # Include masks, fallback to empty list
|
|
829
|
+
}
|
|
830
|
+
)
|
|
590
831
|
self.logger.debug(f"Valid detections after filtering: {len(valid_detections)}")
|
|
591
832
|
return {
|
|
592
833
|
"total_count": sum(counts.values()),
|
|
593
834
|
"per_category_count": counts,
|
|
594
|
-
"detections": valid_detections
|
|
835
|
+
"detections": valid_detections,
|
|
595
836
|
}
|
|
596
837
|
|
|
597
838
|
def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
|
|
@@ -601,12 +842,12 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
601
842
|
# Collect all track_ids in this frame
|
|
602
843
|
frame_track_ids = set()
|
|
603
844
|
for det in detections:
|
|
604
|
-
tid = det.get(
|
|
845
|
+
tid = det.get("track_id")
|
|
605
846
|
if tid is not None:
|
|
606
847
|
frame_track_ids.add(tid)
|
|
607
848
|
# Use persistent total set for unique counting
|
|
608
849
|
total_track_ids = set()
|
|
609
|
-
for s in getattr(self,
|
|
850
|
+
for s in getattr(self, "_per_category_total_track_ids", {}).values():
|
|
610
851
|
total_track_ids.update(s)
|
|
611
852
|
return {
|
|
612
853
|
"total_count": len(total_track_ids),
|
|
@@ -614,7 +855,7 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
614
855
|
"total_unique_track_ids": len(total_track_ids),
|
|
615
856
|
"current_frame_track_ids": list(frame_track_ids),
|
|
616
857
|
"last_update_time": time.time(),
|
|
617
|
-
"total_frames_processed": getattr(self,
|
|
858
|
+
"total_frames_processed": getattr(self, "_total_frame_counter", 0),
|
|
618
859
|
}
|
|
619
860
|
|
|
620
861
|
def _update_tracking_state(self, detections: list):
|
|
@@ -625,7 +866,9 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
625
866
|
"""
|
|
626
867
|
# Lazily initialise storage dicts
|
|
627
868
|
if not hasattr(self, "_per_category_total_track_ids"):
|
|
628
|
-
self._per_category_total_track_ids = {
|
|
869
|
+
self._per_category_total_track_ids = {
|
|
870
|
+
cat: set() for cat in self.target_categories
|
|
871
|
+
}
|
|
629
872
|
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
630
873
|
|
|
631
874
|
for det in detections:
|
|
@@ -645,21 +888,29 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
645
888
|
"""
|
|
646
889
|
Return total unique track_id count for each category.
|
|
647
890
|
"""
|
|
648
|
-
return {
|
|
891
|
+
return {
|
|
892
|
+
cat: len(ids)
|
|
893
|
+
for cat, ids in getattr(self, "_per_category_total_track_ids", {}).items()
|
|
894
|
+
}
|
|
649
895
|
|
|
650
896
|
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
|
651
897
|
"""Format timestamp for video chunks (HH:MM:SS.ms format)."""
|
|
652
898
|
hours = int(timestamp // 3600)
|
|
653
899
|
minutes = int((timestamp % 3600) // 60)
|
|
654
|
-
seconds = round(float(timestamp % 60),2)
|
|
900
|
+
seconds = round(float(timestamp % 60), 2)
|
|
655
901
|
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
656
902
|
|
|
657
903
|
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
|
658
904
|
"""Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
|
|
659
905
|
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
660
|
-
return dt.strftime(
|
|
906
|
+
return dt.strftime("%Y:%m:%d %H:%M:%S")
|
|
661
907
|
|
|
662
|
-
def _get_current_timestamp_str(
|
|
908
|
+
def _get_current_timestamp_str(
|
|
909
|
+
self,
|
|
910
|
+
stream_info: Optional[Dict[str, Any]],
|
|
911
|
+
precision=False,
|
|
912
|
+
frame_id: Optional[str] = None,
|
|
913
|
+
) -> str:
|
|
663
914
|
"""Get formatted current timestamp based on stream type."""
|
|
664
915
|
if not stream_info:
|
|
665
916
|
return "00:00:00.00"
|
|
@@ -667,24 +918,36 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
667
918
|
if precision:
|
|
668
919
|
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
669
920
|
if frame_id:
|
|
670
|
-
start_time = int(frame_id)/stream_info.get(
|
|
921
|
+
start_time = int(frame_id) / stream_info.get(
|
|
922
|
+
"input_settings", {}
|
|
923
|
+
).get("original_fps", 30)
|
|
671
924
|
else:
|
|
672
|
-
start_time = stream_info.get("input_settings", {}).get(
|
|
925
|
+
start_time = stream_info.get("input_settings", {}).get(
|
|
926
|
+
"start_frame", 30
|
|
927
|
+
) / stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
673
928
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
674
929
|
return stream_time_str
|
|
675
930
|
else:
|
|
676
931
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
677
932
|
|
|
678
933
|
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
934
|
+
if frame_id:
|
|
935
|
+
start_time = int(frame_id) / stream_info.get("input_settings", {}).get(
|
|
936
|
+
"original_fps", 30
|
|
937
|
+
)
|
|
938
|
+
else:
|
|
939
|
+
start_time = stream_info.get("input_settings", {}).get(
|
|
940
|
+
"start_frame", 30
|
|
941
|
+
) / stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
942
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
943
|
+
return stream_time_str
|
|
685
944
|
else:
|
|
686
945
|
# For streams, use stream_time from stream_info
|
|
687
|
-
stream_time_str =
|
|
946
|
+
stream_time_str = (
|
|
947
|
+
stream_info.get("input_settings", {})
|
|
948
|
+
.get("stream_info", {})
|
|
949
|
+
.get("stream_time", "")
|
|
950
|
+
)
|
|
688
951
|
if stream_time_str:
|
|
689
952
|
# Parse the high precision timestamp string to get timestamp
|
|
690
953
|
try:
|
|
@@ -699,7 +962,9 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
699
962
|
else:
|
|
700
963
|
return self._format_timestamp_for_stream(time.time())
|
|
701
964
|
|
|
702
|
-
def _get_start_timestamp_str(
|
|
965
|
+
def _get_start_timestamp_str(
|
|
966
|
+
self, stream_info: Optional[Dict[str, Any]], precision=False
|
|
967
|
+
) -> str:
|
|
703
968
|
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
704
969
|
if not stream_info:
|
|
705
970
|
return "00:00:00"
|
|
@@ -716,13 +981,19 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
716
981
|
# For streams, use tracking start time or current time with minutes/seconds reset
|
|
717
982
|
if self._tracking_start_time is None:
|
|
718
983
|
# Try to extract timestamp from stream_time string
|
|
719
|
-
stream_time_str =
|
|
984
|
+
stream_time_str = (
|
|
985
|
+
stream_info.get("input_settings", {})
|
|
986
|
+
.get("stream_info", {})
|
|
987
|
+
.get("stream_time", "")
|
|
988
|
+
)
|
|
720
989
|
if stream_time_str:
|
|
721
990
|
try:
|
|
722
991
|
# Remove " UTC" suffix and parse
|
|
723
992
|
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
724
993
|
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
725
|
-
self._tracking_start_time = dt.replace(
|
|
994
|
+
self._tracking_start_time = dt.replace(
|
|
995
|
+
tzinfo=timezone.utc
|
|
996
|
+
).timestamp()
|
|
726
997
|
except:
|
|
727
998
|
# Fallback to current time if parsing fails
|
|
728
999
|
self._tracking_start_time = time.time()
|
|
@@ -732,7 +1003,7 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
732
1003
|
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
|
733
1004
|
# Reset minutes and seconds to 00:00 for "TOTAL SINCE" format
|
|
734
1005
|
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
735
|
-
return dt.strftime(
|
|
1006
|
+
return dt.strftime("%Y:%m:%d %H:%M:%S")
|
|
736
1007
|
|
|
737
1008
|
# ------------------------------------------------------------------ #
|
|
738
1009
|
# Helper to merge masks back into detections #
|
|
@@ -772,7 +1043,9 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
772
1043
|
if idx in used_raw_indices:
|
|
773
1044
|
continue
|
|
774
1045
|
|
|
775
|
-
iou = self._compute_iou(
|
|
1046
|
+
iou = self._compute_iou(
|
|
1047
|
+
det.get("bounding_box"), raw_det.get("bounding_box")
|
|
1048
|
+
)
|
|
776
1049
|
if iou > best_iou:
|
|
777
1050
|
best_iou = iou
|
|
778
1051
|
best_idx = idx
|
|
@@ -798,12 +1071,11 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
798
1071
|
"category": det.get("category", "unknown"),
|
|
799
1072
|
"confidence": det.get("confidence", 0.0),
|
|
800
1073
|
"bounding_box": det.get("bounding_box", {}),
|
|
801
|
-
"mask": det.get("mask", det.get("masks", None)) # Accept either key
|
|
1074
|
+
"mask": det.get("mask", det.get("masks", None)), # Accept either key
|
|
802
1075
|
}
|
|
803
1076
|
for det in detections
|
|
804
1077
|
]
|
|
805
1078
|
|
|
806
|
-
|
|
807
1079
|
# ------------------------------------------------------------------ #
|
|
808
1080
|
# Canonical ID helpers #
|
|
809
1081
|
# ------------------------------------------------------------------ #
|
|
@@ -901,7 +1173,9 @@ class LicensePlateUseCase(BaseProcessor):
|
|
|
901
1173
|
|
|
902
1174
|
def _format_timestamp(self, timestamp: float) -> str:
|
|
903
1175
|
"""Format a timestamp for human-readable output."""
|
|
904
|
-
return datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
1176
|
+
return datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
1177
|
+
"%Y-%m-%d %H:%M:%S UTC"
|
|
1178
|
+
)
|
|
905
1179
|
|
|
906
1180
|
def _get_tracking_start_time(self) -> str:
|
|
907
1181
|
"""Get the tracking start time, formatted as a string."""
|