matrice 1.0.99126__py3-none-any.whl → 1.0.99128__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.
@@ -1,22 +1,28 @@
1
1
  from typing import Any, Dict, List, Optional
2
- from dataclasses import dataclass, field
2
+ from dataclasses import asdict
3
3
  import time
4
4
  from datetime import datetime, timezone
5
5
 
6
6
  from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
7
7
  from ..utils import (
8
8
  filter_by_confidence,
9
+ filter_by_categories,
9
10
  apply_category_mapping,
10
11
  count_objects_by_category,
12
+ count_objects_in_zones,
13
+ calculate_counting_summary,
14
+ match_results_structure,
11
15
  bbox_smoothing,
12
16
  BBoxSmoothingConfig,
13
17
  BBoxSmoothingTracker
14
18
  )
15
- from ..core.config import BaseConfig, AlertConfig
19
+ from dataclasses import dataclass, field
20
+ from ..core.config import BaseConfig, AlertConfig, ZoneConfig
21
+
16
22
 
17
23
  @dataclass
18
24
  class WeldDefectConfig(BaseConfig):
19
- """Configuration for Weld Defect detection use case."""
25
+ """Configuration for weld defect detection use case."""
20
26
  enable_smoothing: bool = True
21
27
  smoothing_algorithm: str = "observability"
22
28
  smoothing_window_size: int = 20
@@ -41,13 +47,23 @@ class WeldDefectConfig(BaseConfig):
41
47
  }
42
48
  )
43
49
 
50
+
44
51
  class WeldDefectUseCase(BaseProcessor):
45
- """Weld Defect detection use case with smoothing and alerting."""
46
-
52
+ CATEGORY_DISPLAY = {
53
+ "Bad Welding": "Bad Welding",
54
+ "Crack": "Crack",
55
+ "Porosity": "Porosity",
56
+ "Spatters": "Spatters",
57
+ "Good Welding": "Good Welding",
58
+ "Reinforcement": "Reinforcement"
59
+ }
60
+
47
61
  def __init__(self):
48
62
  super().__init__("weld_defect_detection")
49
63
  self.category = "weld"
50
- self.defect_categories = ['Bad Welding', 'Crack', 'Porosity', 'Spatters', 'Good Welding', 'Reinforcement']
64
+ self.CASE_TYPE: Optional[str] = 'weld_defect_detection'
65
+ self.CASE_VERSION: Optional[str] = '1.0'
66
+ self.target_categories = ['Bad Welding', 'Crack', 'Porosity', 'Spatters']
51
67
  self.smoothing_tracker = None
52
68
  self.tracker = None
53
69
  self._total_frame_counter = 0
@@ -57,142 +73,36 @@ class WeldDefectUseCase(BaseProcessor):
57
73
  self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
58
74
  self._track_merge_iou_threshold: float = 0.05
59
75
  self._track_merge_time_window: float = 7.0
76
+ self._ascending_alert_list: List[int] = []
77
+ self.current_incident_end_timestamp: str = "N/A"
60
78
 
