matrice 1.0.99267__py3-none-any.whl → 1.0.99269__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.
@@ -391,6 +391,12 @@ class ProximityConfig(BaseConfig):
391
391
  enable_unique_counting: bool = True
392
392
  time_window_minutes: int = 60
393
393
 
394
+ proximity_threshold_meters: float = 1.0
395
+ proximity_threshold_pixels: float = 250.0
396
+ meters_per_pixel: float = 0.0028
397
+ scene_width_meters: float = 0.0
398
+ scene_height_meters: float = 0.0
399
+
394
400
  # Category mapping
395
401
  person_categories: List[str] = field(default_factory=lambda: ["person"])
396
402
  index_to_category: Optional[Dict[int, str]] = None
@@ -6,9 +6,9 @@ with zone-based analysis, tracking, and alerting capabilities.
6
6
  """
7
7
 
8
8
  from typing import Any, Dict, List, Optional, Set
9
- from dataclasses import asdict
10
9
  import time
11
10
  from datetime import datetime, timezone
11
+ import math
12
12
 
13
13
  from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
14
14
  from ..core.config import ProximityConfig, ZoneConfig, AlertConfig
@@ -65,6 +65,7 @@ class ProximityUseCase(BaseProcessor):
65
65
 
66
66
  # Proximity counting
67
67
  self._total_proximity_count = 0 # Total proximity events across all calls
68
+ self._observed_proximity_pairs: Set[frozenset] = set() # Unique canonical ID pairs seen across frames
68
69
 
69
70
 
70
71
  # --------------------------------------------------------------------- #
@@ -108,7 +109,7 @@ class ProximityUseCase(BaseProcessor):
108
109
  Returns:
109
110
  ProcessingResult: Processing result with standardized agg_summary structure
110
111
  """
111
- start_time = time.time()
112
+ # start_time = time.time()
112
113
 
113
114
  try:
114
115
  # Ensure we have the right config type
@@ -144,8 +145,8 @@ class ProximityUseCase(BaseProcessor):
144
145
  else:
145
146
  return self._process_single_frame(data, config, context, stream_info)
146
147
 
147
- except Exception as e:
148
- self.logger.error(f"Proximity detection failed: {str(e)}", exc_info=True)
148
+ except Exception as e: # noqa: BLE001
149
+ self.logger.error("Proximity detection failed: %s", str(e), exc_info=True)
149
150
 
150
151
  if context:
151
152
  context.mark_completed()
@@ -283,9 +284,13 @@ class ProximityUseCase(BaseProcessor):
283
284
  current_count = counting_summary["categories"].get(category, 0)
284
285
  counting_summary["categories"][category] = current_count + 1
285
286
 
286
- proximity_count = self._count_proximity_events(frame_detections)
287
+ # Update tracking state BEFORE proximity calculation so we have canonical IDs
288
+ self._update_tracking_state(counting_summary)
289
+
290
+ # Calculate unique proximity events for this frame (meters-aware)
291
+ proximity_count = self._count_proximity_events(counting_summary["detections"], config, stream_info)
287
292
  counting_summary["proximity_events"] = proximity_count
288
- counting_summary["total_proximity_count"] = self._total_proximity_count + proximity_count
293
+ counting_summary["total_proximity_count"] = self._total_proximity_count
289
294
 
290
295
  # Step 5: Zone analysis for this frame
291
296
  zone_analysis = {}
@@ -301,9 +306,6 @@ class ProximityUseCase(BaseProcessor):
301
306
  for zone_name, enhanced_data in enhanced_zone_analysis.items():
302
307
  zone_analysis[zone_name] = enhanced_data
303
308
 
304
- # Step 4.5: Always update tracking state (regardless of enable_unique_counting setting)
305
- self._update_tracking_state(counting_summary)
306
-
307
309
  # Step 5: Generate insights and alerts for this frame
308
310
  alerts = self._check_alerts(counting_summary, zone_analysis, config, frame_id)
309
311
 
@@ -402,11 +404,10 @@ class ProximityUseCase(BaseProcessor):
402
404
 
403
405
  # Add zone-specific events if applicable
404
406
  if zone_analysis:
405
- human_text_lines.append(f"\t- ZONE EVENTS:")
407
+ human_text_lines.append("\t- ZONE EVENTS:")
406
408
  for zone_name, zone_count in zone_analysis.items():
