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/cli.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Orquestador del pipeline de reconstrucción 3D (SfM offline + MVS-lite).
|
|
3
|
+
|
|
4
|
+
Un único punto de entrada que encadena las 6 etapas del pipeline, evitando
|
|
5
|
+
tener que recordar el orden de los scripts y mantener el directorio `--out`
|
|
6
|
+
sincronizado a mano. Cada subcomando reutiliza las funciones de los módulos
|
|
7
|
+
correspondientes (core, init_sfm, track_sfm, bundle_adjust, dense_mvs, viewer).
|
|
8
|
+
|
|
9
|
+
Uso típico (con uv):
|
|
10
|
+
|
|
11
|
+
uv run python pipeline.py all data/mi_video.mp4
|
|
12
|
+
uv run python pipeline.py all data/mi_video.mp4 --frontend spglue
|
|
13
|
+
|
|
14
|
+
O etapa por etapa (comparten el mismo directorio de salida):
|
|
15
|
+
|
|
16
|
+
uv run python pipeline.py extract data/mi_video.mp4
|
|
17
|
+
uv run python pipeline.py init
|
|
18
|
+
uv run python pipeline.py track
|
|
19
|
+
uv run python pipeline.py ba
|
|
20
|
+
uv run python pipeline.py dense data/mi_video.mp4
|
|
21
|
+
uv run python pipeline.py view data/mi_video.mp4
|
|
22
|
+
|
|
23
|
+
El directorio de salida por defecto es `outputs/<frontend>/` (p.ej.
|
|
24
|
+
`outputs/sift/`). Todas las etapas leen/escriben ahí: sfm_data.pkl,
|
|
25
|
+
map_state.npy, init_cloud.ply, tracked_cloud.ply, dense_cloud.ply.
|
|
26
|
+
"""
|
|
27
|
+
import argparse
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
from reconstruct3d.core import DEFAULT_FRONTEND, _FRONTEND_REGISTRY
|
|
32
|
+
|
|
33
|
+
FRONTENDS = sorted(_FRONTEND_REGISTRY)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def default_out(frontend):
|
|
37
|
+
return os.path.join("outputs", frontend)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_out(args):
|
|
41
|
+
"""Directorio de salida: --out explícito o outputs/<frontend>/."""
|
|
42
|
+
out = getattr(args, "out", None) or default_out(args.frontend)
|
|
43
|
+
os.makedirs(out, exist_ok=True)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _banner(stage):
|
|
48
|
+
print(f"\n{'=' * 70}\n[ETAPA] {stage}\n{'=' * 70}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resolve_camera(args):
|
|
52
|
+
"""Resuelve los intrínsecos para `extract` con esta prioridad:
|
|
53
|
+
1. --calibrate VIDEO -> calibra ahora y escribe camera.json.
|
|
54
|
+
2. --camera FILE -> carga ese JSON.
|
|
55
|
+
3. ./camera.json -> lo usa si existe (auto).
|
|
56
|
+
4. video de calibración detectado (data/calib/, data/, *calib*) -> calibra.
|
|
57
|
+
5. defaults de CameraConfig (iPhone @ 540x960) si no hay nada.
|
|
58
|
+
Devuelve una CameraConfig o None (None = usar defaults en process_core).
|
|
59
|
+
"""
|
|
60
|
+
from reconstruct3d.core import CameraConfig
|
|
61
|
+
from reconstruct3d import calibrate
|
|
62
|
+
board = getattr(args, "board", (9, 6))
|
|
63
|
+
square = getattr(args, "square", 0.025)
|
|
64
|
+
cam_json = getattr(args, "camera", None)
|
|
65
|
+
calib_video = getattr(args, "calibrate", None)
|
|
66
|
+
|
|
67
|
+
if calib_video:
|
|
68
|
+
out_json = cam_json or calibrate.DEFAULT_CAMERA_JSON
|
|
69
|
+
print(f"[*] Calibrando intrínsecos desde '{calib_video}' -> {out_json}")
|
|
70
|
+
calibrate.run_calibration(calib_video, board=board, square_size=square, out=out_json)
|
|
71
|
+
return CameraConfig.from_json(out_json)
|
|
72
|
+
|
|
73
|
+
if cam_json:
|
|
74
|
+
print(f"[*] Cámara: {cam_json}")
|
|
75
|
+
return CameraConfig.from_json(cam_json)
|
|
76
|
+
|
|
77
|
+
if os.path.exists(calibrate.DEFAULT_CAMERA_JSON):
|
|
78
|
+
print(f"[*] Cámara: {calibrate.DEFAULT_CAMERA_JSON} (auto-detectado)")
|
|
79
|
+
return CameraConfig.from_json(calibrate.DEFAULT_CAMERA_JSON)
|
|
80
|
+
|
|
81
|
+
src = calibrate.find_calibration_source()
|
|
82
|
+
if src:
|
|
83
|
+
print(f"[*] Video de calibración detectado ('{src}'); calibrando automáticamente...")
|
|
84
|
+
calibrate.run_calibration(src, board=board, square_size=square,
|
|
85
|
+
out=calibrate.DEFAULT_CAMERA_JSON)
|
|
86
|
+
return CameraConfig.from_json(calibrate.DEFAULT_CAMERA_JSON)
|
|
87
|
+
|
|
88
|
+
print("[*] Sin camera.json ni video de calibración; usando intrínsecos por defecto. "
|
|
89
|
+
"Pasa --camera o --calibrate para tu dispositivo.")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
94
|
+
# Subcomandos #
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
def cmd_calibrate(args):
|
|
97
|
+
"""Calibra K desde un video de tablero y escribe/crea camera.json."""
|
|
98
|
+
from reconstruct3d import calibrate
|
|
99
|
+
_banner(f"calibrate -> {args.out}")
|
|
100
|
+
calibrate.run_calibration(
|
|
101
|
+
args.video, board=args.board, square_size=args.square,
|
|
102
|
+
k_skip=args.k_skip, max_views=args.max_views,
|
|
103
|
+
interactive=args.interactive, out=args.out,
|
|
104
|
+
)
|
|
105
|
+
return args.out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_extract(args):
|
|
109
|
+
"""Etapa 1: extracción de features + matrices esenciales -> sfm_data.pkl."""
|
|
110
|
+
import pandas as pd
|
|
111
|
+
from reconstruct3d.core import process_core
|
|
112
|
+
|
|
113
|
+
out = _resolve_out(args)
|
|
114
|
+
_banner(f"1/extract ({args.frontend}) -> {out}")
|
|
115
|
+
camera = _resolve_camera(args)
|
|
116
|
+
db = process_core(
|
|
117
|
+
args.video, out, frontend_name=args.frontend,
|
|
118
|
+
k_skip=args.k_skip, m_window=args.m_window,
|
|
119
|
+
max_seconds=args.max_seconds, start_seconds=args.start_seconds,
|
|
120
|
+
camera=camera, reuse_db=not args.force, jobs=getattr(args, "jobs", 0),
|
|
121
|
+
)
|
|
122
|
+
if db.pairwise:
|
|
123
|
+
df = pd.DataFrame(db.pairwise).sort_values("inliers", ascending=False)
|
|
124
|
+
print(f"\nFrontend usado: {db.frontend_name}")
|
|
125
|
+
print("Top candidatos (más inliers):\n",
|
|
126
|
+
df[["frame_i", "frame_j", "inliers", "matches"]].head())
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_init(args):
|
|
131
|
+
"""Etapa 2: triangulación del par semilla -> map_state.npy + init_cloud.ply."""
|
|
132
|
+
from reconstruct3d.init_sfm import init_reconstruction
|
|
133
|
+
|
|
134
|
+
out = _resolve_out(args)
|
|
135
|
+
_banner(f"2/init -> {out}")
|
|
136
|
+
init_reconstruction(out)
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_track(args):
|
|
141
|
+
"""Etapa 3: registro incremental PnP + BA local + fusión -> tracked_cloud.ply."""
|
|
142
|
+
from reconstruct3d.track_sfm import track_frames
|
|
143
|
+
|
|
144
|
+
out = _resolve_out(args)
|
|
145
|
+
_banner(f"3/track -> {out}")
|
|
146
|
+
track_frames(
|
|
147
|
+
out, m_window=args.m_window, min_angle=args.min_angle,
|
|
148
|
+
local_ba=not args.no_local_ba, ba_every=args.ba_every,
|
|
149
|
+
fuse=not args.no_fuse,
|
|
150
|
+
)
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_ba(args):
|
|
155
|
+
"""Etapa 4 (opcional): Bundle Adjustment global -> reescribe el mapa."""
|
|
156
|
+
from reconstruct3d.bundle_adjust import run_bundle_adjustment
|
|
157
|
+
|
|
158
|
+
out = _resolve_out(args)
|
|
159
|
+
_banner(f"4/ba (global) -> {out}")
|
|
160
|
+
run_bundle_adjustment(out, max_reproj_err=args.max_reproj, max_nfev=args.max_nfev)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_dense(args):
|
|
165
|
+
"""Etapa 5: densificación MVS-lite -> dense_cloud.ply."""
|
|
166
|
+
from reconstruct3d.dense_mvs import run_dense
|
|
167
|
+
|
|
168
|
+
out = _resolve_out(args)
|
|
169
|
+
_banner(f"5/dense -> {out}")
|
|
170
|
+
steps = tuple(int(s) for s in str(args.neighbor_steps).split(",") if s.strip())
|
|
171
|
+
run_dense(
|
|
172
|
+
out, args.video, neighbor_steps=steps, min_views=args.min_views,
|
|
173
|
+
num_disp=args.num_disp, block=args.block, voxel=args.voxel,
|
|
174
|
+
jobs=getattr(args, "jobs", 0),
|
|
175
|
+
)
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_mesh(args):
|
|
180
|
+
"""Etapa 6: mallado de la nube densa con CGAL -> mesh.ply."""
|
|
181
|
+
from reconstruct3d import mesh
|
|
182
|
+
# Nube de entrada: posicional `cloud`, o --input, o <out>/dense_cloud.ply.
|
|
183
|
+
cloud = getattr(args, "cloud", None) or getattr(args, "input", None)
|
|
184
|
+
if cloud:
|
|
185
|
+
# Sin --out explícito, mesh.ply va a la carpeta de la nube indicada.
|
|
186
|
+
out = args.out or os.path.dirname(os.path.abspath(cloud)) or "."
|
|
187
|
+
os.makedirs(out, exist_ok=True)
|
|
188
|
+
else:
|
|
189
|
+
out = _resolve_out(args)
|
|
190
|
+
_banner(f"6/mesh ({args.method}) -> {out}")
|
|
191
|
+
mesh.run_mesh(
|
|
192
|
+
out, input_ply=cloud, method=args.method, outlier_pct=args.outlier_pct,
|
|
193
|
+
simplify=args.simplify, smooth=args.smooth, mesh_smooth=args.mesh_smooth,
|
|
194
|
+
min_component=args.min_component, afront_radius=args.afront_radius,
|
|
195
|
+
poisson_spacing_mult=args.poisson_spacing_mult, rebuild=args.rebuild,
|
|
196
|
+
)
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def cmd_view(args):
|
|
201
|
+
"""Etapa 7: visor POV interactivo sobre el mapa reconstruido."""
|
|
202
|
+
from reconstruct3d import viewer
|
|
203
|
+
out = _resolve_out(args)
|
|
204
|
+
_banner(f"7/view -> {out}")
|
|
205
|
+
viewer.main(args.video, out)
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_chunked(args):
|
|
210
|
+
"""Reconstrucción por chunks con solape + fusión de nubes (videos largos)."""
|
|
211
|
+
from reconstruct3d import chunked
|
|
212
|
+
out = _resolve_out(args)
|
|
213
|
+
_banner(f"chunked ({args.frontend}) -> {out}")
|
|
214
|
+
camera = _resolve_camera(args)
|
|
215
|
+
chunked.run_chunked(
|
|
216
|
+
args.video, out, frontend_name=args.frontend, k_skip=args.k_skip, m_window=args.m_window,
|
|
217
|
+
chunk=args.chunk, overlap=args.overlap, min_angle=args.min_angle,
|
|
218
|
+
local_ba=not args.no_local_ba, ba_every=args.ba_every, fuse=not args.no_fuse,
|
|
219
|
+
ba=args.ba, ba_max_nfev=args.ba_max_nfev, chunk_jobs=args.chunk_jobs,
|
|
220
|
+
inner_jobs=args.inner_jobs, camera=camera, min_overlap=args.min_overlap, voxel=args.voxel,
|
|
221
|
+
)
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_all(args):
|
|
226
|
+
"""Pipeline completo: extract -> init -> track -> [ba] -> [dense] -> [mesh] -> [view]."""
|
|
227
|
+
cmd_extract(args)
|
|
228
|
+
cmd_init(args)
|
|
229
|
+
cmd_track(args)
|
|
230
|
+
if not args.no_ba:
|
|
231
|
+
cmd_ba(args)
|
|
232
|
+
if not args.no_dense:
|
|
233
|
+
cmd_dense(args)
|
|
234
|
+
if not args.no_mesh and not args.no_dense:
|
|
235
|
+
try:
|
|
236
|
+
cmd_mesh(args)
|
|
237
|
+
except Exception as e: # noqa: BLE001 - el mallado depende de CGAL nativo
|
|
238
|
+
print(f"[!] Mallado omitido ({e}). Instala CGAL o corre `mesh` aparte.")
|
|
239
|
+
out = _resolve_out(args)
|
|
240
|
+
print(f"\n{'=' * 70}\n[OK] Pipeline completo. Artefactos en: {out}/")
|
|
241
|
+
for name in ("init_cloud.ply", "tracked_cloud.ply", "dense_cloud.ply", "mesh.ply"):
|
|
242
|
+
p = os.path.join(out, name)
|
|
243
|
+
mark = "✓" if os.path.exists(p) else "·"
|
|
244
|
+
print(f" [{mark}] {p}")
|
|
245
|
+
print("=" * 70)
|
|
246
|
+
if args.view:
|
|
247
|
+
cmd_view(args)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --------------------------------------------------------------------------- #
|
|
251
|
+
# Argumentos #
|
|
252
|
+
# --------------------------------------------------------------------------- #
|
|
253
|
+
def _add_common(p, needs_video=True):
|
|
254
|
+
if needs_video:
|
|
255
|
+
p.add_argument("video", help="Ruta al video de entrada (mp4/MOV).")
|
|
256
|
+
p.add_argument("--frontend", choices=FRONTENDS, default=DEFAULT_FRONTEND,
|
|
257
|
+
help=f"Detector+matcher (default: {DEFAULT_FRONTEND}). "
|
|
258
|
+
"'sift', 'orb', 'spglue'=SuperPoint+LightGlue (requiere torch).")
|
|
259
|
+
p.add_argument("--out", default=None,
|
|
260
|
+
help="Directorio de salida (default: outputs/<frontend>/).")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _board_arg(s):
|
|
264
|
+
"""'9x6' -> (9, 6)."""
|
|
265
|
+
parts = str(s).lower().replace("×", "x").split("x")
|
|
266
|
+
if len(parts) != 2:
|
|
267
|
+
raise argparse.ArgumentTypeError(f"Tablero inválido: '{s}'. Usa COLSxROWS, p.ej. 9x6.")
|
|
268
|
+
return int(parts[0]), int(parts[1])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _add_camera_opts(p):
|
|
272
|
+
"""Opciones de cámara/calibración compartidas por extract y all."""
|
|
273
|
+
p.add_argument("--camera", default=None,
|
|
274
|
+
help="JSON con intrínsecos del dispositivo (ver camera.example.json). "
|
|
275
|
+
"Si se omite, se auto-detecta camera.json o un video *calib*.")
|
|
276
|
+
p.add_argument("--calibrate", default=None, metavar="VIDEO_TABLERO",
|
|
277
|
+
help="Calibrar K desde este video de tablero antes de extraer (escribe camera.json).")
|
|
278
|
+
p.add_argument("--board", type=_board_arg, default=(9, 6),
|
|
279
|
+
help="Esquinas internas COLSxROWS del tablero de calibración (default 9x6).")
|
|
280
|
+
p.add_argument("--square", type=float, default=0.025,
|
|
281
|
+
help="Lado del cuadrado en metros para calibración (default 0.025).")
|
|
282
|
+
p.add_argument("--jobs", type=int, default=0,
|
|
283
|
+
help="Hilos para extracción/matching (0=auto=nº de CPUs; 1=secuencial).")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _add_mesh_opts(p):
|
|
287
|
+
"""Opciones de mallado CGAL, compartidas por `mesh` y `all`."""
|
|
288
|
+
p.add_argument("--method", choices=["afront", "poisson"], default="afront",
|
|
289
|
+
help="afront=interpola puntos, conserva color, bordes abiertos (default). "
|
|
290
|
+
"poisson=superficie suave/cerrada, tolera ruido.")
|
|
291
|
+
p.add_argument("--outlier-pct", type=float, default=2.0, help="%% de outliers a eliminar (0=off).")
|
|
292
|
+
p.add_argument("--simplify", type=float, default=0.0,
|
|
293
|
+
help="Celda de grid simplify (0=auto~2x spacing; <0=sin simplificar).")
|
|
294
|
+
p.add_argument("--smooth", type=int, default=0, help="Vecinos de jet smooth de PUNTOS (0=off).")
|
|
295
|
+
p.add_argument("--mesh-smooth", type=int, default=2,
|
|
296
|
+
help="Iteraciones de suavizado tangencial de la MALLA (default 2; 0=off).")
|
|
297
|
+
p.add_argument("--min-component", type=float, default=0.002,
|
|
298
|
+
help="Elimina componentes < FRAC*caras (islas de ruido) (default 0.002; 0=off).")
|
|
299
|
+
p.add_argument("--afront-radius", type=float, default=5.0, help="Longitud máx. de arista (x spacing).")
|
|
300
|
+
p.add_argument("--poisson-spacing-mult", type=float, default=1.0, help="Multiplicador de spacing para Poisson.")
|
|
301
|
+
p.add_argument("--rebuild", action="store_true", help="Recompilar el binario CGAL.")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def build_parser():
|
|
305
|
+
parser = argparse.ArgumentParser(
|
|
306
|
+
description="Pipeline de reconstrucción 3D (SfM offline + densificación MVS).",
|
|
307
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
308
|
+
)
|
|
309
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
310
|
+
|
|
311
|
+
# --- calibrate ---
|
|
312
|
+
pc = sub.add_parser("calibrate", help="Calibrar K desde un video de tablero -> camera.json.")
|
|
313
|
+
pc.add_argument("video", help="Video del patrón de ajedrez (mp4/MOV/avi).")
|
|
314
|
+
pc.add_argument("--board", type=_board_arg, default=(9, 6), help="Esquinas internas COLSxROWS (default 9x6).")
|
|
315
|
+
pc.add_argument("--square", type=float, default=0.025, help="Lado del cuadrado en metros (default 0.025).")
|
|
316
|
+
pc.add_argument("--k-skip", type=int, default=10, help="Revisar 1 de cada k frames (default 10).")
|
|
317
|
+
pc.add_argument("--max-views", type=int, default=40, help="Máx. vistas a usar (default 40).")
|
|
318
|
+
pc.add_argument("--interactive", action="store_true", help="Aceptar cada vista a mano (ventana OpenCV).")
|
|
319
|
+
pc.add_argument("--out", default="camera.json", help="Ruta de salida (default camera.json).")
|
|
320
|
+
pc.set_defaults(func=cmd_calibrate)
|
|
321
|
+
|
|
322
|
+
# --- extract ---
|
|
323
|
+
pe = sub.add_parser("extract", help="Etapa 1: features + matrices esenciales.")
|
|
324
|
+
_add_common(pe)
|
|
325
|
+
pe.add_argument("--k-skip", type=int, default=5, help="Procesa 1 de cada k frames (default 5).")
|
|
326
|
+
pe.add_argument("--m-window", type=int, default=3, help="Ventana de pares ±m vecinos (default 3).")
|
|
327
|
+
pe.add_argument("--max-seconds", type=float, default=None, help="Procesar hasta el segundo N.")
|
|
328
|
+
pe.add_argument("--start-seconds", type=float, default=0.0, help="Empezar desde el segundo N.")
|
|
329
|
+
pe.add_argument("--force", action="store_true", help="Re-extraer aunque exista sfm_data.pkl.")
|
|
330
|
+
_add_camera_opts(pe)
|
|
331
|
+
pe.set_defaults(func=cmd_extract)
|
|
332
|
+
|
|
333
|
+
# --- init ---
|
|
334
|
+
pi = sub.add_parser("init", help="Etapa 2: triangular par semilla.")
|
|
335
|
+
_add_common(pi, needs_video=False)
|
|
336
|
+
pi.set_defaults(func=cmd_init)
|
|
337
|
+
|
|
338
|
+
# --- track ---
|
|
339
|
+
pt = sub.add_parser("track", help="Etapa 3: registro incremental + BA local.")
|
|
340
|
+
_add_common(pt, needs_video=False)
|
|
341
|
+
pt.add_argument("--m-window", type=int, default=3, help="Ventana PnP ±m frames extraídos (default 3).")
|
|
342
|
+
pt.add_argument("--min-angle", type=float, default=1.5, help="Ángulo mínimo de triangulación (grados).")
|
|
343
|
+
pt.add_argument("--no-local-ba", action="store_true", help="Desactiva BA local por ventana.")
|
|
344
|
+
pt.add_argument("--ba-every", type=int, default=1, help="BA local cada N cámaras (default 1).")
|
|
345
|
+
pt.add_argument("--no-fuse", action="store_true", help="Desactiva la fusión de puntos duplicados.")
|
|
346
|
+
pt.set_defaults(func=cmd_track)
|
|
347
|
+
|
|
348
|
+
# --- ba ---
|
|
349
|
+
pb = sub.add_parser("ba", help="Etapa 4: Bundle Adjustment global (opcional).")
|
|
350
|
+
_add_common(pb, needs_video=False)
|
|
351
|
+
pb.add_argument("--max-reproj", type=float, default=4.0, help="Descartar puntos con reproj > px (default 4).")
|
|
352
|
+
pb.add_argument("--max-nfev", type=int, default=300, help="Máx. evaluaciones del optimizador (default 300).")
|
|
353
|
+
pb.set_defaults(func=cmd_ba)
|
|
354
|
+
|
|
355
|
+
# --- dense ---
|
|
356
|
+
pd_ = sub.add_parser("dense", help="Etapa 5: densificación MVS-lite.")
|
|
357
|
+
_add_common(pd_)
|
|
358
|
+
pd_.add_argument("--neighbor-steps", default="1,2", help="Offsets de vecinos (default '1,2').")
|
|
359
|
+
pd_.add_argument("--min-views", type=int, default=2, help="Puntos confirmados por >=N pares (default 2).")
|
|
360
|
+
pd_.add_argument("--num-disp", type=int, default=128, help="numDisparities SGBM (múltiplo de 16).")
|
|
361
|
+
pd_.add_argument("--block", type=int, default=5, help="blockSize SGBM (impar, default 5).")
|
|
362
|
+
pd_.add_argument("--voxel", type=float, default=0.0, help="Tamaño de vóxel (0 = automático).")
|
|
363
|
+
pd_.add_argument("--jobs", type=int, default=0,
|
|
364
|
+
help="Hilos para densificar pares (0=auto=nº de CPUs; 1=secuencial).")
|
|
365
|
+
pd_.set_defaults(func=cmd_dense)
|
|
366
|
+
|
|
367
|
+
# --- mesh ---
|
|
368
|
+
pm = sub.add_parser("mesh", help="Etapa 6: mallado de la nube densa con CGAL.")
|
|
369
|
+
pm.add_argument("cloud", nargs="?", default=None,
|
|
370
|
+
help="Ruta al .ply de entrada (p.ej. outputs/sift/dense_cloud.ply). "
|
|
371
|
+
"Si se omite, usa <out>/dense_cloud.ply.")
|
|
372
|
+
_add_common(pm, needs_video=False)
|
|
373
|
+
pm.add_argument("--input", default=None,
|
|
374
|
+
help="Alternativa a la ruta posicional para la nube de entrada.")
|
|
375
|
+
_add_mesh_opts(pm)
|
|
376
|
+
pm.set_defaults(func=cmd_mesh)
|
|
377
|
+
|
|
378
|
+
# --- chunked ---
|
|
379
|
+
pk = sub.add_parser("chunked", help="Reconstrucción por chunks con solape + fusión (videos largos).")
|
|
380
|
+
_add_common(pk)
|
|
381
|
+
pk.add_argument("--camera", default=None, help="JSON de intrínsecos (auto-detecta si se omite).")
|
|
382
|
+
pk.add_argument("--calibrate", default=None, metavar="VIDEO_TABLERO",
|
|
383
|
+
help="Calibrar K desde este video de tablero antes de reconstruir.")
|
|
384
|
+
pk.add_argument("--board", type=_board_arg, default=(9, 6), help="Tablero COLSxROWS (default 9x6).")
|
|
385
|
+
pk.add_argument("--square", type=float, default=0.025, help="Lado del cuadrado en metros (default 0.025).")
|
|
386
|
+
pk.add_argument("--k-skip", type=int, default=5, help="1 de cada k frames (default 5).")
|
|
387
|
+
pk.add_argument("--m-window", type=int, default=3, help="Ventana de pares/PnP (default 3).")
|
|
388
|
+
pk.add_argument("--chunk", type=int, default=80, help="Frames muestreados por chunk (default 80).")
|
|
389
|
+
pk.add_argument("--overlap", type=int, default=20, help="Frames muestreados de solape (default 20).")
|
|
390
|
+
pk.add_argument("--min-angle", type=float, default=1.5, help="Ángulo mín. de triangulación.")
|
|
391
|
+
pk.add_argument("--no-local-ba", action="store_true", help="Desactiva BA local en cada chunk.")
|
|
392
|
+
pk.add_argument("--ba-every", type=int, default=1)
|
|
393
|
+
pk.add_argument("--no-fuse", action="store_true")
|
|
394
|
+
pk.add_argument("--ba", action="store_true", help="BA global por chunk antes de fusionar.")
|
|
395
|
+
pk.add_argument("--ba-max-nfev", type=int, default=200)
|
|
396
|
+
pk.add_argument("--chunk-jobs", type=int, default=0,
|
|
397
|
+
help="Chunks en paralelo (procesos; 0=auto, 1=secuencial).")
|
|
398
|
+
pk.add_argument("--inner-jobs", type=int, default=1,
|
|
399
|
+
help="Hilos de extracción/matching dentro de cada chunk (default 1).")
|
|
400
|
+
pk.add_argument("--min-overlap", type=int, default=4,
|
|
401
|
+
help="Frames de solape mínimos para alinear un chunk (default 4).")
|
|
402
|
+
pk.add_argument("--voxel", type=float, default=0.0, help="Vóxel de dedup del merge (0=auto).")
|
|
403
|
+
pk.set_defaults(func=cmd_chunked)
|
|
404
|
+
|
|
405
|
+
# --- view ---
|
|
406
|
+
pv = sub.add_parser("view", help="Etapa 6: visor POV interactivo.")
|
|
407
|
+
_add_common(pv)
|
|
408
|
+
pv.set_defaults(func=cmd_view)
|
|
409
|
+
|
|
410
|
+
# --- all ---
|
|
411
|
+
pa = sub.add_parser("all", help="Pipeline completo (extract->init->track->ba->dense).")
|
|
412
|
+
_add_common(pa)
|
|
413
|
+
# Subconjunto de flags útiles para el modo end-to-end.
|
|
414
|
+
pa.add_argument("--k-skip", type=int, default=5)
|
|
415
|
+
pa.add_argument("--m-window", type=int, default=3)
|
|
416
|
+
pa.add_argument("--max-seconds", type=float, default=None)
|
|
417
|
+
pa.add_argument("--start-seconds", type=float, default=0.0)
|
|
418
|
+
pa.add_argument("--force", action="store_true", help="Re-extraer features desde cero.")
|
|
419
|
+
_add_camera_opts(pa)
|
|
420
|
+
pa.add_argument("--min-angle", type=float, default=1.5)
|
|
421
|
+
pa.add_argument("--no-local-ba", action="store_true")
|
|
422
|
+
pa.add_argument("--ba-every", type=int, default=1)
|
|
423
|
+
pa.add_argument("--no-fuse", action="store_true")
|
|
424
|
+
pa.add_argument("--max-reproj", type=float, default=4.0)
|
|
425
|
+
pa.add_argument("--max-nfev", type=int, default=300)
|
|
426
|
+
pa.add_argument("--neighbor-steps", default="1,2")
|
|
427
|
+
pa.add_argument("--min-views", type=int, default=2)
|
|
428
|
+
pa.add_argument("--num-disp", type=int, default=128)
|
|
429
|
+
pa.add_argument("--block", type=int, default=5)
|
|
430
|
+
pa.add_argument("--voxel", type=float, default=0.0)
|
|
431
|
+
pa.add_argument("--no-ba", action="store_true", help="Saltar el BA global.")
|
|
432
|
+
pa.add_argument("--no-dense", action="store_true", help="Saltar la densificación.")
|
|
433
|
+
pa.add_argument("--no-mesh", action="store_true", help="Saltar el mallado CGAL.")
|
|
434
|
+
pa.add_argument("--input", default=None, help=argparse.SUPPRESS) # solo para reusar cmd_mesh
|
|
435
|
+
_add_mesh_opts(pa)
|
|
436
|
+
pa.add_argument("--view", action="store_true", help="Abrir el visor al terminar.")
|
|
437
|
+
pa.set_defaults(func=cmd_all)
|
|
438
|
+
|
|
439
|
+
return parser
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def main(argv=None):
|
|
443
|
+
args = build_parser().parse_args(argv)
|
|
444
|
+
args.func(args)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
main()
|