matrice 1.0.99178__py3-none-any.whl → 1.0.99180__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.
@@ -10,7 +10,6 @@ from typing import Any, Dict, List, Optional
10
10
  from dataclasses import asdict
11
11
  import time
12
12
  from datetime import datetime, timezone
13
- import copy # Added for deep copying detections to preserve original masks
14
13
 
15
14
  from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
16
15
  from ..utils import (
@@ -40,7 +39,7 @@ class PotholeConfig(BaseConfig):
40
39
  smoothing_confidence_range_factor: float = 0.5
41
40
 
42
41
  #confidence thresholds
43
- confidence_threshold: float = 0.6
42
+ confidence_threshold: float = 0.3
44
43
 
45
44
  usecase_categories: List[str] = field(
46
45
  default_factory=lambda: ['pothole']
@@ -54,28 +53,27 @@ class PotholeConfig(BaseConfig):
54
53
 
55
54
  index_to_category: Optional[Dict[int, str]] = field(
56
55
  default_factory=lambda: {
57
- 0:"pothole"
58
-
56
+ 0: 'pothole'
59
57
  }
60
58
  )
61
59
 
62
60
 
63
61
  class PotholeSegmentationUseCase(BaseProcessor):
64
-
65
- # Human-friendly display names for categories
62
+ # Human-friendly display names for categories
66
63
  CATEGORY_DISPLAY = {
67
64
  "pothole": "pothole"
68
65
  }
66
+
69
67
  def __init__(self):
70
68
  super().__init__("pothole_segmentation")
71
69
  self.category = "infrastructure"
72
70
 
71
+ self.CASE_TYPE: Optional[str] = 'pothole_segmentation'
72
+ self.CASE_VERSION: Optional[str] = '1.3'
73
+
73
74
  # List of categories to track
74
75
  self.target_categories = ["pothole"]
75
76
 
76
- self.CASE_TYPE: Optional[str] = 'Pothole_detection'
77
- self.CASE_VERSION: Optional[str] = '1.3'
78
-
79
77
  # Initialize smoothing tracker
80
78
  self.smoothing_tracker = None
81
79
 
@@ -89,13 +87,6 @@ class PotholeSegmentationUseCase(BaseProcessor):
89
87
  # Track start time for "TOTAL SINCE" calculation
90
88
  self._tracking_start_time = None
91
89
 
92
- # ------------------------------------------------------------------ #
93
- # Canonical tracking aliasing to avoid duplicate counts #
94
- # ------------------------------------------------------------------ #
95
- # Maps raw tracker-generated IDs to stable canonical IDs that persist
96
- # even if the underlying tracker re-assigns a new ID after a short
97
- # interruption. This mirrors the logic used in people_counting to
98
- # provide accurate unique counting.
99
90
  self._track_aliases: Dict[Any, Any] = {}
100
91
  self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
101
92
  # Tunable parameters – adjust if necessary for specific scenarios
@@ -108,7 +99,7 @@ class PotholeSegmentationUseCase(BaseProcessor):
108
99
  def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
109
100
  stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
110
101
  """
111
- Main entry point for post-processing.
102
+ Main entry point for post-processing.
112
103
  Applies category mapping, smoothing, counting, alerting, and summary generation.
113
104
  Returns a ProcessingResult with all relevant outputs.
114
105
  """
@@ -124,42 +115,39 @@ class PotholeSegmentationUseCase(BaseProcessor):
124
115
  input_format = match_results_structure(data)
125
116
  context.input_format = input_format
126
117
  context.confidence_threshold = config.confidence_threshold
127
-
128
- # Step 1: Confidence filtering
118
+
129
119
  if config.confidence_threshold is not None:
130
120
  processed_data = filter_by_confidence(data, config.confidence_threshold)
121
+ self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
131
122
  else:
132
123
  processed_data = data
124
+
133
125
  self.logger.debug(f"Did not apply confidence filtering with threshold since nothing was provided")
134
126
 
135
127
  # Step 2: Apply category mapping if provided
136
128
  if config.index_to_category:
137
129
  processed_data = apply_category_mapping(processed_data, config.index_to_category)
130
+ self.logger.debug("Applied category mapping")
138
131
 
139
- # Step 3: Category filtering
140
132
  if config.target_categories:
141
133
  processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
134
+ self.logger.debug(f"Applied category filtering")
142
135
 
143
- # Step 4: Apply bbox smoothing if enabled
144
- # Deep-copy detections so that we preserve the original masks before any
145
- # smoothing/tracking logic potentially removes them.
146
- raw_processed_data = [copy.deepcopy(det) for det in processed_data]
136
+ # Apply bbox smoothing if enabled
147
137
  if config.enable_smoothing:
148
138
  if self.smoothing_tracker is None:
149
139
  smoothing_config = BBoxSmoothingConfig(
150
140
  smoothing_algorithm=config.smoothing_algorithm,
151
141
  window_size=config.smoothing_window_size,
152
142
  cooldown_frames=config.smoothing_cooldown_frames,
153
- confidence_threshold=config.confidence_threshold,
143
+ confidence_threshold=config.confidence_threshold, # Use mask threshold as default
154
144
  confidence_range_factor=config.smoothing_confidence_range_factor,
155
145
  enable_smoothing=True
156
146
  )
157
147
  self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
158
-
159
148
  processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
160
- # Restore masks after smoothing
161
149
 
162
- # Step 5: Advanced tracking (BYTETracker-like)
150
+ # Advanced tracking (BYTETracker-like)
163
151
  try:
164
152
  from ..advanced_tracker import AdvancedTracker
165
153
  from ..advanced_tracker.config import TrackerConfig
@@ -168,24 +156,19 @@ class PotholeSegmentationUseCase(BaseProcessor):
168
156
  if self.tracker is None:
169
157
  tracker_config = TrackerConfig()
170
158
  self.tracker = AdvancedTracker(tracker_config)
171
- self.logger.info("Initialized AdvancedTracker for Monitoring and tracking")
172
-
159
+ self.logger.info("Initialized AdvancedTracker for Monitoring and tracking")
160
+
161
+ # The tracker expects the data in the same format as input
162
+ # It will add track_id and frame_id to each detection
173
163
  processed_data = self.tracker.update(processed_data)
164
+
174
165
  except Exception as e:
175
166
  # If advanced tracker fails, fallback to unsmoothed detections
176
167
  self.logger.warning(f"AdvancedTracker failed: {e}")
177
168
 
178
- # Update tracking state for total count per label
169
+ # Update tracking state for total count per label
179
170
  self._update_tracking_state(processed_data)
180
171
 
181
- # ------------------------------------------------------------------ #
182
- # Re-attach segmentation masks that were present in the original input
183
- # but may have been stripped during smoothing/tracking. We match each
184
- # processed detection back to the raw detection with the highest IoU
185
- # and copy over its "masks" field (if available).
186
- # ------------------------------------------------------------------ #
187
- processed_data = self._attach_masks_to_detections(processed_data, raw_processed_data)
188
-
189
172
  # Update frame counter
190
173
  self._total_frame_counter += 1
191
174
 
@@ -200,19 +183,19 @@ class PotholeSegmentationUseCase(BaseProcessor):
200
183
  frame_number = start_frame
201
184
 
202
185
  # Compute summaries and alerts
203
- general_counting_summary = calculate_counting_summary(data)
204
- counting_summary = self._count_categories(processed_data, config)
205
- # Add total unique counts after tracking using only local state
206
- total_counts = self.get_total_counts()
207
- counting_summary['total_counts'] = total_counts
208
-
186
+ general_counting_summary = calculate_counting_summary(data)
187
+ counting_summary = self._count_categories(processed_data, config)
188
+ # Add total unique counts after tracking using only local state
189
+ total_counts = self.get_total_counts()
190
+ counting_summary['total_counts'] = total_counts
191
+
209
192
  alerts = self._check_alerts(counting_summary, frame_number, config)
210
193
  predictions = self._extract_predictions(processed_data)
211
194
 
212
- # Step: Generate structured events and tracking stats with frame-based keys
195
+ # Step: Generate structured incidents, tracking stats and business analytics with frame-based keys
213
196
  incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
214
- tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number,stream_info)
215
- business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, frame_number, stream_info, is_empty=False)
197
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
198
+ business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
216
199
  summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
