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
|
@@ -120,23 +120,26 @@ class PPEDetectionRepository:
|
|
|
120
120
|
self.session.commit()
|
|
121
121
|
logging.info(f"✅ Inserted detection for Person {person_id}, Attributes: {valid_attributes}")
|
|
122
122
|
|
|
123
|
-
# Trigger detection callback
|
|
123
|
+
# Trigger detection callback with unified data structure
|
|
124
124
|
try:
|
|
125
125
|
from ..core_service import CoreService
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
from ..detection.detection_processing.PPEDetectionProcessor import PPEDetectionProcessor
|
|
127
|
+
|
|
128
|
+
# Create unified detection data using the processor's factory method
|
|
129
|
+
unified_data = PPEDetectionProcessor.create_detection_data(
|
|
130
|
+
pipeline_id=pipeline_id,
|
|
131
|
+
worker_source_id=worker_source_id,
|
|
132
|
+
person_id=person_id,
|
|
133
|
+
detection_id=new_detection.id,
|
|
134
|
+
tracked_obj=tracked_obj,
|
|
135
|
+
image_path=full_image_path,
|
|
136
|
+
image_tile_path=cropped_image_path,
|
|
137
|
+
frame_id=frame_id
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Trigger callbacks
|
|
141
|
+
CoreService.trigger_detection(unified_data)
|
|
142
|
+
|
|
140
143
|
except Exception as e:
|
|
141
144
|
logging.warning(f"⚠️ Failed to trigger PPE detection callback: {e}")
|
|
142
145
|
|
|
@@ -69,19 +69,23 @@ class RestrictedAreaRepository:
|
|
|
69
69
|
# Trigger detection callback
|
|
70
70
|
try:
|
|
71
71
|
from ..core_service import CoreService
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
from ..detection.detection_processing.HumanDetectionProcessor import HumanDetectionProcessor
|
|
73
|
+
|
|
74
|
+
# Create unified detection data using the processor's factory method
|
|
75
|
+
unified_data = HumanDetectionProcessor.create_detection_data(
|
|
76
|
+
pipeline_id=pipeline_id,
|
|
77
|
+
worker_source_id=worker_source_id,
|
|
78
|
+
person_id=person_id,
|
|
79
|
+
detection_id=new_detection.id if hasattr(new_detection, 'id') else f"area_{person_id}_{current_datetime}",
|
|
80
|
+
tracked_obj=tracked_obj,
|
|
81
|
+
image_path=full_image_path,
|
|
82
|
+
image_tile_path=cropped_image_path,
|
|
83
|
+
frame_id=frame_id
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Trigger callbacks
|
|
87
|
+
CoreService.trigger_detection(unified_data)
|
|
88
|
+
|
|
85
89
|
except Exception as e:
|
|
86
90
|
logging.warning(f"⚠️ Failed to trigger area violation callback: {e}")
|
|
87
91
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import cv2
|
|
5
|
+
import queue
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Dict, Optional, Callable, List
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import tempfile
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SharedVideoStreamServer:
|
|
15
|
+
"""
|
|
16
|
+
A shared video stream server that captures from a video device once
|
|
17
|
+
and distributes frames to multiple consumers (both services).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_instances: Dict[int, 'SharedVideoStreamServer'] = {}
|
|
21
|
+
_lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
def __new__(cls, device_index: int):
|
|
24
|
+
with cls._lock:
|
|
25
|
+
if device_index not in cls._instances:
|
|
26
|
+
cls._instances[device_index] = super().__new__(cls)
|
|
27
|
+
cls._instances[device_index]._initialized = False
|
|
28
|
+
return cls._instances[device_index]
|
|
29
|
+
|
|
30
|
+
def __init__(self, device_index: int):
|
|
31
|
+
if self._initialized:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
self._initialized = True
|
|
35
|
+
self.device_index = device_index
|
|
36
|
+
self.cap = None
|
|
37
|
+
self.running = False
|
|
38
|
+
self.capture_thread = None
|
|
39
|
+
self.frame_lock = threading.Lock()
|
|
40
|
+
self.latest_frame = None
|
|
41
|
+
self.frame_timestamp = 0
|
|
42
|
+
|
|
43
|
+
# Consumer management
|
|
44
|
+
self.consumers: Dict[str, Dict] = {} # consumer_id -> {callback, last_frame_time}
|
|
45
|
+
self.consumers_lock = threading.Lock()
|
|
46
|
+
|
|
47
|
+
# Device properties
|
|
48
|
+
self.width = 640
|
|
49
|
+
self.height = 480
|
|
50
|
+
self.fps = 30.0
|
|
51
|
+
|
|
52
|
+
# Shared state file for cross-service coordination
|
|
53
|
+
self.state_file = self._get_state_file_path()
|
|
54
|
+
|
|
55
|
+
logging.info(f"SharedVideoStreamServer initialized for device {device_index}")
|
|
56
|
+
|
|
57
|
+
def _get_state_file_path(self) -> Path:
|
|
58
|
+
"""Get the path for the shared state file."""
|
|
59
|
+
temp_dir = tempfile.gettempdir()
|
|
60
|
+
state_dir = Path(temp_dir) / "nedo-vision-shared-streams"
|
|
61
|
+
state_dir.mkdir(exist_ok=True)
|
|
62
|
+
return state_dir / f"device_{self.device_index}_state.json"
|
|
63
|
+
|
|
64
|
+
def _update_state_file(self):
|
|
65
|
+
"""Update the shared state file with current server status."""
|
|
66
|
+
try:
|
|
67
|
+
state = {
|
|
68
|
+
"device_index": self.device_index,
|
|
69
|
+
"running": self.running,
|
|
70
|
+
"pid": os.getpid(),
|
|
71
|
+
"consumer_count": len(self.consumers),
|
|
72
|
+
"width": self.width,
|
|
73
|
+
"height": self.height,
|
|
74
|
+
"fps": self.fps,
|
|
75
|
+
"last_update": time.time()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
with open(self.state_file, 'w') as f:
|
|
79
|
+
json.dump(state, f, indent=2)
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logging.warning(f"Failed to update state file: {e}")
|
|
83
|
+
|
|
84
|
+
def _read_state_file(self) -> Optional[Dict]:
|
|
85
|
+
"""Read the shared state file."""
|
|
86
|
+
try:
|
|
87
|
+
if self.state_file.exists():
|
|
88
|
+
with open(self.state_file, 'r') as f:
|
|
89
|
+
return json.load(f)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def start_capture(self) -> bool:
|
|
95
|
+
"""Start capturing from the video device."""
|
|
96
|
+
if self.running:
|
|
97
|
+
logging.info(f"Device {self.device_index} capture already running")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Try to open the device
|
|
102
|
+
self.cap = cv2.VideoCapture(self.device_index)
|
|
103
|
+
|
|
104
|
+
if not self.cap.isOpened():
|
|
105
|
+
logging.error(f"Cannot open video device {self.device_index}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
# Get device properties
|
|
109
|
+
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
110
|
+
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
111
|
+
self.fps = float(self.cap.get(cv2.CAP_PROP_FPS))
|
|
112
|
+
|
|
113
|
+
if self.fps <= 0 or self.fps > 240:
|
|
114
|
+
self.fps = 30.0
|
|
115
|
+
|
|
116
|
+
logging.info(f"Device {self.device_index} opened: {self.width}x{self.height} @ {self.fps}fps")
|
|
117
|
+
|
|
118
|
+
# Start capture thread
|
|
119
|
+
self.running = True
|
|
120
|
+
self.capture_thread = threading.Thread(target=self._capture_loop, daemon=True)
|
|
121
|
+
self.capture_thread.start()
|
|
122
|
+
|
|
123
|
+
# Update state file
|
|
124
|
+
self._update_state_file()
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logging.error(f"Error starting capture for device {self.device_index}: {e}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def stop_capture(self):
|
|
133
|
+
"""Stop capturing from the video device."""
|
|
134
|
+
if not self.running:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
logging.info(f"Stopping capture for device {self.device_index}")
|
|
138
|
+
|
|
139
|
+
self.running = False
|
|
140
|
+
|
|
141
|
+
if self.capture_thread:
|
|
142
|
+
self.capture_thread.join(timeout=5)
|
|
143
|
+
|
|
144
|
+
if self.cap:
|
|
145
|
+
self.cap.release()
|
|
146
|
+
self.cap = None
|
|
147
|
+
|
|
148
|
+
# Clean up state file
|
|
149
|
+
try:
|
|
150
|
+
if self.state_file.exists():
|
|
151
|
+
self.state_file.unlink()
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Remove from instances
|
|
156
|
+
with self._lock:
|
|
157
|
+
if self.device_index in self._instances:
|
|
158
|
+
del self._instances[self.device_index]
|
|
159
|
+
|
|
160
|
+
def _capture_loop(self):
|
|
161
|
+
"""Main capture loop that reads frames and distributes to consumers."""
|
|
162
|
+
frame_interval = 1.0 / self.fps
|
|
163
|
+
last_frame_time = 0
|
|
164
|
+
|
|
165
|
+
while self.running and self.cap and self.cap.isOpened():
|
|
166
|
+
try:
|
|
167
|
+
current_time = time.time()
|
|
168
|
+
|
|
169
|
+
# Throttle frame rate
|
|
170
|
+
if current_time - last_frame_time < frame_interval:
|
|
171
|
+
time.sleep(0.001)
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
ret, frame = self.cap.read()
|
|
175
|
+
|
|
176
|
+
if not ret or frame is None:
|
|
177
|
+
logging.warning(f"Failed to read frame from device {self.device_index}")
|
|
178
|
+
time.sleep(0.1)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Update latest frame
|
|
182
|
+
with self.frame_lock:
|
|
183
|
+
self.latest_frame = frame.copy()
|
|
184
|
+
self.frame_timestamp = current_time
|
|
185
|
+
|
|
186
|
+
# Distribute frame to consumers
|
|
187
|
+
self._distribute_frame(frame, current_time)
|
|
188
|
+
|
|
189
|
+
last_frame_time = current_time
|
|
190
|
+
|
|
191
|
+
# Update state file periodically
|
|
192
|
+
if int(current_time) % 5 == 0: # Every 5 seconds
|
|
193
|
+
self._update_state_file()
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logging.error(f"Error in capture loop for device {self.device_index}: {e}")
|
|
197
|
+
time.sleep(0.1)
|
|
198
|
+
|
|
199
|
+
logging.info(f"Capture loop ended for device {self.device_index}")
|
|
200
|
+
|
|
201
|
+
def _distribute_frame(self, frame, timestamp):
|
|
202
|
+
"""Distribute frame to all registered consumers."""
|
|
203
|
+
with self.consumers_lock:
|
|
204
|
+
dead_consumers = []
|
|
205
|
+
|
|
206
|
+
for consumer_id, consumer_info in self.consumers.items():
|
|
207
|
+
try:
|
|
208
|
+
callback = consumer_info['callback']
|
|
209
|
+
callback(frame, timestamp)
|
|
210
|
+
consumer_info['last_frame_time'] = timestamp
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logging.warning(f"Error delivering frame to consumer {consumer_id}: {e}")
|
|
214
|
+
dead_consumers.append(consumer_id)
|
|
215
|
+
|
|
216
|
+
# Remove dead consumers
|
|
217
|
+
for consumer_id in dead_consumers:
|
|
218
|
+
del self.consumers[consumer_id]
|
|
219
|
+
logging.info(f"Removed dead consumer: {consumer_id}")
|
|
220
|
+
|
|
221
|
+
def add_consumer(self, callback: Callable, consumer_id: str = None) -> str:
|
|
222
|
+
"""Add a consumer that will receive frames."""
|
|
223
|
+
if consumer_id is None:
|
|
224
|
+
consumer_id = str(uuid.uuid4())
|
|
225
|
+
|
|
226
|
+
with self.consumers_lock:
|
|
227
|
+
self.consumers[consumer_id] = {
|
|
228
|
+
'callback': callback,
|
|
229
|
+
'added_time': time.time(),
|
|
230
|
+
'last_frame_time': 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
logging.info(f"Added consumer {consumer_id} for device {self.device_index}")
|
|
234
|
+
|
|
235
|
+
# Start capture if this is the first consumer
|
|
236
|
+
if len(self.consumers) == 1 and not self.running:
|
|
237
|
+
self.start_capture()
|
|
238
|
+
|
|
239
|
+
return consumer_id
|
|
240
|
+
|
|
241
|
+
def remove_consumer(self, consumer_id: str):
|
|
242
|
+
"""Remove a consumer."""
|
|
243
|
+
with self.consumers_lock:
|
|
244
|
+
if consumer_id in self.consumers:
|
|
245
|
+
del self.consumers[consumer_id]
|
|
246
|
+
logging.info(f"Removed consumer {consumer_id} for device {self.device_index}")
|
|
247
|
+
|
|
248
|
+
# Stop capture if no more consumers
|
|
249
|
+
if len(self.consumers) == 0 and self.running:
|
|
250
|
+
self.stop_capture()
|
|
251
|
+
|
|
252
|
+
def get_latest_frame(self):
|
|
253
|
+
"""Get the latest captured frame."""
|
|
254
|
+
with self.frame_lock:
|
|
255
|
+
if self.latest_frame is not None:
|
|
256
|
+
return self.latest_frame.copy(), self.frame_timestamp
|
|
257
|
+
return None, 0
|
|
258
|
+
|
|
259
|
+
def get_device_properties(self) -> tuple:
|
|
260
|
+
"""Get device properties."""
|
|
261
|
+
return self.width, self.height, self.fps, "rgb24"
|
|
262
|
+
|
|
263
|
+
def is_running(self) -> bool:
|
|
264
|
+
"""Check if the server is running."""
|
|
265
|
+
return self.running
|
|
266
|
+
|
|
267
|
+
def get_consumer_count(self) -> int:
|
|
268
|
+
"""Get the number of active consumers."""
|
|
269
|
+
with self.consumers_lock:
|
|
270
|
+
return len(self.consumers)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Global function to get or create shared stream server
|
|
274
|
+
def get_shared_stream_server(device_index: int) -> SharedVideoStreamServer:
|
|
275
|
+
"""Get or create a shared stream server for a device."""
|
|
276
|
+
return SharedVideoStreamServer(device_index)
|