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.

Files changed (33) hide show
  1. nedo_vision_worker_core/__init__.py +47 -12
  2. nedo_vision_worker_core/callbacks/DetectionCallbackManager.py +306 -0
  3. nedo_vision_worker_core/callbacks/DetectionCallbackTypes.py +150 -0
  4. nedo_vision_worker_core/callbacks/__init__.py +27 -0
  5. nedo_vision_worker_core/cli.py +24 -34
  6. nedo_vision_worker_core/core_service.py +121 -55
  7. nedo_vision_worker_core/database/DatabaseManager.py +2 -2
  8. nedo_vision_worker_core/detection/BaseDetector.py +2 -1
  9. nedo_vision_worker_core/detection/DetectionManager.py +2 -2
  10. nedo_vision_worker_core/detection/RFDETRDetector.py +23 -5
  11. nedo_vision_worker_core/detection/YOLODetector.py +18 -5
  12. nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +1 -1
  13. nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +57 -3
  14. nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +173 -10
  15. nedo_vision_worker_core/models/ai_model.py +23 -2
  16. nedo_vision_worker_core/pipeline/PipelineProcessor.py +299 -14
  17. nedo_vision_worker_core/pipeline/PipelineSyncThread.py +32 -0
  18. nedo_vision_worker_core/repositories/PPEDetectionRepository.py +18 -15
  19. nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +17 -13
  20. nedo_vision_worker_core/services/SharedVideoStreamServer.py +276 -0
  21. nedo_vision_worker_core/services/VideoSharingDaemon.py +808 -0
  22. nedo_vision_worker_core/services/VideoSharingDaemonManager.py +257 -0
  23. nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +383 -0
  24. nedo_vision_worker_core/streams/StreamSyncThread.py +16 -2
  25. nedo_vision_worker_core/streams/VideoStream.py +267 -246
  26. nedo_vision_worker_core/streams/VideoStreamManager.py +158 -6
  27. nedo_vision_worker_core/tracker/TrackerManager.py +25 -31
  28. nedo_vision_worker_core-0.3.1.dist-info/METADATA +444 -0
  29. {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/RECORD +32 -25
  30. nedo_vision_worker_core-0.2.0.dist-info/METADATA +0 -347
  31. {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/WHEEL +0 -0
  32. {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.dist-info}/entry_points.txt +0 -0
  33. {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.1.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
- self.streams[worker_source_id] = VideoStream(url) # Create and start the VideoStream thread
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
- return list(self.streams.keys())
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
- self.tracker = SFSORT({
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() # Time-based last seen
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
- # Add all instances of multi-instance classes
122
- if label in MULTI_INSTANCE_CLASSES:
127
+ if label in self.multi_instance_classes:
123
128
  filtered_attrs.extend(attrs)
124
-
125
- # Special case: for multi-instance classes, we want to allow multiple instances
126
- # even if they were in exclusive groups, but we need to handle the negative classes properly
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