217
200
 
218
201
  # Extract frame-based dictionaries from the lists
@@ -226,8 +209,9 @@ class PotholeSegmentationUseCase(BaseProcessor):
226
209
  "business_analytics": business_analytics,
227
210
  "alerts": alerts,
228
211
  "human_text": summary}
229
- }
230
-
212
+ }
213
+
214
+
231
215
  context.mark_completed()
232
216
 
233
217
  # Build result object following the new pattern
@@ -289,8 +273,8 @@ class PotholeSegmentationUseCase(BaseProcessor):
289
273
  "threshold_level": threshold,
290
274
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
291
275
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
292
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
293
- }
276
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
277
+ }
294
278
  })
295
279
  elif category in summary.get("per_category_count", {}):
296
280
  count = summary.get("per_category_count", {})[category]
@@ -302,27 +286,25 @@ class PotholeSegmentationUseCase(BaseProcessor):
302
286
  "threshold_level": threshold,
303
287
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
304
288
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
305
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
306
- }
289
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
290
+ }
307
291
  })
308
292
  else:
309
293
  pass
310
294
  return alerts
311
295
 
312
296
  def _generate_incidents(self, counting_summary: Dict, alerts: List, config: PotholeConfig,
313
- frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[
297
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[
314
298
  Dict]:
315
- """Generate structured events for the output format with frame-based keys."""
316
-
317
- # Use frame number as key, fallback to 'current_frame' if not available
318
- frame_key = str(frame_number) if frame_number is not None else "current_frame"
319
- incidents=[]
299
+ """Generate structured incidents for the output format with frame-based keys."""
300
+
301
+ incidents = []
320
302
  total_detections = counting_summary.get("total_count", 0)
321
303
  current_timestamp = self._get_current_timestamp_str(stream_info)
322
304
  camera_info = self.get_camera_info_from_stream(stream_info)
323
305
 
324
306
  self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
325
-
307
+
326
308
  if total_detections > 0:
327
309
  # Determine event level based on thresholds
328
310
  level = "low"
@@ -370,7 +352,7 @@ class PotholeSegmentationUseCase(BaseProcessor):
370
352
  intensity = min(10.0, total_detections / 3.0)
371
353
  self._ascending_alert_list.append(0)
372
354
 
373
- # Generate human text in new format
355
+ # Generate human text in new format
374
356
  human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
375
357
  human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
376
358
  human_text = "\n".join(human_text_lines)
@@ -388,9 +370,9 @@ class PotholeSegmentationUseCase(BaseProcessor):
388
370
  })
