nedo-vision-worker-core 0.2.0__py3-none-any.whl → 0.3.0__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 +47 -5
- 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 +51 -8
- 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 +208 -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.0.dist-info/METADATA +444 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.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.0.dist-info}/WHEEL +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
from .VideoStream import VideoStream
|
|
4
|
+
from .SharedVideoDeviceManager import SharedVideoDeviceManager
|
|
4
5
|
import threading
|
|
5
6
|
|
|
6
7
|
class VideoStreamManager:
|
|
@@ -10,21 +11,85 @@ class VideoStreamManager:
|
|
|
10
11
|
self.streams = {} # Store streams as {worker_source_id: VideoStream}
|
|
11
12
|
self.running = False
|
|
12
13
|
self.lock = threading.Lock() # Add thread lock
|
|
14
|
+
self.shared_device_manager = SharedVideoDeviceManager()
|
|
15
|
+
self.direct_device_streams = {} # Store direct device streams {worker_source_id: latest_frame}
|
|
16
|
+
self.direct_device_locks = {} # Store locks for direct device frame access
|
|
17
|
+
|
|
18
|
+
def _is_direct_device(self, url) -> bool:
|
|
19
|
+
"""Check if URL represents a direct video device."""
|
|
20
|
+
if isinstance(url, str):
|
|
21
|
+
return url.isdigit() or url.startswith('/dev/video')
|
|
22
|
+
return isinstance(url, int)
|
|
13
23
|
|
|
14
24
|
def add_stream(self, worker_source_id, url):
|
|
15
25
|
"""Adds a new video stream if it's not already active."""
|
|
16
|
-
if worker_source_id not in self.streams:
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
if worker_source_id not in self.streams and worker_source_id not in self.direct_device_streams:
|
|
27
|
+
# Check if this is a direct video device
|
|
28
|
+
if self._is_direct_device(url):
|
|
29
|
+
self._add_direct_device_stream(worker_source_id, url)
|
|
30
|
+
else:
|
|
31
|
+
# Regular stream (file, RTSP, etc.)
|
|
32
|
+
stream = VideoStream(url)
|
|
33
|
+
stream.start() # Start the thread
|
|
34
|
+
self.streams[worker_source_id] = stream
|
|
35
|
+
logging.info(f"✅ Added and started video stream: {worker_source_id}")
|
|
19
36
|
else:
|
|
20
37
|
logging.warning(f"⚠️ Stream {worker_source_id} is already active.")
|
|
21
38
|
|
|
39
|
+
def _add_direct_device_stream(self, worker_source_id, url):
|
|
40
|
+
"""Add a direct device stream using the shared device manager."""
|
|
41
|
+
try:
|
|
42
|
+
# Initialize frame storage for this stream
|
|
43
|
+
self.direct_device_streams[worker_source_id] = {
|
|
44
|
+
'url': url,
|
|
45
|
+
'latest_frame': None,
|
|
46
|
+
'last_update': time.time()
|
|
47
|
+
}
|
|
48
|
+
self.direct_device_locks[worker_source_id] = threading.Lock()
|
|
49
|
+
|
|
50
|
+
# Create callback for receiving frames
|
|
51
|
+
def frame_callback(frame):
|
|
52
|
+
with self.direct_device_locks[worker_source_id]:
|
|
53
|
+
self.direct_device_streams[worker_source_id]['latest_frame'] = frame
|
|
54
|
+
self.direct_device_streams[worker_source_id]['last_update'] = time.time()
|
|
55
|
+
|
|
56
|
+
# Subscribe to the shared device
|
|
57
|
+
success = self.shared_device_manager.subscribe_to_device(
|
|
58
|
+
source=url,
|
|
59
|
+
subscriber_id=f"stream_{worker_source_id}",
|
|
60
|
+
callback=frame_callback
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if success:
|
|
64
|
+
logging.info(f"✅ Added direct device stream: {worker_source_id} -> {url}")
|
|
65
|
+
else:
|
|
66
|
+
# Clean up on failure
|
|
67
|
+
if worker_source_id in self.direct_device_streams:
|
|
68
|
+
del self.direct_device_streams[worker_source_id]
|
|
69
|
+
if worker_source_id in self.direct_device_locks:
|
|
70
|
+
del self.direct_device_locks[worker_source_id]
|
|
71
|
+
logging.error(f"❌ Failed to add direct device stream: {worker_source_id}")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logging.error(f"❌ Error adding direct device stream {worker_source_id}: {e}")
|
|
75
|
+
# Clean up on error
|
|
76
|
+
if worker_source_id in self.direct_device_streams:
|
|
77
|
+
del self.direct_device_streams[worker_source_id]
|
|
78
|
+
if worker_source_id in self.direct_device_locks:
|
|
79
|
+
del self.direct_device_locks[worker_source_id]
|
|
80
|
+
|
|
22
81
|
def remove_stream(self, worker_source_id):
|
|
23
82
|
"""Removes and stops a video stream."""
|
|
24
83
|
if not worker_source_id:
|
|
25
84
|
return
|
|
26
85
|
|
|
27
86
|
with self.lock:
|
|
87
|
+
# Check if it's a direct device stream
|
|
88
|
+
if worker_source_id in self.direct_device_streams:
|
|
89
|
+
self._remove_direct_device_stream(worker_source_id)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Check if it's a regular stream
|
|
28
93
|
if worker_source_id not in self.streams:
|
|
29
94
|
logging.warning(f"⚠️ Stream {worker_source_id} not found in manager.")
|
|
30
95
|
return
|
|
@@ -45,6 +110,33 @@ class VideoStreamManager:
|
|
|
45
110
|
|
|
46
111
|
logging.info(f"✅ Stream {worker_source_id} removed successfully.")
|
|
47
112
|
|
|
113
|
+
def _remove_direct_device_stream(self, worker_source_id):
|
|
114
|
+
"""Remove a direct device stream from the shared device manager."""
|
|
115
|
+
try:
|
|
116
|
+
device_info = self.direct_device_streams.get(worker_source_id)
|
|
117
|
+
if device_info:
|
|
118
|
+
url = device_info['url']
|
|
119
|
+
|
|
120
|
+
# Unsubscribe from the shared device
|
|
121
|
+
success = self.shared_device_manager.unsubscribe_from_device(
|
|
122
|
+
source=url,
|
|
123
|
+
subscriber_id=f"stream_{worker_source_id}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if success:
|
|
127
|
+
logging.info(f"✅ Removed direct device stream: {worker_source_id}")
|
|
128
|
+
else:
|
|
129
|
+
logging.warning(f"⚠️ Failed to unsubscribe direct device stream: {worker_source_id}")
|
|
130
|
+
|
|
131
|
+
# Clean up local storage
|
|
132
|
+
if worker_source_id in self.direct_device_streams:
|
|
133
|
+
del self.direct_device_streams[worker_source_id]
|
|
134
|
+
if worker_source_id in self.direct_device_locks:
|
|
135
|
+
del self.direct_device_locks[worker_source_id]
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logging.error(f"❌ Error removing direct device stream {worker_source_id}: {e}")
|
|
139
|
+
|
|
48
140
|
def start_all(self):
|
|
49
141
|
"""Starts all video streams."""
|
|
50
142
|
logging.info("🔄 Starting all video streams...")
|
|
@@ -59,18 +151,31 @@ class VideoStreamManager:
|
|
|
59
151
|
with self.lock:
|
|
60
152
|
# Get a list of IDs to avoid modification during iteration
|
|
61
153
|
stream_ids = list(self.streams.keys())
|
|
154
|
+
direct_stream_ids = list(self.direct_device_streams.keys())
|
|
62
155
|
|
|
63
|
-
# Stop each stream
|
|
156
|
+
# Stop each regular stream
|
|
64
157
|
for worker_source_id in stream_ids:
|
|
65
158
|
try:
|
|
66
159
|
self.remove_stream(worker_source_id)
|
|
67
160
|
except Exception as e:
|
|
68
161
|
logging.error(f"Error stopping stream {worker_source_id}: {e}")
|
|
69
162
|
|
|
163
|
+
# Stop each direct device stream
|
|
164
|
+
for worker_source_id in direct_stream_ids:
|
|
165
|
+
try:
|
|
166
|
+
self.remove_stream(worker_source_id)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logging.error(f"Error stopping direct device stream {worker_source_id}: {e}")
|
|
169
|
+
|
|
70
170
|
self.running = False
|
|
71
171
|
|
|
72
172
|
def get_frame(self, worker_source_id):
|
|
73
173
|
"""Retrieves the latest frame for a specific stream."""
|
|
174
|
+
# Check if it's a direct device stream first
|
|
175
|
+
if worker_source_id in self.direct_device_streams:
|
|
176
|
+
return self._get_direct_device_frame(worker_source_id)
|
|
177
|
+
|
|
178
|
+
# Handle regular streams
|
|
74
179
|
with self.lock: # Add lock protection for stream access
|
|
75
180
|
stream = self.streams.get(worker_source_id)
|
|
76
181
|
if stream is None:
|
|
@@ -98,18 +203,50 @@ class VideoStreamManager:
|
|
|
98
203
|
logging.error(f"Error getting frame from stream {worker_source_id}: {e}")
|
|
99
204
|
return None
|
|
100
205
|
|
|
206
|
+
def _get_direct_device_frame(self, worker_source_id):
|
|
207
|
+
"""Get the latest frame from a direct device stream."""
|
|
208
|
+
try:
|
|
209
|
+
if worker_source_id not in self.direct_device_locks:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
with self.direct_device_locks[worker_source_id]:
|
|
213
|
+
device_info = self.direct_device_streams.get(worker_source_id)
|
|
214
|
+
if not device_info:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
frame = device_info.get('latest_frame')
|
|
218
|
+
last_update = device_info.get('last_update', 0)
|
|
219
|
+
|
|
220
|
+
# Check if frame is too old (5 seconds threshold)
|
|
221
|
+
if time.time() - last_update > 5.0:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
return frame.copy() if frame is not None else None
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logging.error(f"Error getting frame from direct device stream {worker_source_id}: {e}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
101
230
|
def get_active_stream_ids(self):
|
|
102
231
|
"""Returns a list of active stream IDs."""
|
|
103
|
-
|
|
232
|
+
regular_streams = list(self.streams.keys())
|
|
233
|
+
direct_streams = list(self.direct_device_streams.keys())
|
|
234
|
+
return regular_streams + direct_streams
|
|
104
235
|
|
|
105
236
|
def get_stream_url(self, worker_source_id):
|
|
106
237
|
"""Returns the URL of a specific stream."""
|
|
238
|
+
# Check direct device streams first
|
|
239
|
+
if worker_source_id in self.direct_device_streams:
|
|
240
|
+
return self.direct_device_streams[worker_source_id]['url']
|
|
241
|
+
|
|
242
|
+
# Check regular streams
|
|
107
243
|
stream = self.streams.get(worker_source_id)
|
|
108
244
|
return stream.source if stream else None
|
|
109
245
|
|
|
110
246
|
def has_stream(self, worker_source_id):
|
|
111
247
|
"""Checks if a stream is active."""
|
|
112
|
-
return worker_source_id in self.streams
|
|
248
|
+
return (worker_source_id in self.streams or
|
|
249
|
+
worker_source_id in self.direct_device_streams)
|
|
113
250
|
|
|
114
251
|
def is_running(self):
|
|
115
252
|
"""Checks if the manager is running."""
|
|
@@ -117,5 +254,20 @@ class VideoStreamManager:
|
|
|
117
254
|
|
|
118
255
|
def is_video_file(self, worker_source_id):
|
|
119
256
|
"""Check if a stream is a video file."""
|
|
257
|
+
# Direct device streams are never video files
|
|
258
|
+
if worker_source_id in self.direct_device_streams:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
# Check regular streams
|
|
120
262
|
stream = self.streams.get(worker_source_id)
|
|
121
263
|
return stream.is_file if stream else False
|
|
264
|
+
|
|
265
|
+
def get_device_sharing_info(self):
|
|
266
|
+
"""Get information about device sharing."""
|
|
267
|
+
return self.shared_device_manager.get_all_devices_info()
|
|
268
|
+
|
|
269
|
+
def shutdown(self):
|
|
270
|
+
"""Shutdown the manager and clean up all resources."""
|
|
271
|
+
logging.info("Shutting down VideoStreamManager")
|
|
272
|
+
self.stop_all()
|
|
273
|
+
# The SharedVideoDeviceManager is a singleton and will clean up automatically
|
|
@@ -4,8 +4,8 @@ import numpy as np
|
|
|
4
4
|
from .SFSORT import SFSORT
|
|
5
5
|
|
|
6
6
|
class TrackerManager:
|
|
7
|
-
def __init__(self, attribute_labels=None, exclusive_attribute_groups=None):
|
|
8
|
-
|
|
7
|
+
def __init__(self, attribute_labels=None, exclusive_attribute_groups=None, multi_instance_classes=None, tracker_config=None):
|
|
8
|
+
default_config = {
|
|
9
9
|
"dynamic_tuning": True,
|
|
10
10
|
"cth": 0.5,
|
|
11
11
|
"high_th": 0.6,
|
|
@@ -15,14 +15,30 @@ class TrackerManager:
|
|
|
15
15
|
"new_track_th": 0.7,
|
|
16
16
|
"marginal_timeout": 7,
|
|
17
17
|
"central_timeout": 30
|
|
18
|
-
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
config = {**default_config, **(tracker_config or {})}
|
|
21
|
+
self.tracker = SFSORT(config)
|
|
22
|
+
|
|
19
23
|
self.track_uuid_map = {}
|
|
20
24
|
self.track_count_map = {}
|
|
21
25
|
self.track_attributes_presence = {}
|
|
22
26
|
self.track_last_seen = {}
|
|
23
|
-
self.track_timeout_seconds = 5
|
|
27
|
+
self.track_timeout_seconds = config.get("track_timeout_seconds", 5)
|
|
24
28
|
self.attribute_labels = attribute_labels or []
|
|
25
29
|
self.exclusive_attribute_groups = exclusive_attribute_groups or []
|
|
30
|
+
self.multi_instance_classes = multi_instance_classes or []
|
|
31
|
+
|
|
32
|
+
def update_config(self, attribute_labels=None, exclusive_attribute_groups=None, multi_instance_classes=None, tracker_config=None):
|
|
33
|
+
"""Update tracker configuration at runtime"""
|
|
34
|
+
if attribute_labels is not None:
|
|
35
|
+
self.attribute_labels = attribute_labels
|
|
36
|
+
if exclusive_attribute_groups is not None:
|
|
37
|
+
self.exclusive_attribute_groups = exclusive_attribute_groups
|
|
38
|
+
if multi_instance_classes is not None:
|
|
39
|
+
self.multi_instance_classes = multi_instance_classes
|
|
40
|
+
if tracker_config:
|
|
41
|
+
self.track_timeout_seconds = tracker_config.get("track_timeout_seconds", self.track_timeout_seconds)
|
|
26
42
|
|
|
27
43
|
def track_objects(self, detections):
|
|
28
44
|
if not detections:
|
|
@@ -52,7 +68,7 @@ class TrackerManager:
|
|
|
52
68
|
obj_uuid = self._assign_uuid(track_id)
|
|
53
69
|
|
|
54
70
|
self.track_count_map[obj_uuid] += 1
|
|
55
|
-
self.track_last_seen[obj_uuid] = time.time()
|
|
71
|
+
self.track_last_seen[obj_uuid] = time.time()
|
|
56
72
|
|
|
57
73
|
attributes = data.get("attributes", [])
|
|
58
74
|
filtered_attributes = self._filter_exclusive_attributes(attributes)
|
|
@@ -87,7 +103,6 @@ class TrackerManager:
|
|
|
87
103
|
if not attributes:
|
|
88
104
|
return []
|
|
89
105
|
|
|
90
|
-
# Group attributes by label to handle multiple instances
|
|
91
106
|
attrs_by_label = {}
|
|
92
107
|
for attr in attributes:
|
|
93
108
|
label = attr["label"]
|
|
@@ -95,12 +110,6 @@ class TrackerManager:
|
|
|
95
110
|
attrs_by_label[label] = []
|
|
96
111
|
attrs_by_label[label].append(attr)
|
|
97
112
|
|
|
98
|
-
# Multi-instance classes that can have multiple instances per person
|
|
99
|
-
MULTI_INSTANCE_CLASSES = ["boots", "gloves", "goggles", "no_gloves"]
|
|
100
|
-
# Negative classes that are exclusive with their positive counterparts
|
|
101
|
-
NEGATIVE_CLASSES = ["no_helmet", "no_vest", "no_goggles", "no_boots"]
|
|
102
|
-
|
|
103
|
-
# For exclusive groups, keep only the highest confidence per group
|
|
104
113
|
filtered_attrs = []
|
|
105
114
|
for group in self.exclusive_attribute_groups:
|
|
106
115
|
group_attrs = []
|
|
@@ -108,33 +117,18 @@ class TrackerManager:
|
|
|
108
117
|
if label in attrs_by_label:
|
|
109
118
|
group_attrs.extend(attrs_by_label[label])
|
|
110
119
|
if group_attrs:
|
|
111
|
-
# Keep only the highest confidence in this exclusive group
|
|
112
120
|
best = max(group_attrs, key=lambda a: a["confidence"])
|
|
113
121
|
filtered_attrs.append(best)
|
|
114
122
|
|
|
115
|
-
# For multi-instance classes, add all instances (but respect exclusive logic above)
|
|
116
123
|
exclusive_labels = set(l for group in self.exclusive_attribute_groups for l in group)
|
|
117
124
|
for label, attrs in attrs_by_label.items():
|
|
118
|
-
# Skip if already handled by exclusive logic
|
|
119
125
|
if label in exclusive_labels:
|
|
120
126
|
continue
|
|
121
|
-
|
|
122
|
-
if label in MULTI_INSTANCE_CLASSES:
|
|
127
|
+
if label in self.multi_instance_classes:
|
|
123
128
|
filtered_attrs.extend(attrs)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for label, attrs in attrs_by_label.items():
|
|
128
|
-
# If this is a multi-instance class and not a negative class, add all instances
|
|
129
|
-
if label in MULTI_INSTANCE_CLASSES and label not in NEGATIVE_CLASSES:
|
|
130
|
-
# Check which attributes were already added
|
|
131
|
-
for attr in attrs:
|
|
132
|
-
already_added = any(
|
|
133
|
-
a["label"] == label and list(a["bbox"]) == list(attr["bbox"])
|
|
134
|
-
for a in filtered_attrs
|
|
135
|
-
)
|
|
136
|
-
if not already_added:
|
|
137
|
-
filtered_attrs.append(attr)
|
|
129
|
+
else:
|
|
130
|
+
best = max(attrs, key=lambda a: a["confidence"])
|
|
131
|
+
filtered_attrs.append(best)
|
|
138
132
|
|
|
139
133
|
return filtered_attrs
|
|
140
134
|
|