nedo-vision-worker-core 0.2.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 +23 -0
- nedo_vision_worker_core/ai/FrameDrawer.py +144 -0
- nedo_vision_worker_core/ai/ImageDebugger.py +126 -0
- nedo_vision_worker_core/ai/VideoDebugger.py +69 -0
- nedo_vision_worker_core/ai/__init__.py +1 -0
- nedo_vision_worker_core/cli.py +197 -0
- nedo_vision_worker_core/config/ConfigurationManager.py +173 -0
- nedo_vision_worker_core/config/__init__.py +1 -0
- nedo_vision_worker_core/core_service.py +237 -0
- nedo_vision_worker_core/database/DatabaseManager.py +236 -0
- nedo_vision_worker_core/database/__init__.py +1 -0
- nedo_vision_worker_core/detection/BaseDetector.py +22 -0
- nedo_vision_worker_core/detection/DetectionManager.py +83 -0
- nedo_vision_worker_core/detection/RFDETRDetector.py +62 -0
- nedo_vision_worker_core/detection/YOLODetector.py +57 -0
- nedo_vision_worker_core/detection/__init__.py +1 -0
- nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +29 -0
- nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +47 -0
- nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +44 -0
- nedo_vision_worker_core/detection/detection_processing/__init__.py +1 -0
- nedo_vision_worker_core/doctor.py +342 -0
- nedo_vision_worker_core/drawing_assets/blue/inner_corner.png +0 -0
- nedo_vision_worker_core/drawing_assets/blue/inner_frame.png +0 -0
- nedo_vision_worker_core/drawing_assets/blue/line.png +0 -0
- nedo_vision_worker_core/drawing_assets/blue/top_left.png +0 -0
- nedo_vision_worker_core/drawing_assets/blue/top_right.png +0 -0
- nedo_vision_worker_core/drawing_assets/red/inner_corner.png +0 -0
- nedo_vision_worker_core/drawing_assets/red/inner_frame.png +0 -0
- nedo_vision_worker_core/drawing_assets/red/line.png +0 -0
- nedo_vision_worker_core/drawing_assets/red/top_left.png +0 -0
- nedo_vision_worker_core/drawing_assets/red/top_right.png +0 -0
- nedo_vision_worker_core/icons/boots-green.png +0 -0
- nedo_vision_worker_core/icons/boots-red.png +0 -0
- nedo_vision_worker_core/icons/gloves-green.png +0 -0
- nedo_vision_worker_core/icons/gloves-red.png +0 -0
- nedo_vision_worker_core/icons/goggles-green.png +0 -0
- nedo_vision_worker_core/icons/goggles-red.png +0 -0
- nedo_vision_worker_core/icons/helmet-green.png +0 -0
- nedo_vision_worker_core/icons/helmet-red.png +0 -0
- nedo_vision_worker_core/icons/mask-red.png +0 -0
- nedo_vision_worker_core/icons/vest-green.png +0 -0
- nedo_vision_worker_core/icons/vest-red.png +0 -0
- nedo_vision_worker_core/models/__init__.py +20 -0
- nedo_vision_worker_core/models/ai_model.py +41 -0
- nedo_vision_worker_core/models/auth.py +14 -0
- nedo_vision_worker_core/models/config.py +9 -0
- nedo_vision_worker_core/models/dataset_source.py +30 -0
- nedo_vision_worker_core/models/logs.py +9 -0
- nedo_vision_worker_core/models/ppe_detection.py +39 -0
- nedo_vision_worker_core/models/ppe_detection_label.py +20 -0
- nedo_vision_worker_core/models/restricted_area_violation.py +20 -0
- nedo_vision_worker_core/models/user.py +10 -0
- nedo_vision_worker_core/models/worker_source.py +19 -0
- nedo_vision_worker_core/models/worker_source_pipeline.py +21 -0
- nedo_vision_worker_core/models/worker_source_pipeline_config.py +24 -0
- nedo_vision_worker_core/models/worker_source_pipeline_debug.py +15 -0
- nedo_vision_worker_core/models/worker_source_pipeline_detection.py +14 -0
- nedo_vision_worker_core/pipeline/PipelineConfigManager.py +32 -0
- nedo_vision_worker_core/pipeline/PipelineManager.py +133 -0
- nedo_vision_worker_core/pipeline/PipelinePrepocessor.py +40 -0
- nedo_vision_worker_core/pipeline/PipelineProcessor.py +338 -0
- nedo_vision_worker_core/pipeline/PipelineSyncThread.py +202 -0
- nedo_vision_worker_core/pipeline/__init__.py +1 -0
- nedo_vision_worker_core/preprocessing/ImageResizer.py +42 -0
- nedo_vision_worker_core/preprocessing/ImageRoi.py +61 -0
- nedo_vision_worker_core/preprocessing/Preprocessor.py +16 -0
- nedo_vision_worker_core/preprocessing/__init__.py +1 -0
- nedo_vision_worker_core/repositories/AIModelRepository.py +31 -0
- nedo_vision_worker_core/repositories/PPEDetectionRepository.py +146 -0
- nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +90 -0
- nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py +81 -0
- nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py +71 -0
- nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py +79 -0
- nedo_vision_worker_core/repositories/WorkerSourceRepository.py +19 -0
- nedo_vision_worker_core/repositories/__init__.py +1 -0
- nedo_vision_worker_core/streams/RTMPStreamer.py +146 -0
- nedo_vision_worker_core/streams/StreamSyncThread.py +66 -0
- nedo_vision_worker_core/streams/VideoStream.py +324 -0
- nedo_vision_worker_core/streams/VideoStreamManager.py +121 -0
- nedo_vision_worker_core/streams/__init__.py +1 -0
- nedo_vision_worker_core/tracker/SFSORT.py +325 -0
- nedo_vision_worker_core/tracker/TrackerManager.py +163 -0
- nedo_vision_worker_core/tracker/__init__.py +1 -0
- nedo_vision_worker_core/util/BoundingBoxMetrics.py +53 -0
- nedo_vision_worker_core/util/DrawingUtils.py +354 -0
- nedo_vision_worker_core/util/ModelReadinessChecker.py +188 -0
- nedo_vision_worker_core/util/PersonAttributeMatcher.py +70 -0
- nedo_vision_worker_core/util/PersonRestrictedAreaMatcher.py +45 -0
- nedo_vision_worker_core/util/TablePrinter.py +28 -0
- nedo_vision_worker_core/util/__init__.py +1 -0
- nedo_vision_worker_core-0.2.0.dist-info/METADATA +347 -0
- nedo_vision_worker_core-0.2.0.dist-info/RECORD +95 -0
- nedo_vision_worker_core-0.2.0.dist-info/WHEEL +5 -0
- nedo_vision_worker_core-0.2.0.dist-info/entry_points.txt +2 -0
- nedo_vision_worker_core-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nedo Vision Core Library
|
|
3
|
+
|
|
4
|
+
A library for running AI vision processing and detection in the Nedo Vision platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core_service import CoreService
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
__all__ = ["CoreService"]
|
|
11
|
+
|
|
12
|
+
# Convenience functions for callback management
|
|
13
|
+
def register_ppe_detection_callback(callback):
|
|
14
|
+
"""Register a callback for PPE detection events."""
|
|
15
|
+
return CoreService.register_detection_callback('ppe_detection', callback)
|
|
16
|
+
|
|
17
|
+
def register_area_violation_callback(callback):
|
|
18
|
+
"""Register a callback for restricted area violation events."""
|
|
19
|
+
return CoreService.register_detection_callback('area_violation', callback)
|
|
20
|
+
|
|
21
|
+
def register_general_detection_callback(callback):
|
|
22
|
+
"""Register a callback for all detection events."""
|
|
23
|
+
return CoreService.register_detection_callback('general_detection', callback)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
import cv2
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from ..util.DrawingUtils import DrawingUtils
|
|
9
|
+
|
|
10
|
+
class FrameDrawer:
|
|
11
|
+
"""Handles frame processing by drawing objects, annotations, and managing icons."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.icons = {}
|
|
15
|
+
self.violation_labels = []
|
|
16
|
+
self.compliance_labels = []
|
|
17
|
+
self.polygons: List[Tuple[Tuple[int, int, int], List[Tuple[float, float]]]] = []
|
|
18
|
+
self.trails = {}
|
|
19
|
+
self.location_name = "LOCATION"
|
|
20
|
+
|
|
21
|
+
def update_config(self, icons = {}, violation_labels = [], compliance_labels = []):
|
|
22
|
+
self.icons = self._load_icons(icons)
|
|
23
|
+
self.violation_labels = violation_labels
|
|
24
|
+
self.compliance_labels = compliance_labels
|
|
25
|
+
|
|
26
|
+
def _load_icons(self, icon_paths, size=(20, 20)):
|
|
27
|
+
icons = {}
|
|
28
|
+
base_dir = Path(__file__).parent.parent
|
|
29
|
+
|
|
30
|
+
for key, path in icon_paths.items():
|
|
31
|
+
try:
|
|
32
|
+
full_path = base_dir / path
|
|
33
|
+
icon = cv2.imread(str(full_path), cv2.IMREAD_UNCHANGED)
|
|
34
|
+
if icon is None:
|
|
35
|
+
raise FileNotFoundError(f"Icon '{path}' not found at {full_path}.")
|
|
36
|
+
icons[key] = cv2.resize(icon, size)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logging.warning(f"⚠️ Failed to load icon {path}: {e}")
|
|
39
|
+
icons[key] = None
|
|
40
|
+
return icons
|
|
41
|
+
|
|
42
|
+
def _get_color(self, label):
|
|
43
|
+
if label in self.violation_labels:
|
|
44
|
+
return ((0, 0, 255), False)
|
|
45
|
+
elif label in self.compliance_labels:
|
|
46
|
+
return ((255, 141, 11), True)
|
|
47
|
+
else:
|
|
48
|
+
return ((255, 255, 255), None)
|
|
49
|
+
|
|
50
|
+
def _get_color_from_labels(self, labels):
|
|
51
|
+
compliance_count = 0
|
|
52
|
+
for label in labels:
|
|
53
|
+
if label in self.violation_labels:
|
|
54
|
+
return ((0, 0, 255), False)
|
|
55
|
+
elif label in self.compliance_labels:
|
|
56
|
+
compliance_count += 1
|
|
57
|
+
|
|
58
|
+
if labels and compliance_count == len(labels):
|
|
59
|
+
return ((255, 141, 11), True)
|
|
60
|
+
else:
|
|
61
|
+
return ((255, 255, 255), None)
|
|
62
|
+
|
|
63
|
+
def draw_polygons(self, frame):
|
|
64
|
+
height, width = frame.shape[:2]
|
|
65
|
+
for color, normalized_points in self.polygons:
|
|
66
|
+
points = [
|
|
67
|
+
(int(x * width), int(y * height)) for (x, y) in normalized_points
|
|
68
|
+
]
|
|
69
|
+
if len(points) >= 3:
|
|
70
|
+
cv2.polylines(frame, [np.array(points, np.int32)], isClosed=True, color=color, thickness=2)
|
|
71
|
+
|
|
72
|
+
def draw_frame(self, frame, tracked_objects, with_trails=False, trail_length=10):
|
|
73
|
+
current_ids = set()
|
|
74
|
+
|
|
75
|
+
for obj in tracked_objects:
|
|
76
|
+
bbox = obj.get("bbox", [])
|
|
77
|
+
if len(bbox) != 4:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
track_id = obj.get('track_id', -1)
|
|
81
|
+
x1, y1, x2, y2 = map(int, bbox)
|
|
82
|
+
|
|
83
|
+
attributes = obj.get("attributes", [])
|
|
84
|
+
labels = [attr.get("label") for attr in attributes]
|
|
85
|
+
(color, flag) = self._get_color_from_labels(labels)
|
|
86
|
+
DrawingUtils.draw_bbox_info(frame, bbox, (color, flag), f"{track_id}", self.location_name, f"{obj.get('confidence', 0):.2f}")
|
|
87
|
+
|
|
88
|
+
# Trailing
|
|
89
|
+
if with_trails:
|
|
90
|
+
if not hasattr(self, "trails"):
|
|
91
|
+
self.trails = {}
|
|
92
|
+
|
|
93
|
+
if track_id not in self.trails:
|
|
94
|
+
self.trails[track_id] = {
|
|
95
|
+
"points": [],
|
|
96
|
+
"missed_frames": 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
center_x = int((x1 + x2) / 2)
|
|
100
|
+
center_y = int((y1 + y2) / 2)
|
|
101
|
+
|
|
102
|
+
self.trails[track_id]["points"].append((center_x, center_y))
|
|
103
|
+
|
|
104
|
+
# Limit the number of points
|
|
105
|
+
if len(self.trails[track_id]["points"]) > trail_length:
|
|
106
|
+
self.trails[track_id]["points"] = self.trails[track_id]["points"][-trail_length:]
|
|
107
|
+
|
|
108
|
+
points = self.trails[track_id]["points"]
|
|
109
|
+
num_points = len(points)
|
|
110
|
+
|
|
111
|
+
# Draw faded trail
|
|
112
|
+
for i in range(1, num_points):
|
|
113
|
+
cv2.line(frame, points[i-1], points[i], color, 1)
|
|
114
|
+
|
|
115
|
+
current_ids.add(track_id)
|
|
116
|
+
|
|
117
|
+
DrawingUtils.draw_main_bbox(frame, bbox, (color, flag))
|
|
118
|
+
|
|
119
|
+
for attr in attributes:
|
|
120
|
+
attr_bbox = attr.get("bbox", [])
|
|
121
|
+
if len(attr_bbox) != 4:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
attr_label = attr.get("label", "")
|
|
125
|
+
DrawingUtils.draw_inner_box(frame, attr_bbox, self._get_color(attr_label))
|
|
126
|
+
|
|
127
|
+
icon_x = x1
|
|
128
|
+
for (label, icon) in self.icons.items():
|
|
129
|
+
if label in labels:
|
|
130
|
+
DrawingUtils.draw_alpha_overlay(frame, icon, icon_x, y1 - 25)
|
|
131
|
+
icon_x += 25
|
|
132
|
+
|
|
133
|
+
# Cleanup trails for objects that disappeared
|
|
134
|
+
if with_trails and hasattr(self, "trails"):
|
|
135
|
+
for track_id in list(self.trails.keys()):
|
|
136
|
+
if track_id not in current_ids:
|
|
137
|
+
self.trails[track_id]["missed_frames"] += 1
|
|
138
|
+
else:
|
|
139
|
+
self.trails[track_id]["missed_frames"] = 0
|
|
140
|
+
|
|
141
|
+
if self.trails[track_id]["missed_frames"] > 30:
|
|
142
|
+
del self.trails[track_id]
|
|
143
|
+
|
|
144
|
+
return frame
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
class ImageDebugger:
|
|
8
|
+
"""Handles image visualization for AI detections and tracking."""
|
|
9
|
+
def __init__(self, window_name="AI Debugger", display=True):
|
|
10
|
+
"""
|
|
11
|
+
Initializes the ImageDebugger.
|
|
12
|
+
|
|
13
|
+
:param window_name: Name of the display window.
|
|
14
|
+
:param display: Flag to enable visualization (skips if no GUI available).
|
|
15
|
+
"""
|
|
16
|
+
self.window_name = window_name
|
|
17
|
+
self.display = display and os.environ.get("DISPLAY", None) is not None
|
|
18
|
+
|
|
19
|
+
def draw_detections(self, frame, detections):
|
|
20
|
+
"""
|
|
21
|
+
Draws bounding boxes and labels on the given frame.
|
|
22
|
+
|
|
23
|
+
:param frame: The image frame to draw on.
|
|
24
|
+
:param detections: List of detection results.
|
|
25
|
+
:return: Image with drawn detections.
|
|
26
|
+
"""
|
|
27
|
+
for detection in detections:
|
|
28
|
+
label = detection.get("label", "unknown")
|
|
29
|
+
confidence = detection.get("confidence", 0)
|
|
30
|
+
bbox = detection.get("bbox", [])
|
|
31
|
+
|
|
32
|
+
if not bbox or len(bbox) != 4:
|
|
33
|
+
continue # Skip invalid bounding boxes
|
|
34
|
+
|
|
35
|
+
color = self._get_color(label)
|
|
36
|
+
x1, y1, x2, y2 = map(int, bbox)
|
|
37
|
+
|
|
38
|
+
# Draw bounding box
|
|
39
|
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
|
|
40
|
+
|
|
41
|
+
# Draw label and confidence
|
|
42
|
+
label_text = f"{label} ({confidence:.2f})"
|
|
43
|
+
cv2.putText(frame, label_text, (x1, max(y1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
|
|
44
|
+
|
|
45
|
+
return frame
|
|
46
|
+
|
|
47
|
+
def draw_tracked_objects(self, frame, tracked_objects):
|
|
48
|
+
"""
|
|
49
|
+
Draws tracked objects with unique IDs and attribute matches.
|
|
50
|
+
|
|
51
|
+
:param frame: The image frame to draw on.
|
|
52
|
+
:param tracked_objects: List of tracked object results.
|
|
53
|
+
:return: Image with drawn tracking details.
|
|
54
|
+
"""
|
|
55
|
+
for obj in tracked_objects:
|
|
56
|
+
track_id = obj.get("track_id", -1)
|
|
57
|
+
detections = obj.get("detections", 0)
|
|
58
|
+
bbox = obj.get("bbox", [])
|
|
59
|
+
|
|
60
|
+
if not bbox or len(bbox) != 4:
|
|
61
|
+
continue # Skip invalid bounding boxes
|
|
62
|
+
|
|
63
|
+
color = self._get_color("track") # Unique color for tracked objects
|
|
64
|
+
x1, y1, x2, y2 = map(int, bbox)
|
|
65
|
+
|
|
66
|
+
# Draw tracking box
|
|
67
|
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
|
|
68
|
+
|
|
69
|
+
# Draw track ID and detection count
|
|
70
|
+
track_text = f"ID: {track_id} | Detections: {detections}"
|
|
71
|
+
cv2.putText(frame, track_text, (x1, max(y1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
|
|
72
|
+
|
|
73
|
+
# Draw attributes
|
|
74
|
+
for attr in obj.get("attributes", []):
|
|
75
|
+
attr_label = attr.get("label", "unknown")
|
|
76
|
+
attr_conf = attr.get("confidence", 0)
|
|
77
|
+
attr_bbox = attr.get("bbox", [])
|
|
78
|
+
|
|
79
|
+
if attr_bbox and len(attr_bbox) == 4:
|
|
80
|
+
ax1, ay1, ax2, ay2 = map(int, attr_bbox)
|
|
81
|
+
attr_color = self._get_color(attr_label)
|
|
82
|
+
cv2.rectangle(frame, (ax1, ay1), (ax2, ay2), attr_color, 2)
|
|
83
|
+
cv2.putText(frame, f"{attr_label} ({attr_conf:.2f})", (ax1, max(ay1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, attr_color, 2)
|
|
84
|
+
|
|
85
|
+
return frame
|
|
86
|
+
|
|
87
|
+
def show_image(self, frame):
|
|
88
|
+
"""Displays the image in a window without blocking execution."""
|
|
89
|
+
if self.display:
|
|
90
|
+
print("[DEBUG] Showing image...") # Debug print
|
|
91
|
+
cv2.imshow(self.window_name, frame)
|
|
92
|
+
key = cv2.waitKey(10) & 0xFF
|
|
93
|
+
if key == ord('q'): # Allows exit with 'q'
|
|
94
|
+
self.close()
|
|
95
|
+
print("[DEBUG] Image displayed.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def show_image_threaded(self, frame):
|
|
99
|
+
"""Runs show_image in a separate thread to prevent blocking."""
|
|
100
|
+
thread = threading.Thread(target=self.show_image, args=(frame,))
|
|
101
|
+
thread.start()
|
|
102
|
+
|
|
103
|
+
def save_image(self, frame, filename="debug_output.jpg"):
|
|
104
|
+
"""
|
|
105
|
+
Saves the image to a file.
|
|
106
|
+
|
|
107
|
+
:param frame: Image to save.
|
|
108
|
+
:param filename: Name of the output file.
|
|
109
|
+
"""
|
|
110
|
+
cv2.imwrite(filename, frame)
|
|
111
|
+
logging.info(f"📸 Debug image saved as {filename}")
|
|
112
|
+
|
|
113
|
+
def close(self):
|
|
114
|
+
"""Closes all OpenCV windows."""
|
|
115
|
+
cv2.destroyAllWindows()
|
|
116
|
+
cv2.waitKey(1) # Ensure the window fully closes
|
|
117
|
+
|
|
118
|
+
def _get_color(self, label):
|
|
119
|
+
"""
|
|
120
|
+
Generates a unique color for each label.
|
|
121
|
+
|
|
122
|
+
:param label: Label name.
|
|
123
|
+
:return: BGR color tuple.
|
|
124
|
+
"""
|
|
125
|
+
random.seed(hash(label) % 256)
|
|
126
|
+
return tuple(random.randint(100, 255) for _ in range(3))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
# TODO: fix timer error (because of threading)
|
|
7
|
+
class VideoDebugger:
|
|
8
|
+
"""Handles real-time visualization of video streams with object detections."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, enable_visualization=True):
|
|
11
|
+
"""
|
|
12
|
+
Initializes the VideoDebugger with frame drawing and visualization capabilities.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
enable_visualization (bool): Whether to display frames.
|
|
16
|
+
"""
|
|
17
|
+
self.enable_visualization = enable_visualization
|
|
18
|
+
self.windows = {} # Tracks OpenCV windows
|
|
19
|
+
self.lock = threading.Lock() # Thread-safe updates
|
|
20
|
+
self.fps_tracker = defaultdict(lambda: {"start_time": time.time(), "frame_count": 0})
|
|
21
|
+
|
|
22
|
+
def show_frame(self, pipeline_id, worker_source_id, frame):
|
|
23
|
+
"""
|
|
24
|
+
Displays a frame with FPS overlay.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
pipeline_id (str/int): Identifier for the pipeline.
|
|
28
|
+
worker_source_id (str): Identifier for the worker/source.
|
|
29
|
+
frame: The frame to display.
|
|
30
|
+
"""
|
|
31
|
+
if not self.enable_visualization or frame is None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
window_name = f"Pipeline {pipeline_id} - {worker_source_id}"
|
|
35
|
+
with self.lock:
|
|
36
|
+
if window_name not in self.fps_tracker:
|
|
37
|
+
self.fps_tracker[window_name] = {"start_time": time.time(), "frame_count": 0}
|
|
38
|
+
|
|
39
|
+
self.fps_tracker[window_name]["frame_count"] += 1
|
|
40
|
+
elapsed_time = time.time() - self.fps_tracker[window_name]["start_time"]
|
|
41
|
+
fps = self.fps_tracker[window_name]["frame_count"] / max(elapsed_time, 1e-5)
|
|
42
|
+
|
|
43
|
+
# Display FPS on the frame
|
|
44
|
+
cv2.putText(frame, f"FPS: {fps:.2f}", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
|
|
45
|
+
|
|
46
|
+
# Show the window
|
|
47
|
+
if window_name not in self.windows:
|
|
48
|
+
self.windows[window_name] = True # Register window
|
|
49
|
+
|
|
50
|
+
cv2.imshow(window_name, frame)
|
|
51
|
+
|
|
52
|
+
# Close on 'q' press
|
|
53
|
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
54
|
+
self.close_window(window_name)
|
|
55
|
+
|
|
56
|
+
def close_window(self, window_name):
|
|
57
|
+
"""Closes a specific OpenCV window."""
|
|
58
|
+
with self.lock:
|
|
59
|
+
if window_name in self.windows:
|
|
60
|
+
cv2.destroyWindow(window_name)
|
|
61
|
+
del self.windows[window_name]
|
|
62
|
+
|
|
63
|
+
def close_all(self):
|
|
64
|
+
"""Closes all OpenCV windows."""
|
|
65
|
+
with self.lock:
|
|
66
|
+
for window in list(self.windows.keys()):
|
|
67
|
+
cv2.destroyWindow(window)
|
|
68
|
+
self.windows.clear()
|
|
69
|
+
cv2.waitKey(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .core_service import CoreService
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def signal_handler(signum, frame):
|
|
12
|
+
"""Handle system signals for graceful shutdown"""
|
|
13
|
+
logging.info(f"Received signal {signum}, shutting down...")
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
"""Main CLI entry point."""
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description="Nedo Vision Core Library CLI",
|
|
21
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
|
+
epilog="""
|
|
23
|
+
Examples:
|
|
24
|
+
# Start core service with default settings
|
|
25
|
+
nedo-core run
|
|
26
|
+
|
|
27
|
+
# Start with custom drawing assets path
|
|
28
|
+
nedo-core run --drawing-assets /path/to/assets
|
|
29
|
+
|
|
30
|
+
# Start with debug logging
|
|
31
|
+
nedo-core run --log-level DEBUG
|
|
32
|
+
|
|
33
|
+
# Start with custom storage path and RTMP server
|
|
34
|
+
nedo-core run --storage-path /path/to/storage --rtmp-server rtmp://server.com:1935/live
|
|
35
|
+
|
|
36
|
+
# Start with all custom parameters
|
|
37
|
+
nedo-core run --drawing-assets /path/to/assets --log-level DEBUG --storage-path /data --rtmp-server rtmp://server.com:1935/live
|
|
38
|
+
|
|
39
|
+
# Run system diagnostics
|
|
40
|
+
nedo-core doctor
|
|
41
|
+
|
|
42
|
+
Detection Callbacks:
|
|
43
|
+
The core service supports detection callbacks for extensible event handling.
|
|
44
|
+
See example_callbacks.py for usage examples.
|
|
45
|
+
"""
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Add subcommands
|
|
49
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
50
|
+
|
|
51
|
+
# Run command
|
|
52
|
+
run_parser = subparsers.add_parser(
|
|
53
|
+
'run',
|
|
54
|
+
help='Start the core service',
|
|
55
|
+
description='Start the Nedo Vision Core Service'
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Doctor command
|
|
59
|
+
doctor_parser = subparsers.add_parser(
|
|
60
|
+
'doctor',
|
|
61
|
+
help='Run system diagnostics',
|
|
62
|
+
description='Run diagnostic checks for system requirements and dependencies'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
run_parser.add_argument(
|
|
66
|
+
"--drawing-assets",
|
|
67
|
+
default=None,
|
|
68
|
+
help="Path to drawing assets directory (optional, uses bundled assets by default)"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
run_parser.add_argument(
|
|
72
|
+
"--log-level",
|
|
73
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
74
|
+
default="INFO",
|
|
75
|
+
help="Logging level (default: INFO)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
run_parser.add_argument(
|
|
79
|
+
"--storage-path",
|
|
80
|
+
default="data",
|
|
81
|
+
help="Storage path for databases and files (default: data)"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
run_parser.add_argument(
|
|
85
|
+
"--rtmp-server",
|
|
86
|
+
default="rtmp://localhost:1935/live",
|
|
87
|
+
help="RTMP server URL for video streaming (default: rtmp://localhost:1935/live)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Add legacy arguments for backward compatibility (when no subcommand is used)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--drawing-assets",
|
|
93
|
+
help="(Legacy) Path to drawing assets directory (optional, uses bundled assets by default)"
|
|
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)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--version",
|
|
117
|
+
action="version",
|
|
118
|
+
version=f"nedo-vision-core {__version__}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
args = parser.parse_args()
|
|
122
|
+
|
|
123
|
+
# Handle subcommands
|
|
124
|
+
if args.command == 'run':
|
|
125
|
+
run_core_service(args)
|
|
126
|
+
elif args.command == 'doctor':
|
|
127
|
+
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
|
+
else:
|
|
132
|
+
# If no subcommand provided, show help
|
|
133
|
+
parser.print_help()
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_doctor():
|
|
138
|
+
"""Run system diagnostics."""
|
|
139
|
+
try:
|
|
140
|
+
from .doctor import run_diagnostics
|
|
141
|
+
success = run_diagnostics()
|
|
142
|
+
sys.exit(0 if success else 1)
|
|
143
|
+
except ImportError:
|
|
144
|
+
print("❌ Doctor module not available")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"❌ Doctor failed: {e}")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_core_service(args):
|
|
152
|
+
"""Run the core service with the provided arguments."""
|
|
153
|
+
|
|
154
|
+
def run_core_service(args):
|
|
155
|
+
"""Run the core service with the provided arguments."""
|
|
156
|
+
# Set up signal handlers for graceful shutdown
|
|
157
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
158
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
159
|
+
|
|
160
|
+
logger = logging.getLogger(__name__)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Create and start the core service
|
|
164
|
+
service = CoreService(
|
|
165
|
+
drawing_assets_path=args.drawing_assets,
|
|
166
|
+
log_level=args.log_level,
|
|
167
|
+
storage_path=args.storage_path,
|
|
168
|
+
rtmp_server=args.rtmp_server
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
logger.info("🚀 Starting Nedo Vision Core...")
|
|
172
|
+
if args.drawing_assets:
|
|
173
|
+
logger.info(f"🎨 Drawing Assets: {args.drawing_assets}")
|
|
174
|
+
else:
|
|
175
|
+
logger.info("🎨 Drawing Assets: Using bundled assets")
|
|
176
|
+
logger.info(f"📝 Log Level: {args.log_level}")
|
|
177
|
+
logger.info(f"💾 Storage Path: {args.storage_path}")
|
|
178
|
+
logger.info(f"📡 RTMP Server: {args.rtmp_server}")
|
|
179
|
+
logger.info("Press Ctrl+C to stop the service")
|
|
180
|
+
|
|
181
|
+
# Start the service
|
|
182
|
+
success = service.run()
|
|
183
|
+
|
|
184
|
+
if success:
|
|
185
|
+
logger.info("✅ Core service completed successfully")
|
|
186
|
+
else:
|
|
187
|
+
logger.error("❌ Core service failed")
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"❌ Error: {e}")
|
|
192
|
+
traceback.print_exc()
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
main()
|