nedo-vision-worker-core 0.2.0__py3-none-any.whl → 0.3.1__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 nedo-vision-worker-core might be problematic. Click here for more details.
- nedo_vision_worker_core/__init__.py +47 -12
- nedo_vision_worker_core/callbacks/DetectionCallbackManager.py +306 -0
- nedo_vision_worker_core/callbacks/DetectionCallbackTypes.py +150 -0
- nedo_vision_worker_core/callbacks/__init__.py +27 -0
- nedo_vision_worker_core/cli.py +24 -34
- nedo_vision_worker_core/core_service.py +121 -55
- nedo_vision_worker_core/database/DatabaseManager.py +2 -2
- nedo_vision_worker_core/detection/BaseDetector.py +2 -1
- nedo_vision_worker_core/detection/DetectionManager.py +2 -2
- nedo_vision_worker_core/detection/RFDETRDetector.py +23 -5
- nedo_vision_worker_core/detection/YOLODetector.py +18 -5
- nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +1 -1
- nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +57 -3
- nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +173 -10
- nedo_vision_worker_core/models/ai_model.py +23 -2
- nedo_vision_worker_core/pipeline/PipelineProcessor.py +299 -14
- nedo_vision_worker_core/pipeline/PipelineSyncThread.py +32 -0
- nedo_vision_worker_core/repositories/PPEDetectionRepository.py +18 -15
- nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +17 -13
- nedo_vision_worker_core/services/SharedVideoStreamServer.py +276 -0
- nedo_vision_worker_core/services/VideoSharingDaemon.py +808 -0
- nedo_vision_worker_core/services/VideoSharingDaemonManager.py +257 -0
- nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +383 -0
- nedo_vision_worker_core/streams/StreamSyncThread.py +16 -2
- nedo_vision_worker_core/streams/VideoStream.py +267 -246
- nedo_vision_worker_core/streams/VideoStreamManager.py +158 -6
- nedo_vision_worker_core/tracker/TrackerManager.py +25 -31
- nedo_vision_worker_core-0.3.1.dist-info/METADATA +444 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/RECORD +32 -25
- nedo_vision_worker_core-0.2.0.dist-info/METADATA +0 -347
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/WHEEL +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ from .DetectionProcessor import DetectionProcessor
|
|
|
6
6
|
from ...pipeline.PipelineConfigManager import PipelineConfigManager
|
|
7
7
|
from ...repositories.PPEDetectionRepository import PPEDetectionRepository
|
|
8
8
|
from ...util.PersonAttributeMatcher import PersonAttributeMatcher
|
|
9
|
+
from ...callbacks import DetectionType, DetectionAttribute, BoundingBox, DetectionData
|
|
9
10
|
|
|
10
11
|
class PPEDetectionProcessor(DetectionProcessor):
|
|
11
12
|
code = "ppe"
|
|
@@ -15,22 +16,118 @@ class PPEDetectionProcessor(DetectionProcessor):
|
|
|
15
16
|
"vest": "icons/vest-green.png",
|
|
16
17
|
"no_vest": "icons/vest-red.png"
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
violation_labels = ["no_helmet", "no_vest", "no_gloves", "no_goggles", "no_boots"]
|
|
20
|
-
compliance_labels = ["helmet", "vest", "gloves", "goggles", "boots"]
|
|
21
|
-
exclusive_labels = [("helmet", "no_helmet"), ("vest", "no_vest"), ("gloves", "no_gloves"), ("goggles", "no_goggles"), ("boots", "no_boots")]
|
|
22
|
-
|
|
19
|
+
|
|
23
20
|
def __init__(self):
|
|
24
21
|
self.ppe_storage = PPEDetectionRepository()
|
|
25
22
|
self.types = []
|
|
23
|
+
self.ppe_groups = {}
|
|
24
|
+
self.group_thresholds = {}
|
|
25
|
+
self.main_class_threshold = 0.7
|
|
26
|
+
self.main_class = "person"
|
|
27
|
+
|
|
28
|
+
self.labels = ["helmet", "no_helmet", "vest", "no_vest", "gloves", "no_gloves", "goggles", "no_goggles", "boots", "no_boots"]
|
|
29
|
+
self.violation_labels = ["no_helmet", "no_vest", "no_gloves", "no_goggles", "no_boots"]
|
|
30
|
+
self.compliance_labels = ["helmet", "vest", "gloves", "goggles", "boots"]
|
|
31
|
+
self.exclusive_labels = [("helmet", "no_helmet"), ("vest", "no_vest"), ("gloves", "no_gloves"), ("goggles", "no_goggles"), ("boots", "no_boots")]
|
|
32
|
+
|
|
33
|
+
def update(self, config_manager: PipelineConfigManager, ai_model=None):
|
|
34
|
+
config = config_manager.get_feature_config(self.code, {})
|
|
35
|
+
|
|
36
|
+
# Update from AI model
|
|
37
|
+
if ai_model:
|
|
38
|
+
self._update_from_ai_model(ai_model)
|
|
39
|
+
|
|
40
|
+
# Update PPE type configuration
|
|
41
|
+
ppe_type_configs = config.get("ppeType", [])
|
|
42
|
+
self.types = []
|
|
43
|
+
self.group_thresholds = {}
|
|
44
|
+
|
|
45
|
+
for ppe_config in ppe_type_configs:
|
|
46
|
+
if isinstance(ppe_config, dict):
|
|
47
|
+
group = ppe_config.get("group")
|
|
48
|
+
threshold = ppe_config.get("confidenceThreshold", 0.7)
|
|
49
|
+
if group:
|
|
50
|
+
self.types.append(group)
|
|
51
|
+
self.group_thresholds[group] = threshold
|
|
52
|
+
elif isinstance(ppe_config, str):
|
|
53
|
+
# Backward compatibility
|
|
54
|
+
self.types.append(ppe_config)
|
|
55
|
+
self.group_thresholds[ppe_config] = 0.7
|
|
56
|
+
|
|
57
|
+
# Update main class threshold
|
|
58
|
+
self.main_class_threshold = config.get("mainClassConfidenceThreshold", 0.7)
|
|
59
|
+
|
|
60
|
+
def _update_from_ai_model(self, ai_model):
|
|
61
|
+
"""Update processor settings from AI model configuration"""
|
|
62
|
+
if ai_model and hasattr(ai_model, 'ppe_groups') and ai_model.ppe_groups:
|
|
63
|
+
self.ppe_groups = {group.group_name: group for group in ai_model.ppe_groups}
|
|
64
|
+
self._build_labels_from_groups()
|
|
65
|
+
|
|
66
|
+
if ai_model and hasattr(ai_model, 'main_class') and ai_model.main_class:
|
|
67
|
+
self.main_class = ai_model.main_class
|
|
26
68
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
def _build_labels_from_groups(self):
|
|
70
|
+
"""Build standard PPE labels from AI model PPE groups"""
|
|
71
|
+
if not self.ppe_groups:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
labels = []
|
|
75
|
+
violation_labels = []
|
|
76
|
+
compliance_labels = []
|
|
77
|
+
exclusive_labels = []
|
|
78
|
+
|
|
79
|
+
for group_name in self.ppe_groups.items():
|
|
80
|
+
compliance_class = group_name
|
|
81
|
+
violation_class = f"no_{group_name}"
|
|
82
|
+
|
|
83
|
+
# Build label lists using standard naming
|
|
84
|
+
labels.extend([compliance_class, violation_class])
|
|
85
|
+
compliance_labels.append(compliance_class)
|
|
86
|
+
violation_labels.append(violation_class)
|
|
87
|
+
exclusive_labels.append((compliance_class, violation_class))
|
|
88
|
+
|
|
89
|
+
# Update instance variables with dynamic labels
|
|
90
|
+
self.labels = labels
|
|
91
|
+
self.violation_labels = violation_labels
|
|
92
|
+
self.compliance_labels = compliance_labels
|
|
93
|
+
self.exclusive_labels = exclusive_labels
|
|
94
|
+
|
|
95
|
+
def get_multi_instance_classes(self):
|
|
96
|
+
"""Get PPE classes that can have multiple instances per person"""
|
|
97
|
+
multi_instance_base = ["boots", "gloves", "goggles"]
|
|
98
|
+
multi_instance = []
|
|
99
|
+
|
|
100
|
+
for label in self.labels:
|
|
101
|
+
base_label = label.replace("no_", "") if label.startswith("no_") else label
|
|
102
|
+
if base_label in multi_instance_base:
|
|
103
|
+
multi_instance.append(label)
|
|
104
|
+
return multi_instance
|
|
30
105
|
|
|
31
106
|
def process(self, detections: List[Dict[str, Any]], dimension: Tuple[int, int]) -> List[Dict[str, Any]]:
|
|
32
|
-
persons = [d for d in detections if d["label"] ==
|
|
33
|
-
|
|
107
|
+
persons = [d for d in detections if d["label"] == self.main_class]
|
|
108
|
+
|
|
109
|
+
ppe_attributes = []
|
|
110
|
+
for detection in detections:
|
|
111
|
+
label = detection["label"]
|
|
112
|
+
|
|
113
|
+
for group_name in self.types:
|
|
114
|
+
if group_name in self.ppe_groups:
|
|
115
|
+
group_config = self.ppe_groups[group_name]
|
|
116
|
+
|
|
117
|
+
original_compliance = group_config.get("compliance")
|
|
118
|
+
original_violation = group_config.get("violation")
|
|
119
|
+
|
|
120
|
+
if label in [original_compliance, original_violation]:
|
|
121
|
+
if label == original_compliance:
|
|
122
|
+
detection["label"] = group_name
|
|
123
|
+
elif label == original_violation:
|
|
124
|
+
detection["label"] = f"no_{group_name}"
|
|
125
|
+
|
|
126
|
+
ppe_attributes.append(detection)
|
|
127
|
+
break
|
|
128
|
+
elif label == group_name or label == f"no_{group_name}":
|
|
129
|
+
ppe_attributes.append(detection)
|
|
130
|
+
break
|
|
34
131
|
|
|
35
132
|
matched_results = PersonAttributeMatcher.match_persons_with_attributes(
|
|
36
133
|
persons, ppe_attributes, coverage_threshold=0.5
|
|
@@ -38,7 +135,73 @@ class PPEDetectionProcessor(DetectionProcessor):
|
|
|
38
135
|
|
|
39
136
|
return matched_results
|
|
40
137
|
|
|
138
|
+
def get_class_thresholds(self):
|
|
139
|
+
"""Get confidence thresholds for each class using original AI model class names"""
|
|
140
|
+
thresholds = {}
|
|
141
|
+
|
|
142
|
+
for group_name, threshold in self.group_thresholds.items():
|
|
143
|
+
if group_name in self.ppe_groups:
|
|
144
|
+
group_config = self.ppe_groups[group_name]
|
|
145
|
+
|
|
146
|
+
original_compliance = group_config.get("compliance")
|
|
147
|
+
original_violation = group_config.get("violation")
|
|
148
|
+
|
|
149
|
+
if original_compliance:
|
|
150
|
+
thresholds[original_compliance] = threshold
|
|
151
|
+
if original_violation:
|
|
152
|
+
thresholds[original_violation] = threshold
|
|
153
|
+
|
|
154
|
+
thresholds[group_name] = threshold
|
|
155
|
+
thresholds[f"no_{group_name}"] = threshold
|
|
156
|
+
else:
|
|
157
|
+
thresholds[group_name] = threshold
|
|
158
|
+
if not group_name.startswith("no_"):
|
|
159
|
+
thresholds[f"no_{group_name}"] = threshold
|
|
160
|
+
|
|
161
|
+
return thresholds
|
|
162
|
+
|
|
41
163
|
def save_to_db(self, pipeline_id: str, worker_source_id: str, frame_counter: int, tracked_objects: List[Dict[str, Any]], frame: np.ndarray, frame_drawer: FrameDrawer):
|
|
42
164
|
self.ppe_storage.save_ppe_detection(
|
|
43
165
|
pipeline_id, worker_source_id, frame_counter, tracked_objects, frame, frame_drawer
|
|
44
166
|
)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def create_detection_data(pipeline_id: str, worker_source_id: str, person_id: str,
|
|
170
|
+
detection_id: str, tracked_obj: Dict[str, Any],
|
|
171
|
+
image_path: str = "", image_tile_path: str = "",
|
|
172
|
+
frame_id: int = 0) -> DetectionData:
|
|
173
|
+
"""Create DetectionData from PPE detection data."""
|
|
174
|
+
bbox = BoundingBox.from_list(tracked_obj["bbox"])
|
|
175
|
+
|
|
176
|
+
attributes = []
|
|
177
|
+
for attr in tracked_obj.get("attributes", []):
|
|
178
|
+
attr_bbox = None
|
|
179
|
+
if "bbox" in attr:
|
|
180
|
+
attr_bbox = BoundingBox.from_list(attr["bbox"])
|
|
181
|
+
|
|
182
|
+
# Determine if this is a violation based on label
|
|
183
|
+
is_violation = attr["label"].startswith("no_") or attr["label"] in [
|
|
184
|
+
"no_helmet", "no_vest", "no_gloves", "no_goggles", "no_boots"
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
attributes.append(DetectionAttribute(
|
|
188
|
+
label=attr["label"],
|
|
189
|
+
confidence=attr.get("confidence", 1.0),
|
|
190
|
+
count=attr.get("count", 0),
|
|
191
|
+
bbox=attr_bbox,
|
|
192
|
+
is_violation=is_violation
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
return DetectionData(
|
|
196
|
+
detection_type=DetectionType.PPE_DETECTION,
|
|
197
|
+
detection_id=detection_id,
|
|
198
|
+
person_id=person_id,
|
|
199
|
+
pipeline_id=pipeline_id,
|
|
200
|
+
worker_source_id=worker_source_id,
|
|
201
|
+
confidence_score=tracked_obj.get("confidence", 1.0),
|
|
202
|
+
bbox=bbox,
|
|
203
|
+
attributes=attributes,
|
|
204
|
+
image_path=image_path,
|
|
205
|
+
image_tile_path=image_tile_path,
|
|
206
|
+
frame_id=frame_id
|
|
207
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import uuid
|
|
2
|
+
import json
|
|
2
3
|
from sqlalchemy import Column, String, DateTime
|
|
3
|
-
from datetime import datetime
|
|
4
4
|
from ..database.DatabaseManager import Base
|
|
5
5
|
|
|
6
6
|
class AIModelEntity(Base):
|
|
@@ -15,6 +15,9 @@ class AIModelEntity(Base):
|
|
|
15
15
|
download_status = Column(String, nullable=True, default="completed") # pending, downloading, completed, failed
|
|
16
16
|
last_download_attempt = Column(DateTime, nullable=True)
|
|
17
17
|
download_error = Column(String, nullable=True)
|
|
18
|
+
classes = Column(String, nullable=True)
|
|
19
|
+
ppe_class_groups = Column(String, nullable=True)
|
|
20
|
+
main_class = Column(String, nullable=True)
|
|
18
21
|
|
|
19
22
|
def __repr__(self):
|
|
20
23
|
return (
|
|
@@ -38,4 +41,22 @@ class AIModelEntity(Base):
|
|
|
38
41
|
|
|
39
42
|
def has_download_failed(self) -> bool:
|
|
40
43
|
"""Check if the model download has failed."""
|
|
41
|
-
return self.download_status == "failed"
|
|
44
|
+
return self.download_status == "failed"
|
|
45
|
+
|
|
46
|
+
def set_classes(self, classes_list):
|
|
47
|
+
self.classes = json.dumps(classes_list)
|
|
48
|
+
|
|
49
|
+
def get_classes(self):
|
|
50
|
+
return json.loads(self.classes) if self.classes else []
|
|
51
|
+
|
|
52
|
+
def set_ppe_class_groups(self, groups_list):
|
|
53
|
+
self.ppe_class_groups = json.dumps(groups_list)
|
|
54
|
+
|
|
55
|
+
def get_ppe_class_groups(self):
|
|
56
|
+
return json.loads(self.ppe_class_groups) if self.ppe_class_groups else []
|
|
57
|
+
|
|
58
|
+
def set_main_class(self, main_class):
|
|
59
|
+
self.main_class = main_class
|
|
60
|
+
|
|
61
|
+
def get_main_class(self):
|
|
62
|
+
return self.main_class or None
|
|
@@ -54,6 +54,32 @@ class PipelineProcessor:
|
|
|
54
54
|
self.debug_flag = False
|
|
55
55
|
self.debug_repo = WorkerSourcePipelineDebugRepository()
|
|
56
56
|
self.detection_repo = WorkerSourcePipelineDetectionRepository()
|
|
57
|
+
|
|
58
|
+
# Frame recovery mechanism
|
|
59
|
+
self.consecutive_frame_failures = 0
|
|
60
|
+
self.max_consecutive_failures = 150 # 1.5 seconds at 0.01s intervals
|
|
61
|
+
self.last_successful_frame_time = time.time()
|
|
62
|
+
self.stream_recovery_timeout = 30.0 # 30 seconds timeout for stream recovery
|
|
63
|
+
|
|
64
|
+
# HEVC error tracking
|
|
65
|
+
self.hevc_error_count = 0
|
|
66
|
+
self.last_hevc_recovery = 0
|
|
67
|
+
self.hevc_recovery_cooldown = 30.0 # 30 seconds between HEVC recovery attempts
|
|
68
|
+
|
|
69
|
+
def load_model(self, model):
|
|
70
|
+
"""
|
|
71
|
+
Load a new AI model into the detection manager.
|
|
72
|
+
This allows runtime model updates without restarting the pipeline.
|
|
73
|
+
|
|
74
|
+
:param model: The new AI model to load
|
|
75
|
+
"""
|
|
76
|
+
logging.info(f"🔄 Loading new model for pipeline {self.pipeline_id}: {model.name if model else 'None'}")
|
|
77
|
+
self.detection_manager.load_model(model)
|
|
78
|
+
|
|
79
|
+
# Re-initialize detection processor to use the new model configuration
|
|
80
|
+
self._update_detection_processor()
|
|
81
|
+
|
|
82
|
+
logging.info(f"✅ Model updated for pipeline {self.pipeline_id}")
|
|
57
83
|
|
|
58
84
|
def _get_detection_processor_code(self):
|
|
59
85
|
for code in self.detection_processor_codes:
|
|
@@ -83,18 +109,31 @@ class PipelineProcessor:
|
|
|
83
109
|
violation_labels=self.detection_processor.violation_labels,
|
|
84
110
|
compliance_labels=self.detection_processor.compliance_labels,
|
|
85
111
|
)
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
multi_instance_classes = []
|
|
113
|
+
if hasattr(self.detection_processor, 'get_multi_instance_classes'):
|
|
114
|
+
multi_instance_classes = self.detection_processor.get_multi_instance_classes()
|
|
115
|
+
|
|
116
|
+
self.tracker_manager.update_config(
|
|
117
|
+
attribute_labels=self.detection_processor.labels,
|
|
118
|
+
exclusive_attribute_groups=self.detection_processor.exclusive_labels,
|
|
119
|
+
multi_instance_classes=multi_instance_classes
|
|
120
|
+
)
|
|
88
121
|
|
|
89
122
|
def _update_config(self):
|
|
90
123
|
self.config_manager.update(self.pipeline_id)
|
|
91
124
|
self.preprocessor.update(self.config_manager)
|
|
92
125
|
self.detection_interval = self._get_detection_interval()
|
|
93
126
|
self._update_detection_processor()
|
|
127
|
+
|
|
128
|
+
# Reset frame failure counters on config update
|
|
129
|
+
self.consecutive_frame_failures = 0
|
|
130
|
+
self.last_successful_frame_time = time.time()
|
|
131
|
+
|
|
132
|
+
ai_model = self.detection_manager.model_metadata
|
|
94
133
|
|
|
95
134
|
if self.detection_processor:
|
|
96
135
|
config = self.config_manager.get_feature_config(self.detection_processor.code)
|
|
97
|
-
self.detection_processor.update(self.config_manager)
|
|
136
|
+
self.detection_processor.update(self.config_manager, ai_model)
|
|
98
137
|
self.threshold = config.get("minimumDetectionConfidence", 0.7)
|
|
99
138
|
|
|
100
139
|
if self.detection_processor.code == HumanDetectionProcessor.code:
|
|
@@ -102,8 +141,11 @@ class PipelineProcessor:
|
|
|
102
141
|
else:
|
|
103
142
|
self.threshold = 0.7
|
|
104
143
|
self.frame_drawer.update_config()
|
|
105
|
-
self.tracker_manager.
|
|
106
|
-
|
|
144
|
+
self.tracker_manager.update_config(
|
|
145
|
+
attribute_labels=[],
|
|
146
|
+
exclusive_attribute_groups=[],
|
|
147
|
+
multi_instance_classes=[]
|
|
148
|
+
)
|
|
107
149
|
|
|
108
150
|
def process_pipeline(self, video_manager: VideoStreamManager):
|
|
109
151
|
"""
|
|
@@ -115,6 +157,10 @@ class PipelineProcessor:
|
|
|
115
157
|
logging.info(f"🎯 Running pipeline processing for pipeline {pipeline_id} | Source: {worker_source_id}")
|
|
116
158
|
|
|
117
159
|
self._update_config()
|
|
160
|
+
|
|
161
|
+
# Reset failure counters at start
|
|
162
|
+
self.consecutive_frame_failures = 0
|
|
163
|
+
self.last_successful_frame_time = time.time()
|
|
118
164
|
|
|
119
165
|
initial_frame = self._wait_for_frame(video_manager)
|
|
120
166
|
if initial_frame is None:
|
|
@@ -136,14 +182,14 @@ class PipelineProcessor:
|
|
|
136
182
|
frame = video_manager.get_frame(worker_source_id)
|
|
137
183
|
|
|
138
184
|
if frame is None:
|
|
139
|
-
|
|
140
|
-
# Check if stream was removed
|
|
141
|
-
if not video_manager.has_stream(worker_source_id):
|
|
142
|
-
logging.info(f"🛑 Stream {worker_source_id} was removed, stopping pipeline")
|
|
185
|
+
if not self._handle_frame_failure(video_manager, worker_source_id):
|
|
143
186
|
break
|
|
144
|
-
time.sleep(0.01)
|
|
145
187
|
continue
|
|
146
188
|
|
|
189
|
+
# Reset failure counters on successful frame
|
|
190
|
+
self.consecutive_frame_failures = 0
|
|
191
|
+
self.last_successful_frame_time = time.time()
|
|
192
|
+
|
|
147
193
|
self.frame_counter += 1
|
|
148
194
|
|
|
149
195
|
self.frame_drawer.draw_polygons(frame)
|
|
@@ -195,10 +241,26 @@ class PipelineProcessor:
|
|
|
195
241
|
dimension = frame.shape[:2]
|
|
196
242
|
|
|
197
243
|
processed_frame = self.preprocessor.apply(frame)
|
|
198
|
-
|
|
244
|
+
|
|
245
|
+
class_thresholds = {}
|
|
246
|
+
ai_model = self.detection_manager.model_metadata
|
|
247
|
+
|
|
248
|
+
if self.detection_processor:
|
|
249
|
+
if self.detection_processor.code == PPEDetectionProcessor.code:
|
|
250
|
+
class_thresholds.update(self.detection_processor.get_class_thresholds())
|
|
251
|
+
elif self.detection_processor.code == HumanDetectionProcessor.code:
|
|
252
|
+
main_threshold = self.detection_processor.get_main_class_threshold(ai_model)
|
|
253
|
+
if main_threshold and ai_model and ai_model.get_main_class():
|
|
254
|
+
class_thresholds[ai_model.get_main_class()] = main_threshold
|
|
255
|
+
|
|
256
|
+
detections = self.detection_manager.detect_objects(processed_frame, self.threshold, class_thresholds)
|
|
199
257
|
detections = self.preprocessor.revert_detections_bboxes(detections, dimension)
|
|
200
|
-
|
|
201
|
-
|
|
258
|
+
|
|
259
|
+
if self.detection_processor:
|
|
260
|
+
matched_results = self.detection_processor.process(detections, dimension)
|
|
261
|
+
return self.tracker_manager.track_objects(matched_results)
|
|
262
|
+
else:
|
|
263
|
+
return self.tracker_manager.track_objects(detections)
|
|
202
264
|
|
|
203
265
|
|
|
204
266
|
def _detection_worker(self):
|
|
@@ -262,15 +324,200 @@ class PipelineProcessor:
|
|
|
262
324
|
|
|
263
325
|
def _wait_for_frame(self, video_manager, max_retries=10, sleep_time=3):
|
|
264
326
|
"""Waits until a frame is available from the video source."""
|
|
327
|
+
logging.info(f"⏳ Waiting for initial frame from {self.worker_source_id}...")
|
|
328
|
+
|
|
265
329
|
for retry_count in range(max_retries):
|
|
266
330
|
frame = video_manager.get_frame(self.worker_source_id)
|
|
267
331
|
if frame is not None:
|
|
332
|
+
logging.info(f"✅ Initial frame received from {self.worker_source_id}")
|
|
268
333
|
return frame
|
|
334
|
+
|
|
335
|
+
# Check if stream exists
|
|
336
|
+
if not video_manager.has_stream(self.worker_source_id):
|
|
337
|
+
logging.error(f"❌ Stream {self.worker_source_id} not found in video manager")
|
|
338
|
+
return None
|
|
339
|
+
|
|
269
340
|
logging.warning(f"⚠️ Waiting for video stream {self.worker_source_id} (Attempt {retry_count + 1}/{max_retries})...")
|
|
341
|
+
|
|
342
|
+
# Log stream diagnostics on later attempts
|
|
343
|
+
if retry_count >= 3:
|
|
344
|
+
self._log_stream_diagnostics(video_manager, self.worker_source_id)
|
|
345
|
+
|
|
270
346
|
time.sleep(sleep_time)
|
|
271
347
|
|
|
348
|
+
logging.error(f"❌ Failed to get initial frame from {self.worker_source_id} after {max_retries} attempts")
|
|
272
349
|
return None
|
|
273
350
|
|
|
351
|
+
def _handle_frame_failure(self, video_manager, worker_source_id):
|
|
352
|
+
"""
|
|
353
|
+
Handle frame retrieval failures with progressive backoff and recovery attempts.
|
|
354
|
+
Returns False if pipeline should stop, True to continue.
|
|
355
|
+
"""
|
|
356
|
+
self.consecutive_frame_failures += 1
|
|
357
|
+
|
|
358
|
+
# Check if stream was removed
|
|
359
|
+
if not video_manager.has_stream(worker_source_id):
|
|
360
|
+
logging.info(f"🛑 Stream {worker_source_id} was removed, stopping pipeline")
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
# Check for stream recovery timeout
|
|
364
|
+
time_since_last_frame = time.time() - self.last_successful_frame_time
|
|
365
|
+
if time_since_last_frame > self.stream_recovery_timeout:
|
|
366
|
+
logging.error(f"❌ Stream {worker_source_id} recovery timeout ({self.stream_recovery_timeout}s). Stopping pipeline.")
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
# Progressive logging and backoff
|
|
370
|
+
if self.consecutive_frame_failures <= 10:
|
|
371
|
+
# First 10 failures: minimal logging, fast retry
|
|
372
|
+
if self.consecutive_frame_failures % 5 == 1: # Log every 5th failure
|
|
373
|
+
logging.debug(f"⚠️ No frame available for {worker_source_id} (attempt {self.consecutive_frame_failures})")
|
|
374
|
+
time.sleep(0.01)
|
|
375
|
+
elif self.consecutive_frame_failures <= 50:
|
|
376
|
+
# 11-50 failures: moderate logging, slightly longer wait
|
|
377
|
+
if self.consecutive_frame_failures % 10 == 1: # Log every 10th failure
|
|
378
|
+
logging.warning(f"⚠️ No frame available for {worker_source_id} (attempt {self.consecutive_frame_failures}). Stream may be reconnecting...")
|
|
379
|
+
time.sleep(0.05)
|
|
380
|
+
elif self.consecutive_frame_failures <= self.max_consecutive_failures:
|
|
381
|
+
# 51-150 failures: more frequent logging, longer wait
|
|
382
|
+
if self.consecutive_frame_failures % 20 == 1: # Log every 20th failure
|
|
383
|
+
logging.warning(f"⚠️ Persistent frame issues for {worker_source_id} (attempt {self.consecutive_frame_failures}). Checking stream health...")
|
|
384
|
+
self._log_stream_diagnostics(video_manager, worker_source_id)
|
|
385
|
+
|
|
386
|
+
# Attempt HEVC recovery on severe persistent failures (every 60 failures to avoid too frequent reconnections)
|
|
387
|
+
if self.consecutive_frame_failures % 60 == 1:
|
|
388
|
+
# Check if we should attempt HEVC recovery based on error patterns and cooldown
|
|
389
|
+
if self._should_attempt_hevc_recovery(video_manager, worker_source_id):
|
|
390
|
+
logging.info(f"🔧 Attempting HEVC-specific recovery for persistent frame failures...")
|
|
391
|
+
recovery_success = self._handle_hevc_recovery(video_manager, worker_source_id)
|
|
392
|
+
if recovery_success:
|
|
393
|
+
logging.info(f"✅ HEVC recovery successful, continuing pipeline...")
|
|
394
|
+
return True # Continue processing after successful recovery
|
|
395
|
+
|
|
396
|
+
time.sleep(0.1)
|
|
397
|
+
else:
|
|
398
|
+
# Over max failures: critical logging and stop
|
|
399
|
+
logging.error(f"❌ Too many consecutive frame failures for {worker_source_id} ({self.consecutive_frame_failures}). Stopping pipeline.")
|
|
400
|
+
self._log_stream_diagnostics(video_manager, worker_source_id)
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
def _log_stream_diagnostics(self, video_manager, worker_source_id):
|
|
406
|
+
"""Log diagnostic information about the stream state."""
|
|
407
|
+
try:
|
|
408
|
+
stream_url = video_manager.get_stream_url(worker_source_id)
|
|
409
|
+
is_file = video_manager.is_video_file(worker_source_id)
|
|
410
|
+
|
|
411
|
+
# Get stream object for more detailed diagnostics
|
|
412
|
+
if hasattr(video_manager, 'streams') and worker_source_id in video_manager.streams:
|
|
413
|
+
stream = video_manager.streams[worker_source_id]
|
|
414
|
+
state = stream.get_state() if hasattr(stream, 'get_state') else "unknown"
|
|
415
|
+
is_connected = stream.is_connected() if hasattr(stream, 'is_connected') else "unknown"
|
|
416
|
+
|
|
417
|
+
logging.info(f"📊 Stream diagnostics for {worker_source_id}:")
|
|
418
|
+
logging.info(f" URL: {stream_url}")
|
|
419
|
+
logging.info(f" Type: {'Video file' if is_file else 'Live stream'}")
|
|
420
|
+
logging.info(f" State: {state}")
|
|
421
|
+
logging.info(f" Connected: {is_connected}")
|
|
422
|
+
logging.info(f" Time since last frame: {time.time() - self.last_successful_frame_time:.1f}s")
|
|
423
|
+
|
|
424
|
+
# Check for HEVC/codec specific issues
|
|
425
|
+
if hasattr(stream, 'get_codec_info'):
|
|
426
|
+
codec_info = stream.get_codec_info()
|
|
427
|
+
if codec_info:
|
|
428
|
+
logging.info(f" Codec: {codec_info}")
|
|
429
|
+
if 'hevc' in str(codec_info).lower() or 'h265' in str(codec_info).lower():
|
|
430
|
+
logging.warning(f" ⚠️ HEVC stream detected - may experience QP delta or POC reference errors")
|
|
431
|
+
|
|
432
|
+
# Log recent error patterns if available
|
|
433
|
+
if hasattr(stream, 'get_recent_errors'):
|
|
434
|
+
recent_errors = stream.get_recent_errors()
|
|
435
|
+
if recent_errors:
|
|
436
|
+
hevc_errors = [err for err in recent_errors if 'cu_qp_delta' in str(err.get('error', '')) or 'Could not find ref with POC' in str(err.get('error', ''))]
|
|
437
|
+
if hevc_errors:
|
|
438
|
+
logging.warning(f" 🔥 Recent HEVC errors detected: {len(hevc_errors)} codec-related errors")
|
|
439
|
+
self.hevc_error_count += len(hevc_errors)
|
|
440
|
+
|
|
441
|
+
# Log sample of recent HEVC errors for debugging
|
|
442
|
+
for i, err in enumerate(hevc_errors[-3:]): # Show last 3 errors
|
|
443
|
+
logging.warning(f" 🔥 HEVC Error {i+1}: {err.get('error', '')[:100]}...")
|
|
444
|
+
else:
|
|
445
|
+
logging.info(f"📊 Stream {worker_source_id} not found in regular streams, checking direct device streams...")
|
|
446
|
+
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logging.error(f"Error getting stream diagnostics: {e}")
|
|
449
|
+
|
|
450
|
+
def _should_attempt_hevc_recovery(self, video_manager, worker_source_id) -> bool:
|
|
451
|
+
"""
|
|
452
|
+
Determine if HEVC recovery should be attempted based on error patterns and cooldown.
|
|
453
|
+
"""
|
|
454
|
+
current_time = time.time()
|
|
455
|
+
|
|
456
|
+
# Check cooldown period
|
|
457
|
+
if current_time - self.last_hevc_recovery < self.hevc_recovery_cooldown:
|
|
458
|
+
logging.debug(f"HEVC recovery on cooldown ({current_time - self.last_hevc_recovery:.1f}s elapsed)")
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
# Check if stream has HEVC-related errors
|
|
462
|
+
if hasattr(video_manager, 'streams') and worker_source_id in video_manager.streams:
|
|
463
|
+
stream = video_manager.streams[worker_source_id]
|
|
464
|
+
if hasattr(stream, 'get_recent_errors'):
|
|
465
|
+
recent_errors = stream.get_recent_errors(max_age_seconds=60) # Last minute
|
|
466
|
+
hevc_errors = [err for err in recent_errors if
|
|
467
|
+
'cu_qp_delta' in str(err.get('error', '')) or
|
|
468
|
+
'Could not find ref with POC' in str(err.get('error', ''))]
|
|
469
|
+
|
|
470
|
+
if len(hevc_errors) >= 3: # Threshold for HEVC errors
|
|
471
|
+
logging.info(f"HEVC recovery warranted: {len(hevc_errors)} HEVC errors in last minute")
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
# Check if we have accumulated enough general HEVC errors
|
|
475
|
+
if self.hevc_error_count >= 5:
|
|
476
|
+
logging.info(f"HEVC recovery warranted: {self.hevc_error_count} total HEVC errors detected")
|
|
477
|
+
return True
|
|
478
|
+
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def _handle_hevc_recovery(self, video_manager, worker_source_id):
|
|
482
|
+
"""
|
|
483
|
+
Handle HEVC-specific recovery strategies for codec errors.
|
|
484
|
+
This method attempts to recover from common HEVC issues like QP delta and POC reference errors.
|
|
485
|
+
"""
|
|
486
|
+
try:
|
|
487
|
+
self.last_hevc_recovery = time.time() # Update recovery timestamp
|
|
488
|
+
logging.info(f"🔧 Attempting HEVC stream recovery for {worker_source_id}")
|
|
489
|
+
|
|
490
|
+
# Get the stream URL for recreation
|
|
491
|
+
stream_url = video_manager.get_stream_url(worker_source_id)
|
|
492
|
+
if not stream_url:
|
|
493
|
+
logging.error(f" Cannot get stream URL for {worker_source_id}")
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
# Strategy 1: Remove and re-add the stream to reset decoder state
|
|
497
|
+
logging.info(f" Recreating stream {worker_source_id} to reset decoder state...")
|
|
498
|
+
video_manager.remove_stream(worker_source_id)
|
|
499
|
+
time.sleep(1.0) # Give time for cleanup
|
|
500
|
+
|
|
501
|
+
# Re-add the stream
|
|
502
|
+
video_manager.add_stream(worker_source_id, stream_url)
|
|
503
|
+
time.sleep(2.0) # Give time for stream to initialize
|
|
504
|
+
|
|
505
|
+
# Strategy 2: Check if stream was successfully recreated
|
|
506
|
+
if not video_manager.has_stream(worker_source_id):
|
|
507
|
+
logging.error(f" Failed to recreate stream {worker_source_id}")
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
# Strategy 3: Reset failure counters and error counts after recovery attempt
|
|
511
|
+
self.reset_frame_failure_counters()
|
|
512
|
+
self.hevc_error_count = 0 # Reset HEVC error counter
|
|
513
|
+
|
|
514
|
+
logging.info(f"✅ HEVC recovery attempt completed for {worker_source_id}")
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logging.error(f"❌ HEVC recovery failed for {worker_source_id}: {e}")
|
|
519
|
+
return False
|
|
520
|
+
|
|
274
521
|
def stop(self):
|
|
275
522
|
"""Stops the Pipeline processor and cleans up resources."""
|
|
276
523
|
if not self.running: # Prevent multiple stops
|
|
@@ -335,4 +582,42 @@ class PipelineProcessor:
|
|
|
335
582
|
|
|
336
583
|
def enable_debug(self):
|
|
337
584
|
"""Enable debug mode for this pipeline."""
|
|
338
|
-
self.debug_flag = True
|
|
585
|
+
self.debug_flag = True
|
|
586
|
+
# Reset failure counters when debug is enabled as it may help with recovery
|
|
587
|
+
self.consecutive_frame_failures = 0
|
|
588
|
+
self.last_successful_frame_time = time.time()
|
|
589
|
+
|
|
590
|
+
def reset_frame_failure_counters(self):
|
|
591
|
+
"""Reset frame failure counters. Can be called externally to help with recovery."""
|
|
592
|
+
logging.info(f"🔄 Resetting frame failure counters for pipeline {self.pipeline_id}")
|
|
593
|
+
self.consecutive_frame_failures = 0
|
|
594
|
+
self.last_successful_frame_time = time.time()
|
|
595
|
+
self.hevc_error_count = 0 # Also reset HEVC error count
|
|
596
|
+
|
|
597
|
+
def get_hevc_diagnostics(self, video_manager) -> dict:
|
|
598
|
+
"""Get HEVC-specific diagnostics for the pipeline."""
|
|
599
|
+
diagnostics = {
|
|
600
|
+
'hevc_error_count': self.hevc_error_count,
|
|
601
|
+
'last_hevc_recovery': self.last_hevc_recovery,
|
|
602
|
+
'time_since_last_recovery': time.time() - self.last_hevc_recovery,
|
|
603
|
+
'recovery_cooldown_remaining': max(0, self.hevc_recovery_cooldown - (time.time() - self.last_hevc_recovery)),
|
|
604
|
+
'consecutive_failures': self.consecutive_frame_failures,
|
|
605
|
+
'time_since_last_frame': time.time() - self.last_successful_frame_time,
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# Add stream-specific HEVC information
|
|
609
|
+
if hasattr(video_manager, 'streams') and self.worker_source_id in video_manager.streams:
|
|
610
|
+
stream = video_manager.streams[self.worker_source_id]
|
|
611
|
+
|
|
612
|
+
if hasattr(stream, 'get_codec_info'):
|
|
613
|
+
diagnostics['codec'] = stream.get_codec_info()
|
|
614
|
+
|
|
615
|
+
if hasattr(stream, 'get_recent_errors'):
|
|
616
|
+
recent_errors = stream.get_recent_errors(max_age_seconds=300) # Last 5 minutes
|
|
617
|
+
hevc_errors = [err for err in recent_errors if
|
|
618
|
+
'cu_qp_delta' in str(err.get('error', '')) or
|
|
619
|
+
'Could not find ref with POC' in str(err.get('error', ''))]
|
|
620
|
+
diagnostics['recent_hevc_errors'] = len(hevc_errors)
|
|
621
|
+
diagnostics['total_recent_errors'] = len(recent_errors)
|
|
622
|
+
|
|
623
|
+
return diagnostics
|
|
@@ -171,6 +171,38 @@ class PipelineSyncThread(threading.Thread):
|
|
|
171
171
|
else:
|
|
172
172
|
logging.warning(f"⚠️ Pipeline {pid}: {readiness['reason']}")
|
|
173
173
|
|
|
174
|
+
# Case 7: Model metadata has changed (same ID and version, but different properties)
|
|
175
|
+
elif local_model and db_model and local_model.id == db_model.id and local_model.version == db_model.version:
|
|
176
|
+
# Check if model metadata (classes, PPE groups, main_class) has changed
|
|
177
|
+
if self._has_model_metadata_changed(local_model, db_model):
|
|
178
|
+
readiness = ModelReadinessChecker.check_model_readiness(db_model)
|
|
179
|
+
if readiness["ready"]:
|
|
180
|
+
local_proc.load_model(db_model)
|
|
181
|
+
logging.info(f"🔄 Model metadata updated for pipeline {pid}: {db_pipeline.name} "
|
|
182
|
+
f"(same version {db_model.version}, updated properties)")
|
|
183
|
+
else:
|
|
184
|
+
logging.warning(f"⚠️ Pipeline {pid}: {readiness['reason']}")
|
|
185
|
+
|
|
186
|
+
def _has_model_metadata_changed(self, local_model, db_model):
|
|
187
|
+
"""Check if model metadata has changed without version change."""
|
|
188
|
+
# Compare classes
|
|
189
|
+
local_classes = set(local_model.get_classes() or [])
|
|
190
|
+
db_classes = set(db_model.get_classes() or [])
|
|
191
|
+
if local_classes != db_classes:
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
# Compare PPE class groups
|
|
195
|
+
local_ppe_groups = local_model.get_ppe_class_groups() or {}
|
|
196
|
+
db_ppe_groups = db_model.get_ppe_class_groups() or {}
|
|
197
|
+
if local_ppe_groups != db_ppe_groups:
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
# Compare main class
|
|
201
|
+
if local_model.get_main_class() != db_model.get_main_class():
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
return False
|
|
205
|
+
|
|
174
206
|
def _has_pipeline_changed(self, local_pipeline, db_pipeline):
|
|
175
207
|
"""Checks if the pipeline configuration has changed."""
|
|
176
208
|
if db_pipeline.pipeline_status_code == "restart":
|