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
reconstruct3d/chunked.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Reconstrucción por fragmentos (chunks) con solape + fusión de nubes.
|
|
2
|
+
|
|
3
|
+
Para videos largos, reconstruir todo de una vez es lento y acumula deriva. Esta
|
|
4
|
+
estrategia parte el video en fragmentos que se SOLAPAN, reconstruye cada uno de
|
|
5
|
+
forma INDEPENDIENTE (y en PARALELO, ya que no comparten estado), y luego alinea
|
|
6
|
+
las sub-nubes usando los frames compartidos en el solape.
|
|
7
|
+
|
|
8
|
+
Idea de la alineación (Sim(3) por Umeyama):
|
|
9
|
+
Dos chunks contiguos extraen exactamente los mismos frames absolutos en su zona
|
|
10
|
+
de solape (todos muestrean la MISMA rejilla global de k_skip). Cada chunk estima
|
|
11
|
+
la pose de esos frames en su propio sistema de coordenadas y a su propia escala
|
|
12
|
+
arbitraria. Los CENTROS DE CÁMARA de los frames compartidos son entonces
|
|
13
|
+
correspondencias 3D-3D directas entre el sistema del chunk y el sistema global
|
|
14
|
+
-> resolvemos una similaridad (escala s, rotación R, traslación t) con el método
|
|
15
|
+
de Umeyama y la aplicamos a los puntos y cámaras del chunk para llevarlos al
|
|
16
|
+
marco global. Encadenando chunk a chunk, todo queda en un único marco.
|
|
17
|
+
|
|
18
|
+
Salida: `merged_cloud.ply` (nube global) y `merged_state.npy` (poses globales +
|
|
19
|
+
puntos + cámara), compatibles con `dense` y `view`.
|
|
20
|
+
|
|
21
|
+
Uso:
|
|
22
|
+
uv run python chunked.py data/video.mp4 --chunk 80 --overlap 20 --chunk-jobs 4
|
|
23
|
+
uv run python pipeline.py chunked data/video.mp4 --chunk 80 --overlap 20
|
|
24
|
+
"""
|
|
25
|
+
import os
|
|
26
|
+
import argparse
|
|
27
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
28
|
+
|
|
29
|
+
import cv2
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
from reconstruct3d.core import MAP_STATE_FILE, CameraConfig, DEFAULT_FRONTEND, resolve_workers
|
|
33
|
+
from reconstruct3d.init_sfm import export_ply
|
|
34
|
+
|
|
35
|
+
MERGED_CLOUD_FILE = "merged_cloud.ply"
|
|
36
|
+
MERGED_STATE_FILE = "merged_state.npy"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------------- #
|
|
40
|
+
# Alineación de similaridad (Umeyama) #
|
|
41
|
+
# --------------------------------------------------------------------------- #
|
|
42
|
+
def umeyama(src, dst, with_scale=True):
|
|
43
|
+
"""Similaridad que mapea `src` -> `dst`: dst ≈ s·R·src + t.
|
|
44
|
+
|
|
45
|
+
src, dst: (N,3) correspondencias. Devuelve (s, R(3x3), t(3,)).
|
|
46
|
+
Método de Umeyama (1991), robusto a reflexiones vía corrección del
|
|
47
|
+
determinante.
|
|
48
|
+
"""
|
|
49
|
+
src = np.asarray(src, dtype=np.float64)
|
|
50
|
+
dst = np.asarray(dst, dtype=np.float64)
|
|
51
|
+
n = src.shape[0]
|
|
52
|
+
mu_s, mu_d = src.mean(0), dst.mean(0)
|
|
53
|
+
sc, dc = src - mu_s, dst - mu_d
|
|
54
|
+
cov = (dc.T @ sc) / n
|
|
55
|
+
U, D, Vt = np.linalg.svd(cov)
|
|
56
|
+
S = np.eye(3)
|
|
57
|
+
if np.linalg.det(U) * np.linalg.det(Vt) < 0:
|
|
58
|
+
S[2, 2] = -1.0
|
|
59
|
+
R = U @ S @ Vt
|
|
60
|
+
if with_scale:
|
|
61
|
+
var_s = (sc ** 2).sum() / n
|
|
62
|
+
s = float((D * np.diag(S)).sum() / var_s) if var_s > 1e-12 else 1.0
|
|
63
|
+
else:
|
|
64
|
+
s = 1.0
|
|
65
|
+
t = mu_d - s * R @ mu_s
|
|
66
|
+
return s, R, t
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def apply_sim3(pts, s, R, t):
|
|
70
|
+
"""Aplica dst = s·R·pts + t a (N,3)."""
|
|
71
|
+
return (s * (R @ np.asarray(pts, dtype=np.float64).T).T) + t
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _camera_centers(poses):
|
|
75
|
+
"""{frame: pose 4x4 world->cam} -> {frame: centro de cámara (3,) en mundo}."""
|
|
76
|
+
return {f: (-(P[:3, :3].T @ P[:3, 3])).ravel() for f, P in poses.items()}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------------------------------- #
|
|
80
|
+
# Particionado en chunks #
|
|
81
|
+
# --------------------------------------------------------------------------- #
|
|
82
|
+
def plan_chunks(n_frames, k_skip, chunk, overlap):
|
|
83
|
+
"""Parte la rejilla global de frames muestreados en chunks solapados.
|
|
84
|
+
|
|
85
|
+
Todos los chunks muestrean la MISMA rejilla (múltiplos de k_skip desde 0),
|
|
86
|
+
así que los frames del solape tienen índices absolutos idénticos entre chunks
|
|
87
|
+
contiguos (requisito para alinear por centros de cámara compartidos).
|
|
88
|
+
|
|
89
|
+
Devuelve lista de (start_frame, end_frame) en frames absolutos.
|
|
90
|
+
"""
|
|
91
|
+
if overlap >= chunk:
|
|
92
|
+
raise ValueError(f"overlap ({overlap}) debe ser menor que chunk ({chunk}).")
|
|
93
|
+
grid = list(range(0, n_frames, k_skip))
|
|
94
|
+
step = chunk - overlap
|
|
95
|
+
spans = []
|
|
96
|
+
a = 0
|
|
97
|
+
while a < len(grid):
|
|
98
|
+
b = min(a + chunk, len(grid))
|
|
99
|
+
start_frame = grid[a]
|
|
100
|
+
end_frame = grid[b - 1] + 1 # exclusivo; +1 para incluir el último
|
|
101
|
+
spans.append((start_frame, end_frame))
|
|
102
|
+
if b >= len(grid):
|
|
103
|
+
break
|
|
104
|
+
a += step
|
|
105
|
+
return spans
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --------------------------------------------------------------------------- #
|
|
109
|
+
# Worker de chunk (corre en su propio proceso) #
|
|
110
|
+
# --------------------------------------------------------------------------- #
|
|
111
|
+
def _run_chunk_worker(cfg):
|
|
112
|
+
"""Reconstruye un chunk completo (extract->init->track->[ba]) en su out_dir.
|
|
113
|
+
Debe ser top-level y con `cfg` picklable para ProcessPoolExecutor (spawn)."""
|
|
114
|
+
from reconstruct3d.core import process_core, CameraConfig
|
|
115
|
+
from reconstruct3d.init_sfm import init_reconstruction
|
|
116
|
+
from reconstruct3d.track_sfm import track_frames
|
|
117
|
+
|
|
118
|
+
out = cfg["out"]
|
|
119
|
+
os.makedirs(out, exist_ok=True)
|
|
120
|
+
camera = CameraConfig.from_dict(cfg["camera"]) if cfg["camera"] else None
|
|
121
|
+
try:
|
|
122
|
+
process_core(
|
|
123
|
+
cfg["video"], out, frontend_name=cfg["frontend"], k_skip=cfg["k_skip"],
|
|
124
|
+
m_window=cfg["m_window"], start_frame=cfg["start_frame"], end_frame=cfg["end_frame"],
|
|
125
|
+
camera=camera, reuse_db=not cfg["force"], jobs=cfg["inner_jobs"],
|
|
126
|
+
)
|
|
127
|
+
init_reconstruction(out)
|
|
128
|
+
track_frames(out, m_window=cfg["m_window"], min_angle=cfg["min_angle"],
|
|
129
|
+
local_ba=cfg["local_ba"], ba_every=cfg["ba_every"], fuse=cfg["fuse"])
|
|
130
|
+
if cfg["ba"]:
|
|
131
|
+
from reconstruct3d.bundle_adjust import run_bundle_adjustment
|
|
132
|
+
run_bundle_adjustment(out, max_nfev=cfg["ba_max_nfev"])
|
|
133
|
+
return {"chunk": cfg["chunk"], "out": out, "ok": True}
|
|
134
|
+
except Exception as e: # noqa: BLE001 - reportamos y seguimos con otros chunks
|
|
135
|
+
return {"chunk": cfg["chunk"], "out": out, "ok": False, "error": repr(e)}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --------------------------------------------------------------------------- #
|
|
139
|
+
# Orquestación #
|
|
140
|
+
# --------------------------------------------------------------------------- #
|
|
141
|
+
def run_chunked(video_path, out_dir, frontend_name=DEFAULT_FRONTEND, k_skip=5, m_window=3,
|
|
142
|
+
chunk=80, overlap=20, min_angle=1.5, local_ba=True, ba_every=1, fuse=True,
|
|
143
|
+
ba=False, ba_max_nfev=200, chunk_jobs=0, inner_jobs=1, camera=None,
|
|
144
|
+
min_overlap=4, voxel=0.0):
|
|
145
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
146
|
+
cam = camera or CameraConfig()
|
|
147
|
+
|
|
148
|
+
cap = cv2.VideoCapture(video_path)
|
|
149
|
+
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
150
|
+
cap.release()
|
|
151
|
+
if n_frames <= 0:
|
|
152
|
+
raise RuntimeError(f"No se pudo leer el conteo de frames de {video_path}")
|
|
153
|
+
|
|
154
|
+
spans = plan_chunks(n_frames, k_skip, chunk, overlap)
|
|
155
|
+
chunk_jobs = resolve_workers(chunk_jobs, frontend_name)
|
|
156
|
+
chunk_jobs = min(chunk_jobs, len(spans))
|
|
157
|
+
print(f"[chunked] {len(spans)} chunks (chunk={chunk}, overlap={overlap} frames muestreados) "
|
|
158
|
+
f"| procesos paralelos={chunk_jobs} | hilos internos/chunk={inner_jobs}")
|
|
159
|
+
for c, (s, e) in enumerate(spans):
|
|
160
|
+
print(f" chunk {c:02d}: frames absolutos [{s}, {e})")
|
|
161
|
+
|
|
162
|
+
cfgs = []
|
|
163
|
+
for c, (s, e) in enumerate(spans):
|
|
164
|
+
cfgs.append(dict(
|
|
165
|
+
chunk=c, out=os.path.join(out_dir, f"chunk_{c:02d}"), video=video_path,
|
|
166
|
+
frontend=frontend_name, k_skip=k_skip, m_window=m_window,
|
|
167
|
+
start_frame=s, end_frame=e, camera=cam.to_dict(), force=True,
|
|
168
|
+
inner_jobs=inner_jobs, min_angle=min_angle, local_ba=local_ba,
|
|
169
|
+
ba_every=ba_every, fuse=fuse, ba=ba, ba_max_nfev=ba_max_nfev,
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
# --- Fase 1: reconstruir cada chunk (en paralelo) ---
|
|
173
|
+
if chunk_jobs == 1:
|
|
174
|
+
results = [_run_chunk_worker(cfg) for cfg in cfgs]
|
|
175
|
+
else:
|
|
176
|
+
with ProcessPoolExecutor(max_workers=chunk_jobs) as ex:
|
|
177
|
+
results = list(ex.map(_run_chunk_worker, cfgs))
|
|
178
|
+
|
|
179
|
+
for r in sorted(results, key=lambda r: r["chunk"]):
|
|
180
|
+
status = "OK" if r["ok"] else f"FALLÓ ({r.get('error', '')})"
|
|
181
|
+
print(f"[chunked] chunk {r['chunk']:02d}: {status}")
|
|
182
|
+
|
|
183
|
+
# --- Fase 2: alinear y fusionar ---
|
|
184
|
+
return merge_chunks(out_dir, [r["out"] for r in sorted(results, key=lambda r: r["chunk"]) if r["ok"]],
|
|
185
|
+
cam, min_overlap=min_overlap, voxel=voxel)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def merge_chunks(out_dir, chunk_dirs, cam, min_overlap=4, voxel=0.0):
|
|
189
|
+
"""Alinea las sub-nubes por solape (Umeyama) y escribe la nube fusionada."""
|
|
190
|
+
chunks = []
|
|
191
|
+
for d in chunk_dirs:
|
|
192
|
+
p = os.path.join(d, MAP_STATE_FILE)
|
|
193
|
+
if not os.path.exists(p):
|
|
194
|
+
print(f"[merge] {d}: sin {MAP_STATE_FILE}, se omite.")
|
|
195
|
+
continue
|
|
196
|
+
m = np.load(p, allow_pickle=True).item()
|
|
197
|
+
if len(m.get("poses", {})) < 2:
|
|
198
|
+
print(f"[merge] {d}: <2 poses, se omite.")
|
|
199
|
+
continue
|
|
200
|
+
chunks.append((d, m))
|
|
201
|
+
|
|
202
|
+
if not chunks:
|
|
203
|
+
print("[merge] No hay chunks reconstruidos para fusionar.")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# El primer chunk define el marco global.
|
|
207
|
+
global_centers = {} # frame_abs -> centro en marco global
|
|
208
|
+
merged_pts, merged_cols = [], []
|
|
209
|
+
merged_poses = {} # frame_abs -> pose global 4x4 world->cam
|
|
210
|
+
n_used = 0
|
|
211
|
+
|
|
212
|
+
for i, (d, m) in enumerate(chunks):
|
|
213
|
+
poses = m["poses"]
|
|
214
|
+
centers = _camera_centers(poses)
|
|
215
|
+
pts = np.asarray(m["points"], dtype=np.float64)
|
|
216
|
+
cols = np.asarray(m["colors"])
|
|
217
|
+
finite = np.isfinite(pts).all(axis=1)
|
|
218
|
+
pts, cols = pts[finite], cols[finite]
|
|
219
|
+
|
|
220
|
+
if i == 0:
|
|
221
|
+
s, R, t = 1.0, np.eye(3), np.zeros(3)
|
|
222
|
+
else:
|
|
223
|
+
shared = [f for f in centers if f in global_centers]
|
|
224
|
+
if len(shared) < min_overlap:
|
|
225
|
+
print(f"[merge] chunk dir '{os.path.basename(d)}': solape insuficiente "
|
|
226
|
+
f"({len(shared)} < {min_overlap} frames), se omite del merge.")
|
|
227
|
+
continue
|
|
228
|
+
src = np.array([centers[f] for f in shared]) # marco local del chunk
|
|
229
|
+
dst = np.array([global_centers[f] for f in shared]) # marco global
|
|
230
|
+
s, R, t = umeyama(src, dst, with_scale=True)
|
|
231
|
+
# Diagnóstico: residual RMS de la alineación
|
|
232
|
+
res = np.linalg.norm(apply_sim3(src, s, R, t) - dst, axis=1)
|
|
233
|
+
print(f"[merge] chunk dir '{os.path.basename(d)}': {len(shared)} frames de solape, "
|
|
234
|
+
f"escala={s:.3f}, RMS alineación={res.mean():.4f}")
|
|
235
|
+
|
|
236
|
+
merged_pts.append(apply_sim3(pts, s, R, t))
|
|
237
|
+
merged_cols.append(cols)
|
|
238
|
+
# Registrar/actualizar centros y poses globales de este chunk
|
|
239
|
+
for f, c in centers.items():
|
|
240
|
+
gc = apply_sim3(c[None], s, R, t)[0]
|
|
241
|
+
global_centers.setdefault(f, gc)
|
|
242
|
+
if f not in merged_poses:
|
|
243
|
+
merged_poses[f] = _transform_pose(poses[f], s, R, t)
|
|
244
|
+
n_used += 1
|
|
245
|
+
|
|
246
|
+
if not merged_pts:
|
|
247
|
+
print("[merge] Ningún chunk pudo alinearse.")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
pts = np.concatenate(merged_pts)
|
|
251
|
+
cols = np.concatenate(merged_cols)
|
|
252
|
+
|
|
253
|
+
# Dedup del solape por vóxel (opcional). Escala automática si voxel<=0.
|
|
254
|
+
if voxel <= 0:
|
|
255
|
+
center = pts.mean(0)
|
|
256
|
+
scale = float(np.median(np.linalg.norm(pts - center, axis=1)))
|
|
257
|
+
voxel = scale / 600.0 if scale > 0 else 0.0
|
|
258
|
+
if voxel > 0:
|
|
259
|
+
keys = np.floor(pts / voxel).astype(np.int64)
|
|
260
|
+
_, idx = np.unique(keys, axis=0, return_index=True)
|
|
261
|
+
pts, cols = pts[idx], cols[idx]
|
|
262
|
+
|
|
263
|
+
median_pt = np.median(pts, axis=0)
|
|
264
|
+
cam_centers = [c - median_pt for c in global_centers.values()]
|
|
265
|
+
out_ply = os.path.join(out_dir, MERGED_CLOUD_FILE)
|
|
266
|
+
export_ply(out_ply, pts - median_pt, cols, cam_centers)
|
|
267
|
+
|
|
268
|
+
merged_state = {
|
|
269
|
+
"points": pts, "colors": cols, "poses": merged_poses,
|
|
270
|
+
"camera": cam.to_dict(), "frontend": "", "obs": {},
|
|
271
|
+
}
|
|
272
|
+
np.save(os.path.join(out_dir, MERGED_STATE_FILE), merged_state)
|
|
273
|
+
# También como map_state.npy para que `dense`/`view --out <out_dir>` funcionen
|
|
274
|
+
# directo sobre la nube fusionada (ambos leen map_state.npy).
|
|
275
|
+
np.save(os.path.join(out_dir, MAP_STATE_FILE), merged_state)
|
|
276
|
+
print(f"[merge] {n_used} chunks fusionados | {len(pts):,} puntos | "
|
|
277
|
+
f"{len(merged_poses)} cámaras -> {out_ply}")
|
|
278
|
+
print(f"[merge] estado global -> {MERGED_STATE_FILE} (+ copia como {MAP_STATE_FILE} "
|
|
279
|
+
f"para dense/view con --out {out_dir})")
|
|
280
|
+
return out_ply
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _transform_pose(P, s, R, t):
|
|
284
|
+
"""Lleva una pose world->cam del marco local al global bajo la similaridad
|
|
285
|
+
(s,R,t) que mapea puntos locales a globales (X_g = s R X_l + t).
|
|
286
|
+
|
|
287
|
+
Para puntos: X_g = sR X_l + t. La cámara cumple x_cam = R_wc X_l + t_wc.
|
|
288
|
+
En el marco global: x_cam = R_wc' X_g + t_wc' con
|
|
289
|
+
R_wc' = R_wc R^T, t_wc' = t_wc - R_wc' t, y la traslación de cámara
|
|
290
|
+
se reescala por s (la escala del mundo). Se mantiene la orientación de la
|
|
291
|
+
cámara; solo cambia su posición/escala.
|
|
292
|
+
"""
|
|
293
|
+
R_wc, t_wc = P[:3, :3], P[:3, 3]
|
|
294
|
+
R_new = R_wc @ R.T
|
|
295
|
+
t_new = s * t_wc - R_new @ t
|
|
296
|
+
out = np.eye(4)
|
|
297
|
+
out[:3, :3] = R_new
|
|
298
|
+
out[:3, 3] = t_new
|
|
299
|
+
return out
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
p = argparse.ArgumentParser(description="Reconstrucción por chunks con solape + fusión de nubes.")
|
|
304
|
+
p.add_argument("video", help="Video de entrada (mp4/MOV).")
|
|
305
|
+
p.add_argument("--out", default=None, help="Directorio de salida (default outputs/<frontend>/).")
|
|
306
|
+
p.add_argument("--frontend", default=DEFAULT_FRONTEND, help="sift|orb|spglue (default sift).")
|
|
307
|
+
p.add_argument("--camera", default=None, help="JSON de intrínsecos (ver camera.example.json).")
|
|
308
|
+
p.add_argument("--k-skip", type=int, default=5, help="1 de cada k frames (default 5).")
|
|
309
|
+
p.add_argument("--m-window", type=int, default=3, help="Ventana de pares/PnP (default 3).")
|
|
310
|
+
p.add_argument("--chunk", type=int, default=80, help="Frames muestreados por chunk (default 80).")
|
|
311
|
+
p.add_argument("--overlap", type=int, default=20, help="Frames muestreados de solape (default 20).")
|
|
312
|
+
p.add_argument("--min-angle", type=float, default=1.5, help="Ángulo mín. de triangulación.")
|
|
313
|
+
p.add_argument("--no-local-ba", action="store_true", help="Desactiva BA local en cada chunk.")
|
|
314
|
+
p.add_argument("--ba-every", type=int, default=1)
|
|
315
|
+
p.add_argument("--no-fuse", action="store_true")
|
|
316
|
+
p.add_argument("--ba", action="store_true", help="BA global por chunk antes de fusionar.")
|
|
317
|
+
p.add_argument("--ba-max-nfev", type=int, default=200)
|
|
318
|
+
p.add_argument("--chunk-jobs", type=int, default=0,
|
|
319
|
+
help="Chunks reconstruidos en paralelo (procesos; 0=auto, 1=secuencial).")
|
|
320
|
+
p.add_argument("--inner-jobs", type=int, default=1,
|
|
321
|
+
help="Hilos de extracción/matching dentro de cada chunk (default 1).")
|
|
322
|
+
p.add_argument("--min-overlap", type=int, default=4,
|
|
323
|
+
help="Frames de solape mínimos para alinear un chunk (default 4).")
|
|
324
|
+
p.add_argument("--voxel", type=float, default=0.0, help="Vóxel de dedup del merge (0=auto).")
|
|
325
|
+
args = p.parse_args()
|
|
326
|
+
|
|
327
|
+
out = args.out or os.path.join("outputs", args.frontend)
|
|
328
|
+
camera = CameraConfig.from_json(args.camera) if args.camera else None
|
|
329
|
+
run_chunked(
|
|
330
|
+
args.video, out, frontend_name=args.frontend, k_skip=args.k_skip, m_window=args.m_window,
|
|
331
|
+
chunk=args.chunk, overlap=args.overlap, min_angle=args.min_angle,
|
|
332
|
+
local_ba=not args.no_local_ba, ba_every=args.ba_every, fuse=not args.no_fuse,
|
|
333
|
+
ba=args.ba, ba_max_nfev=args.ba_max_nfev, chunk_jobs=args.chunk_jobs,
|
|
334
|
+
inner_jobs=args.inner_jobs, camera=camera, min_overlap=args.min_overlap, voxel=args.voxel,
|
|
335
|
+
)
|