407
409
  zone_total = self._robust_zone_total(zone_count)
408
410
  if zone_total > 0:
409
- zone_intensity = min(10.0, zone_total / 5.0)
410
411
  zone_level = "info"
411
412
  if intensity >= 9:
412
413
  level = "critical"
@@ -432,14 +433,14 @@ class ProximityUseCase(BaseProcessor):
432
433
  incidents.append(event)
433
434
  return incidents
434
435
 
435
- def _generate_tracking_stats(self, counting_summary: Dict, zone_analysis: Dict, config: ProximityConfig, frame_id: str, alerts: Any=[], stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
436
+ def _generate_tracking_stats(self, counting_summary: Dict, zone_analysis: Dict, config: ProximityConfig, frame_id: str, alerts: Any=None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
436
437
  """Generate tracking stats using standardized methods."""
437
438
 
438
439
  total_people = counting_summary.get("total_objects", 0)
439
440
 
440
441
  # Get total count from cached tracking state
441
442
  total_unique_count = self.get_total_count()
442
- current_frame_count = self.get_current_frame_count()
443
+ # current_frame_count = self.get_current_frame_count()
443
444
 
444
445
  # Get camera info using standardized method
445
446
  camera_info = self.get_camera_info_from_stream(stream_info)
@@ -520,7 +521,7 @@ class ProximityUseCase(BaseProcessor):
520
521
  detection_obj = self.create_detection_object(category, bbox)
521
522
  detections.append(detection_obj)
522
523
 
523
- print(detections)
524
+ # detections prepared above are used only for output formatting
524
525
  # Build alerts and alert_settings arrays
525
526
  alert_settings = []
526
527
  if config.alert_config and hasattr(config.alert_config, 'alert_type'):
@@ -534,42 +535,44 @@ class ProximityUseCase(BaseProcessor):
534
535
  }
535
536
  })
536
537
  if zone_analysis:
537
- human_text_lines=[]
538
- current_timestamp = self._get_current_timestamp_str(stream_info, frame_id=frame_id)
539
- start_timestamp = self._get_start_timestamp_str(stream_info)
540
- human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
541
- def robust_zone_total(zone_count):
542
- if isinstance(zone_count, dict):
543
- total = 0
544
- for v in zone_count.values():
545
- if isinstance(v, int):
546
- total += v
547
- elif isinstance(v, list) and total==0:
548
- total += len(v)
549
- return total
550
- elif isinstance(zone_count, list):
551
- return len(zone_count)
552
- elif isinstance(zone_count, int):
553
- return zone_count
554
- else:
555
- return 0
556
- human_text_lines.append(f"\t- People Detected: {total_people}")
557
- human_text_lines.append("")
558
- human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
559
-
560
- for zone_name, zone_count in zone_analysis.items():
561
- zone_total = robust_zone_total(zone_count)
562
- human_text_lines.append(f"\t- Zone name: {zone_name}")
563
- human_text_lines.append(f"\t\t- Total count in zone: {zone_total-1}")
564
-
565
- if total_unique_count > 0:
566
- human_text_lines.append(f"\t- Total unique people in the scene: {total_unique_count}")
567
- if alerts:
568
- for alert in alerts:
569
- human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
538
+ human_text_lines = []
539
+ current_timestamp = self._get_current_timestamp_str(stream_info, frame_id=frame_id)
540
+ start_timestamp = self._get_start_timestamp_str(stream_info)
541
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
542
+
543
+ def robust_zone_total(zone_count):
544
+ if isinstance(zone_count, dict):
545
+ total = 0
546
+ for v in zone_count.values():
547
+ if isinstance(v, int):
548
+ total += v
549
+ elif isinstance(v, list) and total == 0:
550
+ total += len(v)
551
+ return total
552
+ elif isinstance(zone_count, list):
553
+ return len(zone_count)
554
+ elif isinstance(zone_count, int):
555
+ return zone_count
570
556
  else:
571
- human_text_lines.append("Alerts: None")
572
- human_text = "\n".join(human_text_lines)
557
+ return 0
558
+
559
+ human_text_lines.append(f"\t- People Detected: {total_people}")
560
+ human_text_lines.append("")
561
+ human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
562
+
563
+ for zone_name, zone_count in zone_analysis.items():
564
+ zone_total = robust_zone_total(zone_count)
565
+ human_text_lines.append(f"\t- Zone name: {zone_name}")
566
+ human_text_lines.append(f"\t\t- Total count in zone: {zone_total}")
567
+
568
+ if total_unique_count > 0:
569
+ human_text_lines.append(f"\t- Total unique people in the scene: {total_unique_count}")
570
+ if alerts:
571
+ for alert in alerts:
572
+ human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
573
+ else:
574
+ human_text_lines.append("Alerts: None")
575
+ human_text = "\n".join(human_text_lines)
573
576
  else:
