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/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()