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
|
@@ -5,19 +5,54 @@ A library for running AI vision processing and detection in the Nedo Vision plat
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .core_service import CoreService
|
|
8
|
+
from .callbacks import DetectionType, CallbackTrigger, DetectionData, IntervalMetadata
|
|
8
9
|
|
|
9
|
-
__version__ = "0.
|
|
10
|
-
__all__ = [
|
|
10
|
+
__version__ = "0.3.1"
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CoreService",
|
|
13
|
+
"DetectionType",
|
|
14
|
+
"CallbackTrigger",
|
|
15
|
+
"DetectionData",
|
|
16
|
+
"IntervalMetadata",
|
|
17
|
+
"DetectionAttribute",
|
|
18
|
+
"BoundingBox"
|
|
19
|
+
]
|
|
11
20
|
|
|
12
|
-
# Convenience functions for callback
|
|
13
|
-
def
|
|
14
|
-
"""Register
|
|
15
|
-
return CoreService.
|
|
21
|
+
# Convenience functions for common callback patterns
|
|
22
|
+
def register_immediate_ppe_callback(name: str, callback):
|
|
23
|
+
"""Register an immediate PPE detection callback."""
|
|
24
|
+
return CoreService.register_callback(
|
|
25
|
+
name=name,
|
|
26
|
+
callback=callback,
|
|
27
|
+
trigger=CallbackTrigger.ON_NEW_DETECTION,
|
|
28
|
+
detection_types=[DetectionType.PPE_DETECTION]
|
|
29
|
+
)
|
|
16
30
|
|
|
17
|
-
def
|
|
18
|
-
"""Register
|
|
19
|
-
return CoreService.
|
|
31
|
+
def register_immediate_area_callback(name: str, callback):
|
|
32
|
+
"""Register an immediate area violation callback."""
|
|
33
|
+
return CoreService.register_callback(
|
|
34
|
+
name=name,
|
|
35
|
+
callback=callback,
|
|
36
|
+
trigger=CallbackTrigger.ON_NEW_DETECTION,
|
|
37
|
+
detection_types=[DetectionType.AREA_VIOLATION]
|
|
38
|
+
)
|
|
20
39
|
|
|
21
|
-
def
|
|
22
|
-
"""Register
|
|
23
|
-
return CoreService.
|
|
40
|
+
def register_interval_ppe_callback(name: str, callback, interval_seconds: int = 60):
|
|
41
|
+
"""Register an interval-based PPE violation summary callback."""
|
|
42
|
+
return CoreService.register_callback(
|
|
43
|
+
name=name,
|
|
44
|
+
callback=callback,
|
|
45
|
+
trigger=CallbackTrigger.ON_VIOLATION_INTERVAL,
|
|
46
|
+
detection_types=[DetectionType.PPE_DETECTION],
|
|
47
|
+
interval_seconds=interval_seconds
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def register_interval_area_callback(name: str, callback, interval_seconds: int = 60):
|
|
51
|
+
"""Register an interval-based area violation summary callback."""
|
|
52
|
+
return CoreService.register_callback(
|
|
53
|
+
name=name,
|
|
54
|
+
callback=callback,
|
|
55
|
+
trigger=CallbackTrigger.ON_VIOLATION_INTERVAL,
|
|
56
|
+
detection_types=[DetectionType.AREA_VIOLATION],
|
|
57
|
+
interval_seconds=interval_seconds
|
|
58
|
+
)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Callback management system for detection events.
|
|
3
|
+
Supports immediate callbacks (triggered on each detection) and interval-based callbacks
|
|
4
|
+
(triggered periodically based on current violation state).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from typing import Callable, Dict, List, Any, Optional
|
|
11
|
+
from collections import defaultdict, deque
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from .DetectionCallbackTypes import DetectionType, CallbackTrigger, DetectionData
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CallbackConfig:
|
|
20
|
+
"""Configuration for a registered callback."""
|
|
21
|
+
callback: Callable[[DetectionData], None]
|
|
22
|
+
trigger: CallbackTrigger
|
|
23
|
+
detection_types: List[DetectionType]
|
|
24
|
+
interval_seconds: Optional[int] = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
if self.trigger == CallbackTrigger.ON_VIOLATION_INTERVAL and self.interval_seconds is None:
|
|
28
|
+
raise ValueError("interval_seconds is required for ON_VIOLATION_INTERVAL callbacks")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DetectionCallbackManager:
|
|
32
|
+
"""Callback manager with support for immediate and current-state interval callbacks."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._callbacks: Dict[str, CallbackConfig] = {}
|
|
36
|
+
self._current_violations: Dict[str, DetectionData] = {} # Current active violations by key
|
|
37
|
+
self._interval_thread: Optional[threading.Thread] = None
|
|
38
|
+
self._stop_event = threading.Event()
|
|
39
|
+
self._lock = threading.RLock()
|
|
40
|
+
self._last_interval_trigger: Dict[int, datetime] = {} # Track last trigger time per interval
|
|
41
|
+
|
|
42
|
+
self._start_interval_thread()
|
|
43
|
+
|
|
44
|
+
def register_callback(self,
|
|
45
|
+
name: str,
|
|
46
|
+
callback: Callable[[DetectionData], None],
|
|
47
|
+
trigger: CallbackTrigger,
|
|
48
|
+
detection_types: List[DetectionType],
|
|
49
|
+
interval_seconds: Optional[int] = None) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Register a detection callback.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: Unique name for the callback
|
|
55
|
+
callback: Function to call when detection occurs
|
|
56
|
+
trigger: When to trigger the callback (immediate or interval)
|
|
57
|
+
detection_types: Types of detections to listen for
|
|
58
|
+
interval_seconds: For interval callbacks, how often to call (in seconds)
|
|
59
|
+
"""
|
|
60
|
+
with self._lock:
|
|
61
|
+
config = CallbackConfig(
|
|
62
|
+
callback=callback,
|
|
63
|
+
trigger=trigger,
|
|
64
|
+
detection_types=detection_types,
|
|
65
|
+
interval_seconds=interval_seconds
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self._callbacks[name] = config
|
|
69
|
+
logging.info(f"📞 Registered callback '{name}' for {[dt.value for dt in detection_types]} "
|
|
70
|
+
f"with trigger {trigger.value}")
|
|
71
|
+
|
|
72
|
+
def unregister_callback(self, name: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Unregister a callback.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: Name of the callback to remove
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if callback was found and removed, False otherwise
|
|
81
|
+
"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
if name in self._callbacks:
|
|
84
|
+
del self._callbacks[name]
|
|
85
|
+
logging.info(f"📞 Unregistered callback '{name}'")
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def trigger_detection(self, detection_data: DetectionData) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Trigger callbacks for a new detection.
|
|
92
|
+
Callbacks consume the data as-is, with no modification of detection logic.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
detection_data: The detection data to process
|
|
96
|
+
"""
|
|
97
|
+
with self._lock:
|
|
98
|
+
key = f"{detection_data.pipeline_id}_{detection_data.person_id}_{detection_data.detection_type.value}"
|
|
99
|
+
|
|
100
|
+
# Store current violations (only if they have violations)
|
|
101
|
+
if detection_data.has_violations():
|
|
102
|
+
self._current_violations[key] = detection_data
|
|
103
|
+
else:
|
|
104
|
+
# Remove from current violations if no longer violating
|
|
105
|
+
self._current_violations.pop(key, None)
|
|
106
|
+
|
|
107
|
+
# Trigger immediate callbacks for all detections (including non-violations)
|
|
108
|
+
self._trigger_immediate_callbacks(detection_data)
|
|
109
|
+
|
|
110
|
+
def _trigger_immediate_callbacks(self, detection_data: DetectionData) -> None:
|
|
111
|
+
"""Trigger callbacks that should fire immediately on new detections."""
|
|
112
|
+
for name, config in self._callbacks.items():
|
|
113
|
+
if (config.trigger == CallbackTrigger.ON_NEW_DETECTION and
|
|
114
|
+
detection_data.detection_type in config.detection_types):
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
config.callback(detection_data)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logging.error(f"❌ Error in callback '{name}': {e}")
|
|
120
|
+
|
|
121
|
+
def _start_interval_thread(self) -> None:
|
|
122
|
+
"""Start the background thread for interval-based callbacks."""
|
|
123
|
+
self._interval_thread = threading.Thread(
|
|
124
|
+
target=self._interval_processor,
|
|
125
|
+
name="DetectionCallbackInterval",
|
|
126
|
+
daemon=True
|
|
127
|
+
)
|
|
128
|
+
self._interval_thread.start()
|
|
129
|
+
logging.info("🔄 Started detection callback interval processor")
|
|
130
|
+
|
|
131
|
+
def _interval_processor(self) -> None:
|
|
132
|
+
"""Background thread that processes interval-based callbacks."""
|
|
133
|
+
while not self._stop_event.is_set():
|
|
134
|
+
try:
|
|
135
|
+
interval_groups = defaultdict(list)
|
|
136
|
+
with self._lock:
|
|
137
|
+
for name, config in self._callbacks.items():
|
|
138
|
+
if config.trigger == CallbackTrigger.ON_VIOLATION_INTERVAL:
|
|
139
|
+
interval_groups[config.interval_seconds].append((name, config))
|
|
140
|
+
|
|
141
|
+
current_time = datetime.utcnow()
|
|
142
|
+
|
|
143
|
+
# Clean up stale violations
|
|
144
|
+
self._cleanup_stale_violations(current_time)
|
|
145
|
+
|
|
146
|
+
for interval_seconds, callbacks in interval_groups.items():
|
|
147
|
+
self._process_interval_group(current_time, interval_seconds, callbacks)
|
|
148
|
+
|
|
149
|
+
self._stop_event.wait(1.0)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logging.error(f"❌ Error in interval processor: {e}")
|
|
153
|
+
time.sleep(1.0)
|
|
154
|
+
|
|
155
|
+
def _process_interval_group(self, current_time: datetime, interval_seconds: int,
|
|
156
|
+
callbacks: List[tuple]) -> None:
|
|
157
|
+
"""Process callbacks for a specific interval. Only triggers when violations are currently active."""
|
|
158
|
+
# Check if enough time has passed for this interval
|
|
159
|
+
last_trigger = self._last_interval_trigger.get(interval_seconds)
|
|
160
|
+
if last_trigger and (current_time - last_trigger).total_seconds() < interval_seconds:
|
|
161
|
+
return # Not time yet for this interval
|
|
162
|
+
|
|
163
|
+
with self._lock:
|
|
164
|
+
if not self._current_violations:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Only process violations that are still current (updated recently)
|
|
168
|
+
recent_threshold = timedelta(seconds=3) # Must be updated within last 3 seconds
|
|
169
|
+
current_violations = []
|
|
170
|
+
for violation in self._current_violations.values():
|
|
171
|
+
if current_time - violation.timestamp <= recent_threshold:
|
|
172
|
+
current_violations.append(violation)
|
|
173
|
+
|
|
174
|
+
if not current_violations:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
current_violation_summary = self._create_current_state_summary(
|
|
178
|
+
current_violations, current_time
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not current_violation_summary:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Update last trigger time for this interval
|
|
185
|
+
self._last_interval_trigger[interval_seconds] = current_time
|
|
186
|
+
|
|
187
|
+
for summary in current_violation_summary:
|
|
188
|
+
for name, config in callbacks:
|
|
189
|
+
if summary.detection_type in config.detection_types:
|
|
190
|
+
try:
|
|
191
|
+
config.callback(summary)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logging.error(f"❌ Error in interval callback '{name}': {e}")
|
|
194
|
+
|
|
195
|
+
def _create_current_state_summary(self, current_violations: List[DetectionData], current_time: datetime) -> List[DetectionData]:
|
|
196
|
+
"""Create current state detection data from active violations."""
|
|
197
|
+
if not current_violations:
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
# Group by detection type and pipeline
|
|
201
|
+
groups = defaultdict(list)
|
|
202
|
+
for violation in current_violations:
|
|
203
|
+
key = f"{violation.detection_type.value}_{violation.pipeline_id}"
|
|
204
|
+
groups[key].append(violation)
|
|
205
|
+
|
|
206
|
+
summaries = []
|
|
207
|
+
for group_violations in groups.values():
|
|
208
|
+
if not group_violations:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
latest = max(group_violations, key=lambda v: v.timestamp)
|
|
212
|
+
|
|
213
|
+
current_violation_counts = defaultdict(int)
|
|
214
|
+
total_current_violations = 0
|
|
215
|
+
|
|
216
|
+
for violation in group_violations:
|
|
217
|
+
for attr in violation.get_violations():
|
|
218
|
+
current_violation_counts[attr.label] += 1
|
|
219
|
+
total_current_violations += 1
|
|
220
|
+
|
|
221
|
+
if total_current_violations == 0:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
metadata = {
|
|
225
|
+
'current_violation_state': dict(current_violation_counts),
|
|
226
|
+
'total_active_violations': total_current_violations,
|
|
227
|
+
'unique_persons_in_violation': len(set(v.person_id for v in group_violations)),
|
|
228
|
+
'state_timestamp': current_time.isoformat()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
summary = DetectionData(
|
|
232
|
+
detection_type=latest.detection_type,
|
|
233
|
+
detection_id=f"current_state_{latest.detection_type.value}_{int(time.time())}",
|
|
234
|
+
person_id="multiple" if len(set(v.person_id for v in group_violations)) > 1 else latest.person_id,
|
|
235
|
+
pipeline_id=latest.pipeline_id,
|
|
236
|
+
worker_source_id=latest.worker_source_id,
|
|
237
|
+
confidence_score=sum(v.confidence_score for v in group_violations) / len(group_violations),
|
|
238
|
+
bbox=latest.bbox,
|
|
239
|
+
attributes=latest.get_violations(),
|
|
240
|
+
image_path=latest.image_path,
|
|
241
|
+
image_tile_path=latest.image_tile_path,
|
|
242
|
+
timestamp=current_time,
|
|
243
|
+
frame_id=latest.frame_id,
|
|
244
|
+
metadata=metadata
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
summaries.append(summary)
|
|
248
|
+
|
|
249
|
+
return summaries
|
|
250
|
+
|
|
251
|
+
def _cleanup_stale_violations(self, current_time: datetime) -> None:
|
|
252
|
+
"""Remove violations that haven't been updated recently (stale detections)."""
|
|
253
|
+
stale_threshold = timedelta(seconds=5) # More aggressive cleanup
|
|
254
|
+
stale_keys = []
|
|
255
|
+
|
|
256
|
+
for key, violation_data in self._current_violations.items():
|
|
257
|
+
if current_time - violation_data.timestamp > stale_threshold:
|
|
258
|
+
stale_keys.append(key)
|
|
259
|
+
logging.debug(f"🧹 Removing stale violation: {key} (age: {current_time - violation_data.timestamp})")
|
|
260
|
+
|
|
261
|
+
for key in stale_keys:
|
|
262
|
+
self._current_violations.pop(key, None)
|
|
263
|
+
|
|
264
|
+
def get_callback_stats(self) -> Dict[str, Any]:
|
|
265
|
+
"""Get statistics about registered callbacks and recent activity."""
|
|
266
|
+
with self._lock:
|
|
267
|
+
immediate_callbacks = sum(1 for c in self._callbacks.values()
|
|
268
|
+
if c.trigger == CallbackTrigger.ON_NEW_DETECTION)
|
|
269
|
+
interval_callbacks = sum(1 for c in self._callbacks.values()
|
|
270
|
+
if c.trigger == CallbackTrigger.ON_VIOLATION_INTERVAL)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
'total_callbacks': len(self._callbacks),
|
|
274
|
+
'immediate_callbacks': immediate_callbacks,
|
|
275
|
+
'interval_callbacks': interval_callbacks,
|
|
276
|
+
'current_active_violations': len(self._current_violations),
|
|
277
|
+
'callback_names': list(self._callbacks.keys())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
def list_callbacks(self) -> Dict[str, Dict[str, Any]]:
|
|
281
|
+
"""List all registered callbacks with their configurations."""
|
|
282
|
+
with self._lock:
|
|
283
|
+
result = {}
|
|
284
|
+
for name, config in self._callbacks.items():
|
|
285
|
+
result[name] = {
|
|
286
|
+
'trigger': config.trigger.value,
|
|
287
|
+
'detection_types': [dt.value for dt in config.detection_types],
|
|
288
|
+
'interval_seconds': config.interval_seconds,
|
|
289
|
+
'callback_name': config.callback.__name__
|
|
290
|
+
}
|
|
291
|
+
return result
|
|
292
|
+
|
|
293
|
+
def stop(self) -> None:
|
|
294
|
+
"""Stop the callback manager and cleanup resources."""
|
|
295
|
+
logging.info("🛑 Stopping detection callback manager...")
|
|
296
|
+
self._stop_event.set()
|
|
297
|
+
|
|
298
|
+
if self._interval_thread and self._interval_thread.is_alive():
|
|
299
|
+
self._interval_thread.join(timeout=5.0)
|
|
300
|
+
|
|
301
|
+
with self._lock:
|
|
302
|
+
self._callbacks.clear()
|
|
303
|
+
self._current_violations.clear()
|
|
304
|
+
self._last_interval_trigger.clear()
|
|
305
|
+
|
|
306
|
+
logging.info("✅ Detection callback manager stopped")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Detection callback types and unified data structures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Any, List, Optional
|
|
7
|
+
from typing_extensions import TypedDict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectionType(Enum):
|
|
13
|
+
"""Types of detections."""
|
|
14
|
+
PPE_DETECTION = "ppe_detection"
|
|
15
|
+
AREA_VIOLATION = "area_violation"
|
|
16
|
+
GENERAL_DETECTION = "general_detection"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CallbackTrigger(Enum):
|
|
20
|
+
"""When callbacks should be triggered."""
|
|
21
|
+
ON_NEW_DETECTION = "on_new_detection"
|
|
22
|
+
ON_VIOLATION_INTERVAL = "on_violation_interval"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IntervalMetadata(TypedDict):
|
|
26
|
+
"""Metadata structure for interval-based callbacks with current state."""
|
|
27
|
+
current_violation_state: Dict[str, int] # violation_type -> current count
|
|
28
|
+
total_active_violations: int
|
|
29
|
+
unique_persons_in_violation: int
|
|
30
|
+
state_timestamp: str # ISO format datetime when state was captured
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class BoundingBox:
|
|
35
|
+
"""Unified bounding box representation."""
|
|
36
|
+
x1: float
|
|
37
|
+
y1: float
|
|
38
|
+
x2: float
|
|
39
|
+
y2: float
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_list(cls, bbox: List[float]) -> 'BoundingBox':
|
|
43
|
+
"""Create BoundingBox from list [x1, y1, x2, y2]."""
|
|
44
|
+
return cls(x1=bbox[0], y1=bbox[1], x2=bbox[2], y2=bbox[3])
|
|
45
|
+
|
|
46
|
+
def to_list(self) -> List[float]:
|
|
47
|
+
"""Convert to list format [x1, y1, x2, y2]."""
|
|
48
|
+
return [self.x1, self.y1, self.x2, self.y2]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DetectionAttribute:
|
|
53
|
+
"""Represents a detection attribute (e.g., PPE violations)."""
|
|
54
|
+
label: str
|
|
55
|
+
confidence: float
|
|
56
|
+
count: int = 0
|
|
57
|
+
bbox: Optional[BoundingBox] = None
|
|
58
|
+
is_violation: bool = False
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
"""Convert to dictionary representation."""
|
|
62
|
+
result = {
|
|
63
|
+
'label': self.label,
|
|
64
|
+
'confidence': self.confidence,
|
|
65
|
+
'count': self.count,
|
|
66
|
+
'is_violation': self.is_violation
|
|
67
|
+
}
|
|
68
|
+
if self.bbox:
|
|
69
|
+
result['bbox'] = self.bbox.to_list()
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class DetectionData:
|
|
75
|
+
"""Unified data structure for all detection types."""
|
|
76
|
+
|
|
77
|
+
# Core identification
|
|
78
|
+
detection_type: DetectionType
|
|
79
|
+
detection_id: str
|
|
80
|
+
person_id: str
|
|
81
|
+
pipeline_id: str
|
|
82
|
+
worker_source_id: str
|
|
83
|
+
|
|
84
|
+
# Detection details
|
|
85
|
+
confidence_score: float
|
|
86
|
+
bbox: BoundingBox
|
|
87
|
+
attributes: List[DetectionAttribute] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
# Images
|
|
90
|
+
image_path: str = ""
|
|
91
|
+
image_tile_path: str = ""
|
|
92
|
+
|
|
93
|
+
# Timing
|
|
94
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
95
|
+
frame_id: int = 0
|
|
96
|
+
|
|
97
|
+
# Additional metadata
|
|
98
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
def get_violations(self) -> List[DetectionAttribute]:
|
|
101
|
+
"""Get only violation attributes."""
|
|
102
|
+
return [attr for attr in self.attributes if attr.is_violation]
|
|
103
|
+
|
|
104
|
+
def get_compliance(self) -> List[DetectionAttribute]:
|
|
105
|
+
"""Get only compliance attributes."""
|
|
106
|
+
return [attr for attr in self.attributes if not attr.is_violation]
|
|
107
|
+
|
|
108
|
+
def has_violations(self) -> bool:
|
|
109
|
+
"""Check if detection has any violations."""
|
|
110
|
+
return len(self.get_violations()) > 0
|
|
111
|
+
|
|
112
|
+
def get_interval_metadata(self) -> Optional[IntervalMetadata]:
|
|
113
|
+
"""Get typed metadata for interval callbacks."""
|
|
114
|
+
if not self.metadata:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Check if this looks like interval metadata
|
|
118
|
+
required_keys = {'current_violation_state', 'total_active_violations',
|
|
119
|
+
'unique_persons_in_violation', 'state_timestamp'}
|
|
120
|
+
if not required_keys.issubset(self.metadata.keys()):
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Return typed metadata
|
|
124
|
+
return IntervalMetadata(
|
|
125
|
+
current_violation_state=self.metadata['current_violation_state'],
|
|
126
|
+
total_active_violations=self.metadata['total_active_violations'],
|
|
127
|
+
unique_persons_in_violation=self.metadata['unique_persons_in_violation'],
|
|
128
|
+
state_timestamp=self.metadata['state_timestamp']
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
132
|
+
"""Convert to dictionary representation for backward compatibility."""
|
|
133
|
+
return {
|
|
134
|
+
'type': self.detection_type.value,
|
|
135
|
+
'detection_id': self.detection_id,
|
|
136
|
+
'person_id': self.person_id,
|
|
137
|
+
'pipeline_id': self.pipeline_id,
|
|
138
|
+
'worker_source_id': self.worker_source_id,
|
|
139
|
+
'confidence_score': self.confidence_score,
|
|
140
|
+
'bbox': self.bbox.to_list(),
|
|
141
|
+
'attributes': [attr.to_dict() for attr in self.attributes],
|
|
142
|
+
'violations': [attr.to_dict() for attr in self.get_violations()],
|
|
143
|
+
'compliance': [attr.to_dict() for attr in self.get_compliance()],
|
|
144
|
+
'image_path': self.image_path,
|
|
145
|
+
'image_tile_path': self.image_tile_path,
|
|
146
|
+
'timestamp': self.timestamp.isoformat(),
|
|
147
|
+
'frame_id': self.frame_id,
|
|
148
|
+
'has_violations': self.has_violations(),
|
|
149
|
+
'metadata': self.metadata
|
|
150
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Detection callback system for Nedo Vision Worker Core.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .DetectionCallbackTypes import (
|
|
6
|
+
CallbackTrigger,
|
|
7
|
+
DetectionType,
|
|
8
|
+
DetectionAttribute,
|
|
9
|
+
BoundingBox,
|
|
10
|
+
DetectionData,
|
|
11
|
+
IntervalMetadata
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .DetectionCallbackManager import (
|
|
15
|
+
DetectionCallbackManager,
|
|
16
|
+
CallbackConfig
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'DetectionCallbackManager',
|
|
21
|
+
'CallbackTrigger',
|
|
22
|
+
'DetectionType',
|
|
23
|
+
'DetectionAttribute',
|
|
24
|
+
'BoundingBox',
|
|
25
|
+
'DetectionData',
|
|
26
|
+
'IntervalMetadata'
|
|
27
|
+
]
|
nedo_vision_worker_core/cli.py
CHANGED
|
@@ -21,7 +21,7 @@ def main():
|
|
|
21
21
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
22
|
epilog="""
|
|
23
23
|
Examples:
|
|
24
|
-
# Start core service with default settings
|
|
24
|
+
# Start core service with default settings (includes auto video sharing daemon)
|
|
25
25
|
nedo-core run
|
|
26
26
|
|
|
27
27
|
# Start with custom drawing assets path
|
|
@@ -33,12 +33,20 @@ Examples:
|
|
|
33
33
|
# Start with custom storage path and RTMP server
|
|
34
34
|
nedo-core run --storage-path /path/to/storage --rtmp-server rtmp://server.com:1935/live
|
|
35
35
|
|
|
36
|
+
# Start without automatic video sharing daemon
|
|
37
|
+
nedo-core run --disable-video-sharing-daemon
|
|
38
|
+
|
|
36
39
|
# Start with all custom parameters
|
|
37
40
|
nedo-core run --drawing-assets /path/to/assets --log-level DEBUG --storage-path /data --rtmp-server rtmp://server.com:1935/live
|
|
38
41
|
|
|
39
42
|
# Run system diagnostics
|
|
40
43
|
nedo-core doctor
|
|
41
44
|
|
|
45
|
+
Video Sharing Daemon:
|
|
46
|
+
By default, the core service automatically starts video sharing daemons for devices
|
|
47
|
+
as needed. This enables multiple processes to access the same video device simultaneously
|
|
48
|
+
without "device busy" errors. Use --disable-video-sharing-daemon to turn this off.
|
|
49
|
+
|
|
42
50
|
Detection Callbacks:
|
|
43
51
|
The core service supports detection callbacks for extensible event handling.
|
|
44
52
|
See example_callbacks.py for usage examples.
|
|
@@ -83,33 +91,15 @@ Detection Callbacks:
|
|
|
83
91
|
|
|
84
92
|
run_parser.add_argument(
|
|
85
93
|
"--rtmp-server",
|
|
86
|
-
default="rtmp://
|
|
87
|
-
help="RTMP server URL for video streaming (default: rtmp://
|
|
94
|
+
default="rtmp://live.vision.sindika.co.id:1935/live",
|
|
95
|
+
help="RTMP server URL for video streaming (default: rtmp://live.vision.sindika.co.id:1935/live)"
|
|
88
96
|
)
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
parser.add_argument(
|
|
97
|
-
"--log-level",
|
|
98
|
-
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
99
|
-
default="INFO",
|
|
100
|
-
help="(Legacy) Logging level (default: INFO)"
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
parser.add_argument(
|
|
104
|
-
"--storage-path",
|
|
105
|
-
default="data",
|
|
106
|
-
help="(Legacy) Storage path for databases and files (default: data)"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
parser.add_argument(
|
|
110
|
-
"--rtmp-server",
|
|
111
|
-
default="rtmp://localhost:1935/live",
|
|
112
|
-
help="(Legacy) RTMP server URL for video streaming (default: rtmp://localhost:1935/live)"
|
|
98
|
+
run_parser.add_argument(
|
|
99
|
+
"--disable_video_sharing_daemon",
|
|
100
|
+
action="store_true",
|
|
101
|
+
default=False,
|
|
102
|
+
help="Disable automatic video sharing daemon management (default: False)"
|
|
113
103
|
)
|
|
114
104
|
|
|
115
105
|
parser.add_argument(
|
|
@@ -125,11 +115,7 @@ Detection Callbacks:
|
|
|
125
115
|
run_core_service(args)
|
|
126
116
|
elif args.command == 'doctor':
|
|
127
117
|
run_doctor()
|
|
128
|
-
elif hasattr(args, 'drawing_assets') and args.drawing_assets is not None: # Legacy mode - if any arguments are provided without subcommand
|
|
129
|
-
print("⚠️ Warning: Using legacy command format. Consider using 'nedo-core run --drawing-assets ...' instead.")
|
|
130
|
-
run_core_service(args)
|
|
131
118
|
else:
|
|
132
|
-
# If no subcommand provided, show help
|
|
133
119
|
parser.print_help()
|
|
134
120
|
sys.exit(1)
|
|
135
121
|
|
|
@@ -148,9 +134,6 @@ def run_doctor():
|
|
|
148
134
|
sys.exit(1)
|
|
149
135
|
|
|
150
136
|
|
|
151
|
-
def run_core_service(args):
|
|
152
|
-
"""Run the core service with the provided arguments."""
|
|
153
|
-
|
|
154
137
|
def run_core_service(args):
|
|
155
138
|
"""Run the core service with the provided arguments."""
|
|
156
139
|
# Set up signal handlers for graceful shutdown
|
|
@@ -160,12 +143,18 @@ def run_core_service(args):
|
|
|
160
143
|
logger = logging.getLogger(__name__)
|
|
161
144
|
|
|
162
145
|
try:
|
|
146
|
+
# Determine video sharing daemon setting
|
|
147
|
+
enable_daemon = True # Default
|
|
148
|
+
if hasattr(args, 'disable_video_sharing_daemon') and args.disable_video_sharing_daemon:
|
|
149
|
+
enable_daemon = False
|
|
150
|
+
|
|
163
151
|
# Create and start the core service
|
|
164
152
|
service = CoreService(
|
|
165
153
|
drawing_assets_path=args.drawing_assets,
|
|
166
154
|
log_level=args.log_level,
|
|
167
155
|
storage_path=args.storage_path,
|
|
168
|
-
rtmp_server=args.rtmp_server
|
|
156
|
+
rtmp_server=args.rtmp_server,
|
|
157
|
+
enable_video_sharing_daemon=enable_daemon
|
|
169
158
|
)
|
|
170
159
|
|
|
171
160
|
logger.info("🚀 Starting Nedo Vision Core...")
|
|
@@ -176,6 +165,7 @@ def run_core_service(args):
|
|
|
176
165
|
logger.info(f"📝 Log Level: {args.log_level}")
|
|
177
166
|
logger.info(f"💾 Storage Path: {args.storage_path}")
|
|
178
167
|
logger.info(f"📡 RTMP Server: {args.rtmp_server}")
|
|
168
|
+
logger.info(f"🔗 Video Sharing Daemon: {'Enabled' if enable_daemon else 'Disabled'}")
|
|
179
169
|
logger.info("Press Ctrl+C to stop the service")
|
|
180
170
|
|
|
181
171
|
# Start the service
|