alphadeep-capture 0.1.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.
- alphadeep_capture/__init__.py +0 -0
- alphadeep_capture/camera.py +60 -0
- alphadeep_capture/config.py +54 -0
- alphadeep_capture/main.py +520 -0
- alphadeep_capture/ui.py +289 -0
- alphadeep_capture-0.1.0.dist-info/METADATA +82 -0
- alphadeep_capture-0.1.0.dist-info/RECORD +9 -0
- alphadeep_capture-0.1.0.dist-info/WHEEL +4 -0
- alphadeep_capture-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class CameraStream:
|
|
7
|
+
def __init__(self, cam_id, name):
|
|
8
|
+
self.cam_id = cam_id
|
|
9
|
+
self.name = name
|
|
10
|
+
self.cap = cv2.VideoCapture(cam_id)
|
|
11
|
+
# Attempt to set v4l2 backend if on linux, but default works too.
|
|
12
|
+
self.frame = None
|
|
13
|
+
self.running = True
|
|
14
|
+
self.lock = threading.Lock()
|
|
15
|
+
|
|
16
|
+
self.thread = threading.Thread(target=self._update, daemon=True)
|
|
17
|
+
self.thread.start()
|
|
18
|
+
|
|
19
|
+
def _update(self):
|
|
20
|
+
while self.running:
|
|
21
|
+
if self.cap.isOpened():
|
|
22
|
+
ret, frame = self.cap.read()
|
|
23
|
+
if ret:
|
|
24
|
+
# Convert BGR to RGB for PyGame
|
|
25
|
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
26
|
+
with self.lock:
|
|
27
|
+
self.frame = frame_rgb
|
|
28
|
+
else:
|
|
29
|
+
time.sleep(0.1)
|
|
30
|
+
else:
|
|
31
|
+
time.sleep(0.1)
|
|
32
|
+
|
|
33
|
+
def get_frame(self):
|
|
34
|
+
with self.lock:
|
|
35
|
+
if self.frame is not None:
|
|
36
|
+
return self.frame.copy()
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def release(self):
|
|
40
|
+
self.running = False
|
|
41
|
+
self.thread.join(timeout=1.0)
|
|
42
|
+
if self.cap.isOpened():
|
|
43
|
+
self.cap.release()
|
|
44
|
+
|
|
45
|
+
class CameraManager:
|
|
46
|
+
def __init__(self, config_cameras):
|
|
47
|
+
self.streams = []
|
|
48
|
+
for cam in config_cameras:
|
|
49
|
+
# Only up to 4 cameras
|
|
50
|
+
if len(self.streams) >= 4:
|
|
51
|
+
break
|
|
52
|
+
stream = CameraStream(cam['id'], cam.get('name', f"Camera {cam['id']}"))
|
|
53
|
+
self.streams.append(stream)
|
|
54
|
+
|
|
55
|
+
def get_frames(self):
|
|
56
|
+
return [stream.get_frame() for stream in self.streams]
|
|
57
|
+
|
|
58
|
+
def release(self):
|
|
59
|
+
for stream in self.streams:
|
|
60
|
+
stream.release()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
DEFAULT_CONFIG = {
|
|
6
|
+
"cameras": [
|
|
7
|
+
{"id": 0, "name": "Camera 1"},
|
|
8
|
+
{"id": 1, "name": "Camera 2"},
|
|
9
|
+
{"id": 2, "name": "Camera 3"},
|
|
10
|
+
{"id": 3, "name": "Camera 4"}
|
|
11
|
+
],
|
|
12
|
+
"fps": 30,
|
|
13
|
+
"samples_dir": str(Path.home() / "alphadeep_samples"),
|
|
14
|
+
"alphadeep_session": "",
|
|
15
|
+
"alphadeep_adapter": "zero_adapter",
|
|
16
|
+
"alphadeep_task": "",
|
|
17
|
+
"alphadeep_api_key": "",
|
|
18
|
+
"alphadeep_classes": [],
|
|
19
|
+
"alphadeep_workers": 4,
|
|
20
|
+
"autocapture": {
|
|
21
|
+
"length": 2.0,
|
|
22
|
+
"period": 0.5
|
|
23
|
+
},
|
|
24
|
+
"debug": False
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def get_config_path() -> Path:
|
|
28
|
+
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
|
|
29
|
+
config_dir = Path(xdg_config_home) / "alphadeep" / "capture"
|
|
30
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
return config_dir / "config.yaml"
|
|
32
|
+
|
|
33
|
+
def load_config() -> dict:
|
|
34
|
+
config_path = get_config_path()
|
|
35
|
+
if config_path.exists():
|
|
36
|
+
with open(config_path, "r") as f:
|
|
37
|
+
try:
|
|
38
|
+
user_config = yaml.safe_load(f)
|
|
39
|
+
if user_config:
|
|
40
|
+
# Merge with default config
|
|
41
|
+
merged = DEFAULT_CONFIG.copy()
|
|
42
|
+
merged.update(user_config)
|
|
43
|
+
return merged
|
|
44
|
+
except yaml.YAMLError as e:
|
|
45
|
+
print(f"Error loading config: {e}")
|
|
46
|
+
|
|
47
|
+
# If not exists or error, save and return default
|
|
48
|
+
save_config(DEFAULT_CONFIG)
|
|
49
|
+
return DEFAULT_CONFIG.copy()
|
|
50
|
+
|
|
51
|
+
def save_config(config: dict):
|
|
52
|
+
config_path = get_config_path()
|
|
53
|
+
with open(config_path, "w") as f:
|
|
54
|
+
yaml.safe_dump(config, f)
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import base64
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import pygame
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import requests
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
|
|
14
|
+
from .config import load_config, save_config
|
|
15
|
+
from .camera import CameraManager
|
|
16
|
+
from .ui import Button, ConfirmDialog, CommentDialog, DetectionDialog, MessageDialog
|
|
17
|
+
|
|
18
|
+
# Colors
|
|
19
|
+
BG_COLOR = (30, 30, 30)
|
|
20
|
+
PANEL_COLOR = (45, 45, 45)
|
|
21
|
+
TEXT_COLOR = (240, 240, 240)
|
|
22
|
+
BTN_BG = (60, 60, 60)
|
|
23
|
+
BTN_HOVER = (90, 90, 90)
|
|
24
|
+
BTN_ACTIVE = (0, 120, 215)
|
|
25
|
+
ACCENT_COLOR = (138, 43, 226) # Purple-ish
|
|
26
|
+
|
|
27
|
+
def draw_gradient_text(surface, text, font, rect, color1, color2):
|
|
28
|
+
# Simple gradient approach
|
|
29
|
+
text_surf = font.render(text, True, (255, 255, 255))
|
|
30
|
+
gradient = pygame.Surface(text_surf.get_size())
|
|
31
|
+
w = gradient.get_width()
|
|
32
|
+
if w == 0:
|
|
33
|
+
return
|
|
34
|
+
for x in range(w):
|
|
35
|
+
c = (
|
|
36
|
+
int(color1[0] + (color2[0] - color1[0]) * x / w),
|
|
37
|
+
int(color1[1] + (color2[1] - color1[1]) * x / w),
|
|
38
|
+
int(color1[2] + (color2[2] - color1[2]) * x / w),
|
|
39
|
+
)
|
|
40
|
+
pygame.draw.line(gradient, c, (x, 0), (x, gradient.get_height()))
|
|
41
|
+
text_surf.blit(gradient, (0, 0), special_flags=pygame.BLEND_RGB_MULT)
|
|
42
|
+
surface.blit(text_surf, rect)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AlphaDeepCaptureApp:
|
|
46
|
+
def __init__(self):
|
|
47
|
+
pygame.init()
|
|
48
|
+
self.config = load_config()
|
|
49
|
+
|
|
50
|
+
self.screen_width = 1600
|
|
51
|
+
self.screen_height = 1000
|
|
52
|
+
self.screen = pygame.display.set_mode(
|
|
53
|
+
(self.screen_width, self.screen_height),
|
|
54
|
+
pygame.RESIZABLE)
|
|
55
|
+
pygame.display.set_caption("AlphaDeep Capture")
|
|
56
|
+
|
|
57
|
+
self.clock = pygame.time.Clock()
|
|
58
|
+
self.fps = self.config.get("fps", 30)
|
|
59
|
+
|
|
60
|
+
self.cam_manager = CameraManager(self.config.get("cameras", []))
|
|
61
|
+
|
|
62
|
+
self.font_large = pygame.font.SysFont("Inter, Roboto, Arial", 42, bold=True)
|
|
63
|
+
self.font_medium = pygame.font.SysFont("Inter, Roboto, Arial", 31)
|
|
64
|
+
self.font_small = pygame.font.SysFont("Inter, Roboto, Arial", 21)
|
|
65
|
+
|
|
66
|
+
self.current_sample_dir = None
|
|
67
|
+
self.image_count = 0
|
|
68
|
+
|
|
69
|
+
self.auto_capture_active = False
|
|
70
|
+
self.auto_capture_end_time = 0
|
|
71
|
+
self.last_auto_capture_time = 0
|
|
72
|
+
self.auto_capture_period = 0
|
|
73
|
+
|
|
74
|
+
self.active_dialog = None
|
|
75
|
+
|
|
76
|
+
self.logo_img = None
|
|
77
|
+
self.pending_detections = None
|
|
78
|
+
self.pending_message = None
|
|
79
|
+
|
|
80
|
+
for path in [Path("logo.png"), Path(__file__).parent / "logo.png"]:
|
|
81
|
+
if path.exists():
|
|
82
|
+
try:
|
|
83
|
+
self.logo_img = pygame.image.load(str(path)).convert_alpha()
|
|
84
|
+
pygame.display.set_icon(self.logo_img)
|
|
85
|
+
break
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"Failed to load logo: {e}")
|
|
88
|
+
|
|
89
|
+
self.buttons = {}
|
|
90
|
+
self.create_buttons()
|
|
91
|
+
|
|
92
|
+
def create_buttons(self):
|
|
93
|
+
self.buttons['new_sample'] = Button(0, 0, 10, 10, "New Sample", self.font_medium, BTN_BG, TEXT_COLOR, BTN_HOVER)
|
|
94
|
+
self.buttons['capture'] = Button(0, 0, 10, 10, "Capture", self.font_medium, BTN_BG, TEXT_COLOR, BTN_HOVER)
|
|
95
|
+
self.buttons['auto_capture'] = Button(0, 0, 10, 10, "Auto Capture", self.font_medium, BTN_BG, TEXT_COLOR, BTN_ACTIVE)
|
|
96
|
+
self.buttons['preview'] = Button(0, 0, 10, 10, "Preview Sample", self.font_medium, BTN_BG, TEXT_COLOR, BTN_HOVER)
|
|
97
|
+
self.buttons['analyze'] = Button(0, 0, 10, 10, "Analyze", self.font_medium, BTN_BG, TEXT_COLOR, BTN_HOVER)
|
|
98
|
+
self.buttons['comment'] = Button(0, 0, 10, 10, "Comment", self.font_medium, BTN_BG, TEXT_COLOR, BTN_HOVER)
|
|
99
|
+
|
|
100
|
+
def layout_buttons(self, panel_rect, start_y):
|
|
101
|
+
padding = 20
|
|
102
|
+
btn_width = panel_rect.width - padding * 2
|
|
103
|
+
btn_height = 60
|
|
104
|
+
|
|
105
|
+
order = ['new_sample', 'capture', 'auto_capture', 'preview', 'analyze', 'comment']
|
|
106
|
+
for i, key in enumerate(order):
|
|
107
|
+
y = start_y + i * (btn_height + 15)
|
|
108
|
+
self.buttons[key].rect = pygame.Rect(panel_rect.x + padding, y, btn_width, btn_height)
|
|
109
|
+
|
|
110
|
+
def handle_events(self):
|
|
111
|
+
for event in pygame.event.get():
|
|
112
|
+
if event.type == pygame.QUIT:
|
|
113
|
+
return False
|
|
114
|
+
elif event.type == pygame.VIDEORESIZE:
|
|
115
|
+
self.screen_width = event.w
|
|
116
|
+
self.screen_height = event.h
|
|
117
|
+
elif event.type == pygame.KEYDOWN:
|
|
118
|
+
if (event.key == pygame.K_f or event.key == pygame.K_F11) and not self.active_dialog:
|
|
119
|
+
pygame.display.toggle_fullscreen()
|
|
120
|
+
|
|
121
|
+
if self.active_dialog:
|
|
122
|
+
self.active_dialog.handle_event(event)
|
|
123
|
+
if self.active_dialog.resolved:
|
|
124
|
+
self.handle_dialog_result(self.active_dialog)
|
|
125
|
+
self.active_dialog = None
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Check for pending detections from thread
|
|
129
|
+
if self.pending_detections is not None:
|
|
130
|
+
if self.pending_detections:
|
|
131
|
+
self.active_dialog = DetectionDialog(
|
|
132
|
+
int(self.screen_width * 0.8), int(self.screen_height * 0.8),
|
|
133
|
+
self.pending_detections, self.font_medium, self.font_small,
|
|
134
|
+
title_prefix="Detection Results"
|
|
135
|
+
)
|
|
136
|
+
self.active_dialog.type = 'detection'
|
|
137
|
+
self.pending_detections = None
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if self.pending_message is not None:
|
|
141
|
+
title, msg = self.pending_message
|
|
142
|
+
self.active_dialog = MessageDialog(
|
|
143
|
+
500, 150, title, msg, self.font_medium, self.font_small
|
|
144
|
+
)
|
|
145
|
+
self.active_dialog.type = 'message'
|
|
146
|
+
self.pending_message = None
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
for key, btn in self.buttons.items():
|
|
150
|
+
if btn.handle_event(event):
|
|
151
|
+
self.on_button_click(key)
|
|
152
|
+
|
|
153
|
+
if not self.active_dialog:
|
|
154
|
+
mouse_pos = pygame.mouse.get_pos()
|
|
155
|
+
for btn in self.buttons.values():
|
|
156
|
+
btn.update(mouse_pos)
|
|
157
|
+
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def on_button_click(self, key):
|
|
161
|
+
if key == 'new_sample':
|
|
162
|
+
self.new_sample()
|
|
163
|
+
elif key == 'capture':
|
|
164
|
+
self.capture_sample()
|
|
165
|
+
elif key == 'auto_capture':
|
|
166
|
+
self.start_auto_capture()
|
|
167
|
+
elif key == 'preview':
|
|
168
|
+
self.preview_sample()
|
|
169
|
+
elif key == 'analyze':
|
|
170
|
+
self.analyze()
|
|
171
|
+
elif key == 'comment':
|
|
172
|
+
self.add_comment()
|
|
173
|
+
|
|
174
|
+
def handle_dialog_result(self, dialog):
|
|
175
|
+
if dialog.type == 'comment':
|
|
176
|
+
if dialog.result is not None:
|
|
177
|
+
with open(self.current_sample_dir / "comment.txt", "w") as f:
|
|
178
|
+
f.write(dialog.result + "\n")
|
|
179
|
+
|
|
180
|
+
def get_base_dir(self):
|
|
181
|
+
d = Path(self.config.get("samples_dir", str(Path.home() / "alphadeep_samples")))
|
|
182
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
return d
|
|
184
|
+
|
|
185
|
+
def new_sample(self):
|
|
186
|
+
base = self.get_base_dir()
|
|
187
|
+
ts = datetime.datetime.now().isoformat().replace(':', '-')
|
|
188
|
+
self.current_sample_dir = base / f"sample_{ts}"
|
|
189
|
+
self.current_sample_dir.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
self.image_count = 0
|
|
191
|
+
print(f"Created new sample at {self.current_sample_dir}")
|
|
192
|
+
|
|
193
|
+
def capture_sample(self):
|
|
194
|
+
if not self.current_sample_dir:
|
|
195
|
+
self.new_sample()
|
|
196
|
+
|
|
197
|
+
frames = self.cam_manager.get_frames()
|
|
198
|
+
captured = 0
|
|
199
|
+
now_str = datetime.datetime.now().isoformat().replace(":", "-")
|
|
200
|
+
for i, frame in enumerate(frames):
|
|
201
|
+
if frame is not None:
|
|
202
|
+
# frame is RGB, cv2 needs BGR for saving
|
|
203
|
+
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
|
204
|
+
filepath = self.current_sample_dir / f"cam{i}_{now_str}.jpg"
|
|
205
|
+
cv2.imwrite(str(filepath), frame_bgr)
|
|
206
|
+
captured += 1
|
|
207
|
+
|
|
208
|
+
self.image_count += captured
|
|
209
|
+
print(f"Captured {captured} frames.")
|
|
210
|
+
|
|
211
|
+
def start_auto_capture(self):
|
|
212
|
+
if not self.current_sample_dir:
|
|
213
|
+
print("No active sample. Please create a new sample first.")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
ac_config = self.config.get("autocapture", {})
|
|
217
|
+
length = float(ac_config.get("length", 2.0))
|
|
218
|
+
self.auto_capture_period = float(ac_config.get("period", 0.5))
|
|
219
|
+
|
|
220
|
+
self.auto_capture_end_time = time.time() + length
|
|
221
|
+
self.last_auto_capture_time = 0 # Force immediate first capture
|
|
222
|
+
self.auto_capture_active = True
|
|
223
|
+
|
|
224
|
+
self.buttons['capture'].is_disabled = True
|
|
225
|
+
self.buttons['auto_capture'].is_disabled = True
|
|
226
|
+
|
|
227
|
+
def preview_sample(self):
|
|
228
|
+
if not self.current_sample_dir:
|
|
229
|
+
print("No active sample to preview.")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
image_paths = [str(p) for p in self.current_sample_dir.glob("*.jpg")]
|
|
233
|
+
if not image_paths:
|
|
234
|
+
print("No images found in current sample.")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
detections = []
|
|
238
|
+
for p in image_paths:
|
|
239
|
+
img = cv2.imread(p)
|
|
240
|
+
if img is not None:
|
|
241
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
242
|
+
surf = pygame.surfarray.make_surface(np.rot90(img_rgb))
|
|
243
|
+
surf = pygame.transform.flip(surf, True, False)
|
|
244
|
+
detections.append((surf, []))
|
|
245
|
+
|
|
246
|
+
if detections:
|
|
247
|
+
self.active_dialog = DetectionDialog(
|
|
248
|
+
int(self.screen_width * 0.8), int(self.screen_height * 0.8),
|
|
249
|
+
detections, self.font_medium, self.font_small, title_prefix="Preview"
|
|
250
|
+
)
|
|
251
|
+
self.active_dialog.type = 'preview'
|
|
252
|
+
|
|
253
|
+
def analyze(self):
|
|
254
|
+
if self.buttons['analyze'].is_disabled:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
if not self.current_sample_dir:
|
|
258
|
+
print("No sample to analyze.")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
image_paths = [str(p) for p in self.current_sample_dir.glob("*.jpg")]
|
|
262
|
+
if not image_paths:
|
|
263
|
+
print("No images found in current sample.")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
self.buttons['analyze'].text = "Please wait..."
|
|
267
|
+
self.buttons['analyze'].is_disabled = True
|
|
268
|
+
|
|
269
|
+
print(f"Starting analysis of {len(image_paths)} images...")
|
|
270
|
+
t = threading.Thread(target=self._analyze_thread_func, args=(image_paths,), daemon=True)
|
|
271
|
+
t.start()
|
|
272
|
+
|
|
273
|
+
def _analyze_thread_func(self, image_paths):
|
|
274
|
+
try:
|
|
275
|
+
session = self.config.get("alphadeep_session", "")
|
|
276
|
+
adapter = self.config.get("alphadeep_adapter", "zero_adapter")
|
|
277
|
+
task = self.config.get("alphadeep_task", "")
|
|
278
|
+
api_key = self.config.get("alphadeep_api_key", "")
|
|
279
|
+
classes = self.config.get("alphadeep_classes", [])
|
|
280
|
+
workers = self.config.get("alphadeep_workers", 4)
|
|
281
|
+
debug = self.config.get("debug", False)
|
|
282
|
+
|
|
283
|
+
if not session or not task or not api_key:
|
|
284
|
+
print("API configuration missing. Please set alphadeep_session, alphadeep_task, and alphadeep_api_key in config.yaml.")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if debug:
|
|
288
|
+
print(f"[DEBUG] Analyze triggered for {len(image_paths)} images.")
|
|
289
|
+
print(f"[DEBUG] Session: {session}, Task: {task}, Classes: {classes}")
|
|
290
|
+
|
|
291
|
+
url = f"https://agent.alphadeep.ai/api/v1/serve/{session}/{adapter}?task={task}"
|
|
292
|
+
headers = {
|
|
293
|
+
"Authorization": f"Bearer {api_key}",
|
|
294
|
+
"Content-Type": "application/json"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def process_image(p):
|
|
298
|
+
img = cv2.imread(p)
|
|
299
|
+
if img is None:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
img_resized = cv2.resize(img, (448, 448))
|
|
303
|
+
_, buffer = cv2.imencode('.jpg', img_resized)
|
|
304
|
+
b64 = base64.b64encode(buffer).decode('utf-8')
|
|
305
|
+
|
|
306
|
+
payload = {
|
|
307
|
+
"image": b64,
|
|
308
|
+
"classes": classes
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
resp = requests.post(url, json=payload, headers=headers, timeout=5)
|
|
313
|
+
|
|
314
|
+
if debug:
|
|
315
|
+
print(f"[DEBUG] API Response for {p}: Status {resp.status_code}")
|
|
316
|
+
print(f"[DEBUG] Body: {resp.text}")
|
|
317
|
+
|
|
318
|
+
if resp.status_code == 200:
|
|
319
|
+
data = resp.json()
|
|
320
|
+
|
|
321
|
+
if isinstance(data, list) and len(data) > 0:
|
|
322
|
+
result_dict = data[0]
|
|
323
|
+
elif isinstance(data, dict):
|
|
324
|
+
result_dict = data
|
|
325
|
+
else:
|
|
326
|
+
result_dict = {}
|
|
327
|
+
|
|
328
|
+
objects = result_dict.get("objects", [])
|
|
329
|
+
if objects:
|
|
330
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
331
|
+
surf = pygame.surfarray.make_surface(np.rot90(img_rgb))
|
|
332
|
+
surf = pygame.transform.flip(surf, True, False)
|
|
333
|
+
return (surf, objects)
|
|
334
|
+
elif resp.status_code == 429:
|
|
335
|
+
return 429
|
|
336
|
+
else:
|
|
337
|
+
print(f"API returned non-200 status code: {resp.status_code} for {p}")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
print(f"API Error for {p}: {e}")
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
detections = []
|
|
343
|
+
hit_limit = False
|
|
344
|
+
|
|
345
|
+
# Avoid ValueError by ensuring max_workers > 0
|
|
346
|
+
max_workers = max(1, min(workers, len(image_paths)))
|
|
347
|
+
|
|
348
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
349
|
+
# executor.map maintains the original order of image_paths
|
|
350
|
+
results = executor.map(process_image, image_paths)
|
|
351
|
+
for res in results:
|
|
352
|
+
if res == 429:
|
|
353
|
+
hit_limit = True
|
|
354
|
+
elif res is not None:
|
|
355
|
+
detections.append(res)
|
|
356
|
+
|
|
357
|
+
if hit_limit:
|
|
358
|
+
self.pending_message = ("Limit Reached", "Alphadeep token limit reached.")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
self.pending_detections = detections
|
|
362
|
+
finally:
|
|
363
|
+
self.buttons['analyze'].text = "Analyze"
|
|
364
|
+
self.buttons['analyze'].is_disabled = False
|
|
365
|
+
|
|
366
|
+
def add_comment(self):
|
|
367
|
+
if not self.current_sample_dir:
|
|
368
|
+
print("No active sample.")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
initial_text = ""
|
|
372
|
+
comment_file = self.current_sample_dir / "comment.txt"
|
|
373
|
+
if comment_file.exists():
|
|
374
|
+
with open(comment_file, "r") as f:
|
|
375
|
+
initial_text = f.read().strip()
|
|
376
|
+
|
|
377
|
+
self.active_dialog = CommentDialog(
|
|
378
|
+
500, 200, "Comment", initial_text,
|
|
379
|
+
self.font_medium, self.font_small
|
|
380
|
+
)
|
|
381
|
+
self.active_dialog.type = 'comment'
|
|
382
|
+
|
|
383
|
+
def draw(self):
|
|
384
|
+
self.screen.fill(BG_COLOR)
|
|
385
|
+
|
|
386
|
+
# 80/20 Split
|
|
387
|
+
left_width = int(self.screen_width * 0.8)
|
|
388
|
+
right_width = self.screen_width - left_width
|
|
389
|
+
|
|
390
|
+
# Draw Camera Grid
|
|
391
|
+
self.draw_camera_grid(pygame.Rect(0, 0, left_width, self.screen_height))
|
|
392
|
+
|
|
393
|
+
# Draw Right Panel
|
|
394
|
+
panel_rect = pygame.Rect(left_width, 0, right_width, self.screen_height)
|
|
395
|
+
pygame.draw.rect(self.screen, PANEL_COLOR, panel_rect)
|
|
396
|
+
|
|
397
|
+
# Draw Logo and Text
|
|
398
|
+
start_y = 20
|
|
399
|
+
if self.logo_img:
|
|
400
|
+
max_w = right_width - 40
|
|
401
|
+
img_w, img_h = self.logo_img.get_size()
|
|
402
|
+
if img_w > max_w:
|
|
403
|
+
scale = max_w / img_w
|
|
404
|
+
img_w = int(img_w * scale)
|
|
405
|
+
img_h = int(img_h * scale)
|
|
406
|
+
scaled_logo = pygame.transform.smoothscale(self.logo_img, (img_w, img_h))
|
|
407
|
+
else:
|
|
408
|
+
scaled_logo = self.logo_img
|
|
409
|
+
|
|
410
|
+
logo_x = left_width + (right_width - img_w) // 2
|
|
411
|
+
self.screen.blit(scaled_logo, (logo_x, start_y))
|
|
412
|
+
start_y += img_h + 10
|
|
413
|
+
else:
|
|
414
|
+
logo_rect = pygame.Rect(left_width + 20, start_y, right_width - 40, 50)
|
|
415
|
+
draw_gradient_text(self.screen, "AlphaDeep", self.font_large, logo_rect, (0, 191, 255), ACCENT_COLOR)
|
|
416
|
+
start_y += 60
|
|
417
|
+
|
|
418
|
+
# Draw Subtitles
|
|
419
|
+
text1 = self.font_medium.render("AlphaDeep Capture", True, (255, 255, 255))
|
|
420
|
+
text2 = self.font_small.render("For Internal Use only", True, (200, 200, 200))
|
|
421
|
+
text3 = self.font_small.render("AlphaDeep Inc. DE, USA", True, (150, 150, 150))
|
|
422
|
+
|
|
423
|
+
self.screen.blit(text1, (left_width + (right_width - text1.get_width()) // 2, start_y))
|
|
424
|
+
start_y += text1.get_height() + 5
|
|
425
|
+
self.screen.blit(text2, (left_width + (right_width - text2.get_width()) // 2, start_y))
|
|
426
|
+
start_y += text2.get_height() + 5
|
|
427
|
+
self.screen.blit(text3, (left_width + (right_width - text3.get_width()) // 2, start_y))
|
|
428
|
+
start_y += text3.get_height() + 20
|
|
429
|
+
|
|
430
|
+
# Layout and draw buttons
|
|
431
|
+
self.layout_buttons(panel_rect, start_y)
|
|
432
|
+
for btn in self.buttons.values():
|
|
433
|
+
btn.draw(self.screen)
|
|
434
|
+
|
|
435
|
+
# Status Text
|
|
436
|
+
status_y = self.buttons['comment'].rect.bottom + 30
|
|
437
|
+
if self.current_sample_dir:
|
|
438
|
+
path_str = self.current_sample_dir.name
|
|
439
|
+
info_str = f"{path_str} ({self.image_count} imgs)"
|
|
440
|
+
else:
|
|
441
|
+
info_str = "No active sample"
|
|
442
|
+
|
|
443
|
+
text_surf = self.font_small.render(info_str, True, (200, 200, 200))
|
|
444
|
+
if text_surf.get_width() > right_width - 40:
|
|
445
|
+
text_surf = pygame.transform.smoothscale(text_surf, (right_width - 40, text_surf.get_height()))
|
|
446
|
+
self.screen.blit(text_surf, (left_width + 20, status_y))
|
|
447
|
+
|
|
448
|
+
if self.active_dialog:
|
|
449
|
+
self.active_dialog.draw(self.screen)
|
|
450
|
+
|
|
451
|
+
pygame.display.flip()
|
|
452
|
+
|
|
453
|
+
def draw_camera_grid(self, rect):
|
|
454
|
+
frames = self.cam_manager.get_frames()
|
|
455
|
+
num_cams = len(frames)
|
|
456
|
+
if num_cams == 0:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# 2x2 grid
|
|
460
|
+
cell_w = rect.width // 2
|
|
461
|
+
cell_h = rect.height // 2
|
|
462
|
+
|
|
463
|
+
positions = [
|
|
464
|
+
(rect.x, rect.y),
|
|
465
|
+
(rect.x + cell_w, rect.y),
|
|
466
|
+
(rect.x, rect.y + cell_h),
|
|
467
|
+
(rect.x + cell_w, rect.y + cell_h)
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
for i, frame in enumerate(frames):
|
|
471
|
+
if i >= 4:
|
|
472
|
+
break
|
|
473
|
+
if frame is not None:
|
|
474
|
+
# Transpose and flip for Pygame
|
|
475
|
+
surf = pygame.surfarray.make_surface(np.rot90(frame))
|
|
476
|
+
surf = pygame.transform.flip(surf, True, False)
|
|
477
|
+
surf = pygame.transform.scale(surf, (cell_w - 4, cell_h - 4))
|
|
478
|
+
self.screen.blit(surf, (positions[i][0] + 2, positions[i][1] + 2))
|
|
479
|
+
|
|
480
|
+
# Draw label
|
|
481
|
+
label = self.font_small.render(self.cam_manager.streams[i].name, True, (255, 255, 255))
|
|
482
|
+
self.screen.blit(label, (positions[i][0] + 10, positions[i][1] + 10))
|
|
483
|
+
else:
|
|
484
|
+
# Draw placeholder if no frame
|
|
485
|
+
pygame.draw.rect(self.screen, (20, 20, 20), (positions[i][0]+2, positions[i][1]+2, cell_w-4, cell_h-4))
|
|
486
|
+
label = self.font_small.render(f"Waiting for {self.cam_manager.streams[i].name}...", True, (150, 150, 150))
|
|
487
|
+
self.screen.blit(label, (positions[i][0] + 10, positions[i][1] + 10))
|
|
488
|
+
|
|
489
|
+
def run(self):
|
|
490
|
+
running = True
|
|
491
|
+
while running:
|
|
492
|
+
running = self.handle_events()
|
|
493
|
+
|
|
494
|
+
if self.auto_capture_active:
|
|
495
|
+
now = time.time()
|
|
496
|
+
remaining = max(0.0, self.auto_capture_end_time - now)
|
|
497
|
+
|
|
498
|
+
if remaining > 0:
|
|
499
|
+
self.buttons['auto_capture'].text = f"Capturing... {remaining:.1f}s"
|
|
500
|
+
if now - self.last_auto_capture_time >= self.auto_capture_period:
|
|
501
|
+
self.capture_sample()
|
|
502
|
+
self.last_auto_capture_time = now
|
|
503
|
+
else:
|
|
504
|
+
self.auto_capture_active = False
|
|
505
|
+
self.buttons['auto_capture'].text = "Auto Capture"
|
|
506
|
+
self.buttons['capture'].is_disabled = False
|
|
507
|
+
self.buttons['auto_capture'].is_disabled = False
|
|
508
|
+
|
|
509
|
+
self.draw()
|
|
510
|
+
self.clock.tick(self.fps)
|
|
511
|
+
|
|
512
|
+
self.cam_manager.release()
|
|
513
|
+
pygame.quit()
|
|
514
|
+
|
|
515
|
+
def main():
|
|
516
|
+
app = AlphaDeepCaptureApp()
|
|
517
|
+
app.run()
|
|
518
|
+
|
|
519
|
+
if __name__ == "__main__":
|
|
520
|
+
main()
|
alphadeep_capture/ui.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Button:
|
|
5
|
+
def __init__(self, x, y, width, height, text, font, bg_color, text_color, hover_color=None):
|
|
6
|
+
self.rect = pygame.Rect(x, y, width, height)
|
|
7
|
+
self.text = text
|
|
8
|
+
self.font = font
|
|
9
|
+
self.bg_color = bg_color
|
|
10
|
+
self.text_color = text_color
|
|
11
|
+
self.hover_color = hover_color or bg_color
|
|
12
|
+
self.is_hovered = False
|
|
13
|
+
self.is_active = False # Used for toggles
|
|
14
|
+
self.is_disabled = False
|
|
15
|
+
|
|
16
|
+
def draw(self, surface):
|
|
17
|
+
if self.is_disabled:
|
|
18
|
+
color = (80, 80, 80)
|
|
19
|
+
text_color = (150, 150, 150)
|
|
20
|
+
else:
|
|
21
|
+
color = self.hover_color if (self.is_hovered or self.is_active) else self.bg_color
|
|
22
|
+
text_color = self.text_color
|
|
23
|
+
|
|
24
|
+
pygame.draw.rect(surface, color, self.rect, border_radius=8)
|
|
25
|
+
|
|
26
|
+
text_surf = self.font.render(self.text, True, text_color)
|
|
27
|
+
text_rect = text_surf.get_rect(center=self.rect.center)
|
|
28
|
+
surface.blit(text_surf, text_rect)
|
|
29
|
+
|
|
30
|
+
def update(self, mouse_pos):
|
|
31
|
+
if not self.is_disabled:
|
|
32
|
+
self.is_hovered = self.rect.collidepoint(mouse_pos)
|
|
33
|
+
else:
|
|
34
|
+
self.is_hovered = False
|
|
35
|
+
|
|
36
|
+
def handle_event(self, event):
|
|
37
|
+
if self.is_disabled:
|
|
38
|
+
return False
|
|
39
|
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
40
|
+
if self.is_hovered:
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
class ConfirmDialog:
|
|
45
|
+
def __init__(self, width, height, title, message, font_medium, font_small):
|
|
46
|
+
self.rect = pygame.Rect(0, 0, width, height)
|
|
47
|
+
self.title = title
|
|
48
|
+
self.message = message
|
|
49
|
+
self.font_medium = font_medium
|
|
50
|
+
self.font_small = font_small
|
|
51
|
+
|
|
52
|
+
self.result = None
|
|
53
|
+
self.resolved = False
|
|
54
|
+
|
|
55
|
+
self.yes_btn = Button(0, 0, 100, 40, "Yes", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
56
|
+
self.no_btn = Button(0, 0, 100, 40, "No", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
57
|
+
|
|
58
|
+
def handle_event(self, event):
|
|
59
|
+
if self.yes_btn.handle_event(event):
|
|
60
|
+
self.result = True
|
|
61
|
+
self.resolved = True
|
|
62
|
+
elif self.no_btn.handle_event(event):
|
|
63
|
+
self.result = False
|
|
64
|
+
self.resolved = True
|
|
65
|
+
|
|
66
|
+
mouse_pos = pygame.mouse.get_pos()
|
|
67
|
+
self.yes_btn.update(mouse_pos)
|
|
68
|
+
self.no_btn.update(mouse_pos)
|
|
69
|
+
|
|
70
|
+
def draw(self, surface):
|
|
71
|
+
self.rect.center = surface.get_rect().center
|
|
72
|
+
|
|
73
|
+
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
|
|
74
|
+
overlay.fill((0, 0, 0, 150))
|
|
75
|
+
surface.blit(overlay, (0, 0))
|
|
76
|
+
|
|
77
|
+
pygame.draw.rect(surface, (45, 45, 45), self.rect, border_radius=10)
|
|
78
|
+
pygame.draw.rect(surface, (100, 100, 100), self.rect, width=2, border_radius=10)
|
|
79
|
+
|
|
80
|
+
title_surf = self.font_medium.render(self.title, True, (255, 255, 255))
|
|
81
|
+
surface.blit(title_surf, (self.rect.x + 20, self.rect.y + 20))
|
|
82
|
+
|
|
83
|
+
msg_surf = self.font_small.render(self.message, True, (200, 200, 200))
|
|
84
|
+
surface.blit(msg_surf, (self.rect.x + 20, self.rect.y + 60))
|
|
85
|
+
|
|
86
|
+
self.yes_btn.rect.bottomright = (self.rect.centerx - 10, self.rect.bottom - 20)
|
|
87
|
+
self.no_btn.rect.bottomleft = (self.rect.centerx + 10, self.rect.bottom - 20)
|
|
88
|
+
|
|
89
|
+
self.yes_btn.draw(surface)
|
|
90
|
+
self.no_btn.draw(surface)
|
|
91
|
+
|
|
92
|
+
class CommentDialog:
|
|
93
|
+
def __init__(self, width, height, title, initial_text, font_medium, font_small):
|
|
94
|
+
self.rect = pygame.Rect(0, 0, width, height)
|
|
95
|
+
self.title = title
|
|
96
|
+
self.text = initial_text
|
|
97
|
+
self.font_medium = font_medium
|
|
98
|
+
self.font_small = font_small
|
|
99
|
+
|
|
100
|
+
self.result = None
|
|
101
|
+
self.resolved = False
|
|
102
|
+
|
|
103
|
+
self.ok_btn = Button(0, 0, 100, 40, "OK", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
104
|
+
self.cancel_btn = Button(0, 0, 100, 40, "Cancel", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
105
|
+
|
|
106
|
+
def handle_event(self, event):
|
|
107
|
+
if event.type == pygame.KEYDOWN:
|
|
108
|
+
if event.key == pygame.K_RETURN:
|
|
109
|
+
self.result = self.text
|
|
110
|
+
self.resolved = True
|
|
111
|
+
elif event.key == pygame.K_ESCAPE:
|
|
112
|
+
self.result = None
|
|
113
|
+
self.resolved = True
|
|
114
|
+
elif event.key == pygame.K_BACKSPACE:
|
|
115
|
+
self.text = self.text[:-1]
|
|
116
|
+
else:
|
|
117
|
+
self.text += event.unicode
|
|
118
|
+
|
|
119
|
+
if self.ok_btn.handle_event(event):
|
|
120
|
+
self.result = self.text
|
|
121
|
+
self.resolved = True
|
|
122
|
+
elif self.cancel_btn.handle_event(event):
|
|
123
|
+
self.result = None
|
|
124
|
+
self.resolved = True
|
|
125
|
+
|
|
126
|
+
mouse_pos = pygame.mouse.get_pos()
|
|
127
|
+
self.ok_btn.update(mouse_pos)
|
|
128
|
+
self.cancel_btn.update(mouse_pos)
|
|
129
|
+
|
|
130
|
+
def draw(self, surface):
|
|
131
|
+
self.rect.center = surface.get_rect().center
|
|
132
|
+
|
|
133
|
+
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
|
|
134
|
+
overlay.fill((0, 0, 0, 150))
|
|
135
|
+
surface.blit(overlay, (0, 0))
|
|
136
|
+
|
|
137
|
+
pygame.draw.rect(surface, (45, 45, 45), self.rect, border_radius=10)
|
|
138
|
+
pygame.draw.rect(surface, (100, 100, 100), self.rect, width=2, border_radius=10)
|
|
139
|
+
|
|
140
|
+
title_surf = self.font_medium.render(self.title, True, (255, 255, 255))
|
|
141
|
+
surface.blit(title_surf, (self.rect.x + 20, self.rect.y + 20))
|
|
142
|
+
|
|
143
|
+
input_rect = pygame.Rect(self.rect.x + 20, self.rect.y + 70, self.rect.width - 40, 40)
|
|
144
|
+
pygame.draw.rect(surface, (30, 30, 30), input_rect, border_radius=5)
|
|
145
|
+
|
|
146
|
+
# Blinking cursor simulation (simple)
|
|
147
|
+
cursor = "_" if (pygame.time.get_ticks() // 500) % 2 == 0 else ""
|
|
148
|
+
text_surf = self.font_small.render(self.text + cursor, True, (255, 255, 255))
|
|
149
|
+
|
|
150
|
+
surface.set_clip(input_rect)
|
|
151
|
+
surface.blit(text_surf, (input_rect.x + 10, input_rect.y + 10))
|
|
152
|
+
surface.set_clip(None)
|
|
153
|
+
|
|
154
|
+
self.ok_btn.rect.bottomright = (self.rect.centerx - 10, self.rect.bottom - 20)
|
|
155
|
+
self.cancel_btn.rect.bottomleft = (self.rect.centerx + 10, self.rect.bottom - 20)
|
|
156
|
+
|
|
157
|
+
self.ok_btn.draw(surface)
|
|
158
|
+
self.cancel_btn.draw(surface)
|
|
159
|
+
|
|
160
|
+
class MessageDialog:
|
|
161
|
+
def __init__(self, width, height, title, message, font_medium, font_small):
|
|
162
|
+
self.rect = pygame.Rect(0, 0, width, height)
|
|
163
|
+
self.title = title
|
|
164
|
+
self.message = message
|
|
165
|
+
self.font_medium = font_medium
|
|
166
|
+
self.font_small = font_small
|
|
167
|
+
|
|
168
|
+
self.resolved = False
|
|
169
|
+
|
|
170
|
+
self.ok_btn = Button(0, 0, 100, 40, "OK", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
171
|
+
|
|
172
|
+
def handle_event(self, event):
|
|
173
|
+
if self.ok_btn.handle_event(event) or (event.type == pygame.KEYDOWN and (event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE)):
|
|
174
|
+
self.resolved = True
|
|
175
|
+
|
|
176
|
+
mouse_pos = pygame.mouse.get_pos()
|
|
177
|
+
self.ok_btn.update(mouse_pos)
|
|
178
|
+
|
|
179
|
+
def draw(self, surface):
|
|
180
|
+
self.rect.center = surface.get_rect().center
|
|
181
|
+
|
|
182
|
+
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
|
|
183
|
+
overlay.fill((0, 0, 0, 150))
|
|
184
|
+
surface.blit(overlay, (0, 0))
|
|
185
|
+
|
|
186
|
+
pygame.draw.rect(surface, (45, 45, 45), self.rect, border_radius=10)
|
|
187
|
+
pygame.draw.rect(surface, (100, 100, 100), self.rect, width=2, border_radius=10)
|
|
188
|
+
|
|
189
|
+
title_surf = self.font_medium.render(self.title, True, (255, 255, 255))
|
|
190
|
+
surface.blit(title_surf, (self.rect.x + 20, self.rect.y + 20))
|
|
191
|
+
|
|
192
|
+
msg_surf = self.font_small.render(self.message, True, (200, 200, 200))
|
|
193
|
+
surface.blit(msg_surf, (self.rect.x + 20, self.rect.y + 60))
|
|
194
|
+
|
|
195
|
+
self.ok_btn.rect.centerx = self.rect.centerx
|
|
196
|
+
self.ok_btn.rect.bottom = self.rect.bottom - 20
|
|
197
|
+
self.ok_btn.draw(surface)
|
|
198
|
+
|
|
199
|
+
class DetectionDialog:
|
|
200
|
+
def __init__(self, width, height, detections, font_medium, font_small, title_prefix="Detection Results"):
|
|
201
|
+
# detections is a list of tuples: (pygame_surface, list_of_objects)
|
|
202
|
+
self.rect = pygame.Rect(0, 0, width, height)
|
|
203
|
+
self.detections = detections
|
|
204
|
+
self.current_idx = 0
|
|
205
|
+
self.title_prefix = title_prefix
|
|
206
|
+
|
|
207
|
+
self.font_medium = font_medium
|
|
208
|
+
self.font_small = font_small
|
|
209
|
+
self.resolved = False
|
|
210
|
+
|
|
211
|
+
self.close_btn = Button(0, 0, 100, 40, "Close", font_small, (60, 60, 60), (255, 255, 255), (90, 90, 90))
|
|
212
|
+
self.next_btn = Button(0, 0, 100, 40, "Next", font_small, (0, 120, 215), (255, 255, 255), (0, 150, 255))
|
|
213
|
+
|
|
214
|
+
def handle_event(self, event):
|
|
215
|
+
if self.close_btn.handle_event(event) or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
|
|
216
|
+
self.resolved = True
|
|
217
|
+
|
|
218
|
+
if len(self.detections) > 1 and self.current_idx < len(self.detections) - 1:
|
|
219
|
+
if self.next_btn.handle_event(event) or (event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT):
|
|
220
|
+
self.current_idx += 1
|
|
221
|
+
|
|
222
|
+
mouse_pos = pygame.mouse.get_pos()
|
|
223
|
+
self.close_btn.update(mouse_pos)
|
|
224
|
+
self.next_btn.update(mouse_pos)
|
|
225
|
+
|
|
226
|
+
def draw(self, surface):
|
|
227
|
+
self.rect.center = surface.get_rect().center
|
|
228
|
+
|
|
229
|
+
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
|
|
230
|
+
overlay.fill((0, 0, 0, 150))
|
|
231
|
+
surface.blit(overlay, (0, 0))
|
|
232
|
+
|
|
233
|
+
pygame.draw.rect(surface, (45, 45, 45), self.rect, border_radius=10)
|
|
234
|
+
pygame.draw.rect(surface, (100, 100, 100), self.rect, width=2, border_radius=10)
|
|
235
|
+
|
|
236
|
+
title_surf = self.font_medium.render(f"{self.title_prefix} ({self.current_idx + 1}/{max(1, len(self.detections))})", True, (255, 255, 255))
|
|
237
|
+
surface.blit(title_surf, (self.rect.x + 20, self.rect.y + 20))
|
|
238
|
+
|
|
239
|
+
if not self.detections:
|
|
240
|
+
msg = self.font_medium.render("Nothing detected.", True, (200, 200, 200))
|
|
241
|
+
surface.blit(msg, (self.rect.centerx - msg.get_width()//2, self.rect.centery))
|
|
242
|
+
else:
|
|
243
|
+
img_surf, objects = self.detections[self.current_idx]
|
|
244
|
+
|
|
245
|
+
img_rect = img_surf.get_rect()
|
|
246
|
+
max_img_h = self.rect.height - 150
|
|
247
|
+
max_img_w = self.rect.width - 40
|
|
248
|
+
scale = min(max_img_w / img_rect.width, max_img_h / img_rect.height)
|
|
249
|
+
|
|
250
|
+
if scale < 1.0:
|
|
251
|
+
w, h = int(img_rect.width * scale), int(img_rect.height * scale)
|
|
252
|
+
display_surf = pygame.transform.smoothscale(img_surf, (w, h))
|
|
253
|
+
else:
|
|
254
|
+
display_surf = img_surf
|
|
255
|
+
w, h = img_rect.width, img_rect.height
|
|
256
|
+
|
|
257
|
+
draw_x = self.rect.centerx - w // 2
|
|
258
|
+
draw_y = self.rect.centery - h // 2
|
|
259
|
+
|
|
260
|
+
surface.blit(display_surf, (draw_x, draw_y))
|
|
261
|
+
|
|
262
|
+
# Draw bounding boxes
|
|
263
|
+
for obj in objects:
|
|
264
|
+
rx1, ry1 = float(obj.get("x1", 0.0)), float(obj.get("y1", 0.0))
|
|
265
|
+
rx2, ry2 = float(obj.get("x2", 0.0)), float(obj.get("y2", 0.0))
|
|
266
|
+
|
|
267
|
+
x = draw_x + int(rx1 * w)
|
|
268
|
+
y = draw_y + int(ry1 * h)
|
|
269
|
+
bw = int((rx2 - rx1) * w)
|
|
270
|
+
bh = int((ry2 - ry1) * h)
|
|
271
|
+
|
|
272
|
+
box_rect = pygame.Rect(x, y, bw, bh)
|
|
273
|
+
pygame.draw.rect(surface, (0, 255, 0), box_rect, width=2)
|
|
274
|
+
|
|
275
|
+
label_str = f"{obj.get('cls', '?')} {float(obj.get('confidence', 0.0)):.2f}"
|
|
276
|
+
lbl = self.font_small.render(label_str, True, (0, 0, 0), (0, 255, 0))
|
|
277
|
+
surface.blit(lbl, (x, max(draw_y, y - lbl.get_height())))
|
|
278
|
+
|
|
279
|
+
has_next = len(self.detections) > 1 and self.current_idx < len(self.detections) - 1
|
|
280
|
+
|
|
281
|
+
if has_next:
|
|
282
|
+
self.close_btn.rect.bottomright = (self.rect.centerx - 10, self.rect.bottom - 20)
|
|
283
|
+
self.next_btn.rect.bottomleft = (self.rect.centerx + 10, self.rect.bottom - 20)
|
|
284
|
+
self.next_btn.draw(surface)
|
|
285
|
+
else:
|
|
286
|
+
self.close_btn.rect.centerx = self.rect.centerx
|
|
287
|
+
self.close_btn.rect.bottom = self.rect.bottom - 20
|
|
288
|
+
|
|
289
|
+
self.close_btn.draw(surface)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alphadeep-capture
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AlphaDeep Live Capture Tool
|
|
5
|
+
Author-email: "AlphaDeep Inc." <nmilosev@alphadeep.ai>
|
|
6
|
+
License: Apache 2.0
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: numpy>=1.24.0
|
|
9
|
+
Requires-Dist: opencv-python>=4.8.0
|
|
10
|
+
Requires-Dist: pygame>=2.5.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: requests>=2.31.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# AlphaDeep Capture
|
|
16
|
+
|
|
17
|
+
AlphaDeep Capture is a high-performance Python/Pygame application designed for live streaming, data capture, and realtime cloud-inference using the AlphaDeep AI platform.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
This project is built using modern Python tooling and can be installed locally via `pip` or `uv`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone <repository_url>
|
|
25
|
+
cd alphadeep-capture
|
|
26
|
+
|
|
27
|
+
# Install the application and all dependencies
|
|
28
|
+
uv pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
AlphaDeep Capture uses a YAML configuration file stored in your user configuration directory (following the XDG Base Directory standard).
|
|
34
|
+
|
|
35
|
+
**Configuration Path**: `~/.config/alphadeep/capture/config.yaml`
|
|
36
|
+
|
|
37
|
+
If this file does not exist when the app is first launched, a default configuration will be automatically generated.
|
|
38
|
+
|
|
39
|
+
### Example `config.yaml`
|
|
40
|
+
```yaml
|
|
41
|
+
cameras:
|
|
42
|
+
- id: 0
|
|
43
|
+
name: Camera 1
|
|
44
|
+
- id: 1
|
|
45
|
+
name: Camera 2
|
|
46
|
+
|
|
47
|
+
fps: 30
|
|
48
|
+
samples_dir: ~/alphadeep_samples
|
|
49
|
+
|
|
50
|
+
# AlphaDeep API Configuration
|
|
51
|
+
alphadeep_session: "your_session_id"
|
|
52
|
+
alphadeep_adapter: "zero_adapter"
|
|
53
|
+
alphadeep_task: "detection"
|
|
54
|
+
alphadeep_api_key: "your_api_key_here"
|
|
55
|
+
alphadeep_classes:
|
|
56
|
+
- "car"
|
|
57
|
+
- "person"
|
|
58
|
+
alphadeep_workers: 4
|
|
59
|
+
|
|
60
|
+
# Enable verbose console output for debugging API responses
|
|
61
|
+
debug: true
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Adding Your API Key
|
|
65
|
+
In order to use the **Analyze** functionality, you must edit your `config.yaml` and provide your `alphadeep_api_key`, `alphadeep_session`, and `alphadeep_task`.
|
|
66
|
+
|
|
67
|
+
## Running the Application
|
|
68
|
+
|
|
69
|
+
Once installed, you can launch the application directly from your terminal:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
alphadeep-capture
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features and Controls
|
|
76
|
+
|
|
77
|
+
* **New Sample**: Creates a fresh timestamped directory in your `samples_dir` for saving data.
|
|
78
|
+
* **Capture**: Instantly grabs the current frame from all active cameras and saves them as `.jpg` files in the current sample directory.
|
|
79
|
+
* **Preview Sample**: Opens an interactive gallery overlay to view all images captured in the current sample.
|
|
80
|
+
* **Analyze**: Sends the currently captured sample to the AlphaDeep API for remote inference and overlays the bounding box results in the UI.
|
|
81
|
+
* **Comment**: Opens a dialog to attach text notes to the current sample (saved as `comment.txt`).
|
|
82
|
+
* **Sync (Cloud)**: Placeholder feature for synchronizing samples with AlphaDeep cloud storage.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
alphadeep_capture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
alphadeep_capture/camera.py,sha256=13C0UHiCrxpTYk1HnHF81k1CpxYEskQIR9RdIw_Cg9s,1793
|
|
3
|
+
alphadeep_capture/config.py,sha256=kbZ93lPUmaHWcJR2L4aVHBrKAsAmCiNkmQK3_2ZsLQ4,1647
|
|
4
|
+
alphadeep_capture/main.py,sha256=OIMqK746UPgfeS0IjINjAixQAx2yXa3QJ5xsPsal3v8,20932
|
|
5
|
+
alphadeep_capture/ui.py,sha256=Gb2iBVAYoNY7k2l48gjGb4pbNy0kDT6E88I7GmZuO-s,12230
|
|
6
|
+
alphadeep_capture-0.1.0.dist-info/METADATA,sha256=8HJ16V_6VroMUTAbSvZTu3wLOIF5tIVWy0ZhTEusFBs,2653
|
|
7
|
+
alphadeep_capture-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
8
|
+
alphadeep_capture-0.1.0.dist-info/entry_points.txt,sha256=sPy98dmyzMO2zau25HiOppzaTJZ2a4k5ADWGPuHx4hQ,66
|
|
9
|
+
alphadeep_capture-0.1.0.dist-info/RECORD,,
|