574
577
  human_text = self._generate_human_text_for_tracking(total_people, detections, total_unique_count, config, frame_id, alerts, stream_info)
575
578
 
@@ -580,43 +583,152 @@ class ProximityUseCase(BaseProcessor):
580
583
  tracking_stat = self.create_tracking_stats(
581
584
  total_counts, current_counts, detections, human_text, camera_info, alerts, alert_settings, start_time=high_precision_start_timestamp, reset_time=high_precision_reset_timestamp
582
585
  )
583
- print(tracking_stat)
584
586
  return [tracking_stat]
585
587
 
586
- def _count_proximity_events(self, detections):
587
- """Count proximity events between detections."""
588
- proximity_threshold = 400 # pixels
589
- proximity_count = 0
590
-
591
- for i, detection1 in enumerate(detections):
592
- for j, detection2 in enumerate(detections[i+1:], i+1):
593
- bbox1 = detection1.get("bounding_box", {})
594
- bbox2 = detection2.get("bounding_box", {})
595
-
596
- if not bbox1 or not bbox2:
588
+ def _count_proximity_events(self, detections: List[Dict[str, Any]], config: ProximityConfig, stream_info: Optional[Dict[str, Any]] = None) -> int:
589
+ """Count UNIQUE proximity events between detections in a frame.
590
+
591
+ Rules:
592
+ - If canonical track IDs are present, deduplicate by track_id first.
593
+ - Otherwise, deduplicate overlapping boxes using IoU to avoid duplicate detections of the same person.
594
+ - Count each pair once (i < j) using Euclidean distance between bottom-center points.
595
+ - Distance is evaluated in meters when calibration is available. If unavailable, pixel threshold is used as a fallback.
596
+ - Maintain a running set of unique canonical-ID pairs across frames to compute total unique proximity events.
597
+ """
598
+ if not detections:
599
+ return 0
600
+
601
+ # Determine threshold strategy
602
+ meters_per_pixel = self._get_meters_per_pixel(config, stream_info)
603
+ threshold_meters = getattr(config, "proximity_threshold_meters", 1.0)
604
+ threshold_pixels_fallback = getattr(config, "proximity_threshold_pixels", 400.0)
605
+
606
+ iou_duplicate_threshold = getattr(self, "_proximity_iou_duplicate_threshold", 0.5)
607
+
608
+ # Step 1: Deduplicate detections
609
+ unique_detections: List[Dict[str, Any]] = []
610
+ seen_track_ids: Set[Any] = set()
611
+
612
+ for det in detections:
613
+ track_id = det.get("track_id")
614
+ bbox = det.get("bounding_box", det.get("bbox", {}))
615
+ if not bbox:
616
+ continue
617
+
618
+ # Prefer canonical track IDs if present (set earlier in _update_tracking_state)
619
+ if track_id is not None:
620
+ if track_id in seen_track_ids:
597
621
  continue