389
371
 
390
372
  event= self.create_incident(incident_id=self.CASE_TYPE+'_'+str(frame_number), incident_type=self.CASE_TYPE,
391
- severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
392
- start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
393
- level_settings= {"low": 1, "medium": 3, "significant":4, "critical": 7})
373
+ severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
374
+ start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
375
+ level_settings= {"low": 1, "medium": 3, "significant":4, "critical": 7})
394
376
  incidents.append(event)
395
377
 
396
378
  else:
@@ -402,22 +384,23 @@ class PotholeSegmentationUseCase(BaseProcessor):
402
384
  def _generate_tracking_stats(
403
385
  self,
404
386
  counting_summary: Dict,
405
- alerts: Any,
387
+ alerts: List,
406
388
  config: PotholeConfig,
407
389
  frame_number: Optional[int] = None,
408
390
  stream_info: Optional[Dict[str, Any]] = None
409
391
  ) -> List[Dict]:
410
- """Generate structured tracking stats for the output format with frame-based keys, including track_ids_info and detections with masks."""
411
- #frame_key = str(frame_number) if frame_number is not None else "current_frame"
412
- tracking_stats = [] #[{frame_key: []}]
413
- #frame_tracking_stats = tracking_stats[0][frame_key]
414
-
415
- total_detections = counting_summary.get("total_count", 0)
416
- total_counts = counting_summary.get("total_counts", {})
417
- cumulative_total = sum(total_counts.values()) if total_counts else 0
418
- per_category_count = counting_summary.get("per_category_count", {})
419
-
420
- track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
392
+ """Generate structured tracking stats matching eg.json format."""
393
+ camera_info = self.get_camera_info_from_stream(stream_info)
394
+
395
+ # frame_key = str(frame_number) if frame_number is not None else "current_frame"
396
+ # tracking_stats = [{frame_key: []}]
397
+ # frame_tracking_stats = tracking_stats[0][frame_key]
398
+ tracking_stats = []
399
+
400
+ total_detections = counting_summary.get("total_count", 0) #CURRENT total count of all classes
401
+ total_counts_dict = counting_summary.get("total_counts", {}) #TOTAL cumulative counts per class
402
+ cumulative_total = sum(total_counts_dict.values()) if total_counts_dict else 0 #TOTAL combined cumulative count
403
+ per_category_count = counting_summary.get("per_category_count", {}) #CURRENT count per class
421
404
 
