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.
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()
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ alphadeep-capture = alphadeep_capture.main:main