61
- def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
62
- frame_track_ids = set()
63
- for det in detections:
64
- tid = det.get('track_id')
65
- if tid is not None:
66
- frame_track_ids.add(tid)
67
- total_track_ids = set()
68
- for s in getattr(self, '_defect_total_track_ids', {}).values():
69
- total_track_ids.update(s)
70
- return {
71
- "total_count": len(total_track_ids),
72
- "current_frame_count": len(frame_track_ids),
73
- "total_unique_track_ids": len(total_track_ids),
74
- "current_frame_track_ids": list(frame_track_ids),
75
- "last_update_time": time.time(),
76
- "total_frames_processed": getattr(self, '_total_frame_counter', 0)
77
- }
78
-
79
- @staticmethod
80
- def _iou(bbox1, bbox2):
81
- x1 = max(bbox1["xmin"], bbox2["xmin"])
82
- y1 = max(bbox1["ymin"], bbox2["ymin"])
83
- x2 = min(bbox1["xmax"], bbox2["xmax"])
84
- y2 = min(bbox1["ymax"], bbox2["ymax"])
85
- inter_w = max(0, x2 - x1)
86
- inter_h = max(0, y2 - y1)
87
- inter_area = inter_w * inter_h
88
- area1 = (bbox1["xmax"] - bbox1["xmin"]) * (bbox1["ymax"] - bbox1["ymin"])
89
- area2 = (bbox2["xmax"] - bbox2["xmin"]) * (bbox2["ymax"] - bbox2["ymin"])
90
- union = area1 + area2 - inter_area
91
- return 0.0 if union == 0 else inter_area / union
92
-
93
- @staticmethod
94
- def _deduplicate_defects(detections, iou_thresh=0.7):
95
- filtered = []
96
- used = [False] * len(detections)
97
- for i, det in enumerate(detections):
98
- if used[i]:
99
- continue
100
- group = [i]
101
- for j in range(i+1, len(detections)):
102
- if used[j]:
103
- continue
104
- if det.get("category") == detections[j].get("category"):
105
- bbox1 = det.get("bounding_box")
106
- bbox2 = detections[j].get("bounding_box")
107
- if bbox1 and bbox2:
108
- iou = WeldDefectUseCase._iou(bbox1, bbox2)
109
- if iou > iou_thresh:
110
- used[j] = True
111
- group.append(j)
112
- best_idx = max(group, key=lambda idx: detections[idx].get("confidence", 0))
113
- filtered.append(detections[best_idx])
114
- used[best_idx] = True
115
- return filtered
116
-
117
- def _update_defect_tracking_state(self, detections: list):
118
- if not hasattr(self, "_defect_total_track_ids"):
119
- self._defect_total_track_ids = {cat: set() for cat in self.defect_categories}
120
- self._defect_current_frame_track_ids = {cat: set() for cat in self.defect_categories}
121
-
122
- for det in detections:
123
- cat = det.get("category")
124
- raw_track_id = det.get("track_id")
125
- if cat not in self.defect_categories or raw_track_id is None:
126
- continue
127
- bbox = det.get("bounding_box", det.get("bbox"))
128
- canonical_id = self._merge_or_register_track(raw_track_id, bbox)
129
- det["track_id"] = canonical_id
130
- self._defect_total_track_ids.setdefault(cat, set()).add(canonical_id)
131
- self._defect_current_frame_track_ids[cat].add(canonical_id)
132
-
133
- def get_total_defect_counts(self):
134
- return {cat: len(ids) for cat, ids in getattr(self, '_defect_total_track_ids', {}).items()}
135
-
136
- def _format_timestamp_for_video(self, timestamp: float) -> str:
137
- hours = int(timestamp // 3600)
138
- minutes = int((timestamp % 3600) // 60)
139
- seconds = timestamp % 60
140
- return f"{hours:02d}:{minutes:02d}:{seconds:06.2f}"
141
-
142
- def _format_timestamp_for_stream(self, timestamp: float) -> str:
143
- dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
144
- return dt.strftime('%Y:%m:%d %H:%M:%S')
145
-
146
- def _get_current_timestamp_str(self, stream: Optional[Dict[str, Any]]) -> str:
147
- if not stream:
148
- return "00:00:00.00"
149
- if stream.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
150
- stream_time_str = stream.get("video_timestamp", "")
151
- return stream_time_str[:8]
152
- else:
153
- stream_time_str = stream.get("stream_time", "")
154
- if stream_time_str:
155
- try:
156
- timestamp_str = stream_time_str.replace(" UTC", "")
157
- dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
158
- timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
159
- return self._format_timestamp_for_stream(timestamp)
160
- except:
161
- return self._format_timestamp_for_stream(time.time())
162
- return self._format_timestamp_for_stream(time.time())
163
-
164
- def _get_start_timestamp_str(self, stream: Optional[Dict[str, Any]]) -> str:
165
- if not stream:
166
- return "00:00:00"
167
- if stream.get("input_settings", {}).get("is_video_chunk", False) or stream.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
168
- return "00:00:00"
169
- else:
170
- if self._tracking_start_time is None:
171
- stream_time_str = stream.get("stream_time", "")
172
- if stream_time_str:
173
- try:
174
- timestamp_str = stream_time_str.replace(" UTC", "")
175
- dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
176
- self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
177
- except:
178
- self._tracking_start_time = time.time()
179
- else:
180
- self._tracking_start_time = time.time()
181
- dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
182
- dt = dt.replace(minute=0, second=0, microsecond=0)
183
- return dt.strftime('%Y:%m:%d %H:%M:%S')
184
-
185
- def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None, stream: Optional[Dict[str, Any]] = None) -> ProcessingResult:
79
+ def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
80
+ stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
186
81
  start_time = time.time()
187
82
  if not isinstance(config, WeldDefectConfig):
188
- return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
83
+ return self.create_error_result("Invalid config type", usecase=self.name, category=self.category,
84
+ context=context)
189
85
  if context is None:
190
86
  context = ProcessingContext()
191
87
 
192
- processed_data = filter_by_confidence(data, config.confidence_threshold) if config.confidence_threshold else data
88
+ input_format = match_results_structure(data)
89
+ context.input_format = input_format
90
+ context.confidence_threshold = config.confidence_threshold
91
+
92
+ if config.confidence_threshold is not None:
93
+ processed_data = filter_by_confidence(data, config.confidence_threshold)
94
+ self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
95
+ else:
96
+ processed_data = data
97
+ self.logger.debug("No confidence filtering applied")
98
+
193
99
  if config.index_to_category:
194
100
  processed_data = apply_category_mapping(processed_data, config.index_to_category)
195
- processed_data = [d for d in processed_data if d.get('category') in self.defect_categories]
101
+ self.logger.debug("Applied category mapping")
102
+
103
+ if config.target_defect_categories:
104
+ processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
105
+ self.logger.debug("Applied category filtering")
196
106
 
197
107
  if config.enable_smoothing:
198
108
  if self.smoothing_tracker is None:
@@ -213,185 +123,387 @@ class WeldDefectUseCase(BaseProcessor):
213
123
  if self.tracker is None:
214
124
  tracker_config = TrackerConfig()
215
125
  self.tracker = AdvancedTracker(tracker_config)
126
+ self.logger.info("Initialized AdvancedTracker for Weld Defect Detection")
216
127
  processed_data = self.tracker.update(processed_data)
217
128
  except Exception as e:
218
129
  self.logger.warning(f"AdvancedTracker failed: {e}")
219
130
 
220
- processed_data = self._deduplicate_defects(processed_data, iou_thresh=0.95)
221
- self._update_defect_tracking_state(processed_data)
131
+ self._update_tracking_state(processed_data)
222
132
  self._total_frame_counter += 1
223
133
 
224
134
  frame_number = None
225
- if stream:
226
- input_settings = stream.get("input_settings", {})
135
+ if stream_info:
136
+ input_settings = stream_info.get("input_settings", {})
227
137
  start_frame = input_settings.get("start_frame")
228
138
  end_frame = input_settings.get("end_frame")
229
139
  if start_frame is not None and end_frame is not None and start_frame == end_frame:
230
140
  frame_number = start_frame
231
141
 
142
+ general_counting_summary = calculate_counting_summary(data)
232
143
  counting_summary = self._count_categories(processed_data, config)
233
- total_defect_counts = self.get_total_defect_counts()
234
- counting_summary['total_defect_counts'] = total_defect_counts
235
- insights = self._generate_insights(counting_summary, config)
236
- alerts = self._check_alerts(counting_summary, config)
144
+ total_counts = self.get_total_counts()
145
+ counting_summary['total_counts'] = total_counts
146
+ alerts = self._check_alerts(counting_summary, frame_number, config)
237
147
  predictions = self._extract_predictions(processed_data)
238
- summary = self._generate_summary(counting_summary, alerts)
239
148
 
240
- events_list = self._generate_events(counting_summary, alerts, config, frame_number, stream)
241
- tracking_stats_list = self._generate_tracking_stats(counting_summary, insights, summary, config, frame_number, stream)
149
+ incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
150
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
151
+ business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
152
+ summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
242
153
 
243
- events = events_list[0] if events_list else {}
154
+ incidents = incidents_list[0] if incidents_list else {}
244
155
  tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
156
+ business_analytics = business_analytics_list[0] if business_analytics_list else {}
157
+ summary = summary_list[0] if summary_list else {}
158
+ agg_summary = {str(frame_number): {
159
+ "incidents": incidents,
160
+ "tracking_stats": tracking_stats,
161
+ "business_analytics": business_analytics,
162
+ "alerts": alerts,
163
+ "human_text": summary}
164
+ }
245
165
 
246
166
  context.mark_completed()
247
167
  result = self.create_result(
248
- data={
249
- "counting_summary": counting_summary,
250
- "alerts": alerts,
251
- "total_defects": counting_summary.get("total_count", 0),
252
- "events": events,
253
- "tracking_stats": tracking_stats,
254
- },
168
+ data={"agg_summary": agg_summary},
255
169
  usecase=self.name,
256
170
  category=self.category,
257
171
  context=context
258
172
  )
259
- result.summary = summary
260
- result.insights = insights
261
- result.predictions = predictions
262
173
  return result
263
174
 
264
- def reset_tracker(self) -> None:
265
- if self.tracker is not None:
266
- self.tracker.reset()
267
- self.logger.info("AdvancedTracker reset")
175
+ def _check_alerts(self, summary: dict, frame_number: Any, config: WeldDefectConfig) -> List[Dict]:
176
+ def get_trend(data, lookback=900, threshold=0.6):
177
+ window = data[-lookback:] if len(data) >= lookback else data
178
+ if len(window) < 2:
179
+ return True
180
+ increasing = 0
181
+ total = 0
182
+ for i in range(1, len(window)):
183
+ if window[i] >= window[i - 1]:
184
+ increasing += 1
185
+ total += 1
186
+ ratio = increasing / total
187
+ return ratio >= threshold
268
188
 
269
- def reset_defect_tracking(self) -> None:
270
- self._defect_total_track_ids = {cat: set() for cat in self.defect_categories}
271
- self._total_frame_counter = 0
272
- self._global_frame_offset = 0
273
- self._tracking_start_time = None
274
- self._track_aliases.clear()
275
- self._canonical_tracks.clear()
276
- self.logger.info("Weld Defect tracking state reset")
189
+ frame_key = str(frame_number) if frame_number is not None else "current_frame"
190
+ alerts = []
191
+ total_detections = summary.get("total_count", 0)
192
+ total_counts_dict = summary.get("total_counts", {})
193
+ per_category_count = summary.get("per_category_count", {})
277
194
 
278
- def reset_all_tracking(self) -> None:
279
- self.reset_tracker()
280
- self.reset_defect_tracking()
281
- self.logger.info("All Weld Defect tracking state reset")
195
+ if not config.alert_config:
196
+ return alerts
282
197
 
283
- def _generate_events(self, counting_summary: Dict, alerts: List, config: WeldDefectConfig, frame_number: Optional[int] = None, stream: Optional[Dict[str, Any]] = None) -> List[Dict]:
284
- frame_key = str(frame_number) if frame_number is not None else "current_frame"
285
- events = [{frame_key: []}]
286
- frame_events = events[0][frame_key]
287
- total_defects = counting_summary.get("total_count", 0)
198
+ if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
199
+ for category, threshold in config.alert_config.count_thresholds.items():
200
+ if category == "all" and total_detections > threshold:
201
+ alerts.append({
202
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
203
+ "alert_id": f"alert_{category}_{frame_key}",
204
+ "incident_category": self.CASE_TYPE,
205
+ "threshold_level": threshold,
206
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
207
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
208
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
209
+ })
210
+ elif category in per_category_count:
211
+ count = per_category_count[category]
212
+ if count > threshold:
213
+ alerts.append({
214
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
215
+ "alert_id": f"alert_{category}_{frame_key}",
216
+ "incident_category": self.CASE_TYPE,
217
+ "threshold_level": threshold,
218
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
219
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
220
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
221
+ })
222
+ return alerts
288
223
 
289
- if total_defects > 0:
290
- level = "info"
224
+ def _generate_incidents(self, counting_summary: Dict, alerts: List, config: WeldDefectConfig,
225
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
226
+ incidents = []
227
+ total_detections = counting_summary.get("total_count", 0)
228
+ current_timestamp = self._get_current_timestamp_str(stream_info)
229
+ camera_info = self.get_camera_info_from_stream(stream_info)
230
+ self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
231
+
232
+ if total_detections > 0:
233
+ level = "low"
291
234
  intensity = 5.0
235
+ start_timestamp = self._get_start_timestamp_str(stream_info)
236
+ if start_timestamp and self.current_incident_end_timestamp == 'N/A':
237
+ self.current_incident_end_timestamp = 'Incident still active'
238
+ elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
239
+ if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
240
+ self.current_incident_end_timestamp = current_timestamp
241
+ elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
242
+ self.current_incident_end_timestamp = 'N/A'
243
+
292
244
  if config.alert_config and config.alert_config.count_thresholds:
293
- threshold = config.alert_config.count_thresholds.get("all", 5)
294
- intensity = min(10.0, (total_defects / threshold) * 10)
295
- level = "critical" if intensity >= 7 else "warning" if intensity >= 5 else "info"
245
+ threshold = config.alert_config.count_thresholds.get("all", 15)
246
+ intensity = min(10.0, (total_detections / threshold) * 10)
247
+ if intensity >= 9:
248
+ level = "critical"
249
+ self._ascending_alert_list.append(3)
250
+ elif intensity >= 7:
251
+ level = "significant"
252
+ self._ascending_alert_list.append(2)
253
+ elif intensity >= 5:
254
+ level = "medium"
255
+ self._ascending_alert_list.append(1)
256
+ else:
257
+ level = "low"
258
+ self._ascending_alert_list.append(0)
296
259
  else:
297
- level = "critical" if total_defects > 10 else "warning" if total_defects > 5 else "info"
298
- intensity = min(10.0, total_defects / 2.0)
260
+ if total_detections > 30:
261
+ level = "critical"
262
+ intensity = 10.0
263
+ self._ascending_alert_list.append(3)
264
+ elif total_detections > 25:
265
+ level = "significant"
266
+ intensity = 9.0
267
+ self._ascending_alert_list.append(2)
268
+ elif total_detections > 15:
269
+ level = "medium"
270
+ intensity = 7.0
271
+ self._ascending_alert_list.append(1)
272
+ else:
273
+ level = "low"
274
+ intensity = min(10.0, total_detections / 3.0)
275
+ self._ascending_alert_list.append(0)
299
276
 
300
- human_text_lines = ["EVENTS DETECTED:"]
301
- human_text_lines.append(f" - {total_defects} Weld Defect(s) detected [INFO]")
277
+ human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
278
+ human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
302
279
  human_text = "\n".join(human_text_lines)
303
280
 
304
- event = {
305
- "type": "weld_defect_detection",
306
- "stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
307
- "level": level,
308
- "intensity": round(intensity, 1),
309
- "config": {"min_value": 0, "max_value": 10, "level_settings": {"info": 2, "warning": 5, "critical": 7}},
310
- "application_name": "Weld Defect Detection System",
311
- "application_version": "1.0",
312
- "location_info": None,
313
- "human_text": human_text
314
- }
315
- frame_events.append(event)
316
-
317
- for alert in alerts:
318
- total_defects = counting_summary.get("total_count", 0)
319
- intensity_message = "ALERT: Low defect density"
320
- if config.alert_config and config.alert_config.count_thresholds:
321
- threshold = config.alert_config.count_thresholds.get("all", 5)
322
- percentage = (total_defects / threshold) * 100 if threshold > 0 else 0
323
- intensity_message = f"ALERT: {'Severe' if percentage > 70 else 'Heavy' if percentage > 50 else 'Moderate' if percentage > 20 else 'Low'} defect density"
324
- else:
325
- intensity_message = f"ALERT: {'Heavy' if total_defects > 5 else 'Low'} defect density"
326
-
327
- alert_event = {
328
- "type": alert.get("type", "defect_alert"),
329
- "stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
330
- "level": alert.get("severity", "warning"),
331
- "intensity": 8.0,
332
- "config": {"min_value": 0, "max_value": 10, "level_settings": {"info": 2, "warning": 5, "critical": 7}},
333
- "application_name": "Defect Alert System",
334
- "application_version": "1.0",
335
- "location_info": alert.get("zone"),
336
- "human_text": f"{datetime.now(timezone.utc).strftime('%Y-%m-%d-%H:%M:%S UTC')} : {intensity_message}"
337
- }
338
- frame_events.append(alert_event)
281
+ alert_settings = []
282
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
283
+ alert_settings.append({
284
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
285
+ "incident_category": self.CASE_TYPE,
286
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
287
+ "ascending": True,
288
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
289
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
290
+ })
291
+
292
+ event = self.create_incident(
293
+ incident_id=f"{self.CASE_TYPE}_{str(frame_number)}",
294
+ incident_type=self.CASE_TYPE,
295
+ severity_level=level,
296
+ human_text=human_text,
297
+ camera_info=camera_info,
298
+ alerts=alerts,
299
+ alert_settings=alert_settings,
300
+ start_time=start_timestamp,
301
+ end_time=self.current_incident_end_timestamp,
302
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
303
+ )
304
+ incidents.append(event)
305
+ else:
306
+ self._ascending_alert_list.append(0)
307
+ incidents.append({})
339
308
 
340
- return events
309
+ return incidents
341
310
 
342
- def _generate_tracking_stats(self, counting_summary: Dict, insights: List[str], summary: str, config: WeldDefectConfig, frame_number: Optional[int] = None, stream: Optional[Dict[str, Any]] = None) -> List[Dict]:
343
- frame_key = str(frame_number) if frame_number is not None else "current_frame"
344
- tracking_stats = [{frame_key: []}]
345
- frame_tracking_stats = tracking_stats[0][frame_key]
346
-
347
- total_defects = counting_summary.get("total_count", 0)
348
- total_defect_counts = counting_summary.get("total_defect_counts", {})
349
- cumulative_total = sum(total_defect_counts.values()) if total_defect_counts else 0
311
+ def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: WeldDefectConfig,
312
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
313
+ camera_info = self.get_camera_info_from_stream(stream_info)
314
+ tracking_stats = []
315
+ total_detections = counting_summary.get("total_count", 0)
316
+ total_counts_dict = counting_summary.get("total_counts", {})
350
317
  per_category_count = counting_summary.get("per_category_count", {})
351
-
352
- track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
353
- current_timestamp = self._get_current_timestamp_str(stream)
354
- start_timestamp = self._get_start_timestamp_str(stream)
355
-
356
- human_text_lines = []
357
- human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
358
- if total_defects > 0:
359
- category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
360
- if len(category_counts) == 1:
361
- defects_text = category_counts[0] + " detected"
362
- elif len(category_counts) == 2:
363
- defects_text = f"{category_counts[0]} and {category_counts[1]} detected"
318
+ current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
319
+ start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
320
+ high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
321
+ high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
322
+
323
+ total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
324
+ current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
325
+
326
+ detections = []
327
+ for detection in counting_summary.get("detections", []):
328
+ bbox = detection.get("bounding_box", {})
329
+ category = detection.get("category", "defect")
330
+ if detection.get("masks"):
331
+ segmentation = detection.get("masks", [])
332
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
333
+ elif detection.get("segmentation"):
334
+ segmentation = detection.get("segmentation")
335
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
336
+ elif detection.get("mask"):
337
+ segmentation = detection.get("mask")
338
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
364
339
  else:
365
- defects_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
366
- human_text_lines.append(f"\t- {defects_text}")
340
+ detection_obj = self.create_detection_object(category, bbox)
341
+ detections.append(detection_obj)
342
+
343
+ alert_settings = []
344
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
345
+ alert_settings.append({
346
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
347
+ "incident_category": self.CASE_TYPE,
348
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
349
+ "ascending": True,
350
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
351
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
352
+ })
353
+
354
+ human_text_lines = [f"Tracking Statistics:"]
355
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
356
+ for cat, count in per_category_count.items():
357
+ human_text_lines.append(f"\t{cat}: {count}")
358
+ human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
359
+ for cat, count in total_counts_dict.items():
360
+ if count > 0:
361
+ human_text_lines.append(f"\t{cat}: {count}")
362
+ if alerts:
363
+ for alert in alerts:
364
+ human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
367
365
  else:
368
- human_text_lines.append(f"\t- No defects detected")
366
+ human_text_lines.append("Alerts: None")
367
+ human_text = "\n".join(human_text_lines)
369
368
 
370
- human_text_lines.append("")
371
- human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
372
- human_text_lines.append(f"\t- Total Defects Detected: {cumulative_total}")
373
- for cat, count in total_defect_counts.items():
374
- if count > 0:
375
- human_text_lines.append(f"\t- {cat}: {count}")
369
+ reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
370
+ tracking_stat = self.create_tracking_stats(
371
+ total_counts=total_counts,
372
+ current_counts=current_counts,
373
+ detections=detections,
374
+ human_text=human_text,
375
+ camera_info=camera_info,
376
+ alerts=alerts,
377
+ alert_settings=alert_settings,
378
+ reset_settings=reset_settings,
379
+ start_time=high_precision_start_timestamp,
380
+ reset_time=high_precision_reset_timestamp
381
+ )
382
+ tracking_stats.append(tracking_stat)
383
+ return tracking_stats
376
384
 
377
- human_text = "\n".join(human_text_lines)
385
+ def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: WeldDefectConfig,
386
+ stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
387
+ if is_empty:
388
+ return []
389
+
390
+ def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[dict]:
391
+ lines = {}
392
+ lines["Application Name"] = self.CASE_TYPE
393
+ lines["Application Version"] = self.CASE_VERSION
394
+ if len(incidents) > 0:
395
+ lines["Incidents"] = f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
396
+ if len(tracking_stats) > 0:
397
+ lines["Tracking Statistics"] = f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
398
+ if len(business_analytics) > 0:
399
+ lines["Business Analytics"] = f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
400
+ if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
401
+ lines["Summary"] = "No Summary Data"
402
+ return [lines]
378
403
 
379
- tracking_stat = {
380
- "type": "weld_defect_tracking",
381
- "category": "weld",
382
- "count": total_defects,
383
- "insights": insights,
384
- "summary": summary,
385
- "timestamp": datetime.now(timezone.utc).strftime('%Y-%m-%d-%H:%M:%S UTC'),
386
- "human_text": human_text,
387
- "track_ids_info": track_ids_info,
388
- "global_frame_offset": getattr(self, '_global_frame_offset', 0),
389
- "local_frame_id": frame_key,
390
- "detections": counting_summary.get("detections", [])
404
+ def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
405
+ frame_track_ids = set()
406
+ for det in detections:
407
+ tid = det.get('track_id')
408
+ if tid is not None:
409
+ frame_track_ids.add(tid)
410
+ total_track_ids = set()
411
+ for s in getattr(self, '_per_category_total_track_ids', {}).values():
412
+ total_track_ids.update(s)
413
+ return {
414
+ "total_count": len(total_track_ids),
415
+ "current_frame_count": len(frame_track_ids),
416
+ "total_unique_track_ids": len(total_track_ids),
417
+ "current_frame_track_ids": list(frame_track_ids),
418
+ "last_update_time": time.time(),
419
+ "total_frames_processed": getattr(self, '_total_frame_counter', 0)
391
420
  }
392
421
 
393
- frame_tracking_stats.append(tracking_stat)
394
- return tracking_stats
422
+ def _update_tracking_state(self, detections: list):
423
+ if not hasattr(self, "_per_category_total_track_ids"):
424
+ self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
425
+ self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
426
+
427
+ for det in detections:
428
+ cat = det.get("category")
429
+ raw_track_id = det.get("track_id")
430
+ if cat not in self.target_categories or raw_track_id is None:
431
+ continue
432
+ bbox = det.get("bounding_box", det.get("bbox"))
433
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
434
+ det["track_id"] = canonical_id
435
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
436
+ self._current_frame_track_ids[cat].add(canonical_id)
437
+
438
+ def get_total_counts(self):
439
+ return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
440
+
441
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
442
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
443
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
444
+
445
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
446
+ hours = int(timestamp // 3600)
447
+ minutes = int((timestamp % 3600) // 60)
448
+ seconds = round(float(timestamp % 60), 2)
449
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
450
+
451
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str] = None) -> str:
452
+ if not stream_info:
453
+ return "00:00:00.00"
454
+ if precision:
455
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
456
+ if frame_id:
457
+ start_time = int(frame_id) / stream_info.get("input_settings", {}).get("original_fps", 30)
458
+ else:
459
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30) / stream_info.get("input_settings", {}).get("original_fps", 30)
460
+ return self._format_timestamp_for_video(start_time)
461
+ else:
462
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
463
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
464
+ if frame_id:
465
+ start_time = int(frame_id) / stream_info.get("input_settings", {}).get("original_fps", 30)
466
+ else:
467
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30) / stream_info.get("input_settings", {}).get("original_fps", 30)
468
+ return self._format_timestamp_for_video(start_time)
469
+ else:
470
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
471
+ if stream_time_str:
472
+ try:
473
+ timestamp_str = stream_time_str.replace(" UTC", "")
474
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
475
+ timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
476
+ return self._format_timestamp_for_stream(timestamp)
477
+ except:
478
+ return self._format_timestamp_for_stream(time.time())
479
+ else:
480
+ return self._format_timestamp_for_stream(time.time())
481
+
482
+ def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
483
+ if not stream_info:
484
+ return "00:00:00"
485
+ if precision:
486
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
487
+ return "00:00:00"
488
+ else:
489
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
490
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
491
+ return "00:00:00"
492
+ else:
493
+ if self._tracking_start_time is None:
494
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
495
+ if stream_time_str:
496
+ try:
497
+ timestamp_str = stream_time_str.replace(" UTC", "")
498
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
499
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
500
+ except:
501
+ self._tracking_start_time = time.time()
502
+ else:
503
+ self._tracking_start_time = time.time()
504
+ dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
505
+ dt = dt.replace(minute=0, second=0, microsecond=0)
506
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
395
507
 
396
508
  def _count_categories(self, detections: list, config: WeldDefectConfig) -> dict:
397
509
  counts = {}
@@ -413,61 +525,6 @@ class WeldDefectUseCase(BaseProcessor):
413
525
  ]
