matrice 1.0.99146__py3-none-any.whl → 1.0.99148__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.
- matrice/deploy/utils/post_processing/__init__.py +6 -0
- matrice/deploy/utils/post_processing/config.py +2 -0
- matrice/deploy/utils/post_processing/core/config.py +30 -0
- matrice/deploy/utils/post_processing/processor.py +4 -2
- matrice/deploy/utils/post_processing/usecases/__init__.py +3 -0
- matrice/deploy/utils/post_processing/usecases/fire_detection.py +472 -473
- matrice/deploy/utils/post_processing/usecases/smoker_detection.py +833 -0
- {matrice-1.0.99146.dist-info → matrice-1.0.99148.dist-info}/METADATA +1 -1
- {matrice-1.0.99146.dist-info → matrice-1.0.99148.dist-info}/RECORD +12 -11
- {matrice-1.0.99146.dist-info → matrice-1.0.99148.dist-info}/WHEEL +0 -0
- {matrice-1.0.99146.dist-info → matrice-1.0.99148.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice-1.0.99146.dist-info → matrice-1.0.99148.dist-info}/top_level.txt +0 -0
@@ -28,14 +28,14 @@ from ..utils import (
|
|
28
28
|
|
29
29
|
|
30
30
|
# ======================
|
31
|
-
#
|
31
|
+
# Config Definition
|
32
32
|
# ======================
|
33
33
|
|
34
34
|
|
35
35
|
|
36
36
|
@dataclass
|
37
37
|
class FireSmokeConfig(BaseConfig):
|
38
|
-
confidence_threshold: float = 0.
|
38
|
+
confidence_threshold: float = 0.3
|
39
39
|
|
40
40
|
# Only fire and smoke categories included here (exclude normal)
|
41
41
|
fire_smoke_categories: List[str] = field(
|
@@ -61,6 +61,7 @@ class FireSmokeConfig(BaseConfig):
|
|
61
61
|
smoothing_window_size: int = 5
|
62
62
|
smoothing_cooldown_frames: int = 10
|
63
63
|
smoothing_confidence_range_factor: float = 0.2
|
64
|
+
threshold_area: Optional[float] = 307200.0
|
64
65
|
|
65
66
|
def __post_init__(self):
|
66
67
|
if not (0.0 <= self.confidence_threshold <= 1.0):
|
@@ -80,57 +81,14 @@ class FireSmokeUseCase(BaseProcessor):
|
|
80
81
|
def __init__(self):
|
81
82
|
super().__init__("fire_smoke_detection")
|
82
83
|
self.category = "hazard"
|
84
|
+
self.CASE_TYPE: Optional[str] = 'fire_smoke_detection'
|
85
|
+
self.CASE_VERSION: Optional[str] = '1.3'
|
86
|
+
|
83
87
|
self.smoothing_tracker = None # Required for bbox smoothing
|
84
88
|
self._fire_smoke_recent_history = []
|
85
89
|
|
86
|
-
|
87
|
-
|
88
|
-
return {
|
89
|
-
"type": "object",
|
90
|
-
"properties": {
|
91
|
-
"confidence_threshold": {
|
92
|
-
"type": "number",
|
93
|
-
"minimum": 0.0,
|
94
|
-
"maximum": 1.0,
|
95
|
-
"default": 0.5,
|
96
|
-
"description": "Minimum confidence threshold for detections",
|
97
|
-
},
|
98
|
-
"fire_smoke_categories": {
|
99
|
-
"type": "array",
|
100
|
-
"items": {"type": "string"},
|
101
|
-
"default": ["Fire", "Smoke"],
|
102
|
-
"description": "Category names that represent fire and smoke",
|
103
|
-
},
|
104
|
-
"index_to_category": {
|
105
|
-
"type": "object",
|
106
|
-
"additionalProperties": {"type": "string"},
|
107
|
-
"description": "Mapping from category indices to names",
|
108
|
-
},
|
109
|
-
"alert_config": {
|
110
|
-
"type": "object",
|
111
|
-
"properties": {
|
112
|
-
"count_thresholds": {
|
113
|
-
"type": "object",
|
114
|
-
"additionalProperties": {"type": "integer", "minimum": 1},
|
115
|
-
"description": "Count thresholds for alerts",
|
116
|
-
}
|
117
|
-
},
|
118
|
-
},
|
119
|
-
},
|
120
|
-
"required": ["confidence_threshold"],
|
121
|
-
"additionalProperties": False,
|
122
|
-
}
|
123
|
-
|
124
|
-
def create_default_config(self, **overrides) -> FireSmokeConfig:
|
125
|
-
"""Create default configuration with optional overrides."""
|
126
|
-
defaults = {
|
127
|
-
"category": self.category,
|
128
|
-
"usecase": self.name,
|
129
|
-
"confidence_threshold": 0.5,
|
130
|
-
"fire_smoke_categories": ["Fire", "Smoke"],
|
131
|
-
}
|
132
|
-
defaults.update(overrides)
|
133
|
-
return FireSmokeConfig(**defaults)
|
90
|
+
self._ascending_alert_list: List[int] = []
|
91
|
+
self.current_incident_end_timestamp: str = "N/A"
|
134
92
|
|
135
93
|
def process(
|
136
94
|
self,
|
@@ -203,20 +161,10 @@ class FireSmokeUseCase(BaseProcessor):
|
|
203
161
|
fire_smoke_summary = self._calculate_fire_smoke_summary(processed_data, config)
|
204
162
|
general_summary = calculate_counting_summary(processed_data)
|
205
163
|
|
206
|
-
# Step 5:
|
207
|
-
insights = self._generate_insights(fire_smoke_summary, config)
|
208
|
-
alerts = self._check_alerts(fire_smoke_summary, config)
|
209
|
-
|
210
|
-
# Step 6: Metrics
|
211
|
-
metrics = self._calculate_metrics(fire_smoke_summary, config, context)
|
212
|
-
|
213
|
-
# Step 7: Predictions
|
164
|
+
# Step 5: Predictions
|
214
165
|
predictions = self._extract_predictions(processed_data, config)
|
215
166
|
|
216
|
-
# Step
|
217
|
-
summary_text = self._generate_summary(fire_smoke_summary, general_summary, alerts)
|
218
|
-
|
219
|
-
# Step 9: Frame number extraction
|
167
|
+
# Step 6: Frame number extraction
|
220
168
|
frame_number = None
|
221
169
|
if stream_info:
|
222
170
|
input_settings = stream_info.get("input_settings", {})
|
@@ -227,38 +175,45 @@ class FireSmokeUseCase(BaseProcessor):
|
|
227
175
|
elif start_frame is not None:
|
228
176
|
frame_number = start_frame
|
229
177
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
178
|
+
# Step 7: alerts
|
179
|
+
alerts = self._check_alerts(fire_smoke_summary, frame_number, config, stream_info)
|
180
|
+
|
181
|
+
|
182
|
+
# Step 8: Incidents and tracking stats
|
183
|
+
incidents_list = self._generate_incidents(fire_smoke_summary, alerts, config, frame_number=frame_number, stream_info=stream_info)
|
184
|
+
tracking_stats_list = self._generate_tracking_stats(
|
185
|
+
fire_smoke_summary, alerts, config,
|
234
186
|
frame_number=frame_number,
|
235
187
|
stream_info=stream_info
|
236
188
|
)
|
189
|
+
business_analytics_list = self._generate_business_analytics(fire_smoke_summary, alerts, config, stream_info, is_empty=True)
|
190
|
+
|
191
|
+
# Step 9: Human-readable summary
|
192
|
+
summary_list = self._generate_summary(fire_smoke_summary, general_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
|
237
193
|
|
238
194
|
# Finalize context and return result
|
239
195
|
context.processing_time = time.time() - start_time
|
196
|
+
# Extract frame-based dictionaries from the lists
|
197
|
+
incidents = incidents_list[0] if incidents_list else {}
|
198
|
+
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
199
|
+
business_analytics = business_analytics_list[0] if business_analytics_list else {}
|
200
|
+
summary = summary_list[0] if summary_list else {}
|
201
|
+
agg_summary = {str(frame_number): {
|
202
|
+
"incidents": incidents,
|
203
|
+
"tracking_stats": tracking_stats,
|
204
|
+
"business_analytics": business_analytics,
|
205
|
+
"alerts": alerts,
|
206
|
+
"human_text": summary}
|
207
|
+
}
|
208
|
+
|
240
209
|
context.mark_completed()
|
241
210
|
|
242
211
|
result = self.create_result(
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
"total_fire_smoke_detections": fire_smoke_summary.get("total_objects", 0),
|
248
|
-
"total_fire_detections": fire_smoke_summary.get("by_category", {}).get("fire", 0),
|
249
|
-
"total_smoke_detections": fire_smoke_summary.get("by_category", {}).get("smoke", 0),
|
250
|
-
"events": events_dict,
|
251
|
-
"tracking_stats": tracking_stats_dict,
|
252
|
-
},
|
253
|
-
usecase=self.name,
|
254
|
-
category=self.category,
|
255
|
-
context=context,
|
256
|
-
)
|
212
|
+
data={"agg_summary": agg_summary},
|
213
|
+
usecase=self.name,
|
214
|
+
category=self.category,
|
215
|
+
context=context)
|
257
216
|
|
258
|
-
result.summary = summary_text
|
259
|
-
result.insights = insights
|
260
|
-
result.predictions = predictions
|
261
|
-
result.metrics = metrics
|
262
217
|
return result
|
263
218
|
|
264
219
|
|
@@ -272,7 +227,340 @@ class FireSmokeUseCase(BaseProcessor):
|
|
272
227
|
context=context,
|
273
228
|
)
|
274
229
|
|
275
|
-
# ====
|
230
|
+
# ==== Internal Utilities ====
|
231
|
+
def _check_alerts(
|
232
|
+
self, summary: Dict, frame_number:Any, config: FireSmokeConfig, stream_info: Optional[Dict[str, Any]] = None
|
233
|
+
) -> List[Dict]:
|
234
|
+
"""Raise alerts if fire or smoke detected with severity based on intensity."""
|
235
|
+
def get_trend(data, lookback=900, threshold=0.6):
|
236
|
+
'''
|
237
|
+
Determine if the trend is ascending or descending based on actual value progression.
|
238
|
+
Now works with values 0,1,2,3 (not just binary).
|
239
|
+
'''
|
240
|
+
window = data[-lookback:] if len(data) >= lookback else data
|
241
|
+
if len(window) < 2:
|
242
|
+
return True # not enough data to determine trend
|
243
|
+
increasing = 0
|
244
|
+
total = 0
|
245
|
+
for i in range(1, len(window)):
|
246
|
+
if window[i] >= window[i - 1]:
|
247
|
+
increasing += 1
|
248
|
+
total += 1
|
249
|
+
ratio = increasing / total
|
250
|
+
if ratio >= threshold:
|
251
|
+
return True
|
252
|
+
elif ratio <= (1 - threshold):
|
253
|
+
return False
|
254
|
+
|
255
|
+
alerts = []
|
256
|
+
total = summary.get("total_objects", 0)
|
257
|
+
by_category = summary.get("by_category", {})
|
258
|
+
detections = summary.get("detections", [])
|
259
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
260
|
+
|
261
|
+
if total == 0:
|
262
|
+
return []
|
263
|
+
if not config.alert_config:
|
264
|
+
return alerts
|
265
|
+
|
266
|
+
if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
|
267
|
+
|
268
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
269
|
+
if category == "all" and total > threshold:
|
270
|
+
|
271
|
+
alerts.append({
|
272
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
273
|
+
"alert_id": "alert_"+category+'_'+frame_key,
|
274
|
+
"incident_category": self.CASE_TYPE,
|
275
|
+
"threshold_level": threshold,
|
276
|
+
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
277
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
278
|
+
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
279
|
+
}
|
280
|
+
})
|
281
|
+
elif category in summary.get("per_category_count", {}):
|
282
|
+
count = summary.get("per_category_count", {})[category]
|
283
|
+
if count > threshold: # Fixed logic: alert when EXCEEDING threshold
|
284
|
+
alerts.append({
|
285
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
286
|
+
"alert_id": "alert_"+category+'_'+frame_key,
|
287
|
+
"incident_category": self.CASE_TYPE,
|
288
|
+
"threshold_level": threshold,
|
289
|
+
"ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
|
290
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
291
|
+
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
292
|
+
}
|
293
|
+
})
|
294
|
+
else:
|
295
|
+
pass
|
296
|
+
|
297
|
+
return alerts
|
298
|
+
|
299
|
+
def _generate_incidents(
|
300
|
+
self,
|
301
|
+
summary: Dict,
|
302
|
+
alerts: List[Dict],
|
303
|
+
config: FireSmokeConfig,
|
304
|
+
frame_number: Optional[int] = None,
|
305
|
+
stream_info: Optional[Dict[str, Any]] = None
|
306
|
+
) -> Dict:
|
307
|
+
"""Generate structured events for fire and smoke detection output with frame-aware keys."""
|
308
|
+
|
309
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
310
|
+
incidents = []
|
311
|
+
|
312
|
+
total = summary.get("total_objects", 0)
|
313
|
+
by_category = summary.get("by_category", {})
|
314
|
+
detections = summary.get("detections", [])
|
315
|
+
|
316
|
+
total_fire = by_category.get("fire", 0)
|
317
|
+
total_smoke = by_category.get("smoke", 0)
|
318
|
+
current_timestamp = self._get_current_timestamp_str(stream_info)
|
319
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
320
|
+
self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
|
321
|
+
|
322
|
+
if total > 0:
|
323
|
+
# Calculate total bbox area
|
324
|
+
total_area = 0.0
|
325
|
+
|
326
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
327
|
+
if category in summary.get("per_category_count", {}):
|
328
|
+
#count = summary.get("per_category_count", {})[category]
|
329
|
+
start_timestamp = self._get_start_timestamp_str(stream_info)
|
330
|
+
if start_timestamp and self.current_incident_end_timestamp=='N/A':
|
331
|
+
self.current_incident_end_timestamp = 'Incident still active'
|
332
|
+
elif start_timestamp and self.current_incident_end_timestamp=='Incident still active':
|
333
|
+
if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
|
334
|
+
self.current_incident_end_timestamp = current_timestamp
|
335
|
+
elif self.current_incident_end_timestamp!='Incident still active' and self.current_incident_end_timestamp!='N/A':
|
336
|
+
self.current_incident_end_timestamp = 'N/A'
|
337
|
+
|
338
|
+
for det in detections:
|
339
|
+
bbox = det.get("bounding_box") or det.get("bbox")
|
340
|
+
if bbox:
|
341
|
+
xmin = bbox.get("xmin")
|
342
|
+
ymin = bbox.get("ymin")
|
343
|
+
xmax = bbox.get("xmax")
|
344
|
+
ymax = bbox.get("ymax")
|
345
|
+
if None not in (xmin, ymin, xmax, ymax):
|
346
|
+
width = xmax - xmin
|
347
|
+
height = ymax - ymin
|
348
|
+
if width > 0 and height > 0:
|
349
|
+
total_area += width * height
|
350
|
+
|
351
|
+
threshold_area = config.threshold_area # 307200.0 | Same threshold as insights
|
352
|
+
|
353
|
+
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
354
|
+
|
355
|
+
if config.alert_config and config.alert_config.count_thresholds:
|
356
|
+
if intensity_pct >= 60:
|
357
|
+
level = "critical"
|
358
|
+
self._ascending_alert_list.append(3)
|
359
|
+
elif intensity_pct >= 40:
|
360
|
+
level = "significant"
|
361
|
+
self._ascending_alert_list.append(2)
|
362
|
+
elif intensity_pct >= 5:
|
363
|
+
level = "medium"
|
364
|
+
self._ascending_alert_list.append(1)
|
365
|
+
else:
|
366
|
+
level = "low"
|
367
|
+
self._ascending_alert_list.append(0)
|
368
|
+
else:
|
369
|
+
if intensity_pct > 60:
|
370
|
+
level = "critical"
|
371
|
+
intensity = 10.0
|
372
|
+
self._ascending_alert_list.append(3)
|
373
|
+
elif intensity_pct > 40:
|
374
|
+
level = "significant"
|
375
|
+
intensity = 9.0
|
376
|
+
self._ascending_alert_list.append(2)
|
377
|
+
elif intensity_pct > 4:
|
378
|
+
level = "medium"
|
379
|
+
intensity = 7.0
|
380
|
+
self._ascending_alert_list.append(1)
|
381
|
+
else:
|
382
|
+
level = "low"
|
383
|
+
intensity = min(10.0, intensity_pct / 3.0)
|
384
|
+
self._ascending_alert_list.append(0)
|
385
|
+
|
386
|
+
# Generate human text in new format
|
387
|
+
human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
|
388
|
+
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
|
389
|
+
human_text = "\n".join(human_text_lines)
|
390
|
+
|
391
|
+
alert_settings=[]
|
392
|
+
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
393
|
+
alert_settings.append({
|
394
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
395
|
+
"incident_category": self.CASE_TYPE,
|
396
|
+
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
397
|
+
"ascending": True,
|
398
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
399
|
+
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
400
|
+
}
|
401
|
+
})
|
402
|
+
|
403
|
+
event= self.create_incident(incident_id=self.CASE_TYPE+'_'+str(frame_number), incident_type=self.CASE_TYPE,
|
404
|
+
severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
405
|
+
start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
|
406
|
+
level_settings= {"low": 1, "medium": 5, "significant":40, "critical": 60})
|
407
|
+
incidents.append(event)
|
408
|
+
|
409
|
+
else:
|
410
|
+
self._ascending_alert_list.append(0)
|
411
|
+
incidents.append({})
|
412
|
+
return incidents
|
413
|
+
|
414
|
+
def _generate_tracking_stats(
|
415
|
+
self,
|
416
|
+
summary: Dict,
|
417
|
+
alerts: List,
|
418
|
+
config: FireSmokeConfig,
|
419
|
+
frame_number: Optional[int] = None,
|
420
|
+
stream_info: Optional[Dict[str, Any]] = None
|
421
|
+
) -> Dict:
|
422
|
+
"""Generate structured tracking stats for fire and smoke detection with frame-based keys."""
|
423
|
+
|
424
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
425
|
+
tracking_stats = []
|
426
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
427
|
+
|
428
|
+
total = summary.get("total_objects", 0)
|
429
|
+
by_category = summary.get("by_category", {})
|
430
|
+
detections = summary.get("detections", [])
|
431
|
+
|
432
|
+
total_fire = by_category.get("fire", 0)
|
433
|
+
total_smoke = by_category.get("smoke", 0)
|
434
|
+
|
435
|
+
# Maintain rolling detection history
|
436
|
+
if frame_number is not None:
|
437
|
+
self._fire_smoke_recent_history.append({
|
438
|
+
"frame": frame_number,
|
439
|
+
"fire": total_fire,
|
440
|
+
"smoke": total_smoke,
|
441
|
+
})
|
442
|
+
if len(self._fire_smoke_recent_history) > 150:
|
443
|
+
self._fire_smoke_recent_history.pop(0)
|
444
|
+
|
445
|
+
# Generate human-readable tracking text (people-style format)
|
446
|
+
current_timestamp = self._get_current_timestamp_str(stream_info)
|
447
|
+
start_timestamp = self._get_start_timestamp_str(stream_info)
|
448
|
+
# Create high precision timestamps for input_timestamp and reset_timestamp
|
449
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
|
450
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
451
|
+
|
452
|
+
|
453
|
+
# Build total_counts array in expected format
|
454
|
+
total_counts = []
|
455
|
+
if total > 0:
|
456
|
+
total_counts.append({
|
457
|
+
"category": 'Fire/Smoke', #TODO: Discuss and fix what to do with this
|
458
|
+
"count": 1
|
459
|
+
})
|
460
|
+
|
461
|
+
# Build current_counts array in expected format
|
462
|
+
current_counts = []
|
463
|
+
if total > 0: # Include even if 0 when there are detections
|
464
|
+
current_counts.append({
|
465
|
+
"category": 'Fire/Smoke', #TODO: Discuss and fix what to do with this
|
466
|
+
"count": 1
|
467
|
+
})
|
468
|
+
|
469
|
+
human_lines = [f"CURRENT FRAME @ {current_timestamp}:"]
|
470
|
+
if total_fire > 0:
|
471
|
+
human_lines.append(f"\t- Fire regions detected: {total_fire}")
|
472
|
+
if total_smoke > 0:
|
473
|
+
human_lines.append(f"\t- Smoke clouds detected: {total_smoke}")
|
474
|
+
if total_fire == 0 and total_smoke == 0:
|
475
|
+
human_lines.append(f"\t- No fire or smoke detected")
|
476
|
+
|
477
|
+
human_lines.append("")
|
478
|
+
human_lines.append(f"ALERTS SINCE @ {start_timestamp}:")
|
479
|
+
|
480
|
+
recent_fire_detected = any(entry.get("fire", 0) > 0 for entry in self._fire_smoke_recent_history)
|
481
|
+
recent_smoke_detected = any(entry.get("smoke", 0) > 0 for entry in self._fire_smoke_recent_history)
|
482
|
+
|
483
|
+
if recent_fire_detected:
|
484
|
+
human_lines.append(f"\t- Fire alert")
|
485
|
+
if recent_smoke_detected:
|
486
|
+
human_lines.append(f"\t- Smoke alert")
|
487
|
+
if not recent_fire_detected and not recent_smoke_detected:
|
488
|
+
human_lines.append(f"\t- No fire or smoke detected in recent frames")
|
489
|
+
|
490
|
+
human_text = "\n".join(human_lines)
|
491
|
+
|
492
|
+
# Prepare detections without confidence scores (as per eg.json)
|
493
|
+
detections = []
|
494
|
+
for detection in summary.get("detections", []):
|
495
|
+
bbox = detection.get("bounding_box", {})
|
496
|
+
category = detection.get("category", "Fire/Smoke")
|
497
|
+
# Include segmentation if available (like in eg.json)
|
498
|
+
if detection.get("masks"):
|
499
|
+
segmentation= detection.get("masks", [])
|
500
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
501
|
+
elif detection.get("segmentation"):
|
502
|
+
segmentation= detection.get("segmentation")
|
503
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
504
|
+
elif detection.get("mask"):
|
505
|
+
segmentation= detection.get("mask")
|
506
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
|
507
|
+
else:
|
508
|
+
detection_obj = self.create_detection_object(category, bbox)
|
509
|
+
detections.append(detection_obj)
|
510
|
+
|
511
|
+
# Build alert_settings array in expected format
|
512
|
+
alert_settings = []
|
513
|
+
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
514
|
+
alert_settings.append({
|
515
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
516
|
+
"incident_category": self.CASE_TYPE,
|
517
|
+
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
518
|
+
"ascending": True,
|
519
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
|
520
|
+
getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
|
521
|
+
}
|
522
|
+
})
|
523
|
+
|
524
|
+
reset_settings=[
|
525
|
+
{
|
526
|
+
"interval_type": "daily",
|
527
|
+
"reset_time": {
|
528
|
+
"value": 9,
|
529
|
+
"time_unit": "hour"
|
530
|
+
}
|
531
|
+
}
|
532
|
+
]
|
533
|
+
|
534
|
+
tracking_stat=self.create_tracking_stats(total_counts=total_counts, current_counts=current_counts,
|
535
|
+
detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
536
|
+
reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
|
537
|
+
reset_time=high_precision_reset_timestamp)
|
538
|
+
|
539
|
+
|
540
|
+
tracking_stats.append(tracking_stat)
|
541
|
+
return tracking_stats
|
542
|
+
|
543
|
+
def _generate_summary(
|
544
|
+
self, summary: dict, general_summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List
|
545
|
+
) -> str:
|
546
|
+
"""
|
547
|
+
Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
|
548
|
+
"""
|
549
|
+
lines = {}
|
550
|
+
lines["Application Name"] = self.CASE_TYPE
|
551
|
+
lines["Application Version"] = self.CASE_VERSION
|
552
|
+
if len(incidents) > 0:
|
553
|
+
lines["Incidents:"]=f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
|
554
|
+
if len(tracking_stats) > 0:
|
555
|
+
lines["Tracking Statistics:"]=f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
|
556
|
+
if len(business_analytics) > 0:
|
557
|
+
lines["Business Analytics:"]=f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
|
558
|
+
|
559
|
+
if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
|
560
|
+
lines["Summary"] = "No Summary Data"
|
561
|
+
|
562
|
+
return [lines]
|
563
|
+
|
276
564
|
def _calculate_fire_smoke_summary(
|
277
565
|
self, data: Any, config: FireSmokeConfig
|
278
566
|
) -> Dict[str, Any]:
|
@@ -304,113 +592,17 @@ class FireSmokeUseCase(BaseProcessor):
|
|
304
592
|
|
305
593
|
return {"total_objects": 0, "by_category": {}, "detections": []}
|
306
594
|
|
307
|
-
def
|
308
|
-
|
309
|
-
|
310
|
-
"""Generate insights using bbox area for intensity."""
|
311
|
-
|
312
|
-
insights = []
|
313
|
-
|
314
|
-
total = summary.get("total_objects", 0)
|
315
|
-
by_category = summary.get("by_category", {})
|
316
|
-
detections = summary.get("detections", [])
|
317
|
-
|
318
|
-
total_fire = by_category.get("fire", 0)
|
319
|
-
total_smoke = by_category.get("smoke", 0)
|
320
|
-
|
321
|
-
if total == 0:
|
322
|
-
insights.append("EVENT: No fire or smoke detected in the scene")
|
323
|
-
else:
|
324
|
-
if total_fire > 0:
|
325
|
-
insights.append(f"EVENT: {total_fire} fire region{'s' if total_fire != 1 else ''} detected")
|
326
|
-
if total_smoke > 0:
|
327
|
-
insights.append(f"EVENT: {total_smoke} smoke cloud{'s' if total_smoke != 1 else ''} detected")
|
328
|
-
|
329
|
-
fire_percent = (total_fire / total) * 100 if total else 0
|
330
|
-
smoke_percent = (total_smoke / total) * 100 if total else 0
|
331
|
-
insights.append(f"ANALYSIS: {fire_percent:.1f}% fire, {smoke_percent:.1f}% smoke in detected hazards")
|
332
|
-
|
333
|
-
# Calculate total bbox area using xmin, ymin, xmax, ymax format
|
334
|
-
total_area = 0.0
|
335
|
-
for det in detections:
|
336
|
-
bbox = det.get("bounding_box") or det.get("bbox")
|
337
|
-
if bbox:
|
338
|
-
xmin = bbox.get("xmin")
|
339
|
-
ymin = bbox.get("ymin")
|
340
|
-
xmax = bbox.get("xmax")
|
341
|
-
ymax = bbox.get("ymax")
|
342
|
-
if None not in (xmin, ymin, xmax, ymax):
|
343
|
-
width = xmax - xmin
|
344
|
-
height = ymax - ymin
|
345
|
-
if width > 0 and height > 0:
|
346
|
-
total_area += width * height
|
347
|
-
|
348
|
-
# Threshold area (configurable if you want)
|
349
|
-
threshold_area = 10000.0
|
350
|
-
|
351
|
-
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
352
|
-
|
353
|
-
if intensity_pct < 20:
|
354
|
-
insights.append(f"INTENSITY: Low fire/smoke activity ({intensity_pct:.1f}% area coverage)")
|
355
|
-
elif intensity_pct <= 50:
|
356
|
-
insights.append(f"INTENSITY: Moderate fire/smoke activity ({intensity_pct:.1f}%)")
|
357
|
-
elif intensity_pct <= 80:
|
358
|
-
insights.append(f"INTENSITY: High fire/smoke activity ({intensity_pct:.1f}%)")
|
359
|
-
else:
|
360
|
-
insights.append(f"INTENSITY: Very high fire/smoke activity — critical hazard ({intensity_pct:.1f}%)")
|
361
|
-
|
362
|
-
return insights
|
363
|
-
|
364
|
-
def _check_alerts(
|
365
|
-
self, summary: Dict, config: FireSmokeConfig
|
366
|
-
) -> List[Dict]:
|
367
|
-
"""Raise alerts if fire or smoke detected with severity based on intensity."""
|
368
|
-
|
369
|
-
alerts = []
|
370
|
-
total = summary.get("total_objects", 0)
|
371
|
-
by_category = summary.get("by_category", {})
|
372
|
-
detections = summary.get("detections", [])
|
373
|
-
|
374
|
-
if total == 0:
|
595
|
+
def _generate_business_analytics(self, counting_summary: Dict, alerts:Any, config: FireSmokeConfig, stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
|
596
|
+
"""Generate standardized business analytics for the agg_summary structure."""
|
597
|
+
if is_empty:
|
375
598
|
return []
|
376
599
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
ymin = bbox.get("ymin")
|
384
|
-
xmax = bbox.get("xmax")
|
385
|
-
ymax = bbox.get("ymax")
|
386
|
-
if None not in (xmin, ymin, xmax, ymax):
|
387
|
-
width = xmax - xmin
|
388
|
-
height = ymax - ymin
|
389
|
-
if width > 0 and height > 0:
|
390
|
-
total_area += width * height
|
391
|
-
|
392
|
-
threshold_area = 10000.0 # Same threshold as insights
|
393
|
-
|
394
|
-
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
395
|
-
|
396
|
-
# Determine alert severity
|
397
|
-
if intensity_pct > 80:
|
398
|
-
severity = "critical"
|
399
|
-
elif intensity_pct > 50:
|
400
|
-
severity = "warning"
|
401
|
-
else:
|
402
|
-
severity = "info"
|
403
|
-
|
404
|
-
alert = {
|
405
|
-
"type": "fire_smoke_alert",
|
406
|
-
"message": f"{total} fire/smoke detection{'s' if total != 1 else ''} with intensity {intensity_pct:.1f}%",
|
407
|
-
"severity": severity,
|
408
|
-
"detected_fire": by_category.get("fire", 0),
|
409
|
-
"detected_smoke": by_category.get("smoke", 0),
|
410
|
-
}
|
411
|
-
|
412
|
-
alerts.append(alert)
|
413
|
-
return alerts
|
600
|
+
#-----IF YOUR USECASE NEEDS BUSINESS ANALYTICS, YOU CAN USE THIS FUNCTION------#
|
601
|
+
#camera_info = self.get_camera_info_from_stream(stream_info)
|
602
|
+
# business_analytics = self.create_business_analytics(nalysis_name, statistics,
|
603
|
+
# human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
604
|
+
# reset_settings)
|
605
|
+
# return business_analytics
|
414
606
|
|
415
607
|
def _calculate_metrics(
|
416
608
|
self,
|
@@ -489,273 +681,55 @@ class FireSmokeUseCase(BaseProcessor):
|
|
489
681
|
self.logger.warning(f"Failed to extract predictions: {str(e)}")
|
490
682
|
|
491
683
|
return predictions
|
492
|
-
|
493
|
-
def
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
summary_parts = []
|
505
|
-
|
506
|
-
if total_fire > 0:
|
507
|
-
summary_parts.append(
|
508
|
-
f"{total_fire} fire region{'s' if total_fire != 1 else ''} detected"
|
509
|
-
)
|
510
|
-
|
511
|
-
if total_smoke > 0:
|
512
|
-
summary_parts.append(
|
513
|
-
f"{total_smoke} smoke cloud{'s' if total_smoke != 1 else ''} detected"
|
514
|
-
)
|
515
|
-
|
516
|
-
if alerts:
|
517
|
-
alert_count = len(alerts)
|
518
|
-
summary_parts.append(
|
519
|
-
f"{alert_count} alert{'s' if alert_count != 1 else ''}"
|
520
|
-
)
|
521
|
-
|
522
|
-
return ", ".join(summary_parts)
|
523
|
-
|
524
|
-
def _generate_events(
|
525
|
-
self,
|
526
|
-
summary: Dict,
|
527
|
-
alerts: List[Dict],
|
528
|
-
config: FireSmokeConfig,
|
529
|
-
frame_number: Optional[int] = None
|
530
|
-
) -> Dict:
|
531
|
-
"""Generate structured events for fire and smoke detection output with frame-aware keys."""
|
532
|
-
from datetime import datetime, timezone
|
533
|
-
|
534
|
-
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
535
|
-
events = {frame_key: []}
|
536
|
-
frame_events = events[frame_key]
|
537
|
-
|
538
|
-
total = summary.get("total_objects", 0)
|
539
|
-
by_category = summary.get("by_category", {})
|
540
|
-
detections = summary.get("detections", [])
|
541
|
-
|
542
|
-
total_fire = by_category.get("fire", 0)
|
543
|
-
total_smoke = by_category.get("smoke", 0)
|
544
|
-
|
545
|
-
if total > 0:
|
546
|
-
# Calculate total detection area
|
547
|
-
total_area = 0.0
|
548
|
-
for det in detections:
|
549
|
-
bbox = det.get("bounding_box") or det.get("bbox")
|
550
|
-
if bbox:
|
551
|
-
xmin = bbox.get("xmin")
|
552
|
-
ymin = bbox.get("ymin")
|
553
|
-
xmax = bbox.get("xmax")
|
554
|
-
ymax = bbox.get("ymax")
|
555
|
-
if None not in (xmin, ymin, xmax, ymax):
|
556
|
-
width = xmax - xmin
|
557
|
-
height = ymax - ymin
|
558
|
-
if width > 0 and height > 0:
|
559
|
-
total_area += width * height
|
560
|
-
|
561
|
-
threshold_area = 10000.0
|
562
|
-
intensity = min(10.0, (total_area / threshold_area) * 10)
|
563
|
-
|
564
|
-
if intensity >= 7:
|
565
|
-
level = "critical"
|
566
|
-
elif intensity >= 5:
|
567
|
-
level = "warning"
|
568
|
-
else:
|
569
|
-
level = "info"
|
570
|
-
|
571
|
-
# Use consistent formatting for human_text
|
572
|
-
human_lines = []
|
573
|
-
if total_fire > 0:
|
574
|
-
human_lines.append(" - fire detected")
|
575
|
-
if total_smoke > 0:
|
576
|
-
human_lines.append(" - smoke detected")
|
577
|
-
if total_fire == 0 and total_smoke == 0:
|
578
|
-
human_lines.append(" - no fire or smoke detected")
|
579
|
-
|
580
|
-
fire_smoke_event = {
|
581
|
-
"type": "fire_smoke_detection",
|
582
|
-
"stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
|
583
|
-
"level": level,
|
584
|
-
"intensity": round(intensity, 1),
|
585
|
-
"config": {
|
586
|
-
"min_value": 0,
|
587
|
-
"max_value": 10,
|
588
|
-
"level_settings": {"info": 2, "warning": 5, "critical": 7},
|
684
|
+
|
685
|
+
def get_config_schema(self) -> Dict[str, Any]:
|
686
|
+
"""Get configuration schema for fire and smoke detection."""
|
687
|
+
return {
|
688
|
+
"type": "object",
|
689
|
+
"properties": {
|
690
|
+
"confidence_threshold": {
|
691
|
+
"type": "number",
|
692
|
+
"minimum": 0.0,
|
693
|
+
"maximum": 1.0,
|
694
|
+
"default": 0.5,
|
695
|
+
"description": "Minimum confidence threshold for detections",
|
589
696
|
},
|
590
|
-
"
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
"type": alert.get("type", "fire_smoke_alert"),
|
611
|
-
"stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
|
612
|
-
"level": alert.get("severity", "warning"),
|
613
|
-
"intensity": 8.0,
|
614
|
-
"config": {
|
615
|
-
"min_value": 0,
|
616
|
-
"max_value": 10,
|
617
|
-
"level_settings": {"info": 2, "warning": 5, "critical": 7},
|
697
|
+
"fire_smoke_categories": {
|
698
|
+
"type": "array",
|
699
|
+
"items": {"type": "string"},
|
700
|
+
"default": ["Fire", "Smoke"],
|
701
|
+
"description": "Category names that represent fire and smoke",
|
702
|
+
},
|
703
|
+
"index_to_category": {
|
704
|
+
"type": "object",
|
705
|
+
"additionalProperties": {"type": "string"},
|
706
|
+
"description": "Mapping from category indices to names",
|
707
|
+
},
|
708
|
+
"alert_config": {
|
709
|
+
"type": "object",
|
710
|
+
"properties": {
|
711
|
+
"count_thresholds": {
|
712
|
+
"type": "object",
|
713
|
+
"additionalProperties": {"type": "integer", "minimum": 1},
|
714
|
+
"description": "Count thresholds for alerts",
|
715
|
+
}
|
716
|
+
},
|
618
717
|
},
|
619
|
-
"application_name": "Fire and Smoke Alert System",
|
620
|
-
"application_version": "1.0",
|
621
|
-
"location_info": None,
|
622
|
-
"human_text": alert_text,
|
623
|
-
}
|
624
|
-
frame_events.append(alert_event)
|
625
|
-
|
626
|
-
return events
|
627
|
-
|
628
|
-
def _generate_tracking_stats(
|
629
|
-
self,
|
630
|
-
summary: Dict,
|
631
|
-
insights: List[str],
|
632
|
-
summary_text: str,
|
633
|
-
config: FireSmokeConfig,
|
634
|
-
frame_number: Optional[int] = None,
|
635
|
-
stream_info: Optional[Dict[str, Any]] = None
|
636
|
-
) -> Dict:
|
637
|
-
"""Generate structured tracking stats for fire and smoke detection with frame-based keys."""
|
638
|
-
|
639
|
-
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
640
|
-
tracking_stats = {frame_key: []}
|
641
|
-
frame_tracking_stats = tracking_stats[frame_key]
|
642
|
-
|
643
|
-
total = summary.get("total_objects", 0)
|
644
|
-
by_category = summary.get("by_category", {})
|
645
|
-
detections = summary.get("detections", [])
|
646
|
-
|
647
|
-
total_fire = by_category.get("fire", 0)
|
648
|
-
total_smoke = by_category.get("smoke", 0)
|
649
|
-
|
650
|
-
# Maintain rolling detection history
|
651
|
-
if frame_number is not None:
|
652
|
-
self._fire_smoke_recent_history.append({
|
653
|
-
"frame": frame_number,
|
654
|
-
"fire": total_fire,
|
655
|
-
"smoke": total_smoke,
|
656
|
-
})
|
657
|
-
if len(self._fire_smoke_recent_history) > 150:
|
658
|
-
self._fire_smoke_recent_history.pop(0)
|
659
|
-
|
660
|
-
# Compute total bbox area for intensity percentage
|
661
|
-
total_area = 0.0
|
662
|
-
for det in detections:
|
663
|
-
bbox = det.get("bounding_box") or det.get("bbox")
|
664
|
-
if bbox:
|
665
|
-
xmin = bbox.get("xmin")
|
666
|
-
ymin = bbox.get("ymin")
|
667
|
-
xmax = bbox.get("xmax")
|
668
|
-
ymax = bbox.get("ymax")
|
669
|
-
if None not in (xmin, ymin, xmax, ymax):
|
670
|
-
width = xmax - xmin
|
671
|
-
height = ymax - ymin
|
672
|
-
if width > 0 and height > 0:
|
673
|
-
total_area += width * height
|
674
|
-
|
675
|
-
threshold_area = 10000.0
|
676
|
-
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
677
|
-
|
678
|
-
# Generate human-readable tracking text (people-style format)
|
679
|
-
current_timestamp = self._get_current_timestamp_str(stream_info)
|
680
|
-
start_timestamp = self._get_start_timestamp_str(stream_info)
|
681
|
-
|
682
|
-
human_lines = [f"CURRENT FRAME @ {current_timestamp}:"]
|
683
|
-
if total_fire > 0:
|
684
|
-
human_lines.append(f"\t- Fire regions detected: {total_fire}")
|
685
|
-
if total_smoke > 0:
|
686
|
-
human_lines.append(f"\t- Smoke clouds detected: {total_smoke}")
|
687
|
-
if total_fire == 0 and total_smoke == 0:
|
688
|
-
human_lines.append(f"\t- No fire or smoke detected")
|
689
|
-
|
690
|
-
human_lines.append("")
|
691
|
-
human_lines.append(f"ALERTS SINCE @ {start_timestamp}:")
|
692
|
-
|
693
|
-
recent_fire_detected = any(entry.get("fire", 0) > 0 for entry in self._fire_smoke_recent_history)
|
694
|
-
recent_smoke_detected = any(entry.get("smoke", 0) > 0 for entry in self._fire_smoke_recent_history)
|
695
|
-
|
696
|
-
if recent_fire_detected:
|
697
|
-
human_lines.append(f"\t- Fire alert")
|
698
|
-
if recent_smoke_detected:
|
699
|
-
human_lines.append(f"\t- Smoke alert")
|
700
|
-
if not recent_fire_detected and not recent_smoke_detected:
|
701
|
-
human_lines.append(f"\t- No fire or smoke detected in recent frames")
|
702
|
-
|
703
|
-
human_text = "\n".join(human_lines)
|
704
|
-
|
705
|
-
tracking_stat = {
|
706
|
-
"all_results_for_tracking": {
|
707
|
-
"total_detections": total,
|
708
|
-
"total_fire": total_fire,
|
709
|
-
"total_smoke": total_smoke,
|
710
|
-
"intensity_percentage": intensity_pct,
|
711
|
-
"fire_smoke_summary": summary,
|
712
|
-
"unique_count": self._count_unique_tracks(summary)
|
713
718
|
},
|
714
|
-
"
|
719
|
+
"required": ["confidence_threshold"],
|
720
|
+
"additionalProperties": False,
|
715
721
|
}
|
716
722
|
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
self,
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
frame_number: Optional[int] = None,
|
728
|
-
stream_info: Optional[Dict[str, Any]] = None,
|
729
|
-
) -> str:
|
730
|
-
"""Generate structured and formatted human_text for tracking stats."""
|
731
|
-
current_time_str = self._get_current_timestamp_str(stream_info)
|
732
|
-
start_time_str = self._get_start_timestamp_str(stream_info)
|
733
|
-
|
734
|
-
human_text_lines = []
|
735
|
-
human_text_lines.append(f"CURRENT FRAME @ {current_time_str}:")
|
736
|
-
|
737
|
-
if total_fire > 0:
|
738
|
-
human_text_lines.append("\t- fire detected")
|
739
|
-
if total_smoke > 0:
|
740
|
-
human_text_lines.append("\t- smoke detected")
|
741
|
-
if total_fire == 0 and total_smoke == 0:
|
742
|
-
human_text_lines.append("\t- no fire or smoke detected")
|
743
|
-
|
744
|
-
human_text_lines.append("") # Empty line for spacing
|
745
|
-
human_text_lines.append(f"ALERTS SINCE @ {start_time_str}:")
|
746
|
-
|
747
|
-
# Look into 150-frame history
|
748
|
-
recent_fire_detected = any(entry.get("fire", 0) > 0 for entry in self._fire_smoke_recent_history)
|
749
|
-
recent_smoke_detected = any(entry.get("smoke", 0) > 0 for entry in self._fire_smoke_recent_history)
|
750
|
-
|
751
|
-
if recent_fire_detected:
|
752
|
-
human_text_lines.append("\t- Fire alert")
|
753
|
-
if recent_smoke_detected:
|
754
|
-
human_text_lines.append("\t- Smoke alert")
|
755
|
-
if not recent_fire_detected and not recent_smoke_detected:
|
756
|
-
human_text_lines.append("\t- No fire or smoke detected in recent frames")
|
757
|
-
|
758
|
-
return "\n".join(human_text_lines)
|
723
|
+
def create_default_config(self, **overrides) -> FireSmokeConfig:
|
724
|
+
"""Create default configuration with optional overrides."""
|
725
|
+
defaults = {
|
726
|
+
"category": self.category,
|
727
|
+
"usecase": self.name,
|
728
|
+
"confidence_threshold": 0.5,
|
729
|
+
"fire_smoke_categories": ["Fire", "Smoke"],
|
730
|
+
}
|
731
|
+
defaults.update(overrides)
|
732
|
+
return FireSmokeConfig(**defaults)
|
759
733
|
|
760
734
|
def _count_unique_tracks(self, summary: Dict) -> Optional[int]:
|
761
735
|
"""Count unique track IDs from detections, if tracking info exists."""
|
@@ -771,63 +745,88 @@ class FireSmokeUseCase(BaseProcessor):
|
|
771
745
|
|
772
746
|
return len(unique_tracks) if unique_tracks else None
|
773
747
|
|
774
|
-
def
|
748
|
+
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
749
|
+
"""Format timestamp for video chunks (HH:MM:SS.ms format)."""
|
750
|
+
hours = int(timestamp // 3600)
|
751
|
+
minutes = int((timestamp % 3600) // 60)
|
752
|
+
seconds = round(float(timestamp % 60),2)
|
753
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
754
|
+
|
755
|
+
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
775
756
|
"""Get formatted current timestamp based on stream type."""
|
776
757
|
if not stream_info:
|
777
758
|
return "00:00:00.00"
|
759
|
+
# is_video_chunk = stream_info.get("input_settings", {}).get("is_video_chunk", False)
|
760
|
+
if precision:
|
761
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
762
|
+
if frame_id:
|
763
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
764
|
+
else:
|
765
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
766
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
767
|
+
return stream_time_str
|
768
|
+
else:
|
769
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
778
770
|
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
771
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
772
|
+
if frame_id:
|
773
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
774
|
+
else:
|
775
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
776
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
777
|
+
return stream_time_str
|
786
778
|
else:
|
787
|
-
|
779
|
+
# For streams, use stream_time from stream_info
|
780
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
788
781
|
if stream_time_str:
|
782
|
+
# Parse the high precision timestamp string to get timestamp
|
789
783
|
try:
|
784
|
+
# Remove " UTC" suffix and parse
|
790
785
|
timestamp_str = stream_time_str.replace(" UTC", "")
|
791
786
|
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
792
787
|
timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
|
793
788
|
return self._format_timestamp_for_stream(timestamp)
|
794
789
|
except:
|
790
|
+
# Fallback to current time if parsing fails
|
795
791
|
return self._format_timestamp_for_stream(time.time())
|
796
792
|
else:
|
797
793
|
return self._format_timestamp_for_stream(time.time())
|
798
794
|
|
799
|
-
def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]]) -> str:
|
800
|
-
"""Get formatted start timestamp for 'SINCE'
|
795
|
+
def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
|
796
|
+
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
801
797
|
if not stream_info:
|
802
798
|
return "00:00:00"
|
799
|
+
if precision:
|
800
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
801
|
+
return "00:00:00"
|
802
|
+
else:
|
803
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
803
804
|
|
804
|
-
|
805
|
-
|
806
|
-
if is_video_chunk or stream_info.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
|
805
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
806
|
+
# If video format, start from 00:00:00
|
807
807
|
return "00:00:00"
|
808
808
|
else:
|
809
|
+
# For streams, use tracking start time or current time with minutes/seconds reset
|
809
810
|
if self._tracking_start_time is None:
|
810
|
-
|
811
|
+
# Try to extract timestamp from stream_time string
|
812
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
811
813
|
if stream_time_str:
|
812
814
|
try:
|
815
|
+
# Remove " UTC" suffix and parse
|
813
816
|
timestamp_str = stream_time_str.replace(" UTC", "")
|
814
817
|
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
815
818
|
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
816
819
|
except:
|
820
|
+
# Fallback to current time if parsing fails
|
817
821
|
self._tracking_start_time = time.time()
|
818
822
|
else:
|
819
823
|
self._tracking_start_time = time.time()
|
820
824
|
|
821
825
|
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
826
|
+
# Reset minutes and seconds to 00:00 for "TOTAL SINCE" format
|
822
827
|
dt = dt.replace(minute=0, second=0, microsecond=0)
|
823
828
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
824
829
|
|
825
|
-
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
826
|
-
hours = int(timestamp // 3600)
|
827
|
-
minutes = int((timestamp % 3600) // 60)
|
828
|
-
seconds = timestamp % 60
|
829
|
-
return f"{hours:02d}:{minutes:02d}:{seconds:06.2f}"
|
830
|
-
|
831
830
|
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
832
831
|
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
833
832
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|