matrice-analytics 0.1.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +146 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3291 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +660 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from dataclasses import asdict, dataclass, field
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import copy
|
|
6
|
+
import tempfile
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import zipfile
|
|
10
|
+
import os
|
|
11
|
+
import requests
|
|
12
|
+
import logging
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from matrice_analytics.post_processing.core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
|
|
16
|
+
from matrice_analytics.post_processing.utils import (
|
|
17
|
+
filter_by_confidence,
|
|
18
|
+
filter_by_categories,
|
|
19
|
+
# apply_category_mapping,
|
|
20
|
+
count_objects_by_category,
|
|
21
|
+
count_objects_in_zones,
|
|
22
|
+
calculate_counting_summary,
|
|
23
|
+
match_results_structure,
|
|
24
|
+
bbox_smoothing,
|
|
25
|
+
BBoxSmoothingConfig,
|
|
26
|
+
BBoxSmoothingTracker
|
|
27
|
+
)
|
|
28
|
+
# External dependencies
|
|
29
|
+
import cv2
|
|
30
|
+
import numpy as np
|
|
31
|
+
#import torch
|
|
32
|
+
import re
|
|
33
|
+
from matrice_analytics.post_processing.core.config import BaseConfig, AlertConfig, ZoneConfig
|
|
34
|
+
|
|
35
|
+
# Try to import optional age/gender dependencies (fail gracefully if missing)
|
|
36
|
+
ort = None
|
|
37
|
+
Image = None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
import onnxruntime as ort
|
|
41
|
+
from PIL import Image
|
|
42
|
+
print("✓ Age/Gender dependencies available (onnxruntime, PIL)")
|
|
43
|
+
except ImportError as e:
|
|
44
|
+
error_msg = str(e)
|
|
45
|
+
print(f"⚠ Age/Gender dependencies not available: {e}")
|
|
46
|
+
|
|
47
|
+
# Check if it's a NumPy version mismatch issue and provide helpful message
|
|
48
|
+
if "NumPy" in error_msg and ("1.x" in error_msg or "2." in error_msg):
|
|
49
|
+
print("→ DETECTED: NumPy version mismatch!")
|
|
50
|
+
print(" onnxruntime was compiled with NumPy 1.x but NumPy 2.x is installed")
|
|
51
|
+
print(" To fix manually: pip install 'numpy<2' --force-reinstall")
|
|
52
|
+
else:
|
|
53
|
+
print(" To enable manually: pip install onnxruntime-gpu pillow")
|
|
54
|
+
|
|
55
|
+
print("→ Age/Gender detection will be disabled")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"⚠ Error importing Age/Gender dependencies: {e}")
|
|
58
|
+
print("→ Age/Gender detection will be disabled")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def apply_category_mapping(results: Any, index_to_category: Dict[str, str]) -> Any:
|
|
62
|
+
"""
|
|
63
|
+
Apply category index to name mapping.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
results: Detection or tracking results
|
|
67
|
+
index_to_category: Mapping from category index to category name
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Results with mapped category names
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def map_detection(
|
|
74
|
+
detection: Dict[str, Any], index_to_category: Dict[str, str]
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""Map a single detection."""
|
|
77
|
+
detection = detection.copy()
|
|
78
|
+
category_id = str(detection.get("class_id", detection.get("class_id")))
|
|
79
|
+
index_to_category = {str(k): str(v) for k, v in index_to_category.items()}
|
|
80
|
+
if category_id in index_to_category:
|
|
81
|
+
detection["category"] = index_to_category[category_id]
|
|
82
|
+
detection["class_id"] = category_id
|
|
83
|
+
return detection
|
|
84
|
+
|
|
85
|
+
if isinstance(results, list):
|
|
86
|
+
# Detection format
|
|
87
|
+
return [map_detection(r, index_to_category) for r in results]
|
|
88
|
+
|
|
89
|
+
elif isinstance(results, dict):
|
|
90
|
+
# Check if it's a simple classification result
|
|
91
|
+
if "category" in results or "class_id" in results:
|
|
92
|
+
return map_detection(results, index_to_category)
|
|
93
|
+
|
|
94
|
+
# Frame-based format
|
|
95
|
+
mapped_results = {}
|
|
96
|
+
for frame_id, detections in results.items():
|
|
97
|
+
if isinstance(detections, list):
|
|
98
|
+
mapped_results[frame_id] = [
|
|
99
|
+
map_detection(d, index_to_category) for d in detections
|
|
100
|
+
]
|
|
101
|
+
else:
|
|
102
|
+
mapped_results[frame_id] = detections
|
|
103
|
+
|
|
104
|
+
return mapped_results
|
|
105
|
+
|
|
106
|
+
return results
|
|
107
|
+
|
|
108
|
+
def load_model_from_checkpoint(checkpoint_path,local_path):
|
|
109
|
+
"""
|
|
110
|
+
Load a model from checkpoint URL
|
|
111
|
+
"""
|
|
112
|
+
if ort is None:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"onnxruntime is not available. Cannot load age/gender detection models.\n"
|
|
115
|
+
"Please install: pip install onnxruntime-gpu\n"
|
|
116
|
+
"Or downgrade numpy: pip install 'numpy<2'"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
print(f"Loading model from checkpoint: {checkpoint_path}")
|
|
121
|
+
|
|
122
|
+
# Check if checkpoint is a URL
|
|
123
|
+
if checkpoint_path.startswith(('http://', 'https://')):
|
|
124
|
+
# Download checkpoint from URL
|
|
125
|
+
response = requests.get(checkpoint_path, timeout = (30,200))
|
|
126
|
+
if response.status_code == 200:
|
|
127
|
+
with open(local_path, 'wb') as f:
|
|
128
|
+
f.write(response.content)
|
|
129
|
+
checkpoint_path = local_path
|
|
130
|
+
print(f"Downloaded checkpoint to {local_path}")
|
|
131
|
+
else:
|
|
132
|
+
print(f"Failed to download checkpoint from {checkpoint_path}")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Load the model from the checkpoint
|
|
136
|
+
model = ort.InferenceSession(checkpoint_path, providers=["CUDAExecutionProvider"])
|
|
137
|
+
print(f"{local_path} Model loaded successfully from checkpoint")
|
|
138
|
+
return model
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"Error loading model from checkpoint: {e}")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class AgeGenderConfig(BaseConfig):
|
|
147
|
+
"""Configuration for age and gender detection use case in age and gender detection."""
|
|
148
|
+
enable_smoothing: bool = False
|
|
149
|
+
smoothing_algorithm: str = "observability" # "window" or "observability"
|
|
150
|
+
smoothing_window_size: int = 20
|
|
151
|
+
smoothing_cooldown_frames: int = 5
|
|
152
|
+
smoothing_confidence_range_factor: float = 0.5
|
|
153
|
+
confidence_threshold: float = 0.2
|
|
154
|
+
frame_skip: int = 1
|
|
155
|
+
fps: Optional[float] = None
|
|
156
|
+
bbox_format: str = "auto"
|
|
157
|
+
age_url:Any = "https://s3.us-west-2.amazonaws.com/testing.resources/datasets/age_detection_prod_model.onnx"
|
|
158
|
+
gender_url:Any = "https://s3.us-west-2.amazonaws.com/testing.resources/datasets/gender_detection_prod_model.onnx"
|
|
159
|
+
usecase_categories: List[str] = field(default_factory=lambda: ['FACE'])
|
|
160
|
+
target_categories: List[str] = field(default_factory=lambda: ['FACE'])
|
|
161
|
+
alert_config: Optional[AlertConfig] = None
|
|
162
|
+
index_to_category: Optional[Dict[int, str]] = field(default_factory=lambda: {0: "FACE"})
|
|
163
|
+
|
|
164
|
+
def validate(self) -> List[str]:
|
|
165
|
+
"""Validate configuration parameters."""
|
|
166
|
+
errors = super().validate()
|
|
167
|
+
if self.confidence_threshold < 0 or self.confidence_threshold > 1:
|
|
168
|
+
errors.append("confidence_threshold must be between 0 and 1")
|
|
169
|
+
if self.frame_skip <= 0:
|
|
170
|
+
errors.append("frame_skip must be positive")
|
|
171
|
+
if self.bbox_format not in ["auto", "xmin_ymin_xmax_ymax", "x_y_width_height"]:
|
|
172
|
+
errors.append("bbox_format must be one of: auto, xmin_ymin_xmax_ymax, x_y_width_height")
|
|
173
|
+
if self.smoothing_window_size <= 0:
|
|
174
|
+
errors.append("smoothing_window_size must be positive")
|
|
175
|
+
if self.smoothing_cooldown_frames < 0:
|
|
176
|
+
errors.append("smoothing_cooldown_frames cannot be negative")
|
|
177
|
+
if self.smoothing_confidence_range_factor <= 0:
|
|
178
|
+
errors.append("smoothing_confidence_range_factor must be positive")
|
|
179
|
+
return errors
|
|
180
|
+
|
|
181
|
+
def __post_init__(self):
|
|
182
|
+
if self.gender_url and self.age_url:
|
|
183
|
+
try:
|
|
184
|
+
self.age_url = load_model_from_checkpoint(self.age_url, "age_detection_prod_model.onnx")
|
|
185
|
+
self.gender_url = load_model_from_checkpoint(self.gender_url, "gender_detection_prod_model.onnx")
|
|
186
|
+
print("✓ Age and Gender models loaded successfully")
|
|
187
|
+
except RuntimeError as e:
|
|
188
|
+
print(f"✗ Failed to load Age/Gender models: {e}")
|
|
189
|
+
print("→ Age/Gender detection will be disabled")
|
|
190
|
+
self.age_url = None
|
|
191
|
+
self.gender_url = None
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"✗ Unexpected error loading Age/Gender models: {e}")
|
|
194
|
+
import traceback
|
|
195
|
+
traceback.print_exc()
|
|
196
|
+
self.age_url = None
|
|
197
|
+
self.gender_url = None
|
|
198
|
+
else:
|
|
199
|
+
print("⚠ Age and Gender model URLs not provided - detection will be disabled")
|
|
200
|
+
self.age_url = None
|
|
201
|
+
self.gender_url = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AgeGenderUseCase(BaseProcessor):
|
|
205
|
+
def __init__(self):
|
|
206
|
+
super().__init__("age_gender_detection")
|
|
207
|
+
self.category = "age_gender_detection"
|
|
208
|
+
self.target_categories = ['FACE']
|
|
209
|
+
self.CASE_TYPE: Optional[str] = 'age_gender_detection'
|
|
210
|
+
self.CASE_VERSION: Optional[str] = '1.3'
|
|
211
|
+
self.smoothing_tracker = None
|
|
212
|
+
self.tracker = None
|
|
213
|
+
self._total_frame_counter = 0
|
|
214
|
+
self._global_frame_offset = 0
|
|
215
|
+
self._tracking_start_time = None
|
|
216
|
+
self._track_aliases: Dict[Any, Any] = {}
|
|
217
|
+
self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
|
|
218
|
+
self._track_merge_iou_threshold: float = 0.05
|
|
219
|
+
self._track_merge_time_window: float = 7.0
|
|
220
|
+
self._ascending_alert_list: List[int] = []
|
|
221
|
+
self.current_incident_end_timestamp: str = "N/A"
|
|
222
|
+
self.all_track_data: List[str] = []
|
|
223
|
+
|
|
224
|
+
self.start_timer = None
|
|
225
|
+
self.age: Dict[str:Any] = {}
|
|
226
|
+
self.gender: Dict[str:Any] = {}
|
|
227
|
+
#self.reset_timer = "2025-08-19-04:22:47.187574 UTC"
|
|
228
|
+
|
|
229
|
+
def reset_tracker(self) -> None:
|
|
230
|
+
"""Reset the advanced tracker instance."""
|
|
231
|
+
if self.tracker is not None:
|
|
232
|
+
self.tracker.reset()
|
|
233
|
+
self.logger.info("AdvancedTracker reset for new tracking session")
|
|
234
|
+
|
|
235
|
+
def reset_plate_tracking(self) -> None:
|
|
236
|
+
"""Reset plate tracking state."""
|
|
237
|
+
self._seen_plate_texts = set()
|
|
238
|
+
# CHANGE: Reset _tracked_plate_texts
|
|
239
|
+
self._tracked_plate_texts = {}
|
|
240
|
+
self._total_frame_counter = 0
|
|
241
|
+
self._global_frame_offset = 0
|
|
242
|
+
self._text_history = {}
|
|
243
|
+
self._unique_plate_texts = {}
|
|
244
|
+
self.logger.info("Plate tracking state reset")
|
|
245
|
+
|
|
246
|
+
def reset_all_tracking(self) -> None:
|
|
247
|
+
"""Reset both advanced tracker and plate tracking state."""
|
|
248
|
+
self.reset_tracker()
|
|
249
|
+
self.reset_plate_tracking()
|
|
250
|
+
self.logger.info("All plate tracking state reset")
|
|
251
|
+
|
|
252
|
+
def helper(self,detections, input_bytes, config):
|
|
253
|
+
for det in detections:
|
|
254
|
+
bbox = det.get('bounding_box')
|
|
255
|
+
xmin = int(bbox.get('xmin'))
|
|
256
|
+
xmax = int(bbox.get('xmax'))
|
|
257
|
+
ymin = int(bbox.get('ymin'))
|
|
258
|
+
ymax = int(bbox.get('xmax'))
|
|
259
|
+
track_id = det.get('track_id')
|
|
260
|
+
print(xmin,xmax,ymin,ymax)
|
|
261
|
+
|
|
262
|
+
nparr = np.frombuffer(input_bytes, np.uint8) # convert bytes to numpy array
|
|
263
|
+
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # decode image
|
|
264
|
+
|
|
265
|
+
# Step 2: Convert PIL → NumPy array
|
|
266
|
+
rgb_image = np.array(image)
|
|
267
|
+
|
|
268
|
+
face = rgb_image[ymin:ymax,xmin:xmax]
|
|
269
|
+
if face.size == 0:
|
|
270
|
+
continue
|
|
271
|
+
face_resized = cv2.resize(face, (224, 224))
|
|
272
|
+
face_resized = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB)
|
|
273
|
+
face_resized = np.expand_dims(face_resized, axis=0).astype(np.float32) / 255.0
|
|
274
|
+
|
|
275
|
+
# Age Prediction
|
|
276
|
+
age_preds = config.age_url.run(None, {"input": face_resized})[0][0]
|
|
277
|
+
predicted_age = int(np.sum(age_preds * np.arange(0, 101)))
|
|
278
|
+
confidence_age = float(np.max(age_preds))
|
|
279
|
+
# Gender prediction
|
|
280
|
+
gender_preds = config.gender_url.run(None, {"input": face_resized})[0][0]
|
|
281
|
+
predicted_gender = "Man" if np.argmax(gender_preds) == 1 else "Woman"
|
|
282
|
+
confidence_gen = float(np.max(gender_preds))
|
|
283
|
+
|
|
284
|
+
print("________________________CHECK_______________________________________")
|
|
285
|
+
print(predicted_age, predicted_gender)
|
|
286
|
+
print("________________________CHECK_______________________________________")
|
|
287
|
+
|
|
288
|
+
if track_id:
|
|
289
|
+
track_id = str(track_id)
|
|
290
|
+
|
|
291
|
+
if track_id not in self.gender:
|
|
292
|
+
self.gender[track_id] = []
|
|
293
|
+
self.gender[track_id].append(predicted_gender)
|
|
294
|
+
|
|
295
|
+
if track_id not in self.age:
|
|
296
|
+
self.age[track_id] = []
|
|
297
|
+
self.age[track_id].append(predicted_age)
|
|
298
|
+
return {"Age Data":self.age,"Gender Data":self.gender}
|
|
299
|
+
|
|
300
|
+
def process(self, data: Any, config: ConfigProtocol, input_bytes: Optional[bytes] = None,
|
|
301
|
+
context: Optional[ProcessingContext] = None, stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
|
|
302
|
+
processing_start = time.time()
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
if config.age_url is None and config.gender_url is None:
|
|
306
|
+
return self.create_error_result("Model not loaded",
|
|
307
|
+
usecase=self.name, category=self.category, context=context)
|
|
308
|
+
|
|
309
|
+
if not isinstance(config, AgeGenderConfig):
|
|
310
|
+
return self.create_error_result("Invalid configuration type for age gender detection",
|
|
311
|
+
usecase=self.name, category=self.category, context=context)
|
|
312
|
+
|
|
313
|
+
if context is None:
|
|
314
|
+
context = ProcessingContext()
|
|
315
|
+
|
|
316
|
+
if not input_bytes:
|
|
317
|
+
return self.create_error_result("input_bytes (video/image) is required for age gender detection",
|
|
318
|
+
usecase=self.name, category=self.category, context=context)
|
|
319
|
+
|
|
320
|
+
if isinstance(getattr(config, 'alert_config', None), dict):
|
|
321
|
+
try:
|
|
322
|
+
config.alert_config = AlertConfig(**config.alert_config) # type: ignore[arg-type]
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
input_format = match_results_structure(data)
|
|
327
|
+
context.input_format = input_format
|
|
328
|
+
context.confidence_threshold = config.confidence_threshold
|
|
329
|
+
|
|
330
|
+
self.logger.info(f"Processing age gender detection with format: {input_format.value}")
|
|
331
|
+
|
|
332
|
+
# Step 1: Apply confidence filtering 1
|
|
333
|
+
print("---------CONFIDENCE FILTERING",config.confidence_threshold)
|
|
334
|
+
|
|
335
|
+
processed_data = filter_by_confidence(data, config.confidence_threshold)
|
|
336
|
+
print("---------DATA1--------------",processed_data)
|
|
337
|
+
self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
|
|
338
|
+
|
|
339
|
+
# Step 2: Apply category mapping if provided
|
|
340
|
+
if config.index_to_category:
|
|
341
|
+
processed_data = apply_category_mapping(processed_data, config.index_to_category)
|
|
342
|
+
self.logger.debug("Applied category mapping")
|
|
343
|
+
print("---------DATA2--------------",processed_data)
|
|
344
|
+
# Step 3: Filter to target categories
|
|
345
|
+
if self.target_categories:
|
|
346
|
+
processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
|
|
347
|
+
self.logger.debug(f"Applied category filtering")
|
|
348
|
+
|
|
349
|
+
# effective_targets = getattr(config, 'target_categories', self.target_categories) or self.target_categories
|
|
350
|
+
# targets_lower = {str(cat).lower() for cat in effective_targets}
|
|
351
|
+
# processed_data = [d for d in processed_data if str(d.get('category', '')).lower() in targets_lower]
|
|
352
|
+
|
|
353
|
+
self.logger.debug("Applied category filtering")
|
|
354
|
+
|
|
355
|
+
raw_processed_data = [copy.deepcopy(det) for det in processed_data]
|
|
356
|
+
print("---------DATA2--------------",processed_data)
|
|
357
|
+
# Step 4: Apply bounding box smoothing if enabled
|
|
358
|
+
if config.enable_smoothing:
|
|
359
|
+
if self.smoothing_tracker is None:
|
|
360
|
+
smoothing_config = BBoxSmoothingConfig(
|
|
361
|
+
smoothing_algorithm=config.smoothing_algorithm,
|
|
362
|
+
window_size=config.smoothing_window_size,
|
|
363
|
+
cooldown_frames=config.smoothing_cooldown_frames,
|
|
364
|
+
confidence_threshold=config.confidence_threshold,
|
|
365
|
+
confidence_range_factor=config.smoothing_confidence_range_factor,
|
|
366
|
+
enable_smoothing=True
|
|
367
|
+
)
|
|
368
|
+
self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
|
|
369
|
+
processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
|
|
370
|
+
|
|
371
|
+
# Step 5: Apply advanced tracking
|
|
372
|
+
try:
|
|
373
|
+
from matrice_analytics.post_processing.advanced_tracker import AdvancedTracker
|
|
374
|
+
from matrice_analytics.post_processing.advanced_tracker import TrackerConfig
|
|
375
|
+
if self.tracker is None:
|
|
376
|
+
tracker_config = TrackerConfig(
|
|
377
|
+
track_high_thresh=float(config.confidence_threshold),
|
|
378
|
+
track_low_thresh=max(0.05, float(config.confidence_threshold) / 2),
|
|
379
|
+
new_track_thresh=float(config.confidence_threshold)
|
|
380
|
+
)
|
|
381
|
+
self.tracker = AdvancedTracker(tracker_config)
|
|
382
|
+
self.logger.info(f"Initialized AdvancedTracker with thresholds: high={tracker_config.track_high_thresh}, "
|
|
383
|
+
f"low={tracker_config.track_low_thresh}, new={tracker_config.new_track_thresh}")
|
|
384
|
+
processed_data = self.tracker.update(processed_data)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
self.logger.warning(f"AdvancedTracker failed: {e}")
|
|
387
|
+
print("---------DATA3--------------",processed_data)
|
|
388
|
+
# Step 6: Update tracking state
|
|
389
|
+
self._update_tracking_state(processed_data)
|
|
390
|
+
print("---------DATA4--------------",processed_data)
|
|
391
|
+
# Step 7: Attach masks to detections
|
|
392
|
+
processed_data = self._attach_masks_to_detections(processed_data, raw_processed_data)
|
|
393
|
+
|
|
394
|
+
# Step 10: Update frame counter
|
|
395
|
+
self._total_frame_counter += 1
|
|
396
|
+
|
|
397
|
+
# Step 11: Extract frame information
|
|
398
|
+
frame_number = None
|
|
399
|
+
if stream_info:
|
|
400
|
+
input_settings = stream_info.get("input_settings", {})
|
|
401
|
+
start_frame = input_settings.get("start_frame")
|
|
402
|
+
end_frame = input_settings.get("end_frame")
|
|
403
|
+
if start_frame is not None and end_frame is not None and start_frame == end_frame:
|
|
404
|
+
frame_number = start_frame
|
|
405
|
+
|
|
406
|
+
# Step 12: Calculate summaries
|
|
407
|
+
|
|
408
|
+
det = self.helper(processed_data,input_bytes,config)
|
|
409
|
+
print("----------------------HELPER--------------------------------")
|
|
410
|
+
print(det)
|
|
411
|
+
print("----------------------HELPER--------------------------------")
|
|
412
|
+
|
|
413
|
+
counting_summary = self._count_categories(processed_data, config,det)
|
|
414
|
+
counting_summary['total_counts'] = self.get_total_counts()
|
|
415
|
+
|
|
416
|
+
print("---------------------------------COUNTING SUMMARY------------------------------")
|
|
417
|
+
print(counting_summary)
|
|
418
|
+
print("---------------------------------COUNTING SUMMARY------------------------------")
|
|
419
|
+
|
|
420
|
+
# Step 13: Generate alerts and summaries
|
|
421
|
+
alerts = self._check_alerts(counting_summary, frame_number, config)
|
|
422
|
+
incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
|
|
423
|
+
tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
|
|
424
|
+
business_analytics_list = []
|
|
425
|
+
summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
|
|
426
|
+
track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
|
|
427
|
+
# Step 14: Build result
|
|
428
|
+
incidents = incidents_list[0] if incidents_list else {}
|
|
429
|
+
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
430
|
+
business_analytics = business_analytics_list[0] if business_analytics_list else {}
|
|
431
|
+
summary = summary_list[0] if summary_list else {}
|
|
432
|
+
agg_summary = {str(frame_number): {
|
|
433
|
+
"incidents": incidents,
|
|
434
|
+
"tracking_stats": tracking_stats,
|
|
435
|
+
"business_analytics": business_analytics,
|
|
436
|
+
"alerts": alerts,
|
|
437
|
+
"human_text": summary,
|
|
438
|
+
}}
|
|
439
|
+
|
|
440
|
+
context.mark_completed()
|
|
441
|
+
result = self.create_result(
|
|
442
|
+
data={"agg_summary": agg_summary},
|
|
443
|
+
usecase=self.name,
|
|
444
|
+
category=self.category,
|
|
445
|
+
context=context
|
|
446
|
+
)
|
|
447
|
+
proc_time = time.time() - processing_start
|
|
448
|
+
processing_latency_ms = proc_time * 1000.0
|
|
449
|
+
processing_fps = (1.0 / proc_time) if proc_time > 0 else None
|
|
450
|
+
# Log the performance metrics using the module-level logger
|
|
451
|
+
print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
return result
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
self.logger.error(f"Age and Gender Detection failed: {str(e)}", exc_info=True)
|
|
458
|
+
if context:
|
|
459
|
+
context.mark_completed()
|
|
460
|
+
return self.create_error_result(str(e), type(e).__name__, usecase=self.name, category=self.category, context=context)
|
|
461
|
+
|
|
462
|
+
def _get_frame_detections(self, data: Any, frame_key: str) -> List[Dict[str, Any]]:
|
|
463
|
+
"""Extract detections for a specific frame from data."""
|
|
464
|
+
if isinstance(data, dict):
|
|
465
|
+
return data.get(frame_key, [])
|
|
466
|
+
elif isinstance(data, list):
|
|
467
|
+
return data
|
|
468
|
+
else:
|
|
469
|
+
return []
|
|
470
|
+
|
|
471
|
+
def _count_categories(self, detections: List[Dict], config: AgeGenderConfig, data) -> Dict[str, Any]:
|
|
472
|
+
"""Count unique licence-plate texts per frame and attach detections."""
|
|
473
|
+
total_count = set()
|
|
474
|
+
valid_detections: List[Dict[str, Any]] = []
|
|
475
|
+
for det in detections:
|
|
476
|
+
if not all(k in det for k in ['category', 'confidence', 'bounding_box']):
|
|
477
|
+
continue
|
|
478
|
+
cat = det.get('category', 'Person')
|
|
479
|
+
track_id = det['track_id']
|
|
480
|
+
total_count.add(det['track_id'])
|
|
481
|
+
|
|
482
|
+
if track_id not in self.all_track_data:
|
|
483
|
+
self.all_track_data.append(track_id)
|
|
484
|
+
|
|
485
|
+
counts = {"Person": len(total_count)} if total_count else {}
|
|
486
|
+
|
|
487
|
+
valid_detections.append({
|
|
488
|
+
"bounding_box": det.get("bounding_box"),
|
|
489
|
+
"category": cat,
|
|
490
|
+
"confidence": det.get("confidence"),
|
|
491
|
+
"track_id": det.get('track_id'),
|
|
492
|
+
"frame_id": det.get("frame_id"),
|
|
493
|
+
"masks": det.get("masks", []),
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
print(data)
|
|
497
|
+
# Case 1: if data is a single dict
|
|
498
|
+
if isinstance(data, dict):
|
|
499
|
+
cats = [data] # wrap in list so loop works
|
|
500
|
+
# Case 2: if data is already a list of dicts
|
|
501
|
+
elif isinstance(data, list):
|
|
502
|
+
cats = data
|
|
503
|
+
else:
|
|
504
|
+
raise TypeError(f"Unexpected type for data: {type(data)}")
|
|
505
|
+
|
|
506
|
+
results = []
|
|
507
|
+
latest_result = {}
|
|
508
|
+
for cat in cats:
|
|
509
|
+
age_data = cat.get("Age Data", {})
|
|
510
|
+
gender_data = cat.get("Gender Data", {})
|
|
511
|
+
|
|
512
|
+
latest_age = {track_id: preds[-1] for track_id, preds in age_data.items() if preds}
|
|
513
|
+
latest_gender = {track_id: preds[-1] for track_id, preds in gender_data.items() if preds}
|
|
514
|
+
latest_result.update({
|
|
515
|
+
"Latest Age": latest_age,
|
|
516
|
+
"Latest Gender": latest_gender
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
# --- Most common gender ---
|
|
520
|
+
most_common_gender = {}
|
|
521
|
+
for track_id, preds in gender_data.items():
|
|
522
|
+
counter = Counter(preds)
|
|
523
|
+
most_common, count = counter.most_common(1)[0]
|
|
524
|
+
most_common_gender[track_id] = [most_common]
|
|
525
|
+
|
|
526
|
+
# --- Mean age ---
|
|
527
|
+
mean_age = {}
|
|
528
|
+
for track_id, preds in age_data.items():
|
|
529
|
+
if preds: # make sure list not empty
|
|
530
|
+
mean_age[track_id] = int(np.mean(preds))
|
|
531
|
+
|
|
532
|
+
results.append({
|
|
533
|
+
"Mean Age": mean_age,
|
|
534
|
+
"Most Common Gender": most_common_gender
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
"total_count": len(total_count),
|
|
540
|
+
"per_category_count": counts,
|
|
541
|
+
"detections": valid_detections,
|
|
542
|
+
"Age_Gender_Data": results[0] if isinstance(data, dict) else results,
|
|
543
|
+
"latest": latest_result
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def _generate_tracking_stats(self, counting_summary: Dict, alerts: Any, config: AgeGenderConfig,
|
|
547
|
+
frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
|
548
|
+
"""Generate structured tracking stats with frame-based keys."""
|
|
549
|
+
tracking_stats = []
|
|
550
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
551
|
+
total_counts = counting_summary.get("total_count", {})
|
|
552
|
+
# cumulative_total = sum(set(total_counts.values())) if total_counts else 0
|
|
553
|
+
per_category_count = counting_summary.get("per_category_count", {})
|
|
554
|
+
track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
|
|
555
|
+
current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
|
|
556
|
+
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
557
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
|
|
558
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
|
|
559
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
560
|
+
age_gender_data = counting_summary.get("Age_Gender_Data")
|
|
561
|
+
curr_frame_data = counting_summary.get("latest")
|
|
562
|
+
current_counts = [f"{curr_frame_data['Latest Age'][track_id]}-{curr_frame_data['Latest Gender'].get(track_id, 'Unknown')}"
|
|
563
|
+
for track_id in curr_frame_data['Latest Age']
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
human_text_lines = []
|
|
567
|
+
print("counting_summary", counting_summary)
|
|
568
|
+
human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
|
|
569
|
+
human_text_lines.append(f"\tPerson Detected: {len(current_counts)}")
|
|
570
|
+
if total_detections > 0:
|
|
571
|
+
for track_id in curr_frame_data['Latest Age']:
|
|
572
|
+
age = curr_frame_data['Latest Age'][track_id]
|
|
573
|
+
gender = curr_frame_data['Latest Gender'].get(track_id, "Unknown")
|
|
574
|
+
human_text_lines.append(f"\t\t{age}-{gender}")
|
|
575
|
+
else:
|
|
576
|
+
human_text_lines.append(f"\t- No detections")
|
|
577
|
+
age_gender_pairs = [
|
|
578
|
+
f"{age_gender_data['Mean Age'][tid]}-{age_gender_data['Most Common Gender'][tid][0]}"
|
|
579
|
+
for tid in age_gender_data['Mean Age']
|
|
580
|
+
]
|
|
581
|
+
pair_counts = Counter(age_gender_pairs)
|
|
582
|
+
result_list = [(pair,count) for pair, count in pair_counts.items()]
|
|
583
|
+
human_text_lines.append("")
|
|
584
|
+
human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
|
|
585
|
+
human_text_lines.append(f"\t- Total Detected: {len(age_gender_data['Mean Age'])}")
|
|
586
|
+
for pair, count in result_list:
|
|
587
|
+
human_text_lines.append(f"\t\t{pair}:{count}")
|
|
588
|
+
|
|
589
|
+
# total_counts_list = [{"category": cat, "count": count} for cat, count in total_counts.items() if count > 0 or cumulative_total > 0]
|
|
590
|
+
|
|
591
|
+
human_text = "\n".join(human_text_lines)
|
|
592
|
+
detections = []
|
|
593
|
+
for detection in counting_summary.get("detections", []):
|
|
594
|
+
bbox = detection.get("bounding_box", {})
|
|
595
|
+
category = detection.get("category", "FACE")
|
|
596
|
+
if category == "FACE":
|
|
597
|
+
category = "Person"
|
|
598
|
+
#plate_text = detection.get("plate_text", "")
|
|
599
|
+
segmentation = detection.get("masks", detection.get("segmentation", detection.get("mask", [])))
|
|
600
|
+
detection_obj = self.create_detection_object(category, bbox, segmentation=None)
|
|
601
|
+
detections.append(detection_obj)
|
|
602
|
+
|
|
603
|
+
alert_settings = []
|
|
604
|
+
if config.alert_config and hasattr(config.alert_config, 'alert_type'):
|
|
605
|
+
alert_settings.append({
|
|
606
|
+
"alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
|
|
607
|
+
"incident_category": self.CASE_TYPE,
|
|
608
|
+
"threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
|
|
609
|
+
"ascending": True,
|
|
610
|
+
"settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
|
|
611
|
+
getattr(config.alert_config, 'alert_value', ['JSON']))}
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
if alerts:
|
|
615
|
+
human_text_lines.append(f"Alerts: {alerts[0].get('settings', {})}")
|
|
616
|
+
else:
|
|
617
|
+
human_text_lines.append("Alerts: None")
|
|
618
|
+
|
|
619
|
+
human_text = "\n".join(human_text_lines)
|
|
620
|
+
reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
|
|
621
|
+
|
|
622
|
+
tracking_stat = self.create_tracking_stats(
|
|
623
|
+
total_counts=total_counts,
|
|
624
|
+
current_counts=current_counts,
|
|
625
|
+
detections=detections,
|
|
626
|
+
human_text=human_text,
|
|
627
|
+
camera_info=camera_info,
|
|
628
|
+
alerts=alerts,
|
|
629
|
+
alert_settings=alert_settings,
|
|
630
|
+
reset_settings=reset_settings,
|
|
631
|
+
start_time=high_precision_start_timestamp,
|
|
632
|
+
reset_time=high_precision_reset_timestamp
|
|
633
|
+
)
|
|
634
|
+
tracking_stats.append(tracking_stat)
|
|
635
|
+
print(tracking_stats)
|
|
636
|
+
return tracking_stats
|
|
637
|
+
|
|
638
|
+
def _check_alerts(self, summary: Dict, frame_number: Any, config: AgeGenderConfig) -> List[Dict]:
|
|
639
|
+
"""Check if any alert thresholds are exceeded."""
|
|
640
|
+
def get_trend(data, lookback=900, threshold=0.6):
|
|
641
|
+
window = data[-lookback:] if len(data) >= lookback else data
|
|
642
|
+
if len(window) < 2:
|
|
643
|
+
return True
|
|
644
|
+
increasing = sum(1 for i in range(1, len(window)) if window[i] >= window[i - 1])
|
|
645
|
+
return increasing / (len(window) - 1) >= threshold
|
|
646
|
+
|
|
647
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
648
|
+
alerts = []
|
|
649
|
+
total_detections = summary.get("total_count", 0)
|
|
650
|
+
# total_counts_dict = summary.get("total_counts", {})
|
|
651
|
+
# cumulative_total = sum(total_counts_dict.values()) if total_counts_dict else 0
|
|
652
|
+
per_category_count = summary.get("per_category_count", {})
|
|
653
|
+
|
|
654
|
+
if not config.alert_config:
|
|
655
|
+
return alerts
|
|
656
|
+
|
|
657
|
+
# Extract thresholds regardless of dict/dataclass
|
|
658
|
+
_alert_cfg = config.alert_config
|
|
659
|
+
_thresholds = getattr(_alert_cfg, 'count_thresholds', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('count_thresholds')
|
|
660
|
+
_types = getattr(_alert_cfg, 'alert_type', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('alert_type')
|
|
661
|
+
_values = getattr(_alert_cfg, 'alert_value', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('alert_value')
|
|
662
|
+
_types = _types if isinstance(_types, list) else (list(_types) if _types is not None else ['Default'])
|
|
663
|
+
_values = _values if isinstance(_values, list) else (list(_values) if _values is not None else ['JSON'])
|
|
664
|
+
if _thresholds:
|
|
665
|
+
for category, threshold in _thresholds.items():
|
|
666
|
+
if category == "all" and total_detections > threshold:
|
|
667
|
+
alerts.append({
|
|
668
|
+
"alert_type": _types,
|
|
669
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
670
|
+
"incident_category": self.CASE_TYPE,
|
|
671
|
+
"threshold_level": threshold,
|
|
672
|
+
"ascending": get_trend(self._ascending_alert_list),
|
|
673
|
+
"settings": {t: v for t, v in zip(_types, _values)}
|
|
674
|
+
})
|
|
675
|
+
elif category in per_category_count and per_category_count[category] > threshold:
|
|
676
|
+
alerts.append({
|
|
677
|
+
"alert_type": _types,
|
|
678
|
+
"alert_id": f"alert_{category}_{frame_key}",
|
|
679
|
+
"incident_category": self.CASE_TYPE,
|
|
680
|
+
"threshold_level": threshold,
|
|
681
|
+
"ascending": get_trend(self._ascending_alert_list),
|
|
682
|
+
"settings": {t: v for t, v in zip(_types, _values)}
|
|
683
|
+
})
|
|
684
|
+
return alerts
|
|
685
|
+
|
|
686
|
+
def _generate_incidents(self, counting_summary: Dict, alerts: List, config: AgeGenderConfig,
|
|
687
|
+
frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
|
688
|
+
"""Generate structured incidents."""
|
|
689
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
690
|
+
incidents = []
|
|
691
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
692
|
+
current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
|
|
693
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
694
|
+
|
|
695
|
+
self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
|
|
696
|
+
|
|
697
|
+
if total_detections > 0:
|
|
698
|
+
level = "low"
|
|
699
|
+
intensity = 5.0
|
|
700
|
+
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
701
|
+
if start_timestamp and self.current_incident_end_timestamp == 'N/A':
|
|
702
|
+
self.current_incident_end_timestamp = 'Incident still active'
|
|
703
|
+
elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
|
|
704
|
+
if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
|
|
705
|
+
self.current_incident_end_timestamp = current_timestamp
|
|
706
|
+
elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
|
|
707
|
+
self.current_incident_end_timestamp = 'N/A'
|
|
708
|
+
|
|
709
|
+
if config.alert_config and config.alert_config.count_thresholds:
|
|
710
|
+
threshold = config.alert_config.count_thresholds.get("all", 15)
|
|
711
|
+
intensity = min(10.0, (total_detections / threshold) * 10)
|
|
712
|
+
if intensity >= 9:
|
|
713
|
+
level = "critical"
|
|
714
|
+
self._ascending_alert_list.append(3)
|
|
715
|
+
elif intensity >= 7:
|
|
716
|
+
level = "significant"
|
|
717
|
+
self._ascending_alert_list.append(2)
|
|
718
|
+
elif intensity >= 5:
|
|
719
|
+
level = "medium"
|
|
720
|
+
self._ascending_alert_list.append(1)
|
|
721
|
+
else:
|
|
722
|
+
level = "low"
|
|
723
|
+
self._ascending_alert_list.append(0)
|
|
724
|
+
else:
|
|
725
|
+
if total_detections > 30:
|
|
726
|
+
level = "critical"
|
|
727
|
+
intensity = 10.0
|
|
728
|
+
self._ascending_alert_list.append(3)
|
|
729
|
+
elif total_detections > 25:
|
|
730
|
+
level = "significant"
|
|
731
|
+
intensity = 9.0
|
|
732
|
+
self._ascending_alert_list.append(2)
|
|
733
|
+
elif total_detections > 15:
|
|
734
|
+
level = "medium"
|
|
735
|
+
intensity = 7.0
|
|
736
|
+
self._ascending_alert_list.append(1)
|
|
737
|
+
else:
|
|
738
|
+
level = "low"
|
|
739
|
+
intensity = min(10.0, total_detections / 3.0)
|
|
740
|
+
self._ascending_alert_list.append(0)
|
|
741
|
+
|
|
742
|
+
human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
|
|
743
|
+
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
|
|
744
|
+
human_text = "\n".join(human_text_lines)
|
|
745
|
+
|
|
746
|
+
alert_settings = []
|
|
747
|
+
if config.alert_config:
|
|
748
|
+
_alert_cfg = config.alert_config
|
|
749
|
+
_types = getattr(_alert_cfg, 'alert_type', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('alert_type')
|
|
750
|
+
_values = getattr(_alert_cfg, 'alert_value', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('alert_value')
|
|
751
|
+
_thresholds = getattr(_alert_cfg, 'count_thresholds', None) if not isinstance(_alert_cfg, dict) else _alert_cfg.get('count_thresholds')
|
|
752
|
+
_types = _types if isinstance(_types, list) else (list(_types) if _types is not None else ['Default'])
|
|
753
|
+
_values = _values if isinstance(_values, list) else (list(_values) if _values is not None else ['JSON'])
|
|
754
|
+
alert_settings.append({
|
|
755
|
+
"alert_type": _types,
|
|
756
|
+
"incident_category": self.CASE_TYPE,
|
|
757
|
+
"threshold_level": _thresholds or {},
|
|
758
|
+
"ascending": True,
|
|
759
|
+
"settings": {t: v for t, v in zip(_types, _values)}
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
event = self.create_incident(
|
|
763
|
+
incident_id=f"{self.CASE_TYPE}_{frame_key}",
|
|
764
|
+
incident_type=self.CASE_TYPE,
|
|
765
|
+
severity_level=level,
|
|
766
|
+
human_text=human_text,
|
|
767
|
+
camera_info=camera_info,
|
|
768
|
+
alerts=alerts,
|
|
769
|
+
alert_settings=alert_settings,
|
|
770
|
+
start_time=start_timestamp,
|
|
771
|
+
end_time=self.current_incident_end_timestamp,
|
|
772
|
+
level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
|
|
773
|
+
)
|
|
774
|
+
incidents.append(event)
|
|
775
|
+
else:
|
|
776
|
+
self._ascending_alert_list.append(0)
|
|
777
|
+
incidents.append({})
|
|
778
|
+
|
|
779
|
+
return incidents
|
|
780
|
+
|
|
781
|
+
def _generate_summary(self, summary: Dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[Dict]:
|
|
782
|
+
"""Generate a human-readable summary."""
|
|
783
|
+
"""
|
|
784
|
+
Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
|
|
785
|
+
"""
|
|
786
|
+
lines = []
|
|
787
|
+
lines.append("Application Name: "+self.CASE_TYPE)
|
|
788
|
+
lines.append("Application Version: "+self.CASE_VERSION)
|
|
789
|
+
if len(incidents) > 0:
|
|
790
|
+
lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
|
|
791
|
+
if len(tracking_stats) > 0:
|
|
792
|
+
lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
|
|
793
|
+
if len(business_analytics) > 0:
|
|
794
|
+
lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
|
|
795
|
+
|
|
796
|
+
if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
|
|
797
|
+
lines.append("Summary: "+"No Summary Data")
|
|
798
|
+
|
|
799
|
+
return ["\n".join(lines)]
|
|
800
|
+
|
|
801
|
+
def _update_tracking_state(self, detections: List[Dict]):
|
|
802
|
+
"""Track unique track_ids per category."""
|
|
803
|
+
if not hasattr(self, "_per_category_total_track_ids"):
|
|
804
|
+
self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
|
|
805
|
+
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
806
|
+
|
|
807
|
+
for det in detections:
|
|
808
|
+
cat = det.get("category")
|
|
809
|
+
raw_track_id = det.get("track_id")
|
|
810
|
+
if cat not in self.target_categories or raw_track_id is None:
|
|
811
|
+
continue
|
|
812
|
+
bbox = det.get("bounding_box", det.get("bbox"))
|
|
813
|
+
canonical_id = self._merge_or_register_track(raw_track_id, bbox)
|
|
814
|
+
det["track_id"] = canonical_id
|
|
815
|
+
self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
|
|
816
|
+
self._current_frame_track_ids[cat].add(canonical_id)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def get_total_counts(self):
|
|
820
|
+
"""Return total unique age-gender encountered so far."""
|
|
821
|
+
return {'FACE': len(self.all_track_data)}
|
|
822
|
+
|
|
823
|
+
def _get_track_ids_info(self, detections: List[Dict]) -> Dict[str, Any]:
|
|
824
|
+
"""Get detailed information about track IDs."""
|
|
825
|
+
frame_track_ids = {det.get('track_id') for det in detections if det.get('track_id') is not None}
|
|
826
|
+
total_track_ids = set()
|
|
827
|
+
for s in getattr(self, '_per_category_total_track_ids', {}).values():
|
|
828
|
+
total_track_ids.update(s)
|
|
829
|
+
return {
|
|
830
|
+
"total_count": len(total_track_ids),
|
|
831
|
+
"current_frame_count": len(frame_track_ids),
|
|
832
|
+
"total_unique_track_ids": len(total_track_ids),
|
|
833
|
+
"current_frame_track_ids": list(frame_track_ids),
|
|
834
|
+
"last_update_time": time.time(),
|
|
835
|
+
"total_frames_processed": getattr(self, '_total_frame_counter', 0)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
def _compute_iou(self, box1: Any, box2: Any) -> float:
|
|
839
|
+
"""Compute IoU between two bounding boxes."""
|
|
840
|
+
def _bbox_to_list(bbox):
|
|
841
|
+
if bbox is None:
|
|
842
|
+
return []
|
|
843
|
+
if isinstance(bbox, list):
|
|
844
|
+
return bbox[:4] if len(bbox) >= 4 else []
|
|
845
|
+
if isinstance(bbox, dict):
|
|
846
|
+
if "xmin" in bbox:
|
|
847
|
+
return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
|
|
848
|
+
if "x1" in bbox:
|
|
849
|
+
return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
|
|
850
|
+
values = [v for v in bbox.values() if isinstance(v, (int, float))]
|
|
851
|
+
return values[:4] if len(values) >= 4 else []
|
|
852
|
+
return []
|
|
853
|
+
|
|
854
|
+
l1 = _bbox_to_list(box1)
|
|
855
|
+
l2 = _bbox_to_list(box2)
|
|
856
|
+
if len(l1) < 4 or len(l2) < 4:
|
|
857
|
+
return 0.0
|
|
858
|
+
x1_min, y1_min, x1_max, y1_max = l1
|
|
859
|
+
x2_min, y2_min, x2_max, y2_max = l2
|
|
860
|
+
x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
|
|
861
|
+
y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
|
|
862
|
+
x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
|
|
863
|
+
y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
|
|
864
|
+
inter_x_min = max(x1_min, x2_min)
|
|
865
|
+
inter_y_min = max(y1_min, y2_min)
|
|
866
|
+
inter_x_max = min(x1_max, x2_max)
|
|
867
|
+
inter_y_max = min(y1_max, y2_max)
|
|
868
|
+
inter_w = max(0.0, inter_x_max - inter_x_min)
|
|
869
|
+
inter_h = max(0.0, inter_y_max - inter_y_min)
|
|
870
|
+
inter_area = inter_w * inter_h
|
|
871
|
+
area1 = (x1_max - x1_min) * (y1_max - y1_min)
|
|
872
|
+
area2 = (x2_max - x2_min) * (y2_max - y2_min)
|
|
873
|
+
union_area = area1 + area2 - inter_area
|
|
874
|
+
return (inter_area / union_area) if union_area > 0 else 0.0
|
|
875
|
+
|
|
876
|
+
def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
|
|
877
|
+
"""Return a stable canonical ID for a raw tracker ID."""
|
|
878
|
+
if raw_id is None or bbox is None:
|
|
879
|
+
return raw_id
|
|
880
|
+
now = time.time()
|
|
881
|
+
if raw_id in self._track_aliases:
|
|
882
|
+
canonical_id = self._track_aliases[raw_id]
|
|
883
|
+
track_info = self._canonical_tracks.get(canonical_id)
|
|
884
|
+
if track_info is not None:
|
|
885
|
+
track_info["last_bbox"] = bbox
|
|
886
|
+
track_info["last_update"] = now
|
|
887
|
+
track_info["raw_ids"].add(raw_id)
|
|
888
|
+
return canonical_id
|
|
889
|
+
for canonical_id, info in self._canonical_tracks.items():
|
|
890
|
+
if now - info["last_update"] > self._track_merge_time_window:
|
|
891
|
+
continue
|
|
892
|
+
iou = self._compute_iou(bbox, info["last_bbox"])
|
|
893
|
+
if iou >= self._track_merge_iou_threshold:
|
|
894
|
+
self._track_aliases[raw_id] = canonical_id
|
|
895
|
+
info["last_bbox"] = bbox
|
|
896
|
+
info["last_update"] = now
|
|
897
|
+
info["raw_ids"].add(raw_id)
|
|
898
|
+
return canonical_id
|
|
899
|
+
canonical_id = raw_id
|
|
900
|
+
self._track_aliases[raw_id] = canonical_id
|
|
901
|
+
self._canonical_tracks[canonical_id] = {
|
|
902
|
+
"last_bbox": bbox,
|
|
903
|
+
"last_update": now,
|
|
904
|
+
"raw_ids": {raw_id},
|
|
905
|
+
}
|
|
906
|
+
return canonical_id
|
|
907
|
+
|
|
908
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
909
|
+
"""Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
|
|
910
|
+
|
|
911
|
+
The input can be either:
|
|
912
|
+
1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
|
|
913
|
+
string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
914
|
+
2. A string already following the same layout.
|
|
915
|
+
|
|
916
|
+
The returned value preserves the overall format of the input but truncates or pads
|
|
917
|
+
the fractional seconds portion to **exactly two digits**.
|
|
918
|
+
|
|
919
|
+
Example
|
|
920
|
+
-------
|
|
921
|
+
>>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
|
|
922
|
+
'2025-08-19-04:22:47.18 UTC'
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
# Convert numeric timestamps to the expected string representation first
|
|
926
|
+
if isinstance(timestamp, (int, float)):
|
|
927
|
+
timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
|
|
928
|
+
'%Y-%m-%d-%H:%M:%S.%f UTC'
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
# Ensure we are working with a string from here on
|
|
932
|
+
if not isinstance(timestamp, str):
|
|
933
|
+
return str(timestamp)
|
|
934
|
+
|
|
935
|
+
# If there is no fractional component, simply return the original string
|
|
936
|
+
if '.' not in timestamp:
|
|
937
|
+
return timestamp
|
|
938
|
+
|
|
939
|
+
# Split out the main portion (up to the decimal point)
|
|
940
|
+
main_part, fractional_and_suffix = timestamp.split('.', 1)
|
|
941
|
+
|
|
942
|
+
# Separate fractional digits from the suffix (typically ' UTC')
|
|
943
|
+
if ' ' in fractional_and_suffix:
|
|
944
|
+
fractional_part, suffix = fractional_and_suffix.split(' ', 1)
|
|
945
|
+
suffix = ' ' + suffix # Re-attach the space removed by split
|
|
946
|
+
else:
|
|
947
|
+
fractional_part, suffix = fractional_and_suffix, ''
|
|
948
|
+
|
|
949
|
+
# Guarantee exactly two digits for the fractional part
|
|
950
|
+
fractional_part = (fractional_part + '00')[:2]
|
|
951
|
+
|
|
952
|
+
return f"{main_part}.{fractional_part}{suffix}"
|
|
953
|
+
|
|
954
|
+
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
|
955
|
+
"""Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
|
|
956
|
+
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
957
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
958
|
+
|
|
959
|
+
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
|
960
|
+
"""Format timestamp for video chunks (HH:MM:SS.ms format)."""
|
|
961
|
+
hours = int(timestamp // 3600)
|
|
962
|
+
minutes = int((timestamp % 3600) // 60)
|
|
963
|
+
seconds = round(float(timestamp % 60), 2)
|
|
964
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
965
|
+
|
|
966
|
+
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
967
|
+
"""Get formatted current timestamp based on stream type."""
|
|
968
|
+
|
|
969
|
+
if not stream_info:
|
|
970
|
+
return "00:00:00.00"
|
|
971
|
+
if precision:
|
|
972
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
973
|
+
if frame_id:
|
|
974
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
975
|
+
else:
|
|
976
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
977
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
978
|
+
|
|
979
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
980
|
+
else:
|
|
981
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
982
|
+
|
|
983
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
984
|
+
if frame_id:
|
|
985
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
986
|
+
else:
|
|
987
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
988
|
+
|
|
989
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
993
|
+
else:
|
|
994
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
995
|
+
if stream_time_str:
|
|
996
|
+
try:
|
|
997
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
998
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
999
|
+
timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1000
|
+
return self._format_timestamp_for_stream(timestamp)
|
|
1001
|
+
except:
|
|
1002
|
+
return self._format_timestamp_for_stream(time.time())
|
|
1003
|
+
else:
|
|
1004
|
+
return self._format_timestamp_for_stream(time.time())
|
|
1005
|
+
|
|
1006
|
+
def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
|
|
1007
|
+
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
1008
|
+
if not stream_info:
|
|
1009
|
+
return "00:00:00"
|
|
1010
|
+
|
|
1011
|
+
if precision:
|
|
1012
|
+
if self.start_timer is None:
|
|
1013
|
+
self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
|
|
1014
|
+
return self._format_timestamp(self.start_timer)
|
|
1015
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1016
|
+
self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
|
|
1017
|
+
return self._format_timestamp(self.start_timer)
|
|
1018
|
+
else:
|
|
1019
|
+
return self._format_timestamp(self.start_timer)
|
|
1020
|
+
|
|
1021
|
+
if self.start_timer is None:
|
|
1022
|
+
self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
|
|
1023
|
+
return self._format_timestamp(self.start_timer)
|
|
1024
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
1025
|
+
self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
|
|
1026
|
+
return self._format_timestamp(self.start_timer)
|
|
1027
|
+
|
|
1028
|
+
else:
|
|
1029
|
+
if self.start_timer is not None:
|
|
1030
|
+
return self._format_timestamp(self.start_timer)
|
|
1031
|
+
|
|
1032
|
+
if self._tracking_start_time is None:
|
|
1033
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
1034
|
+
if stream_time_str:
|
|
1035
|
+
try:
|
|
1036
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
1037
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
1038
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1039
|
+
except:
|
|
1040
|
+
self._tracking_start_time = time.time()
|
|
1041
|
+
else:
|
|
1042
|
+
self._tracking_start_time = time.time()
|
|
1043
|
+
|
|
1044
|
+
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
|
1045
|
+
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
1046
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1047
|
+
|
|
1048
|
+
def _get_tracking_start_time(self) -> str:
|
|
1049
|
+
"""Get the tracking start time, formatted as a string."""
|
|
1050
|
+
if self._tracking_start_time is None:
|
|
1051
|
+
return "N/A"
|
|
1052
|
+
return self._format_timestamp(self._tracking_start_time)
|
|
1053
|
+
|
|
1054
|
+
def _set_tracking_start_time(self) -> None:
|
|
1055
|
+
"""Set the tracking start time to the current time."""
|
|
1056
|
+
self._tracking_start_time = time.time()
|
|
1057
|
+
|
|
1058
|
+
def _attach_masks_to_detections(self, processed_detections: List[Dict[str, Any]], raw_detections: List[Dict[str, Any]],
|
|
1059
|
+
iou_threshold: float = 0.5) -> List[Dict[str, Any]]:
|
|
1060
|
+
"""Attach segmentation masks from raw detections to processed detections."""
|
|
1061
|
+
if not processed_detections or not raw_detections:
|
|
1062
|
+
for det in processed_detections:
|
|
1063
|
+
det.setdefault("masks", [])
|
|
1064
|
+
return processed_detections
|
|
1065
|
+
|
|
1066
|
+
used_raw_indices = set()
|
|
1067
|
+
for det in processed_detections:
|
|
1068
|
+
best_iou = 0.0
|
|
1069
|
+
best_idx = None
|
|
1070
|
+
for idx, raw_det in enumerate(raw_detections):
|
|
1071
|
+
if idx in used_raw_indices:
|
|
1072
|
+
continue
|
|
1073
|
+
iou = self._compute_iou(det.get("bounding_box"), raw_det.get("bounding_box"))
|
|
1074
|
+
if iou > best_iou:
|
|
1075
|
+
best_iou = iou
|
|
1076
|
+
best_idx = idx
|
|
1077
|
+
if best_idx is not None and best_iou >= iou_threshold:
|
|
1078
|
+
raw_det = raw_detections[best_idx]
|
|
1079
|
+
masks = raw_det.get("masks", raw_det.get("mask"))
|
|
1080
|
+
if masks is not None:
|
|
1081
|
+
det["masks"] = masks
|
|
1082
|
+
used_raw_indices.add(best_idx)
|
|
1083
|
+
else:
|
|
1084
|
+
det.setdefault("masks", ["EMPTY"])
|
|
1085
|
+
return processed_detections
|