422
405
  current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
423
406
  start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
@@ -426,33 +409,16 @@ class PotholeSegmentationUseCase(BaseProcessor):
426
409
  high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
427
410
  high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
428
411
 
429
- camera_info = self.get_camera_info_from_stream(stream_info)
430
- human_text_lines = []
412
+
413
+ # Build total_counts array in expected format
414
+ total_counts = []
415
+ for cat, count in total_counts_dict.items():
416
+ if count > 0:
417
+ total_counts.append({
418
+ "category": cat,
419
+ "count": count
420
+ })
431
421
 
432
- # CURRENT FRAME section
433
- human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
434
- if total_detections > 0:
435
- category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
436
- if len(category_counts) == 1:
437
- detection_text = category_counts[0] + " detected"
438
- elif len(category_counts) == 2:
439
- detection_text = f"{category_counts[0]} and {category_counts[1]} detected"
440
- else:
441
- detection_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
442
- human_text_lines.append(f"\t- {detection_text}")
443
- else:
444
- human_text_lines.append(f"\t- No detections")
445
-
446
- human_text_lines.append("") # spacing
447
-
448
- # TOTAL SINCE section
449
- human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
450
- human_text_lines.append(f"\t- Total Detected: {cumulative_total}")
451
- # Add category-wise counts
452
- if total_counts:
453
- for cat, count in total_counts.items():
454
- if count > 0: # Only include categories with non-zero counts
455
- human_text_lines.append(f"\t- {cat}: {count}")
456
422
  # Build current_counts array in expected format
457
423
  current_counts = []
458
424
  for cat, count in per_category_count.items():
@@ -462,9 +428,6 @@ class PotholeSegmentationUseCase(BaseProcessor):
462
428
  "count": count
463
429
  })
464
430
 
465
- human_text = "\n".join(human_text_lines)
466
-
467
- # Include detections with masks from counting_summary
468
431
  # Prepare detections without confidence scores (as per eg.json)
469
432
  detections = []
470
433
  for detection in counting_summary.get("detections", []):
@@ -493,10 +456,24 @@ class PotholeSegmentationUseCase(BaseProcessor):
493
456
  "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
494
457
  "ascending": True,
495
458
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
496
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
497
- }
459
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
460
+ }
498
461
  })
499
462
 
463
+ # Generate human_text in expected format
464
+ human_text_lines = [f"Tracking Statistics:"]
465
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
466
+ human_text_lines.append(f"Potholes Detected - ")
467
+
468
+ for cat, count in per_category_count.items():
469
+ human_text_lines.append(f"\t{cat}: {count}")
470
+
471
+ human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
472
+ human_text_lines.append(f"Total Potholes Detected - ")
473
+ for cat, count in total_counts_dict.items():
474
+ if count > 0:
475
+ human_text_lines.append(f"\t{cat}: {count}")
476
+
500
477
  if alerts:
501
478
  for alert in alerts:
502
479
  human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
@@ -504,7 +481,7 @@ class PotholeSegmentationUseCase(BaseProcessor):
504
481
  human_text_lines.append("Alerts: None")
505
482
 
506
483
  human_text = "\n".join(human_text_lines)