598
-
599
- center1 = get_bbox_center(bbox1)
600
- center2 = get_bbox_center(bbox2)
601
-
602
- if center1 and center2:
603
- distance = abs((center1[0] - center2[0]) + (center1[1] - center2[1]))
604
- if distance < proximity_threshold:
605
- proximity_count += 1
606
-
607
- return proximity_count
622
+ seen_track_ids.add(track_id)
623
+ unique_detections.append(det)
624
+ continue
625
+
626
+ # Fallback: IoU-based dedup for pure detection outputs without track IDs
627
+ is_duplicate = False
628
+ for kept in unique_detections:
629
+ kept_bbox = kept.get("bounding_box", kept.get("bbox", {}))
630
+ if self._compute_iou(bbox, kept_bbox) >= iou_duplicate_threshold:
631
+ is_duplicate = True
632
+ break
633
+ if not is_duplicate:
634
+ unique_detections.append(det)
635
+
636
+ # Step 2: Compute proximity pairs uniquely (i < j)
637
+ frame_unique_count = 0
638
+
639
+ for i in range(len(unique_detections)):
640
+ det_i = unique_detections[i]
641
+ bbox_i = det_i.get("bounding_box", det_i.get("bbox", {}))
642
+ p_i = get_bbox_bottom25_center(bbox_i) or get_bbox_center(bbox_i)
643
+ if not p_i:
644
+ continue
645
+
646
+ for j in range(i + 1, len(unique_detections)):
647
+ det_j = unique_detections[j]
648
+ bbox_j = det_j.get("bounding_box", det_j.get("bbox", {}))
649
+ p_j = get_bbox_bottom25_center(bbox_j) or get_bbox_center(bbox_j)
650
+ if not p_j:
651
+ continue
652
+
653
+ dx = float(p_i[0]) - float(p_j[0])
654
+ dy = float(p_i[1]) - float(p_j[1])
655
+ pixel_distance = math.hypot(dx, dy)
656
+
657
+ is_close = False
658
+ if meters_per_pixel:
659
+ meters_distance = pixel_distance * float(meters_per_pixel)
660
+ if meters_distance < float(threshold_meters):
661
+ is_close = True
662
+ else:
663
+ if pixel_distance < float(threshold_pixels_fallback):
664
+ is_close = True
665
+
666
+ if not is_close:
667
+ continue
668
+
669
+ frame_unique_count += 1
670
+
671
+ # Update global unique proximity pairs only when both have canonical IDs
672
+ id_i = det_i.get("track_id")
673
+ id_j = det_j.get("track_id")
674
+ if id_i is not None and id_j is not None:
675
+ pair_key = frozenset({id_i, id_j})
676
+ if pair_key not in self._observed_proximity_pairs:
677
+ self._observed_proximity_pairs.add(pair_key)
678
+ self._total_proximity_count += 1
679
+
680
+ return frame_unique_count
681
+
682
+ def _get_meters_per_pixel(self, config: ProximityConfig, stream_info: Optional[Dict[str, Any]] = None) -> Optional[float]:
683
+ """Compute meters-per-pixel scale using config and optional stream_info.
684
+
685
+ Priority:
686
+ 1) config.meters_per_pixel (direct override)
687
+ 2) config.scene_width_meters + frame width in pixels
688
+ 3) config.scene_height_meters + frame height in pixels
689
+ Returns None if insufficient information.
690
+ """
691
+ # Direct override
692
+ if hasattr(config, "meters_per_pixel") and getattr(config, "meters_per_pixel"):
693
+ try:
694
+ return float(getattr(config, "meters_per_pixel"))
695
+ except Exception: # noqa: BLE001
696
+ pass
697
+
698
+ width_px = None
699
+ height_px = None
700
+ if stream_info and isinstance(stream_info, dict):
701
+ input_settings = stream_info.get("input_settings", {}) or {}
702
+ resolution = input_settings.get("resolution", {}) or {}
703
+ width_px = resolution.get("width") or input_settings.get("frame_width")
704
+ height_px = resolution.get("height") or input_settings.get("frame_height")
705
+
706
+ # Derive from scene real-world width
707
+ if hasattr(config, "scene_width_meters") and getattr(config, "scene_width_meters") and width_px:
708
+ try:
709
+ return float(getattr(config, "scene_width_meters")) / float(width_px)
710
+ except Exception: # noqa: BLE001
711
+ pass
712
+
713
+ # Derive from scene real-world height
714
+ if hasattr(config, "scene_height_meters") and getattr(config, "scene_height_meters") and height_px:
715
+ try:
716
+ return float(getattr(config, "scene_height_meters")) / float(height_px)
717
+ except Exception: # noqa: BLE001
718
+ pass
719
+
720
+ return None
608
721
 
609
722
  def _generate_human_text_for_tracking(
610
- self,
611
- total_people: int,
612
- detections,
613
- total_unique_count: int,
614
- config: ProximityConfig,
615
- frame_id: str,
616
- alerts:Any=[],
723
+ self,
724
+ total_people: int,
725
+ detections,
726
+ total_unique_count: int,
727
+ config: ProximityConfig,
728
+ frame_id: str,
729
+ alerts: Any = None,
617
730
  stream_info: Optional[Dict[str, Any]] = None) -> str:
618
731
  """Generate human-readable text for tracking stats in old format."""
619
- from datetime import datetime, timezone
620
732
 
621
733
  human_text_lines=[]
