matrice-analytics 0.1.43__py3-none-any.whl → 0.1.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of matrice-analytics might be problematic. Click here for more details.
- matrice_analytics/post_processing/config.py +1 -1
- matrice_analytics/post_processing/face_reg/face_recognition.py +145 -99
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +24 -19
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +6 -4
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +3 -1
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/usecases/color/clip.py +4 -3
- matrice_analytics/post_processing/usecases/color/color_mapper.py +1 -1
- matrice_analytics/post_processing/usecases/color_detection.py +89 -54
- matrice_analytics/post_processing/usecases/fire_detection.py +89 -55
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +81 -46
- matrice_analytics/post_processing/usecases/people_counting.py +29 -28
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +89 -54
- {matrice_analytics-0.1.43.dist-info → matrice_analytics-0.1.45.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.43.dist-info → matrice_analytics-0.1.45.dist-info}/RECORD +18 -17
- {matrice_analytics-0.1.43.dist-info → matrice_analytics-0.1.45.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.43.dist-info → matrice_analytics-0.1.45.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.43.dist-info → matrice_analytics-0.1.45.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import cv2
|
|
4
|
+
import json
|
|
5
|
+
import importlib
|
|
6
|
+
import argparse
|
|
7
|
+
from ultralytics import YOLO
|
|
8
|
+
from src.matrice_analytics.post_processing.core.base import ProcessingContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UseCaseTestProcessor:
|
|
12
|
+
"""
|
|
13
|
+
A flexible YOLO-based video processor for testing different post-processing use cases.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, file_name, config_name, usecase_name, model_path, video_path, post_process=None, max_frames=None):
|
|
17
|
+
self.file_name = file_name
|
|
18
|
+
self.config_name = config_name
|
|
19
|
+
self.usecase_name = usecase_name
|
|
20
|
+
self.model_path = model_path
|
|
21
|
+
self.video_path = video_path
|
|
22
|
+
self.post_process = post_process
|
|
23
|
+
self.max_frames = max_frames
|
|
24
|
+
self.json_dir = "jsons"
|
|
25
|
+
|
|
26
|
+
self._setup_environment()
|
|
27
|
+
self.ConfigClass, self.UsecaseClass = self._load_usecase()
|
|
28
|
+
self.config = self._initialize_config()
|
|
29
|
+
self.processor = self.UsecaseClass()
|
|
30
|
+
self.model = YOLO(self.model_path)
|
|
31
|
+
os.makedirs(self.json_dir, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
def _setup_environment(self):
|
|
34
|
+
"""Ensure project root is added to sys.path."""
|
|
35
|
+
project_root = os.path.abspath("/content/py_analytics")
|
|
36
|
+
if project_root not in sys.path:
|
|
37
|
+
sys.path.append(project_root)
|
|
38
|
+
|
|
39
|
+
def _load_usecase(self):
|
|
40
|
+
"""Dynamically import config and usecase classes."""
|
|
41
|
+
module_path = f"src.matrice_analytics.post_processing.usecases.{self.file_name}"
|
|
42
|
+
module = importlib.import_module(module_path)
|
|
43
|
+
return getattr(module, self.config_name), getattr(module, self.usecase_name)
|
|
44
|
+
|
|
45
|
+
def _initialize_config(self):
|
|
46
|
+
"""Initialize config object, applying overrides if provided."""
|
|
47
|
+
if self.post_process:
|
|
48
|
+
return self.ConfigClass(**self.post_process)
|
|
49
|
+
return self.ConfigClass()
|
|
50
|
+
|
|
51
|
+
def _serialize_result(self, result):
|
|
52
|
+
"""Convert result object into JSON-serializable dict."""
|
|
53
|
+
def to_serializable(obj):
|
|
54
|
+
if hasattr(obj, "to_dict"):
|
|
55
|
+
return obj.to_dict()
|
|
56
|
+
if hasattr(obj, "__dict__"):
|
|
57
|
+
return obj.__dict__
|
|
58
|
+
return str(obj)
|
|
59
|
+
return json.loads(json.dumps(result, default=to_serializable))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def process_video(self):
|
|
63
|
+
"""Run YOLO inference on video and post-process frame by frame."""
|
|
64
|
+
cap = cv2.VideoCapture(self.video_path)
|
|
65
|
+
if not cap.isOpened():
|
|
66
|
+
raise ValueError(f"Failed to open video at {self.video_path}")
|
|
67
|
+
|
|
68
|
+
frame_idx = 0
|
|
69
|
+
stream_info = {
|
|
70
|
+
'input_settings': {
|
|
71
|
+
'start_frame': 0,
|
|
72
|
+
'original_fps': cap.get(cv2.CAP_PROP_FPS),
|
|
73
|
+
'camera_info': {'id': 'cam1', 'name': 'Test Camera'}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
print(f"\nStarting video processing: {self.video_path}")
|
|
78
|
+
print(f"Model: {self.model_path}")
|
|
79
|
+
print(f"Output directory: {self.json_dir}\n")
|
|
80
|
+
|
|
81
|
+
while cap.isOpened():
|
|
82
|
+
ret, frame = cap.read()
|
|
83
|
+
if not ret:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
87
|
+
results = self.model(frame_rgb)
|
|
88
|
+
|
|
89
|
+
detections = []
|
|
90
|
+
for xyxy, conf, cls in zip(results[0].boxes.xyxy, results[0].boxes.conf, results[0].boxes.cls):
|
|
91
|
+
x1, y1, x2, y2 = xyxy.tolist()
|
|
92
|
+
detections.append({
|
|
93
|
+
'category_id': int(cls),
|
|
94
|
+
'confidence': conf.item(),
|
|
95
|
+
'bounding_box': {
|
|
96
|
+
'xmin': int(x1),
|
|
97
|
+
'ymin': int(y1),
|
|
98
|
+
'xmax': int(x2),
|
|
99
|
+
'ymax': int(y2)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
success, encoded_image = cv2.imencode(".jpg", frame)
|
|
104
|
+
input_bytes = encoded_image.tobytes() if success else None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = self.processor.process(
|
|
108
|
+
detections, self.config, input_bytes, ProcessingContext(), stream_info
|
|
109
|
+
)
|
|
110
|
+
except TypeError:
|
|
111
|
+
result = self.processor.process(
|
|
112
|
+
detections, self.config, ProcessingContext(), stream_info
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
json_path = os.path.join(self.json_dir, f"frame_{frame_idx:04d}.json")
|
|
116
|
+
with open(json_path, "w") as f:
|
|
117
|
+
json.dump(self._serialize_result(result), f, indent=2)
|
|
118
|
+
|
|
119
|
+
print(f"Frame {frame_idx} processed — detections: {len(detections)} — saved: {json_path}")
|
|
120
|
+
|
|
121
|
+
frame_idx += 1
|
|
122
|
+
stream_info['input_settings']['start_frame'] += 1
|
|
123
|
+
|
|
124
|
+
if self.max_frames and frame_idx >= self.max_frames:
|
|
125
|
+
print(f"\nMax frame limit ({self.max_frames}) reached.")
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
cap.release()
|
|
129
|
+
print(f"\nProcessing complete. JSON outputs saved in: {self.json_dir}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
parser = argparse.ArgumentParser(description="YOLO Use Case Test Processor")
|
|
134
|
+
|
|
135
|
+
parser.add_argument("--file_name", type=str, required=True,
|
|
136
|
+
help="Usecase file name under src/matrice_analytics/post_processing/usecases/")
|
|
137
|
+
parser.add_argument("--config_name", type=str, required=True,
|
|
138
|
+
help="Config class name (e.g., PeopleCountingConfig)")
|
|
139
|
+
parser.add_argument("--usecase_name", type=str, required=True,
|
|
140
|
+
help="Use case class name (e.g., PeopleCountingUseCase)")
|
|
141
|
+
parser.add_argument("--model_path", type=str, required=True,
|
|
142
|
+
help="Path to YOLO model file (.pt)")
|
|
143
|
+
parser.add_argument("--video_path", type=str, required=True,
|
|
144
|
+
help="Path to input video")
|
|
145
|
+
parser.add_argument("--post_process", type=json.loads, default=None,
|
|
146
|
+
help="JSON string for config overrides, e.g. '{\"min_confidence\": 0.5}'")
|
|
147
|
+
parser.add_argument("--max_frames", type=int, default=None,
|
|
148
|
+
help="Limit number of frames processed")
|
|
149
|
+
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
processor = UseCaseTestProcessor(
|
|
153
|
+
file_name=args.file_name,
|
|
154
|
+
config_name=args.config_name,
|
|
155
|
+
usecase_name=args.usecase_name,
|
|
156
|
+
model_path=args.model_path,
|
|
157
|
+
video_path=args.video_path,
|
|
158
|
+
post_process=args.post_process,
|
|
159
|
+
max_frames=args.max_frames
|
|
160
|
+
)
|
|
161
|
+
processor.process_video()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -11,20 +11,21 @@ subprocess.run(
|
|
|
11
11
|
cmd,
|
|
12
12
|
stdout=log_file,
|
|
13
13
|
stderr=subprocess.STDOUT,
|
|
14
|
-
preexec_fn=os.setpgrp
|
|
14
|
+
# preexec_fn=os.setpgrp
|
|
15
15
|
)
|
|
16
16
|
cmd = ["pip", "install", "httpx", "aiohttp", "filterpy"]
|
|
17
17
|
subprocess.run(
|
|
18
18
|
cmd,
|
|
19
19
|
stdout=log_file,
|
|
20
20
|
stderr=subprocess.STDOUT,
|
|
21
|
-
preexec_fn=os.setpgrp
|
|
21
|
+
# preexec_fn=os.setpgrp
|
|
22
22
|
)
|
|
23
23
|
log_file.close()
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
from typing import List, Dict, Tuple, Optional
|
|
27
27
|
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
28
29
|
import cv2
|
|
29
30
|
import io
|
|
30
31
|
import threading
|
|
@@ -371,7 +372,7 @@ class ClipProcessor:
|
|
|
371
372
|
cmd,
|
|
372
373
|
stdout=log_file,
|
|
373
374
|
stderr=subprocess.STDOUT,
|
|
374
|
-
preexec_fn=os.setpgrp
|
|
375
|
+
# preexec_fn=os.setpgrp
|
|
375
376
|
)
|
|
376
377
|
|
|
377
378
|
# Determine and enforce providers (prefer CUDA only)
|
|
@@ -1617,6 +1617,53 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
1617
1617
|
seconds = round(float(timestamp % 60),2)
|
|
1618
1618
|
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
1619
1619
|
|
|
1620
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
1621
|
+
"""Format a timestamp to match the current timestamp format: YYYY:MM:DD HH:MM:SS.
|
|
1622
|
+
|
|
1623
|
+
The input can be either:
|
|
1624
|
+
1. A numeric Unix timestamp (``float`` / ``int``) – it will be converted to datetime.
|
|
1625
|
+
2. A string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
1626
|
+
|
|
1627
|
+
The returned value will be in the format: YYYY:MM:DD HH:MM:SS (no milliseconds, no UTC suffix).
|
|
1628
|
+
|
|
1629
|
+
Example
|
|
1630
|
+
-------
|
|
1631
|
+
>>> self._format_timestamp("2025-10-27-19:31:20.187574 UTC")
|
|
1632
|
+
'2025:10:27 19:31:20'
|
|
1633
|
+
"""
|
|
1634
|
+
|
|
1635
|
+
# Convert numeric timestamps to datetime first
|
|
1636
|
+
if isinstance(timestamp, (int, float)):
|
|
1637
|
+
dt = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
1638
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1639
|
+
|
|
1640
|
+
# Ensure we are working with a string from here on
|
|
1641
|
+
if not isinstance(timestamp, str):
|
|
1642
|
+
return str(timestamp)
|
|
1643
|
+
|
|
1644
|
+
# Remove ' UTC' suffix if present
|
|
1645
|
+
timestamp_clean = timestamp.replace(' UTC', '').strip()
|
|
1646
|
+
|
|
1647
|
+
# Remove milliseconds if present (everything after the last dot)
|
|
1648
|
+
if '.' in timestamp_clean:
|
|
1649
|
+
timestamp_clean = timestamp_clean.split('.')[0]
|
|
1650
|
+
|
|
1651
|
+
# Parse the timestamp string and convert to desired format
|
|
1652
|
+
try:
|
|
1653
|
+
# Handle format: YYYY-MM-DD-HH:MM:SS
|
|
1654
|
+
if timestamp_clean.count('-') >= 2:
|
|
1655
|
+
# Replace first two dashes with colons for date part, third with space
|
|
1656
|
+
parts = timestamp_clean.split('-')
|
|
1657
|
+
if len(parts) >= 4:
|
|
1658
|
+
# parts = ['2025', '10', '27', '19:31:20']
|
|
1659
|
+
formatted = f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
|
|
1660
|
+
return formatted
|
|
1661
|
+
except Exception:
|
|
1662
|
+
pass
|
|
1663
|
+
|
|
1664
|
+
# If parsing fails, return the cleaned string as-is
|
|
1665
|
+
return timestamp_clean
|
|
1666
|
+
|
|
1620
1667
|
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
1621
1668
|
"""Get formatted current timestamp based on stream type."""
|
|
1622
1669
|
|
|
@@ -1630,7 +1677,6 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
1630
1677
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
1631
1678
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
1632
1679
|
|
|
1633
|
-
|
|
1634
1680
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
1635
1681
|
else:
|
|
1636
1682
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
@@ -1642,7 +1688,8 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
1642
1688
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
1643
1689
|
|
|
1644
1690
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
1645
|
-
|
|
1691
|
+
|
|
1692
|
+
|
|
1646
1693
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
1647
1694
|
else:
|
|
1648
1695
|
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
@@ -1661,26 +1708,60 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
1661
1708
|
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
1662
1709
|
if not stream_info:
|
|
1663
1710
|
return "00:00:00"
|
|
1664
|
-
|
|
1711
|
+
|
|
1665
1712
|
if precision:
|
|
1666
1713
|
if self.start_timer is None:
|
|
1667
|
-
|
|
1714
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1715
|
+
if not candidate or candidate == "NA":
|
|
1716
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1717
|
+
self.start_timer = candidate
|
|
1668
1718
|
return self._format_timestamp(self.start_timer)
|
|
1669
1719
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1670
|
-
|
|
1720
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1721
|
+
if not candidate or candidate == "NA":
|
|
1722
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1723
|
+
self.start_timer = candidate
|
|
1671
1724
|
return self._format_timestamp(self.start_timer)
|
|
1672
1725
|
else:
|
|
1673
1726
|
return self._format_timestamp(self.start_timer)
|
|
1674
1727
|
|
|
1675
1728
|
if self.start_timer is None:
|
|
1676
|
-
|
|
1729
|
+
# Prefer direct input_settings.stream_time if available and not NA
|
|
1730
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1731
|
+
if not candidate or candidate == "NA":
|
|
1732
|
+
# Fallback to nested stream_info.stream_time used by current timestamp path
|
|
1733
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
1734
|
+
if stream_time_str:
|
|
1735
|
+
try:
|
|
1736
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
1737
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
1738
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1739
|
+
candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1740
|
+
except:
|
|
1741
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1742
|
+
else:
|
|
1743
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1744
|
+
self.start_timer = candidate
|
|
1677
1745
|
return self._format_timestamp(self.start_timer)
|
|
1678
1746
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1679
|
-
|
|
1747
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1748
|
+
if not candidate or candidate == "NA":
|
|
1749
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
1750
|
+
if stream_time_str:
|
|
1751
|
+
try:
|
|
1752
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
1753
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
1754
|
+
ts = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1755
|
+
candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1756
|
+
except:
|
|
1757
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1758
|
+
else:
|
|
1759
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1760
|
+
self.start_timer = candidate
|
|
1680
1761
|
return self._format_timestamp(self.start_timer)
|
|
1681
1762
|
|
|
1682
1763
|
else:
|
|
1683
|
-
if self.start_timer is not None:
|
|
1764
|
+
if self.start_timer is not None and self.start_timer != "NA":
|
|
1684
1765
|
return self._format_timestamp(self.start_timer)
|
|
1685
1766
|
|
|
1686
1767
|
if self._tracking_start_time is None:
|
|
@@ -1699,52 +1780,6 @@ class ColorDetectionUseCase(BaseProcessor):
|
|
|
1699
1780
|
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
1700
1781
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1701
1782
|
|
|
1702
|
-
def _format_timestamp(self, timestamp: Any) -> str:
|
|
1703
|
-
"""Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
|
|
1704
|
-
|
|
1705
|
-
The input can be either:
|
|
1706
|
-
1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
|
|
1707
|
-
string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
1708
|
-
2. A string already following the same layout.
|
|
1709
|
-
|
|
1710
|
-
The returned value preserves the overall format of the input but truncates or pads
|
|
1711
|
-
the fractional seconds portion to **exactly two digits**.
|
|
1712
|
-
|
|
1713
|
-
Example
|
|
1714
|
-
-------
|
|
1715
|
-
>>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
|
|
1716
|
-
'2025-08-19-04:22:47.18 UTC'
|
|
1717
|
-
"""
|
|
1718
|
-
|
|
1719
|
-
# Convert numeric timestamps to the expected string representation first
|
|
1720
|
-
if isinstance(timestamp, (int, float)):
|
|
1721
|
-
timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
1722
|
-
'%Y-%m-%d-%H:%M:%S.%f UTC'
|
|
1723
|
-
)
|
|
1724
|
-
|
|
1725
|
-
# Ensure we are working with a string from here on
|
|
1726
|
-
if not isinstance(timestamp, str):
|
|
1727
|
-
return str(timestamp)
|
|
1728
|
-
|
|
1729
|
-
# If there is no fractional component, simply return the original string
|
|
1730
|
-
if '.' not in timestamp:
|
|
1731
|
-
return timestamp
|
|
1732
|
-
|
|
1733
|
-
# Split out the main portion (up to the decimal point)
|
|
1734
|
-
main_part, fractional_and_suffix = timestamp.split('.', 1)
|
|
1735
|
-
|
|
1736
|
-
# Separate fractional digits from the suffix (typically ' UTC')
|
|
1737
|
-
if ' ' in fractional_and_suffix:
|
|
1738
|
-
fractional_part, suffix = fractional_and_suffix.split(' ', 1)
|
|
1739
|
-
suffix = ' ' + suffix # Re-attach the space removed by split
|
|
1740
|
-
else:
|
|
1741
|
-
fractional_part, suffix = fractional_and_suffix, ''
|
|
1742
|
-
|
|
1743
|
-
# Guarantee exactly two digits for the fractional part
|
|
1744
|
-
fractional_part = (fractional_part + '00')[:2]
|
|
1745
|
-
|
|
1746
|
-
return f"{main_part}.{fractional_part}{suffix}"
|
|
1747
|
-
|
|
1748
1783
|
def _get_tracking_start_time(self) -> str:
|
|
1749
1784
|
"""Get the tracking start time, formatted as a string."""
|
|
1750
1785
|
if self._tracking_start_time is None:
|
|
@@ -892,11 +892,58 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
892
892
|
seconds = round(float(timestamp % 60), 2)
|
|
893
893
|
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
894
894
|
|
|
895
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
896
|
+
"""Format a timestamp to match the current timestamp format: YYYY:MM:DD HH:MM:SS.
|
|
897
|
+
|
|
898
|
+
The input can be either:
|
|
899
|
+
1. A numeric Unix timestamp (``float`` / ``int``) – it will be converted to datetime.
|
|
900
|
+
2. A string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
901
|
+
|
|
902
|
+
The returned value will be in the format: YYYY:MM:DD HH:MM:SS (no milliseconds, no UTC suffix).
|
|
903
|
+
|
|
904
|
+
Example
|
|
905
|
+
-------
|
|
906
|
+
>>> self._format_timestamp("2025-10-27-19:31:20.187574 UTC")
|
|
907
|
+
'2025:10:27 19:31:20'
|
|
908
|
+
"""
|
|
909
|
+
|
|
910
|
+
# Convert numeric timestamps to datetime first
|
|
911
|
+
if isinstance(timestamp, (int, float)):
|
|
912
|
+
dt = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
913
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
914
|
+
|
|
915
|
+
# Ensure we are working with a string from here on
|
|
916
|
+
if not isinstance(timestamp, str):
|
|
917
|
+
return str(timestamp)
|
|
918
|
+
|
|
919
|
+
# Remove ' UTC' suffix if present
|
|
920
|
+
timestamp_clean = timestamp.replace(' UTC', '').strip()
|
|
921
|
+
|
|
922
|
+
# Remove milliseconds if present (everything after the last dot)
|
|
923
|
+
if '.' in timestamp_clean:
|
|
924
|
+
timestamp_clean = timestamp_clean.split('.')[0]
|
|
925
|
+
|
|
926
|
+
# Parse the timestamp string and convert to desired format
|
|
927
|
+
try:
|
|
928
|
+
# Handle format: YYYY-MM-DD-HH:MM:SS
|
|
929
|
+
if timestamp_clean.count('-') >= 2:
|
|
930
|
+
# Replace first two dashes with colons for date part, third with space
|
|
931
|
+
parts = timestamp_clean.split('-')
|
|
932
|
+
if len(parts) >= 4:
|
|
933
|
+
# parts = ['2025', '10', '27', '19:31:20']
|
|
934
|
+
formatted = f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
|
|
935
|
+
return formatted
|
|
936
|
+
except Exception:
|
|
937
|
+
pass
|
|
938
|
+
|
|
939
|
+
# If parsing fails, return the cleaned string as-is
|
|
940
|
+
return timestamp_clean
|
|
941
|
+
|
|
895
942
|
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
896
943
|
"""Get formatted current timestamp based on stream type."""
|
|
944
|
+
|
|
897
945
|
if not stream_info:
|
|
898
946
|
return "00:00:00.00"
|
|
899
|
-
|
|
900
947
|
if precision:
|
|
901
948
|
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
902
949
|
if frame_id:
|
|
@@ -905,7 +952,6 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
905
952
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
906
953
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
907
954
|
|
|
908
|
-
|
|
909
955
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
910
956
|
else:
|
|
911
957
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
@@ -917,7 +963,8 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
917
963
|
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
918
964
|
|
|
919
965
|
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
920
|
-
|
|
966
|
+
|
|
967
|
+
|
|
921
968
|
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
922
969
|
else:
|
|
923
970
|
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
@@ -939,23 +986,57 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
939
986
|
|
|
940
987
|
if precision:
|
|
941
988
|
if self.start_timer is None:
|
|
942
|
-
|
|
989
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
990
|
+
if not candidate or candidate == "NA":
|
|
991
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
992
|
+
self.start_timer = candidate
|
|
943
993
|
return self._format_timestamp(self.start_timer)
|
|
944
994
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
945
|
-
|
|
995
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
996
|
+
if not candidate or candidate == "NA":
|
|
997
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
998
|
+
self.start_timer = candidate
|
|
946
999
|
return self._format_timestamp(self.start_timer)
|
|
947
1000
|
else:
|
|
948
1001
|
return self._format_timestamp(self.start_timer)
|
|
949
1002
|
|
|
950
1003
|
if self.start_timer is None:
|
|
951
|
-
|
|
1004
|
+
# Prefer direct input_settings.stream_time if available and not NA
|
|
1005
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1006
|
+
if not candidate or candidate == "NA":
|
|
1007
|
+
# Fallback to nested stream_info.stream_time used by current timestamp path
|
|
1008
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
1009
|
+
if stream_time_str:
|
|
1010
|
+
try:
|
|
1011
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
1012
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
1013
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1014
|
+
candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1015
|
+
except:
|
|
1016
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1017
|
+
else:
|
|
1018
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1019
|
+
self.start_timer = candidate
|
|
952
1020
|
return self._format_timestamp(self.start_timer)
|
|
953
1021
|
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
954
|
-
|
|
1022
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
1023
|
+
if not candidate or candidate == "NA":
|
|
1024
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
1025
|
+
if stream_time_str:
|
|
1026
|
+
try:
|
|
1027
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
1028
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
1029
|
+
ts = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1030
|
+
candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1031
|
+
except:
|
|
1032
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1033
|
+
else:
|
|
1034
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
1035
|
+
self.start_timer = candidate
|
|
955
1036
|
return self._format_timestamp(self.start_timer)
|
|
956
1037
|
|
|
957
1038
|
else:
|
|
958
|
-
if self.start_timer is not None:
|
|
1039
|
+
if self.start_timer is not None and self.start_timer != "NA":
|
|
959
1040
|
return self._format_timestamp(self.start_timer)
|
|
960
1041
|
|
|
961
1042
|
if self._tracking_start_time is None:
|
|
@@ -974,52 +1055,6 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
974
1055
|
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
975
1056
|
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
976
1057
|
|
|
977
|
-
def _format_timestamp(self, timestamp: Any) -> str:
|
|
978
|
-
"""Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
|
|
979
|
-
|
|
980
|
-
The input can be either:
|
|
981
|
-
1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
|
|
982
|
-
string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
983
|
-
2. A string already following the same layout.
|
|
984
|
-
|
|
985
|
-
The returned value preserves the overall format of the input but truncates or pads
|
|
986
|
-
the fractional seconds portion to **exactly two digits**.
|
|
987
|
-
|
|
988
|
-
Example
|
|
989
|
-
-------
|
|
990
|
-
>>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
|
|
991
|
-
'2025-08-19-04:22:47.18 UTC'
|
|
992
|
-
"""
|
|
993
|
-
|
|
994
|
-
# Convert numeric timestamps to the expected string representation first
|
|
995
|
-
if isinstance(timestamp, (int, float)):
|
|
996
|
-
timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
997
|
-
'%Y-%m-%d-%H:%M:%S.%f UTC'
|
|
998
|
-
)
|
|
999
|
-
|
|
1000
|
-
# Ensure we are working with a string from here on
|
|
1001
|
-
if not isinstance(timestamp, str):
|
|
1002
|
-
return str(timestamp)
|
|
1003
|
-
|
|
1004
|
-
# If there is no fractional component, simply return the original string
|
|
1005
|
-
if '.' not in timestamp:
|
|
1006
|
-
return timestamp
|
|
1007
|
-
|
|
1008
|
-
# Split out the main portion (up to the decimal point)
|
|
1009
|
-
main_part, fractional_and_suffix = timestamp.split('.', 1)
|
|
1010
|
-
|
|
1011
|
-
# Separate fractional digits from the suffix (typically ' UTC')
|
|
1012
|
-
if ' ' in fractional_and_suffix:
|
|
1013
|
-
fractional_part, suffix = fractional_and_suffix.split(' ', 1)
|
|
1014
|
-
suffix = ' ' + suffix # Re-attach the space removed by split
|
|
1015
|
-
else:
|
|
1016
|
-
fractional_part, suffix = fractional_and_suffix, ''
|
|
1017
|
-
|
|
1018
|
-
# Guarantee exactly two digits for the fractional part
|
|
1019
|
-
fractional_part = (fractional_part + '00')[:2]
|
|
1020
|
-
|
|
1021
|
-
return f"{main_part}.{fractional_part}{suffix}"
|
|
1022
|
-
|
|
1023
1058
|
def get_duration_seconds(self, start_time, end_time):
|
|
1024
1059
|
def parse_relative_time(t):
|
|
1025
1060
|
"""Parse HH:MM:SS(.f) manually into timedelta"""
|
|
@@ -1109,4 +1144,3 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
1109
1144
|
return (1,1)
|
|
1110
1145
|
|
|
1111
1146
|
|
|
1112
|
-
|