507
- reset_settings = [
484
+ reset_settings=[
508
485
  {
509
486
  "interval_type": "daily",
510
487
  "reset_time": {
@@ -515,14 +492,14 @@ class PotholeSegmentationUseCase(BaseProcessor):
515
492
  ]
516
493
 
517
494
  tracking_stat=self.create_tracking_stats(total_counts=total_counts, current_counts=current_counts,
518
- detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
519
- reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
520
- reset_time=high_precision_reset_timestamp)
495
+ detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
496
+ reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
497
+ reset_time=high_precision_reset_timestamp)
521
498
 
522
499
  tracking_stats.append(tracking_stat)
523
500
  return tracking_stats
524
501
 
525
- def _generate_business_analytics(self, counting_summary: Dict, zone_analysis: Dict, config: PotholeConfig, stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
502
+ def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: PotholeConfig, stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
526
503
  """Generate standardized business analytics for the agg_summary structure."""
527
504
  if is_empty:
528
505
  return []
@@ -553,36 +530,6 @@ class PotholeSegmentationUseCase(BaseProcessor):
553
530
 
554
531
  return [lines]
555
532
 
556
-
557
- def _count_categories(self, detections: list, config: PotholeConfig) -> dict:
558
- """
559
- Count the number of detections per category and return a summary dict.
560
- The detections list is expected to have 'track_id' (from tracker), 'category', 'bounding_box', 'masks', etc.
561
- Output structure will include 'track_id' and 'masks' for each detection as per AdvancedTracker output.
562
- """
563
- counts = {}
564
- valid_detections = []
565
- for det in detections:
566
- cat = det.get('category', 'unknown')
567
- if not all(k in det for k in ['category', 'confidence', 'bounding_box']): # Validate required fields
568
- self.logger.warning(f"Skipping invalid detection: {det}")
569
- continue
570
- counts[cat] = counts.get(cat, 0) + 1
571
- valid_detections.append({
572
- "bounding_box": det.get("bounding_box"),
573
- "category": det.get("category"),
574
- "confidence": det.get("confidence"),
575
- "track_id": det.get("track_id"),
576
- "frame_id": det.get("frame_id"),
577
- "masks": det.get("masks", det.get("mask", [])) # Include masks, fallback to empty list
578
- })
579
- self.logger.debug(f"Valid detections after filtering: {len(valid_detections)}")
580
- return {
581
- "total_count": sum(counts.values()),
582
- "per_category_count": counts,
583
- "detections": valid_detections
584
- }
585
-
586
533
  def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
587
534
  """
588
535
  Get detailed information about track IDs (per frame).
@@ -636,37 +583,45 @@ class PotholeSegmentationUseCase(BaseProcessor):
636
583
  """
637
584
  return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
638
585
 
639
- def _format_timestamp_for_video(self, timestamp: float) -> str:
640
- """Format timestamp for video chunks (HH:MM:SS.ms format)."""
641
- hours = int(timestamp // 3600)
642
- minutes = int((timestamp % 3600) // 60)
643
- seconds = timestamp % 60
644
- return f"{hours:02d}:{minutes:02d}:{seconds:06.2f}"
645
586
 
646
587
  def _format_timestamp_for_stream(self, timestamp: float) -> str:
647
588
  """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
648
589
  dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
649
590
  return dt.strftime('%Y:%m:%d %H:%M:%S')
650
591
 
651
- def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
592
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
593
+ """Format timestamp for video chunks (HH:MM:SS.ms format)."""
594
+ hours = int(timestamp // 3600)
595
+ minutes = int((timestamp % 3600) // 60)
596
+ seconds = round(float(timestamp % 60),2)
597
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
598
+
599
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
652
600
  """Get formatted current timestamp based on stream type."""
653
601
  if not stream_info:
654
602
  return "00:00:00.00"
655
-
603
+ # is_video_chunk = stream_info.get("input_settings", {}).get("is_video_chunk", False)
656
604
  if precision:
657
- if stream_info.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
658
- stream_time_str = stream_info.get("video_timestamp", "")
659
- return stream_time_str[:8]
605
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
606
+ if frame_id:
607
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
608
+ else:
609
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
610
+ stream_time_str = self._format_timestamp_for_video(start_time)
611
+ return stream_time_str
660
612
  else:
661
613
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
662
614
 
663
- if stream_info.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
664
- # If video format, return video timestamp
665
- stream_time_str = stream_info.get("video_timestamp", "")
666
- return stream_time_str[:8]
615
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
616
+ if frame_id:
617
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
618
+ else:
619
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
620
+ stream_time_str = self._format_timestamp_for_video(start_time)
621
+ return stream_time_str
667
622
  else:
668
623
  # For streams, use stream_time from stream_info
669
- stream_time_str = stream_info.get("stream_time", "")
624
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
670
625
  if stream_time_str:
671
626
  # Parse the high precision timestamp string to get timestamp
672
627
  try:
@@ -685,23 +640,20 @@ class PotholeSegmentationUseCase(BaseProcessor):
685
640
  """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
686
641
  if not stream_info:
687
642
  return "00:00:00"
688
-
689
- is_video_chunk = stream_info.get("input_settings", {}).get("is_video_chunk", False)
690
643
  if precision:
691
- if stream_info.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
644
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
692
645
  return "00:00:00"
693
646
  else:
694
647
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
695
648
 
696
-
697
- if stream_info.get("input_settings", {}).get("stream_type", "video_file") == "video_file":
649
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
698
650
  # If video format, start from 00:00:00
699
651
  return "00:00:00"
700
652
  else:
701
653
  # For streams, use tracking start time or current time with minutes/seconds reset
702
654
  if self._tracking_start_time is None:
703
655
  # Try to extract timestamp from stream_time string
704
- stream_time_str = stream_info.get("stream_time", "")
656
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
705
657
  if stream_time_str:
706
658
  try:
707
659
  # Remove " UTC" suffix and parse
@@ -719,60 +671,31 @@ class PotholeSegmentationUseCase(BaseProcessor):
719
671
  dt = dt.replace(minute=0, second=0, microsecond=0)
720
672
  return dt.strftime('%Y:%m:%d %H:%M:%S')
721
673
 
722
- # ------------------------------------------------------------------ #
723
- # Helper to merge masks back into detections #
724
- # ------------------------------------------------------------------ #
725
- def _attach_masks_to_detections(
726
- self,
727
- processed_detections: List[Dict[str, Any]],
728
- raw_detections: List[Dict[str, Any]],
729
- iou_threshold: float = 0.5,
730
- ) -> List[Dict[str, Any]]:
674
+ def _count_categories(self, detections: list, config: PotholeConfig) -> dict:
731
675
  """
732
- Attach segmentation masks from the original `raw_detections` list to the
733
- `processed_detections` list returned after smoothing/tracking.
734
-
735
- Matching between detections is performed using Intersection-over-Union
736
- (IoU) of the bounding boxes. For each processed detection we select the
737
- raw detection with the highest IoU above `iou_threshold` and copy its
738
- `masks` (or `mask`) field. If no suitable match is found, the detection
739
- keeps an empty list for `masks` to maintain a consistent schema.
676
+ Count the number of detections per category and return a summary dict.
677
+ The detections list is expected to have 'track_id' (from tracker), 'category', 'bounding_box', etc.
678
+ Output structure will include 'track_id' for each detection as per AdvancedTracker output.
740
679
  """
741
-
742
- if not processed_detections or not raw_detections:
743
- # Nothing to do – ensure masks key exists for downstream logic.
744
- for det in processed_detections:
745
- det.setdefault("masks", [])
746
- return processed_detections
747
-
748
- # Track which raw detections have already been matched to avoid
749
- # assigning the same mask to multiple processed detections.
750
- used_raw_indices = set()
751
-
752
- for det in processed_detections:
753
- best_iou = 0.0
754
- best_idx = None
755
-
756
- for idx, raw_det in enumerate(raw_detections):
757
- if idx in used_raw_indices:
758
- continue
759
-
760
- iou = self._compute_iou(det.get("bounding_box"), raw_det.get("bounding_box"))
761
- if iou > best_iou:
762
- best_iou = iou
763
- best_idx = idx
764
-
765
- if best_idx is not None and best_iou >= iou_threshold:
766
- raw_det = raw_detections[best_idx]
767
- masks = raw_det.get("masks", raw_det.get("mask"))
768
- if masks is not None:
769
- det["masks"] = masks
770
- used_raw_indices.add(best_idx)
771
- else:
772
- # No adequate match – default to empty list to keep schema consistent.
773
- det.setdefault("masks", ["EMPTY"])
774
-
775
- return processed_detections
680
+ counts = {}
681
+ for det in detections:
682
+ cat = det.get('category', 'unknown')
683
+ counts[cat] = counts.get(cat, 0) + 1
684
+ # Each detection dict will now include 'track_id' (and possibly 'frame_id')
685
+ return {
686
+ "total_count": sum(counts.values()),
687
+ "per_category_count": counts,
688
+ "detections": [
689
+ {
690
+ "bounding_box": det.get("bounding_box"),
691
+ "category": det.get("category"),
692
+ "confidence": det.get("confidence"),
693
+ "track_id": det.get("track_id"),
694
+ "frame_id": det.get("frame_id")
695
+ }
696
+ for det in detections
697
+ ]
698
+ }
776
699
 
777
700
  def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
778
701
  """
@@ -782,13 +705,11 @@ class PotholeSegmentationUseCase(BaseProcessor):
782
705
  {
783
706
  "category": det.get("category", "unknown"),
784
707
  "confidence": det.get("confidence", 0.0),
785
- "bounding_box": det.get("bounding_box", {}),
786
- "mask": det.get("mask", det.get("masks", None)) # Accept either key
708
+ "bounding_box": det.get("bounding_box", {})
787
709
  }
788
710
  for det in detections
789
711
  ]
790
712
 
791
-
792
713
  # ------------------------------------------------------------------ #
793
714
  # Canonical ID helpers #
794
715
  # ------------------------------------------------------------------ #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice
3
- Version: 1.0.99178
3
+ Version: 1.0.99180
4
4
  Summary: SDK for connecting to matrice.ai services
5
5
  Home-page: https://github.com/matrice-ai/python-sdk
6
6
  Author: Matrice.ai
@@ -189,7 +189,7 @@ matrice/deploy/utils/post_processing/usecases/pedestrian_detection.py,sha256=hPF
189
189
  matrice/deploy/utils/post_processing/usecases/people_counting.py,sha256=mDJOwcrs9OO4jIbJVr_ItWvjjGP2mgGFYlrP3R-mH2E,76528
190
190
  matrice/deploy/utils/post_processing/usecases/pipeline_detection.py,sha256=VsLTXMAqx0tRw7Olrxqx7SBLolZR7p2aFOrdSXLS-kE,30796
191
191
  matrice/deploy/utils/post_processing/usecases/plaque_segmentation_img.py,sha256=d__a0PkkObYVoC-Q5-2bFVfeyKnQHtB5xVAKVOCeFyk,41925
192
- matrice/deploy/utils/post_processing/usecases/pothole_segmentation.py,sha256=6Mv8SoEE5CGItY7S0g-SY5Lb3DV-WWVMlpEp04a86a8,43197
192
+ matrice/deploy/utils/post_processing/usecases/pothole_segmentation.py,sha256=shxBDw9U59g2HPkNJTNPLBtXM10Zly_xC3QeB1oQrUE,39443
193
193
  matrice/deploy/utils/post_processing/usecases/ppe_compliance.py,sha256=G9P9j9E9nfNJInHJxmK1Lb4daFBlG5hq0aqotTLvFFE,30146
194
194
  matrice/deploy/utils/post_processing/usecases/price_tag_detection.py,sha256=09Tp6MGAHh95s-NSAp-4WC9iCc20sajWApuUBAvgXiQ,39880
195
195
  matrice/deploy/utils/post_processing/usecases/road_lane_detection.py,sha256=V_KxwBtAHSNkyoH8sXw-U-P3J8ToXtX3ncc69gn6Tds,31591
@@ -227,8 +227,8 @@ matrice/deployment/camera_manager.py,sha256=ReBZqm1CNXRImKcbcZ4uWAT3TUWkof1D28oB
227
227
  matrice/deployment/deployment.py,sha256=PLIUD-PxTaC2Zxb3Y12wUddsryV-OJetjCjLoSUh7S4,48103
228
228
  matrice/deployment/inference_pipeline.py,sha256=bXLgd29ViA7o0c7YWLFJl1otBUQfTPb61jS6VawQB0Y,37918
229
229
  matrice/deployment/streaming_gateway_manager.py,sha256=w5swGsuFVfZIdOm2ZuBHRHlRdYYJMLopLsf2gb91lQ8,20946
230
- matrice-1.0.99178.dist-info/licenses/LICENSE.txt,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
231
- matrice-1.0.99178.dist-info/METADATA,sha256=eSLuCEZTq9CA6O6UdX5dYf6p-Tca5_aJrgFrOIZ1hGE,14624
232
- matrice-1.0.99178.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
233
- matrice-1.0.99178.dist-info/top_level.txt,sha256=P97js8ur6o5ClRqMH3Cjoab_NqbJ6sOQ3rJmVzKBvMc,8
234
- matrice-1.0.99178.dist-info/RECORD,,
230
+ matrice-1.0.99180.dist-info/licenses/LICENSE.txt,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
231
+ matrice-1.0.99180.dist-info/METADATA,sha256=h-xuX7F-7q5n5QY1M_WYSAJrUGwjS96CSGO2dK-3iUA,14624
232
+ matrice-1.0.99180.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
233
+ matrice-1.0.99180.dist-info/top_level.txt,sha256=P97js8ur6o5ClRqMH3Cjoab_NqbJ6sOQ3rJmVzKBvMc,8
234
+ matrice-1.0.99180.dist-info/RECORD,,