622
734
  current_timestamp = self._get_current_timestamp_str(stream_info, precision=True, frame_id=frame_id)
@@ -624,9 +736,8 @@ class ProximityUseCase(BaseProcessor):
624
736
 
625
737
  human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
626
738
 
627
- # Add proximity count to human text
628
- # Add proximity count to human text
629
- proximity_count = self._count_proximity_events(detections)
739
+ # Add proximity count to human text (meters-aware)
740
+ proximity_count = self._count_proximity_events(detections, config, stream_info)
630
741
  if proximity_count > 0:
631
742
  human_text_lines.append(f"\t- Current Frame Proximity: {proximity_count}")
632
743
  else:
@@ -635,7 +746,10 @@ class ProximityUseCase(BaseProcessor):
635
746
  human_text_lines.append("")
636
747
  if proximity_count > 0:
637
748
  human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
638
- human_text_lines.append(f"\t- Total Proximity Count: {self._total_proximity_count + proximity_count}")
749
+ # If tracking IDs are available, _total_proximity_count already includes this frame's new pairs
750
+ # Otherwise, fall back to showing the current frame's count
751
+ total_unique = self._total_proximity_count if self._total_proximity_count > 0 else proximity_count
752
+ human_text_lines.append(f"\t- Total Proximity Count: {total_unique}")
639
753
 
640
754
  if alerts:
641
755
  for alert in alerts:
@@ -712,7 +826,6 @@ class ProximityUseCase(BaseProcessor):
712
826
  for zone_name, threshold in config.alert_config.occupancy_thresholds.items():
713
827
  if zone_name in zone_analysis:
714
828
  # Calculate zone_count robustly (supports int, list, dict values)
715
- print('ZONEEE',zone_name, zone_analysis[zone_name])
716
829
  zone_count = self._robust_zone_total(zone_analysis[zone_name])
717
830
  if zone_count >= threshold:
718
831
  alerts.append({
@@ -771,7 +884,7 @@ class ProximityUseCase(BaseProcessor):
771
884
 
772
885
  return business_analytics
773
886
 
774
- def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
887
+ def _generate_summary(self, _summary: dict, incidents: List, tracking_stats: List, business_analytics: List, _alerts: List) -> List[str]:
775
888
  """
776
889
  Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
777
890
  """
@@ -880,8 +993,8 @@ class ProximityUseCase(BaseProcessor):
880
993
  prediction["frame_id"] = frame_id
881
994
  predictions.append(prediction)
882
995
 
883
- except Exception as e:
884
- self.logger.warning(f"Failed to extract predictions: {str(e)}")
996
+ except Exception as e: # noqa: BLE001
997
+ self.logger.warning("Failed to extract predictions: %s", str(e))
885
998
 
886
999
  return predictions
887
1000
 
@@ -939,7 +1052,9 @@ class ProximityUseCase(BaseProcessor):
939
1052
  current_frame_tracks.add(canonical_id)
940
1053
 
941
1054
  # Update total track IDs with new canonical IDs from current frame
942
- old_total_count = len(self._total_track_ids)
1055
+ # old_total_count can be used for debugging or analytics if needed
1056
+ # Keeping it for potential future use but suppressing linter warning
1057
+ old_total_count = len(self._total_track_ids) # noqa: F841
943
1058
  self._total_track_ids.update(current_frame_tracks)
944
1059
  self._current_frame_track_ids = current_frame_tracks
945
1060
 
@@ -952,10 +1067,12 @@ class ProximityUseCase(BaseProcessor):
952
1067
  new_tracks = current_frame_tracks - (self._total_track_ids - current_frame_tracks)
953
1068
  if new_tracks:
954
1069
  self.logger.debug(
955
- f"Tracking state updated: {len(new_tracks)} new canonical track IDs added, total unique tracks: {self._total_count}")
1070
+ "Tracking state updated: %s new canonical track IDs added, total unique tracks: %s",
1071
+ len(new_tracks), self._total_count)
956
1072
  else:
957
1073
  self.logger.debug(
958
- f"Tracking state updated: {len(current_frame_tracks)} current frame canonical tracks, total unique tracks: {self._total_count}")
1074
+ "Tracking state updated: %s current frame canonical tracks, total unique tracks: %s",
1075
+ len(current_frame_tracks), self._total_count)
959
1076
 
960
1077
  def get_total_count(self) -> int:
