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
@@ -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
- detection_data = {
127
- 'type': 'ppe_detection',
128
- 'pipeline_id': pipeline_id,
129
- 'worker_source_id': worker_source_id,
130
- 'person_id': person_id,
131
- 'detection_id': new_detection.id,
132
- 'attributes': valid_attributes,
133
- 'confidence_score': tracked_obj.get("confidence", 1.0),
134
- 'bbox': bbox,
135
- 'image_path': full_image_path,
136
- 'image_tile_path': cropped_image_path,
137
- 'timestamp': current_datetime
138
- }
139
- CoreService.trigger_detection_callback('ppe_detection', detection_data)
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
- detection_data = {
73
- 'type': 'area_violation',
74
- 'pipeline_id': pipeline_id,
75
- 'worker_source_id': worker_source_id,
76
- 'person_id': person_id,
77
- 'detection_id': new_detection.id if hasattr(new_detection, 'id') else None,
78
- 'confidence_score': tracked_obj.get("confidence", 1.0),
79
- 'bbox': bbox,
80
- 'image_path': full_image_path,
81
- 'image_tile_path': cropped_image_path,
82
- 'timestamp': current_datetime
83
- }
84
- CoreService.trigger_detection_callback('area_violation', detection_data)
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)