414
526
  }
415
527
 
416
- CATEGORY_DISPLAY = {
417
- "Bad Welding": "Bad Welding",
418
- "Crack": "Crack",
419
- "Porosity": "Porosity",
420
- "Spatters": "Spatters",
421
- "Good Welding": "Good Welding",
422
- "Reinforcement": "Reinforcement"
423
- }
424
-
425
- def _generate_insights(self, summary: dict, config: WeldDefectConfig) -> List[str]:
426
- insights = []
427
- per_cat = summary.get("per_category_count", {})
428
- total_defects = summary.get("total_count", 0)
429
-
430
- if total_defects == 0:
431
- insights.append("No weld defects detected in the scene")
432
- return insights
433
- insights.append(f"EVENT: Detected {total_defects} weld defect(s) in the scene")
434
- intensity_threshold = config.alert_config.count_thresholds.get("all", 5) if config.alert_config and config.alert_config.count_thresholds else 5
435
- percentage = (total_defects / intensity_threshold) * 100
436
- insights.append(f"INTENSITY: {'Severe' if percentage > 70 else 'Heavy' if percentage > 50 else 'Moderate' if percentage > 20 else 'Low'} defect density ({percentage:.1f}% of capacity)")
437
- for cat, count in per_cat.items():
438
- display = self.CATEGORY_DISPLAY.get(cat, cat)
439
- insights.append(f"{display}: {count}")
440
- return insights
441
-
442
- def _check_alerts(self, summary: dict, config: WeldDefectConfig) -> List[Dict]:
443
- alerts = []
444
- if not config.alert_config:
445
- return alerts
446
- total = summary.get("total_count", 0)
447
- if config.alert_config.count_thresholds:
448
- for category, threshold in config.alert_config.count_thresholds.items():
449
- if category == "all" and total >= threshold:
450
- alerts.append({
451
- "type": "count_threshold",
452
- "severity": "warning",
453
- "message": f"Total defect count ({total}) exceeds threshold ({threshold})",
454
- "category": category,
455
- "current_count": total,
456
- "threshold": threshold
457
- })
458
- elif category in summary.get("per_category_count", {}):
459
- count = summary.get("per_category_count", {})[category]
460
- if count >= threshold:
461
- alerts.append({
462
- "type": "count_threshold",
463
- "severity": "warning",
464
- "message": f"{category} count ({count}) exceeds threshold ({threshold})",
465
- "category": category,
466
- "current_count": count,
467
- "threshold": threshold
468
- })
469
- return alerts
470
-
471
528
  def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