961
1078
  """Get the total count of unique people tracked across all calls."""
@@ -972,7 +1089,7 @@ class ProximityUseCase(BaseProcessor):
972
1089
  def set_global_frame_offset(self, offset: int) -> None:
973
1090
  """Set the global frame offset for video chunk processing."""
974
1091
  self._global_frame_offset = offset
975
- self.logger.info(f"Global frame offset set to: {offset}")
1092
+ self.logger.info("Global frame offset set to: %s", offset)
976
1093
 
977
1094
  def get_global_frame_offset(self) -> int:
978
1095
  """Get the current global frame offset."""
@@ -982,7 +1099,7 @@ class ProximityUseCase(BaseProcessor):
982
1099
  """Update global frame offset after processing a chunk."""
983
1100
  old_offset = self._global_frame_offset
984
1101
  self._global_frame_offset += frames_in_chunk
985
- self.logger.info(f"Global frame offset updated: {old_offset} -> {self._global_frame_offset} (added {frames_in_chunk} frames)")
1102
+ self.logger.info("Global frame offset updated: %s -> %s (added %s frames)", old_offset, self._global_frame_offset, frames_in_chunk)
986
1103
 
987
1104
  def get_global_frame_id(self, local_frame_id: str) -> str:
988
1105
  """Convert local frame ID to global frame ID."""
@@ -1093,14 +1210,14 @@ class ProximityUseCase(BaseProcessor):
1093
1210
  # Update timestamp
1094
1211
  self._last_update_time = time.time()
1095
1212
 
1096
- self.logger.info(f"Cleared {old_current_count} current frame tracks and {cleared_zone_tracks} zone current tracks. Cumulative total preserved: {self._total_count}")
1213
+ self.logger.info("Cleared %s current frame tracks and %s zone current tracks. Cumulative total preserved: %s", old_current_count, cleared_zone_tracks, self._total_count)
1097
1214
  return old_current_count
1098
1215
 
1099
1216
  def reset_frame_counter(self) -> None:
1100
1217
  """Reset only the frame counter."""
1101
1218
  old_count = self._total_frame_counter
1102
1219
  self._total_frame_counter = 0
1103
- self.logger.info(f"Frame counter reset from {old_count} to 0")
1220
+ self.logger.info("Frame counter reset from %s to 0", old_count)
1104
1221
 
1105
1222
  def clear_expired_tracks(self, max_age_seconds: float = 300.0) -> int:
