stereo-charuco-pipeline 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.
- recorder/__init__.py +90 -0
- recorder/auto_calibrate.py +493 -0
- recorder/calibration_ui.py +1106 -0
- recorder/calibration_ui_advanced.py +1013 -0
- recorder/camera.py +51 -0
- recorder/cli.py +122 -0
- recorder/config.py +75 -0
- recorder/configs/default.yaml +38 -0
- recorder/ffmpeg.py +137 -0
- recorder/paths.py +87 -0
- recorder/pipeline_ui.py +1838 -0
- recorder/project_manager.py +329 -0
- recorder/smart_recorder.py +478 -0
- recorder/ui.py +136 -0
- recorder/viz_3d.py +220 -0
- stereo_charuco_pipeline-0.1.0.dist-info/METADATA +10 -0
- stereo_charuco_pipeline-0.1.0.dist-info/RECORD +19 -0
- stereo_charuco_pipeline-0.1.0.dist-info/WHEEL +4 -0
- stereo_charuco_pipeline-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Manager UI: Select or create a project before launching the pipeline.
|
|
3
|
+
|
|
4
|
+
Shows existing projects under the projects base directory and allows
|
|
5
|
+
creating new timestamped project folders with the correct scaffold.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import tkinter as tk
|
|
16
|
+
from tkinter import ttk, messagebox
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Theme constants (match calibration_ui / pipeline_ui)
|
|
21
|
+
BG = "#2b2b2b"
|
|
22
|
+
BG_PANEL = "#333333"
|
|
23
|
+
BG_LIST = "#1a1a1a"
|
|
24
|
+
FG = "#e0e0e0"
|
|
25
|
+
FG_DIM = "#888888"
|
|
26
|
+
ACCENT = "#4CAF50"
|
|
27
|
+
ACCENT_BLUE = "#2196F3"
|
|
28
|
+
ACCENT_ORANGE = "#FF9800"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ProjectContext:
|
|
33
|
+
"""Carries dynamic project paths through the unified workflow."""
|
|
34
|
+
project_dir: Path
|
|
35
|
+
project_name: str
|
|
36
|
+
is_new: bool
|
|
37
|
+
needs_calibration: bool # True if calibration/intrinsic/port_1.mp4 missing
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class _ProjectInfo:
|
|
42
|
+
"""Internal: scanned project metadata for display."""
|
|
43
|
+
name: str
|
|
44
|
+
path: Path
|
|
45
|
+
has_calib_videos: bool
|
|
46
|
+
calibrated: bool
|
|
47
|
+
num_recordings: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProjectManagerUI(tk.Tk):
|
|
51
|
+
"""Project selection/creation dialog."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, projects_base: Path):
|
|
54
|
+
super().__init__()
|
|
55
|
+
|
|
56
|
+
self.projects_base = projects_base
|
|
57
|
+
self.projects_base.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
self.result: Optional[ProjectContext] = None
|
|
60
|
+
self._projects: list[_ProjectInfo] = []
|
|
61
|
+
|
|
62
|
+
self.title("Stereo Pipeline - Project Manager")
|
|
63
|
+
self.geometry("700x450")
|
|
64
|
+
self.configure(bg=BG)
|
|
65
|
+
self.resizable(False, False)
|
|
66
|
+
|
|
67
|
+
self._build_ui()
|
|
68
|
+
self._scan_and_display()
|
|
69
|
+
|
|
70
|
+
# Center window on screen
|
|
71
|
+
self.update_idletasks()
|
|
72
|
+
x = (self.winfo_screenwidth() - 700) // 2
|
|
73
|
+
y = (self.winfo_screenheight() - 450) // 2
|
|
74
|
+
self.geometry(f"700x450+{x}+{y}")
|
|
75
|
+
|
|
76
|
+
# ======================================================================
|
|
77
|
+
# UI
|
|
78
|
+
# ======================================================================
|
|
79
|
+
|
|
80
|
+
def _build_ui(self):
|
|
81
|
+
# Title
|
|
82
|
+
tk.Label(
|
|
83
|
+
self, text="Project Manager",
|
|
84
|
+
font=("Segoe UI", 16, "bold"), fg=FG, bg=BG,
|
|
85
|
+
).pack(pady=(15, 5))
|
|
86
|
+
|
|
87
|
+
tk.Label(
|
|
88
|
+
self, text=f"Base: {self.projects_base}",
|
|
89
|
+
font=("Segoe UI", 9), fg=FG_DIM, bg=BG,
|
|
90
|
+
).pack(pady=(0, 10))
|
|
91
|
+
|
|
92
|
+
# Main content: list + details side by side
|
|
93
|
+
content = tk.Frame(self, bg=BG)
|
|
94
|
+
content.pack(fill=tk.BOTH, expand=True, padx=15)
|
|
95
|
+
|
|
96
|
+
# ── Left: project list ──
|
|
97
|
+
left = tk.Frame(content, bg=BG)
|
|
98
|
+
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 8))
|
|
99
|
+
|
|
100
|
+
tk.Label(
|
|
101
|
+
left, text="Projects", font=("Segoe UI", 10, "bold"),
|
|
102
|
+
fg=FG, bg=BG, anchor="w",
|
|
103
|
+
).pack(fill=tk.X)
|
|
104
|
+
|
|
105
|
+
list_frame = tk.Frame(left, bg=BG_LIST)
|
|
106
|
+
list_frame.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
|
|
107
|
+
|
|
108
|
+
self._listbox = tk.Listbox(
|
|
109
|
+
list_frame, bg=BG_LIST, fg=FG,
|
|
110
|
+
font=("Consolas", 10), selectmode=tk.SINGLE,
|
|
111
|
+
selectbackground=ACCENT_BLUE, selectforeground="white",
|
|
112
|
+
highlightthickness=0, bd=0,
|
|
113
|
+
)
|
|
114
|
+
scrollbar = ttk.Scrollbar(list_frame, orient="vertical",
|
|
115
|
+
command=self._listbox.yview)
|
|
116
|
+
self._listbox.configure(yscrollcommand=scrollbar.set)
|
|
117
|
+
|
|
118
|
+
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
119
|
+
self._listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
120
|
+
self._listbox.bind("<<ListboxSelect>>", self._on_select)
|
|
121
|
+
self._listbox.bind("<Double-Button-1>", lambda e: self._on_open())
|
|
122
|
+
|
|
123
|
+
# ── Right: details panel ──
|
|
124
|
+
right = tk.Frame(content, bg=BG_PANEL, width=250)
|
|
125
|
+
right.pack(side=tk.RIGHT, fill=tk.Y)
|
|
126
|
+
right.pack_propagate(False)
|
|
127
|
+
|
|
128
|
+
tk.Label(
|
|
129
|
+
right, text="Details", font=("Segoe UI", 10, "bold"),
|
|
130
|
+
fg=FG, bg=BG_PANEL, anchor="w",
|
|
131
|
+
).pack(fill=tk.X, padx=10, pady=(10, 5))
|
|
132
|
+
|
|
133
|
+
self._detail_name = tk.Label(
|
|
134
|
+
right, text="(select a project)", font=("Segoe UI", 10),
|
|
135
|
+
fg=FG_DIM, bg=BG_PANEL, anchor="w", wraplength=220,
|
|
136
|
+
)
|
|
137
|
+
self._detail_name.pack(fill=tk.X, padx=10, pady=2)
|
|
138
|
+
|
|
139
|
+
self._detail_calib = tk.Label(
|
|
140
|
+
right, text="", font=("Segoe UI", 9),
|
|
141
|
+
fg=FG_DIM, bg=BG_PANEL, anchor="w",
|
|
142
|
+
)
|
|
143
|
+
self._detail_calib.pack(fill=tk.X, padx=10, pady=2)
|
|
144
|
+
|
|
145
|
+
self._detail_status = tk.Label(
|
|
146
|
+
right, text="", font=("Segoe UI", 9),
|
|
147
|
+
fg=FG_DIM, bg=BG_PANEL, anchor="w",
|
|
148
|
+
)
|
|
149
|
+
self._detail_status.pack(fill=tk.X, padx=10, pady=2)
|
|
150
|
+
|
|
151
|
+
self._detail_recordings = tk.Label(
|
|
152
|
+
right, text="", font=("Segoe UI", 9),
|
|
153
|
+
fg=FG_DIM, bg=BG_PANEL, anchor="w",
|
|
154
|
+
)
|
|
155
|
+
self._detail_recordings.pack(fill=tk.X, padx=10, pady=2)
|
|
156
|
+
|
|
157
|
+
self._detail_path = tk.Label(
|
|
158
|
+
right, text="", font=("Segoe UI", 8),
|
|
159
|
+
fg=FG_DIM, bg=BG_PANEL, anchor="w", wraplength=220,
|
|
160
|
+
)
|
|
161
|
+
self._detail_path.pack(fill=tk.X, padx=10, pady=(10, 2))
|
|
162
|
+
|
|
163
|
+
# ── Bottom: buttons ──
|
|
164
|
+
btn_frame = tk.Frame(self, bg=BG)
|
|
165
|
+
btn_frame.pack(fill=tk.X, padx=15, pady=15)
|
|
166
|
+
|
|
167
|
+
self.btn_new = tk.Button(
|
|
168
|
+
btn_frame, text="New Project",
|
|
169
|
+
font=("Segoe UI", 10, "bold"),
|
|
170
|
+
bg=ACCENT, fg="white", activebackground="#388E3C",
|
|
171
|
+
relief="flat", padx=20, pady=6,
|
|
172
|
+
command=self._on_new,
|
|
173
|
+
)
|
|
174
|
+
self.btn_new.pack(side=tk.LEFT)
|
|
175
|
+
|
|
176
|
+
self.btn_open = tk.Button(
|
|
177
|
+
btn_frame, text="Open Project",
|
|
178
|
+
font=("Segoe UI", 10, "bold"),
|
|
179
|
+
bg=ACCENT_BLUE, fg="white", activebackground="#1565C0",
|
|
180
|
+
relief="flat", padx=20, pady=6,
|
|
181
|
+
state=tk.DISABLED,
|
|
182
|
+
command=self._on_open,
|
|
183
|
+
)
|
|
184
|
+
self.btn_open.pack(side=tk.LEFT, padx=10)
|
|
185
|
+
|
|
186
|
+
self.btn_cancel = tk.Button(
|
|
187
|
+
btn_frame, text="Cancel",
|
|
188
|
+
font=("Segoe UI", 10),
|
|
189
|
+
bg="#555", fg=FG,
|
|
190
|
+
relief="flat", padx=15, pady=6,
|
|
191
|
+
command=self.destroy,
|
|
192
|
+
)
|
|
193
|
+
self.btn_cancel.pack(side=tk.RIGHT)
|
|
194
|
+
|
|
195
|
+
# ======================================================================
|
|
196
|
+
# Scanning
|
|
197
|
+
# ======================================================================
|
|
198
|
+
|
|
199
|
+
def _scan_projects(self) -> list[_ProjectInfo]:
|
|
200
|
+
"""Scan for valid project directories."""
|
|
201
|
+
found: list[_ProjectInfo] = []
|
|
202
|
+
|
|
203
|
+
def _check_dir(d: Path):
|
|
204
|
+
if not d.is_dir() or d.name.startswith((".", "__")):
|
|
205
|
+
return
|
|
206
|
+
has_calib = (d / "calibration").exists()
|
|
207
|
+
has_rec = (d / "recordings").exists()
|
|
208
|
+
if not has_calib and not has_rec and not d.name.startswith("project_"):
|
|
209
|
+
return
|
|
210
|
+
has_calib_videos = (
|
|
211
|
+
(d / "calibration" / "intrinsic" / "port_1.mp4").exists()
|
|
212
|
+
and (d / "calibration" / "intrinsic" / "port_2.mp4").exists()
|
|
213
|
+
)
|
|
214
|
+
calibrated = (d / "camera_array.toml").exists()
|
|
215
|
+
num_rec = 0
|
|
216
|
+
rec_dir = d / "recordings"
|
|
217
|
+
if rec_dir.exists():
|
|
218
|
+
num_rec = sum(1 for rd in rec_dir.iterdir()
|
|
219
|
+
if rd.is_dir() and rd.name.startswith("session_"))
|
|
220
|
+
found.append(_ProjectInfo(
|
|
221
|
+
name=d.name,
|
|
222
|
+
path=d,
|
|
223
|
+
has_calib_videos=has_calib_videos,
|
|
224
|
+
calibrated=calibrated,
|
|
225
|
+
num_recordings=num_rec,
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
# Scan 1 level deep
|
|
229
|
+
for item in sorted(self.projects_base.iterdir()):
|
|
230
|
+
_check_dir(item)
|
|
231
|
+
# Scan 2 levels deep (e.g. caliscope/caliscope_project)
|
|
232
|
+
if item.is_dir() and not item.name.startswith((".", "__")):
|
|
233
|
+
for sub in sorted(item.iterdir()):
|
|
234
|
+
_check_dir(sub)
|
|
235
|
+
|
|
236
|
+
return found
|
|
237
|
+
|
|
238
|
+
def _scan_and_display(self):
|
|
239
|
+
"""Scan projects and update the listbox."""
|
|
240
|
+
self._projects = self._scan_projects()
|
|
241
|
+
self._listbox.delete(0, tk.END)
|
|
242
|
+
|
|
243
|
+
for p in self._projects:
|
|
244
|
+
status = ""
|
|
245
|
+
if p.calibrated:
|
|
246
|
+
status = " [calibrated]"
|
|
247
|
+
elif p.has_calib_videos:
|
|
248
|
+
status = " [has videos]"
|
|
249
|
+
else:
|
|
250
|
+
status = " [new]"
|
|
251
|
+
self._listbox.insert(tk.END, f" {p.name}{status}")
|
|
252
|
+
|
|
253
|
+
if not self._projects:
|
|
254
|
+
self._detail_name.config(text="No projects found.\nClick 'New Project' to create one.")
|
|
255
|
+
|
|
256
|
+
# ======================================================================
|
|
257
|
+
# Event handlers
|
|
258
|
+
# ======================================================================
|
|
259
|
+
|
|
260
|
+
def _on_select(self, event=None):
|
|
261
|
+
"""Handle project list selection."""
|
|
262
|
+
sel = self._listbox.curselection()
|
|
263
|
+
if not sel:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
idx = sel[0]
|
|
267
|
+
p = self._projects[idx]
|
|
268
|
+
|
|
269
|
+
self.btn_open.config(state=tk.NORMAL)
|
|
270
|
+
|
|
271
|
+
self._detail_name.config(text=p.name, fg=FG)
|
|
272
|
+
|
|
273
|
+
if p.calibrated:
|
|
274
|
+
self._detail_calib.config(text="Calibration: Complete", fg=ACCENT)
|
|
275
|
+
elif p.has_calib_videos:
|
|
276
|
+
self._detail_calib.config(text="Calibration: Videos ready", fg=ACCENT_ORANGE)
|
|
277
|
+
else:
|
|
278
|
+
self._detail_calib.config(text="Calibration: Not started", fg=FG_DIM)
|
|
279
|
+
|
|
280
|
+
if p.calibrated:
|
|
281
|
+
self._detail_status.config(text="Status: Ready to record & reconstruct", fg=ACCENT)
|
|
282
|
+
elif p.has_calib_videos:
|
|
283
|
+
self._detail_status.config(text="Status: Needs auto-calibration", fg=ACCENT_ORANGE)
|
|
284
|
+
else:
|
|
285
|
+
self._detail_status.config(text="Status: Needs calibration videos", fg=FG_DIM)
|
|
286
|
+
|
|
287
|
+
self._detail_recordings.config(
|
|
288
|
+
text=f"Recordings: {p.num_recordings}",
|
|
289
|
+
fg=FG if p.num_recordings > 0 else FG_DIM,
|
|
290
|
+
)
|
|
291
|
+
self._detail_path.config(text=f"Path: {p.path}")
|
|
292
|
+
|
|
293
|
+
def _on_new(self):
|
|
294
|
+
"""Create a new project."""
|
|
295
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
296
|
+
project_name = f"project_{ts}"
|
|
297
|
+
project_dir = self.projects_base / project_name
|
|
298
|
+
|
|
299
|
+
# Create scaffold
|
|
300
|
+
(project_dir / "calibration" / "intrinsic").mkdir(parents=True, exist_ok=True)
|
|
301
|
+
(project_dir / "calibration" / "extrinsic").mkdir(parents=True, exist_ok=True)
|
|
302
|
+
(project_dir / "recordings").mkdir(parents=True, exist_ok=True)
|
|
303
|
+
(project_dir / "raw_output").mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
logger.info(f"Created new project: {project_dir}")
|
|
306
|
+
|
|
307
|
+
self.result = ProjectContext(
|
|
308
|
+
project_dir=project_dir,
|
|
309
|
+
project_name=project_name,
|
|
310
|
+
is_new=True,
|
|
311
|
+
needs_calibration=True,
|
|
312
|
+
)
|
|
313
|
+
self.destroy()
|
|
314
|
+
|
|
315
|
+
def _on_open(self):
|
|
316
|
+
"""Open the selected project."""
|
|
317
|
+
sel = self._listbox.curselection()
|
|
318
|
+
if not sel:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
p = self._projects[sel[0]]
|
|
322
|
+
|
|
323
|
+
self.result = ProjectContext(
|
|
324
|
+
project_dir=p.path,
|
|
325
|
+
project_name=p.name,
|
|
326
|
+
is_new=False,
|
|
327
|
+
needs_calibration=not p.has_calib_videos,
|
|
328
|
+
)
|
|
329
|
+
self.destroy()
|