472
529
  return [
473
530
  {
@@ -478,25 +535,6 @@ class WeldDefectUseCase(BaseProcessor):
478
535
  for det in detections
479
536
  ]
480
537
 
481
- def _generate_summary(self, summary: dict, alerts: List) -> str:
482
- total = summary.get("total_count", 0)
483
- per_cat = summary.get("per_category_count", {})
484
- cumulative = summary.get("total_defect_counts", {})
485
- cumulative_total = sum(cumulative.values()) if cumulative else 0
486
- lines = []
487
- if total > 0:
488
- lines.append(f"{total} Weld Defect(s) detected")
489
- if per_cat:
490
- lines.append("Defects:")
491
- for cat, count in per_cat.items():
492
- lines.append(f"\t{cat}: {count}")
493
- else:
494
- lines.append("No weld defect detected")
495
- lines.append(f"Total defects detected: {cumulative_total}")
496
- if alerts:
497
- lines.append(f"{len(alerts)} alert(s)")
498
- return "\n".join(lines)
499
-
500
538
  def _compute_iou(self, box1: Any, box2: Any) -> float:
501
539
  def _bbox_to_list(bbox):
502
540
  if bbox is None:
@@ -518,31 +556,25 @@ class WeldDefectUseCase(BaseProcessor):
518
556
  return 0.0
519
557
  x1_min, y1_min, x1_max, y1_max = l1
520
558
  x2_min, y2_min, x2_max, y2_max = l2
521
-
522
559
  x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
523
560
  y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
524
561
  x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
525
562
  y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
526
-
527
563
  inter_x_min = max(x1_min, x2_min)
528
564
  inter_y_min = max(y1_min, y2_min)
529
565
  inter_x_max = min(x1_max, x2_max)
530
566
  inter_y_max = min(y1_max, y2_max)
531
-
532
567
  inter_w = max(0.0, inter_x_max - inter_x_min)
533
568
  inter_h = max(0.0, inter_y_max - inter_y_min)
534
569
  inter_area = inter_w * inter_h
535
-
536
570
  area1 = (x1_max - x1_min) * (y1_max - y1_min)
537
571
  area2 = (x2_max - x2_min) * (y2_max - y2_min)
538
572
  union_area = area1 + area2 - inter_area
539
-
540
573
  return (inter_area / union_area) if union_area > 0 else 0.0
541
574
 
542
575
  def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
543
576
  if raw_id is None or bbox is None:
544
577
  return raw_id
545
-
546
578
  now = time.time()
547
579
  if raw_id in self._track_aliases:
548
580
  canonical_id = self._track_aliases[raw_id]
@@ -552,7 +584,6 @@ class WeldDefectUseCase(BaseProcessor):
552
584
  track_info["last_update"] = now
553
585
  track_info["raw_ids"].add(raw_id)
554
586
  return canonical_id
555
-
556
587
  for canonical_id, info in self._canonical_tracks.items():
557
588
  if now - info["last_update"] > self._track_merge_time_window:
558
589
  continue
@@ -563,7 +594,6 @@ class WeldDefectUseCase(BaseProcessor):
563
594
  info["last_update"] = now
564
595
  info["raw_ids"].add(raw_id)
565
596
  return canonical_id
566
-
567
597
  canonical_id = raw_id
568
598
  self._track_aliases[raw_id] = canonical_id
569
599
  self._canonical_tracks[canonical_id] = {
@@ -577,7 +607,9 @@ class WeldDefectUseCase(BaseProcessor):
577
607
  return datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
578
608
 
579
609
  def _get_tracking_start_time(self) -> str:
580
- return self._format_timestamp(self._tracking_start_time) if self._tracking_start_time else "N/A"
610
+ if self._tracking_start_time is None:
611
+ return "N/A"
612
+ return self._format_timestamp(self._tracking_start_time)
581
613
 
582
614
  def _set_tracking_start_time(self) -> None:
583
615
  self._tracking_start_time = time.time()