1106
1223
  """
@@ -1125,7 +1242,7 @@ class ProximityUseCase(BaseProcessor):
1125
1242
  if current_time - self._last_update_time > max_age_seconds:
1126
1243
  # Use the safe method that preserves cumulative totals
1127
1244
  cleared_count = self.clear_current_frame_tracking()
1128
- self.logger.info(f"Manual cleanup: cleared {cleared_count} expired current frame tracks (age > {max_age_seconds}s)")
1245
+ self.logger.info("Manual cleanup: cleared %s expired current frame tracks (age > %ss)", cleared_count, max_age_seconds)
1129
1246
  return cleared_count
1130
1247
  return 0
1131
1248
 
@@ -1267,12 +1384,12 @@ class ProximityUseCase(BaseProcessor):
1267
1384
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
1268
1385
 
1269
1386
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
1270
- if frame_id:
1271
- start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
1272
- else:
1273
- start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
1274
- stream_time_str = self._format_timestamp_for_video(start_time)
1275
- return stream_time_str
1387
+ if frame_id:
1388
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
1389
+ else:
1390
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
1391
+ stream_time_str = self._format_timestamp_for_video(start_time)
1392
+ return stream_time_str
1276
1393
  else:
1277
1394
  # For streams, use stream_time from stream_info
1278
1395
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
@@ -1284,7 +1401,7 @@ class ProximityUseCase(BaseProcessor):
1284
1401
  dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
1285
1402
  timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
1286
1403
  return self._format_timestamp_for_stream(timestamp)
1287
- except:
1404
+ except Exception: # noqa: BLE001
1288
1405
  # Fallback to current time if parsing fails
1289
1406
  return self._format_timestamp_for_stream(time.time())
1290
1407
  else:
@@ -1316,7 +1433,7 @@ class ProximityUseCase(BaseProcessor):
1316
1433
  timestamp_str = stream_time_str.replace(" UTC", "")
1317
1434
  dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
1318
1435
  self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
1319
- except:
1436
+ except Exception: # noqa: BLE001
1320
1437
  # Fallback to current time if parsing fails
1321
1438
  self._tracking_start_time = time.time()
1322
1439
  else:
@@ -1450,7 +1567,7 @@ class ProximityUseCase(BaseProcessor):
1450
1567
  info["last_update"] = now
1451
1568
  info["raw_ids"].add(raw_id)
1452
1569
  self.logger.debug(
1453
- f"Merged raw track {raw_id} into canonical track {canonical_id} (IoU={iou:.2f})")
1570
+ "Merged raw track %s into canonical track %s (IoU=%.2f)", raw_id, canonical_id, iou)
1454
1571
  return canonical_id
1455
1572
 
1456
1573
  # No match found – create a new canonical track
@@ -1461,7 +1578,7 @@ class ProximityUseCase(BaseProcessor):
1461
1578
  "last_update": now,
1462
1579
  "raw_ids": {raw_id},
1463
1580
  }
1464
- self.logger.debug(f"Registered new canonical track {canonical_id}")
1581
+ self.logger.debug("Registered new canonical track %s", canonical_id)
1465
1582
  return canonical_id
1466
1583
 
1467
1584
  def _format_timestamp(self, timestamp: float) -> str:
@@ -1530,6 +1647,33 @@ class ProximityUseCase(BaseProcessor):
1530
1647
  "default": True,
1531
1648
  "description": "Enable unique proximity detection using tracking"
1532
1649
  },
1650
+ "proximity_threshold_meters": {
1651
+ "type": "number",
1652
+ "minimum": 0.1,
1653
+ "default": 1.0,
1654
+ "description": "Distance threshold in meters to consider two people in proximity"
1655
+ },
1656
+ "meters_per_pixel": {
1657
+ "type": "number",
1658
+ "minimum": 0,
1659
+ "description": "Direct meters-per-pixel calibration override. If set, used for distance conversion."
1660
+ },
1661
+ "scene_width_meters": {
1662
+ "type": "number",
1663
+ "minimum": 0,
1664
+ "description": "Real-world width of the scene captured by the frame (meters). Used to derive meters-per-pixel with frame width."
1665
+ },
1666
+ "scene_height_meters": {
1667
+ "type": "number",
1668
+ "minimum": 0,
1669
+ "description": "Real-world height of the scene captured by the frame (meters). Used to derive meters-per-pixel with frame height."
1670
+ },
1671
+ "proximity_threshold_pixels": {
1672
+ "type": "number",
1673
+ "minimum": 1,
1674
+ "default": 400,
1675
+ "description": "Fallback pixel threshold if no calibration is available"
1676
+ },
1533
1677
  "time_window_minutes": {
1534
1678
  "type": "integer",
1535
1679
  "minimum": 1,
@@ -1603,5 +1747,5 @@ class ProximityUseCase(BaseProcessor):
1603
1747
  self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
1604
1748
 
1605
1749
  smoothed_data = bbox_smoothing(data, self.smoothing_tracker.config, self.smoothing_tracker)
1606
- self.logger.debug(f"Applied bbox smoothing to tracking results")
1750
+ self.logger.debug("Applied bbox smoothing to tracking results")
1607
1751
  return smoothed_data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice
3
- Version: 1.0.99267
3
+ Version: 1.0.99269
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
@@ -140,7 +140,7 @@ matrice/deploy/utils/post_processing/advanced_tracker/strack.py,sha256=rVH2xOysZ
140
140
  matrice/deploy/utils/post_processing/advanced_tracker/tracker.py,sha256=D-PKZ2Pxutmlu--icyxuxjvnWBrzrmZcEChYS0nx00M,14328
141
141
  matrice/deploy/utils/post_processing/core/__init__.py,sha256=MPMj_iRv--PfKBpYN12IjReAzSU7aRMVD6VW-LC95-M,1379
142
142
  matrice/deploy/utils/post_processing/core/base.py,sha256=V_DmaMLtrIunrN8Aq9iLeMIQPlkbCE-9d7n0Yz-nKQg,28228
143
- matrice/deploy/utils/post_processing/core/config.py,sha256=rU6EWou2HOA_4Ks0LUTSczQbT04jhN1FofuAQ7dtrZ4,102355
143
+ matrice/deploy/utils/post_processing/core/config.py,sha256=ogmNUbfOoC4SE-SmUEkqIKBeR0p0j3M0hF2_KvzpNp0,102560
144
144
  matrice/deploy/utils/post_processing/core/config_utils.py,sha256=Y_Czm9RmtHuxzBZzGUBA57JRyx5r6tzrM5l89Dbdf_w,28871
145
145
  matrice/deploy/utils/post_processing/test_cases/__init__.py,sha256=zUU2kKrIcCl8WeyjjQViwp7PWTZlKPuF8M2pZkxoNNQ,42
146
146
  matrice/deploy/utils/post_processing/test_cases/run_tests.py,sha256=RBFGvxFR-gozxnQFzkWLrs90vLlp8Bsn-Z7MLQrNw4o,4731
@@ -204,7 +204,7 @@ matrice/deploy/utils/post_processing/usecases/plaque_segmentation_img.py,sha256=
204
204
  matrice/deploy/utils/post_processing/usecases/pothole_segmentation.py,sha256=jXTb8ZqInp5xJ-O3Zp3zQBiryFVD0-WBbhW6Kux_NDo,44905
205
205
  matrice/deploy/utils/post_processing/usecases/ppe_compliance.py,sha256=G9P9j9E9nfNJInHJxmK1Lb4daFBlG5hq0aqotTLvFFE,30146
206
206
  matrice/deploy/utils/post_processing/usecases/price_tag_detection.py,sha256=09Tp6MGAHh95s-NSAp-4WC9iCc20sajWApuUBAvgXiQ,39880
207
- matrice/deploy/utils/post_processing/usecases/proximity_detection.py,sha256=vgQCVep4yJnZZlOh33M2x2p4N980dYwDifam8_2-Gwc,78406
207
+ matrice/deploy/utils/post_processing/usecases/proximity_detection.py,sha256=QyknlaCyvzRg0j9Vndi1prYNW1elz0ARNBimHRyBak4,85207
208
208
  matrice/deploy/utils/post_processing/usecases/road_lane_detection.py,sha256=V_KxwBtAHSNkyoH8sXw-U-P3J8ToXtX3ncc69gn6Tds,31591
209
209
  matrice/deploy/utils/post_processing/usecases/road_traffic_density.py,sha256=YiHQ0kKhXglagHPvygywxMqZAw8s0WharrBQqLQj2q4,40311
210
210
  matrice/deploy/utils/post_processing/usecases/road_view_segmentation.py,sha256=BcBbOOg5622KuvzKrzs9cJW1wkRoIIcOab0N7BONQKQ,44986
@@ -243,8 +243,8 @@ matrice/deployment/camera_manager.py,sha256=e1Lc81RJP5wUWRdTgHO6tMWF9BkBdHOSVyx3
243
243
  matrice/deployment/deployment.py,sha256=HFt151eWq6iqIAMsQvurpV2WNxW6Cx_gIUVfnVy5SWE,48093
244
244
  matrice/deployment/inference_pipeline.py,sha256=6b4Mm3-qt-Zy0BeiJfFQdImOn3FzdNCY-7ET7Rp8PMk,37911
245
245
  matrice/deployment/streaming_gateway_manager.py,sha256=ifYGl3g25wyU39HwhPQyI2OgF3M6oIqKMWt8RXtMxY8,21401
246
- matrice-1.0.99267.dist-info/licenses/LICENSE.txt,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
247
- matrice-1.0.99267.dist-info/METADATA,sha256=AKbp9IcGh4knRNdZ5uVslhXEEu4Xl92xoXvJRBt9jPw,14624
248
- matrice-1.0.99267.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
249
- matrice-1.0.99267.dist-info/top_level.txt,sha256=P97js8ur6o5ClRqMH3Cjoab_NqbJ6sOQ3rJmVzKBvMc,8
250
- matrice-1.0.99267.dist-info/RECORD,,
246
+ matrice-1.0.99269.dist-info/licenses/LICENSE.txt,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
247
+ matrice-1.0.99269.dist-info/METADATA,sha256=y9DrDgXE7DfQ8kc1rALCkIrPanFK1zTR3Eoc8DxVVyo,14624
248
+ matrice-1.0.99269.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
249
+ matrice-1.0.99269.dist-info/top_level.txt,sha256=P97js8ur6o5ClRqMH3Cjoab_NqbJ6sOQ3rJmVzKBvMc,8
250
+ matrice-1.0.99269.dist-info/RECORD,,