reconstruct3d 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.
- reconstruct3d/__init__.py +17 -0
- reconstruct3d/api.py +218 -0
- reconstruct3d/bundle_adjust.py +370 -0
- reconstruct3d/calibrate.py +199 -0
- reconstruct3d/cgal_mesh/CMakeLists.txt +22 -0
- reconstruct3d/cgal_mesh/mesh_reconstruct.cpp +312 -0
- reconstruct3d/chunked.py +335 -0
- reconstruct3d/cli.py +448 -0
- reconstruct3d/core.py +515 -0
- reconstruct3d/dense_mvs.py +256 -0
- reconstruct3d/init_sfm.py +151 -0
- reconstruct3d/mesh.py +99 -0
- reconstruct3d/track_sfm.py +253 -0
- reconstruct3d/viewer.py +108 -0
- reconstruct3d-0.1.0.dist-info/METADATA +416 -0
- reconstruct3d-0.1.0.dist-info/RECORD +19 -0
- reconstruct3d-0.1.0.dist-info/WHEEL +4 -0
- reconstruct3d-0.1.0.dist-info/entry_points.txt +2 -0
- reconstruct3d-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
from reconstruct3d.core import (
|
|
7
|
+
SfMDatabase, Features, create_frontend,
|
|
8
|
+
filter_triangulation,
|
|
9
|
+
DB_FILE, MAP_STATE_FILE, TRACKED_CLOUD_FILE,
|
|
10
|
+
)
|
|
11
|
+
from reconstruct3d.init_sfm import export_ply
|
|
12
|
+
from reconstruct3d.bundle_adjust import run_local_ba, fuse_map_points
|
|
13
|
+
|
|
14
|
+
sys.modules['__main__'].Features = Features
|
|
15
|
+
|
|
16
|
+
def triangulate_new_points(k, ref, map3d, db, cam, frontend, min_angle=1.5):
|
|
17
|
+
"""
|
|
18
|
+
Triangula nuevas correspondencias entre el frame k y un frame de referencia.
|
|
19
|
+
Impone restricciones epipolares, finitud espacial y quiralidad (Z > 0).
|
|
20
|
+
"""
|
|
21
|
+
fk, f_ref = db.features[k], db.features[ref]
|
|
22
|
+
matches = frontend.match(fk, f_ref)
|
|
23
|
+
if not matches: return 0
|
|
24
|
+
|
|
25
|
+
pts_k = fk.pts[[m[0] for m in matches]]
|
|
26
|
+
pts_ref = f_ref.pts[[m[1] for m in matches]]
|
|
27
|
+
|
|
28
|
+
E, mask = cv2.findEssentialMat(pts_k, pts_ref, cam.K, cv2.RANSAC, 0.999, 1.0)
|
|
29
|
+
if E is None: return 0
|
|
30
|
+
|
|
31
|
+
valid_m = np.where(mask.ravel() > 0)[0]
|
|
32
|
+
new_matches = []
|
|
33
|
+
|
|
34
|
+
# Filtrar: Solo triangular si NINGUNO de los keypoints existe ya en el mapa 3D
|
|
35
|
+
for idx in valid_m:
|
|
36
|
+
k_idx, ref_idx = matches[idx]
|
|
37
|
+
if k_idx not in map3d['obs'].get(k, {}) and ref_idx not in map3d['obs'].get(ref, {}):
|
|
38
|
+
new_matches.append((k_idx, ref_idx))
|
|
39
|
+
|
|
40
|
+
if not new_matches: return 0
|
|
41
|
+
|
|
42
|
+
pts_k_new = np.float32([fk.pts[m[0]] for m in new_matches])
|
|
43
|
+
pts_ref_new = np.float32([f_ref.pts[m[1]] for m in new_matches])
|
|
44
|
+
colors_new = np.array([fk.colors[m[0]] for m in new_matches])
|
|
45
|
+
|
|
46
|
+
# Matrices de proyección P = K[R|t]
|
|
47
|
+
Pk = cam.K @ map3d['poses'][k][:3, :4]
|
|
48
|
+
Pref = cam.K @ map3d['poses'][ref][:3, :4]
|
|
49
|
+
|
|
50
|
+
pts4d = cv2.triangulatePoints(Pk, Pref, pts_k_new.T, pts_ref_new.T)
|
|
51
|
+
pts3d = (pts4d[:3, :] / pts4d[3, :]).T
|
|
52
|
+
|
|
53
|
+
valid_mask = filter_triangulation(
|
|
54
|
+
pts3d, map3d['poses'][k], map3d['poses'][ref],
|
|
55
|
+
pts_k_new, pts_ref_new, cam.K,
|
|
56
|
+
max_reproj_err=6.0, min_angle_deg=min_angle,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
valid_pts = pts3d[valid_mask]
|
|
60
|
+
valid_colors = colors_new[valid_mask]
|
|
61
|
+
valid_matches = [new_matches[i] for i, v in enumerate(valid_mask) if v]
|
|
62
|
+
|
|
63
|
+
# Inyectar al mapa global
|
|
64
|
+
if len(valid_pts) > 0:
|
|
65
|
+
start_idx = len(map3d['points'])
|
|
66
|
+
map3d['points'] = np.vstack((map3d['points'], valid_pts))
|
|
67
|
+
map3d['colors'] = np.vstack((map3d['colors'], valid_colors))
|
|
68
|
+
|
|
69
|
+
for i, (k_idx, ref_idx) in enumerate(valid_matches):
|
|
70
|
+
pt_idx = start_idx + i
|
|
71
|
+
map3d['obs'][k][k_idx] = pt_idx
|
|
72
|
+
map3d['obs'][ref][ref_idx] = pt_idx
|
|
73
|
+
|
|
74
|
+
return len(valid_pts)
|
|
75
|
+
|
|
76
|
+
def track_frames(out_dir, m_window=3, min_angle=1.5, local_ba=True, ba_every=1, fuse=True):
|
|
77
|
+
db = SfMDatabase()
|
|
78
|
+
db.load(os.path.join(out_dir, DB_FILE))
|
|
79
|
+
map_path = os.path.join(out_dir, MAP_STATE_FILE)
|
|
80
|
+
map3d = np.load(map_path, allow_pickle=True).item()
|
|
81
|
+
# Si el map_state es viejo y no trae 'frontend'/'camera', heredarlo de la DB
|
|
82
|
+
if 'frontend' not in map3d:
|
|
83
|
+
map3d['frontend'] = db.frontend_name
|
|
84
|
+
|
|
85
|
+
cam = db.get_camera() # intrínsecos persistidos (consistentes con extract/init)
|
|
86
|
+
if 'camera' not in map3d:
|
|
87
|
+
map3d['camera'] = cam.to_dict()
|
|
88
|
+
print(f"[*] Frontend activo: '{db.frontend_name}'")
|
|
89
|
+
frontend = create_frontend(db.frontend_name)
|
|
90
|
+
|
|
91
|
+
frames_all = sorted(db.features.keys())
|
|
92
|
+
registered = set(map3d['poses'].keys())
|
|
93
|
+
|
|
94
|
+
# Espaciado real entre frames extraídos (= k_skip usado en core.py). Se
|
|
95
|
+
# deriva de los datos para no tener que mantenerlo sincronizado a mano: la
|
|
96
|
+
# ventana de vecinos para PnP es ±m_window frames extraídos.
|
|
97
|
+
diffs = np.diff(frames_all)
|
|
98
|
+
step = int(np.median(diffs)) if len(diffs) else 1
|
|
99
|
+
window = step * m_window
|
|
100
|
+
print(f"[*] Espaciado detectado: {step} frames | ventana PnP: ±{window} frames ({m_window} vecinos)")
|
|
101
|
+
|
|
102
|
+
ba_msg = f"BA local cada {ba_every} reg." if local_ba else "OFF"
|
|
103
|
+
print(f"[*] Iniciando tracking incremental. Camaras base: {len(registered)} | Puntos: {len(map3d['points'])} | {ba_msg}")
|
|
104
|
+
pts_of = lambda f: db.features[f].pts
|
|
105
|
+
reg_count = 0
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
candidates = [f for f in frames_all if f not in registered and any(abs(f - r) <= window for r in registered)]
|
|
109
|
+
if not candidates:
|
|
110
|
+
print("[!] No hay más candidatos viables en la ventana. Fin del tracking.")
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# Registrar primero el candidato MÁS CERCANO a una cámara ya registrada:
|
|
114
|
+
# baseline corto + mucho solape = PnP bien condicionado y mínimo arrastre
|
|
115
|
+
# de error. Procesarlos por índice de frame registraba saltos largos
|
|
116
|
+
# primero (geometría débil) y propagaba el error frame a frame.
|
|
117
|
+
candidates.sort(key=lambda f: min(abs(f - r) for r in registered))
|
|
118
|
+
|
|
119
|
+
progress_made = False
|
|
120
|
+
|
|
121
|
+
for k in candidates:
|
|
122
|
+
print(f" -> Evaluando frame {k:04d}...", end="\r")
|
|
123
|
+
fk = db.features[k]
|
|
124
|
+
obs_dict = {}
|
|
125
|
+
|
|
126
|
+
ref_frames = [r for r in registered if abs(k - r) <= window]
|
|
127
|
+
for ref in ref_frames:
|
|
128
|
+
f_ref = db.features[ref]
|
|
129
|
+
matches = frontend.match(fk, f_ref)
|
|
130
|
+
if not matches: continue
|
|
131
|
+
|
|
132
|
+
pts_k = fk.pts[[m[0] for m in matches]]
|
|
133
|
+
pts_ref = f_ref.pts[[m[1] for m in matches]]
|
|
134
|
+
E, mask = cv2.findEssentialMat(pts_k, pts_ref, cam.K, cv2.RANSAC, 0.999, 1.0)
|
|
135
|
+
if E is None: continue
|
|
136
|
+
|
|
137
|
+
valid_m = np.where(mask.ravel() > 0)[0]
|
|
138
|
+
for idx in valid_m:
|
|
139
|
+
k_idx, ref_idx = matches[idx]
|
|
140
|
+
if ref_idx in map3d['obs'].get(ref, {}):
|
|
141
|
+
obs_dict[k_idx] = map3d['obs'][ref][ref_idx]
|
|
142
|
+
|
|
143
|
+
if len(obs_dict) < 8:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
k_indices = list(obs_dict.keys())
|
|
147
|
+
p2d_candidate = np.array([fk.pts[idx] for idx in k_indices], dtype=np.float32)
|
|
148
|
+
p3d_candidate = np.array([map3d['points'][obs_dict[idx]] for idx in k_indices], dtype=np.float32)
|
|
149
|
+
|
|
150
|
+
# Semilla: pose de la cámara registrada más cercana. Como ahora
|
|
151
|
+
# registramos de cerca a lejos, es un guess muy bueno y estabiliza PnP.
|
|
152
|
+
r_near = min(registered, key=lambda r: abs(k - r))
|
|
153
|
+
rvec0, _ = cv2.Rodrigues(map3d['poses'][r_near][:3, :3])
|
|
154
|
+
tvec0 = map3d['poses'][r_near][:3, 3].reshape(3, 1).astype(np.float64)
|
|
155
|
+
|
|
156
|
+
succ, rvec, tvec, inliers = cv2.solvePnPRansac(
|
|
157
|
+
p3d_candidate, p2d_candidate, cam.K, None,
|
|
158
|
+
rvec=rvec0.copy(), tvec=tvec0.copy(), useExtrinsicGuess=True,
|
|
159
|
+
reprojectionError=3.0, iterationsCount=100, confidence=0.999,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if succ and inliers is not None and len(inliers) > 8:
|
|
163
|
+
# Refinar la pose con TODOS los inliers (LM): RANSAC solo ajusta
|
|
164
|
+
# sobre la muestra mínima, esto minimiza la reproyección global.
|
|
165
|
+
inl = inliers.ravel()
|
|
166
|
+
rvec, tvec = cv2.solvePnPRefineLM(
|
|
167
|
+
p3d_candidate[inl], p2d_candidate[inl], cam.K, None, rvec, tvec)
|
|
168
|
+
R, _ = cv2.Rodrigues(rvec)
|
|
169
|
+
map3d['poses'][k] = np.vstack((np.hstack((R, tvec)), [0,0,0,1]))
|
|
170
|
+
registered.add(k)
|
|
171
|
+
|
|
172
|
+
if k not in map3d['obs']: map3d['obs'][k] = {}
|
|
173
|
+
for i_inlier in inliers.ravel():
|
|
174
|
+
idx_2d = k_indices[i_inlier]
|
|
175
|
+
map3d['obs'][k][idx_2d] = obs_dict[idx_2d]
|
|
176
|
+
|
|
177
|
+
new_pts_total = 0
|
|
178
|
+
for ref in ref_frames:
|
|
179
|
+
new_pts_total += triangulate_new_points(k, ref, map3d, db, cam, frontend, min_angle=min_angle)
|
|
180
|
+
|
|
181
|
+
# BA LOCAL: re-optimiza ventana (k + vecinos) + sus puntos justo
|
|
182
|
+
# tras insertarlos, antes de que el error se propague al siguiente
|
|
183
|
+
# frame. Fija ancla + cámaras externas (ataca #2 arrastre y #4 lazo).
|
|
184
|
+
reg_count += 1
|
|
185
|
+
if local_ba and reg_count % ba_every == 0:
|
|
186
|
+
win = [r for r in registered if abs(k - r) <= window]
|
|
187
|
+
run_local_ba(map3d, pts_of, cam.K, win, max_nfev=20)
|
|
188
|
+
|
|
189
|
+
print(f"[+] Frame {k:04d} registrado | PnP Inliers: {len(inliers):03d} | Nuevos Puntos 3D: {new_pts_total}")
|
|
190
|
+
|
|
191
|
+
np.save(map_path, map3d)
|
|
192
|
+
|
|
193
|
+
# --- FASE DE EXPORTACIÓN CON CENTRADO ESPACIAL ---
|
|
194
|
+
# Calcular la mediana ignorando potenciales (aunque improbables) NaNs residuales
|
|
195
|
+
valid_global = np.isfinite(map3d['points']).all(axis=1)
|
|
196
|
+
if np.sum(valid_global) > 0:
|
|
197
|
+
median_pt = np.median(map3d['points'][valid_global], axis=0)
|
|
198
|
+
else:
|
|
199
|
+
median_pt = np.zeros(3)
|
|
200
|
+
|
|
201
|
+
centered_points = map3d['points'][valid_global] - median_pt
|
|
202
|
+
valid_colors = map3d['colors'][valid_global]
|
|
203
|
+
|
|
204
|
+
cam_centers = [-pose[:3, :3].T @ pose[:3, 3:] for pose in map3d['poses'].values()]
|
|
205
|
+
centered_cams = [c.ravel() - median_pt for c in cam_centers]
|
|
206
|
+
|
|
207
|
+
export_ply(os.path.join(out_dir, TRACKED_CLOUD_FILE), centered_points, valid_colors, centered_cams)
|
|
208
|
+
|
|
209
|
+
progress_made = True
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not progress_made:
|
|
213
|
+
print(f"\n[!] Bucle estancado. No se lograron registrar más cámaras de los {len(candidates)} candidatos.")
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# FUSIÓN DE PUNTOS (#1 asociación de datos): une duplicados del mismo punto
|
|
217
|
+
# físico triangulado por separado en frames distintos (tracks disjuntos),
|
|
218
|
+
# acortando la nube y reforzando restricciones. Conservador (vóxel fino).
|
|
219
|
+
if fuse:
|
|
220
|
+
valid = np.isfinite(map3d['points']).all(axis=1)
|
|
221
|
+
if valid.sum() > 0:
|
|
222
|
+
scale = float(np.median(np.linalg.norm(
|
|
223
|
+
map3d['points'][valid] - np.median(map3d['points'][valid], axis=0), axis=1)))
|
|
224
|
+
voxel = scale / 800.0
|
|
225
|
+
n_fused = fuse_map_points(map3d, voxel)
|
|
226
|
+
print(f"[*] Fusión de puntos: {n_fused} duplicados unidos (vóxel={voxel:.4f}).")
|
|
227
|
+
np.save(map_path, map3d)
|
|
228
|
+
valid = np.isfinite(map3d['points']).all(axis=1)
|
|
229
|
+
median_pt = np.median(map3d['points'][valid], axis=0)
|
|
230
|
+
cam_centers = [-(p[:3, :3].T @ p[:3, 3:]).ravel() - median_pt for p in map3d['poses'].values()]
|
|
231
|
+
export_ply(os.path.join(out_dir, TRACKED_CLOUD_FILE),
|
|
232
|
+
map3d['points'][valid] - median_pt, map3d['colors'][valid], cam_centers)
|
|
233
|
+
|
|
234
|
+
print(f"\nTracking finalizado. Cámaras: {len(map3d['poses'])} | Puntos totales: {len(map3d['points'])}")
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
parser = argparse.ArgumentParser(description="SfM track: incremental camera registration")
|
|
238
|
+
parser.add_argument("--out", required=True, help="Output dir from core/init runs.")
|
|
239
|
+
parser.add_argument("--m-window", type=int, default=3,
|
|
240
|
+
help="Ventana PnP: registrar usando ±m frames extraídos como referencia "
|
|
241
|
+
"(default: 3). El espaciado real (k_skip) se autodetecta de los datos.")
|
|
242
|
+
parser.add_argument("--min-angle", type=float, default=1.5,
|
|
243
|
+
help="Ángulo mínimo de triangulación en grados (default: 1.5). Más alto "
|
|
244
|
+
"descarta puntos de baseline corta que generan ruido de escala.")
|
|
245
|
+
parser.add_argument("--no-local-ba", action="store_true",
|
|
246
|
+
help="Desactiva el Bundle Adjustment local por ventana deslizante.")
|
|
247
|
+
parser.add_argument("--ba-every", type=int, default=1,
|
|
248
|
+
help="Ejecutar BA local cada N cámaras registradas (default: 1 = siempre).")
|
|
249
|
+
parser.add_argument("--no-fuse", action="store_true",
|
|
250
|
+
help="Desactiva la fusión de puntos duplicados al final del tracking.")
|
|
251
|
+
args = parser.parse_args()
|
|
252
|
+
track_frames(args.out, m_window=args.m_window, min_angle=args.min_angle,
|
|
253
|
+
local_ba=not args.no_local_ba, ba_every=args.ba_every, fuse=not args.no_fuse)
|
reconstruct3d/viewer.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
from reconstruct3d.core import CameraConfig, MAP_STATE_FILE
|
|
7
|
+
|
|
8
|
+
def main(video_path, out_dir):
|
|
9
|
+
map3d = np.load(os.path.join(out_dir, MAP_STATE_FILE), allow_pickle=True).item()
|
|
10
|
+
cam = CameraConfig.from_dict(map3d.get('camera')) # intrínsecos del map_state
|
|
11
|
+
|
|
12
|
+
points3d = map3d['points']
|
|
13
|
+
colors3d = map3d['colors']
|
|
14
|
+
|
|
15
|
+
cap = cv2.VideoCapture(video_path)
|
|
16
|
+
registered_frames = sorted(map3d['poses'].keys())
|
|
17
|
+
num_frames = len(registered_frames)
|
|
18
|
+
|
|
19
|
+
window_name = "POV Proyeccion 3D"
|
|
20
|
+
cv2.namedWindow(window_name)
|
|
21
|
+
|
|
22
|
+
# Trackbars para Opacidad y Línea de tiempo
|
|
23
|
+
cv2.createTrackbar("Fondo %", window_name, 50, 100, lambda x: None)
|
|
24
|
+
cv2.createTrackbar("Frame", window_name, 0, max(0, num_frames - 1), lambda x: None)
|
|
25
|
+
|
|
26
|
+
# En Windows la ventana y sus trackbars no quedan registradas hasta que el
|
|
27
|
+
# event loop de HighGUI hace un ciclo; sin este "pump" inicial el primer
|
|
28
|
+
# getTrackbarPos lanza "NULL window".
|
|
29
|
+
cv2.imshow(window_name, np.zeros((10, 10, 3), dtype=np.uint8))
|
|
30
|
+
cv2.waitKey(1)
|
|
31
|
+
|
|
32
|
+
# Variables de estado para evitar re-renderizados innecesarios
|
|
33
|
+
last_idx = -1
|
|
34
|
+
last_alpha = -1
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
# 1. Leer estado actual de los sliders
|
|
38
|
+
idx = cv2.getTrackbarPos("Frame", window_name)
|
|
39
|
+
alpha_val = cv2.getTrackbarPos("Fondo %", window_name)
|
|
40
|
+
|
|
41
|
+
# 2. Solo procesar si el usuario movió algún control
|
|
42
|
+
if idx != last_idx or alpha_val != last_alpha:
|
|
43
|
+
f_idx = registered_frames[idx]
|
|
44
|
+
alpha = alpha_val / 100.0
|
|
45
|
+
|
|
46
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, f_idx)
|
|
47
|
+
ret, frame = cap.read()
|
|
48
|
+
if not ret:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
frame = cam.preprocess(frame)
|
|
52
|
+
h, w = frame.shape[:2]
|
|
53
|
+
|
|
54
|
+
pose = map3d['poses'][f_idx]
|
|
55
|
+
R, t = pose[:3, :3], pose[:3, 3]
|
|
56
|
+
|
|
57
|
+
# Transformación isométrica al espacio de cámara local
|
|
58
|
+
pts_c = (R @ points3d.T).T + t
|
|
59
|
+
|
|
60
|
+
# Restricción de quiralidad (Z > 0)
|
|
61
|
+
mask_z = pts_c[:, 2] > 0
|
|
62
|
+
pts_c_valid = pts_c[mask_z]
|
|
63
|
+
colors_valid = colors3d[mask_z]
|
|
64
|
+
|
|
65
|
+
# Composición de fondo
|
|
66
|
+
black_bg = np.zeros_like(frame)
|
|
67
|
+
view = cv2.addWeighted(frame, alpha, black_bg, 1 - alpha, 0)
|
|
68
|
+
|
|
69
|
+
if len(pts_c_valid) > 0:
|
|
70
|
+
# Proyección en plano imagen
|
|
71
|
+
fx, fy = cam.K[0, 0], cam.K[1, 1]
|
|
72
|
+
cx, cy = cam.K[0, 2], cam.K[1, 2]
|
|
73
|
+
|
|
74
|
+
u = (fx * (pts_c_valid[:, 0] / pts_c_valid[:, 2]) + cx).astype(int)
|
|
75
|
+
v = (fy * (pts_c_valid[:, 1] / pts_c_valid[:, 2]) + cy).astype(int)
|
|
76
|
+
|
|
77
|
+
# Filtrado de límites del sensor
|
|
78
|
+
valid_img = (u >= 0) & (u < w) & (v >= 0) & (v < h)
|
|
79
|
+
u, v = u[valid_img], v[valid_img]
|
|
80
|
+
final_colors = colors_valid[valid_img]
|
|
81
|
+
|
|
82
|
+
# Renderizado de inliers con color original
|
|
83
|
+
for pt_u, pt_v, color in zip(u, v, final_colors):
|
|
84
|
+
c = (int(color[0]), int(color[1]), int(color[2]))
|
|
85
|
+
cv2.circle(view, (pt_u, pt_v), 2, c, -1)
|
|
86
|
+
|
|
87
|
+
# Actualizar título de la ventana con el frame real
|
|
88
|
+
cv2.setWindowTitle(window_name, f"POV Proyeccion 3D | Frame de Video: {f_idx}")
|
|
89
|
+
cv2.imshow(window_name, view)
|
|
90
|
+
|
|
91
|
+
# Guardar estado
|
|
92
|
+
last_idx = idx
|
|
93
|
+
last_alpha = alpha_val
|
|
94
|
+
|
|
95
|
+
# Mantener el bucle vivo y escuchar la tecla 'q' o 'ESC' para salir
|
|
96
|
+
key = cv2.waitKey(30) & 0xFF
|
|
97
|
+
if key == ord('q') or key == 27:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
cap.release()
|
|
101
|
+
cv2.destroyAllWindows()
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
parser = argparse.ArgumentParser(description="POV viewer over reconstructed map")
|
|
105
|
+
parser.add_argument("video", help="Path to input video (mp4)")
|
|
106
|
+
parser.add_argument("--out", required=True, help="Output dir containing map_state.npy (from track_sfm.py).")
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
main(args.video, args.out)
|