reconstruct3d 0.1.0__tar.gz → 0.1.2__tar.gz

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.
Files changed (70) hide show
  1. reconstruct3d-0.1.2/.dockerignore +18 -0
  2. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.gitignore +1 -1
  3. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/PKG-INFO +1 -1
  4. reconstruct3d-0.1.2/backend/.gitignore +2 -0
  5. reconstruct3d-0.1.2/backend/Dockerfile +27 -0
  6. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/backend/app.py +75 -8
  7. reconstruct3d-0.1.2/backend/store.py +87 -0
  8. reconstruct3d-0.1.2/config/default.yaml +37 -0
  9. reconstruct3d-0.1.2/config/iphone.yaml +45 -0
  10. reconstruct3d-0.1.2/data/README.md +23 -0
  11. reconstruct3d-0.1.2/docker-compose.yml +39 -0
  12. reconstruct3d-0.1.2/examples/ejemplo_uso.py +58 -0
  13. reconstruct3d-0.1.2/frontend/Dockerfile +14 -0
  14. reconstruct3d-0.1.2/frontend/jsconfig.json +8 -0
  15. reconstruct3d-0.1.2/frontend/package-lock.json +3432 -0
  16. reconstruct3d-0.1.2/frontend/package.json +34 -0
  17. reconstruct3d-0.1.2/frontend/postcss.config.js +6 -0
  18. reconstruct3d-0.1.2/frontend/src/App.jsx +291 -0
  19. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/Viewer.jsx +1 -1
  20. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/api.js +14 -0
  21. reconstruct3d-0.1.2/frontend/src/components/ui/badge.jsx +25 -0
  22. reconstruct3d-0.1.2/frontend/src/components/ui/button.jsx +35 -0
  23. reconstruct3d-0.1.2/frontend/src/components/ui/card.jsx +34 -0
  24. reconstruct3d-0.1.2/frontend/src/components/ui/checkbox.jsx +24 -0
  25. reconstruct3d-0.1.2/frontend/src/components/ui/input.jsx +17 -0
  26. reconstruct3d-0.1.2/frontend/src/components/ui/label.jsx +14 -0
  27. reconstruct3d-0.1.2/frontend/src/components/ui/progress.jsx +19 -0
  28. reconstruct3d-0.1.2/frontend/src/components/ui/scroll-area.jsx +21 -0
  29. reconstruct3d-0.1.2/frontend/src/components/ui/select.jsx +69 -0
  30. reconstruct3d-0.1.2/frontend/src/index.css +43 -0
  31. reconstruct3d-0.1.2/frontend/src/lib/utils.js +8 -0
  32. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/main.jsx +1 -1
  33. reconstruct3d-0.1.2/frontend/tailwind.config.js +31 -0
  34. reconstruct3d-0.1.2/frontend/vite.config.js +24 -0
  35. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/__init__.py +8 -1
  36. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/api.py +6 -3
  37. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/calibrate.py +36 -0
  38. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/core.py +28 -6
  39. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/dense_mvs.py +60 -19
  40. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/track_sfm.py +12 -1
  41. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/uv.lock +1 -2
  42. reconstruct3d-0.1.0/backend/.gitignore +0 -1
  43. reconstruct3d-0.1.0/frontend/package-lock.json +0 -1687
  44. reconstruct3d-0.1.0/frontend/package.json +0 -20
  45. reconstruct3d-0.1.0/frontend/src/App.jsx +0 -196
  46. reconstruct3d-0.1.0/frontend/src/styles.css +0 -77
  47. reconstruct3d-0.1.0/frontend/vite.config.js +0 -13
  48. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.github/workflows/publish.yml +0 -0
  49. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.python-version +0 -0
  50. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/CLAUDE.md +0 -0
  51. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/LICENSE +0 -0
  52. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/README.md +0 -0
  53. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/backend/__init__.py +0 -0
  54. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/camera.example.json +0 -0
  55. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/camera.json +0 -0
  56. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/data/.gitkeep +0 -0
  57. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/docs.md +0 -0
  58. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/.gitignore +0 -0
  59. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/index.html +0 -0
  60. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/pipeline.py +0 -0
  61. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/pyproject.toml +0 -0
  62. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/bundle_adjust.py +0 -0
  63. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/.gitignore +0 -0
  64. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/CMakeLists.txt +0 -0
  65. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/mesh_reconstruct.cpp +0 -0
  66. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/chunked.py +0 -0
  67. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cli.py +0 -0
  68. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/init_sfm.py +0 -0
  69. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/mesh.py +0 -0
  70. {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/viewer.py +0 -0
@@ -0,0 +1,18 @@
1
+ # Mantener el contexto de build liviano. node_modules se reinstala dentro de la
2
+ # imagen; outputs/data/jobs y artefactos son regenerables o pesados.
3
+ .git
4
+ **/__pycache__
5
+ **/*.pyc
6
+ .venv
7
+ node_modules
8
+ frontend/node_modules
9
+ frontend/dist
10
+ outputs
11
+ data
12
+ backend/jobs
13
+ backend/jobs.db
14
+ reconstruct3d/cgal_mesh/build
15
+ dist
16
+ *.ply
17
+ *.npy
18
+ *.pkl
@@ -30,4 +30,4 @@ outputs/
30
30
 
31
31
  # OS noise
32
32
  .DS_Store
33
- Thumbs.db
33
+ Thumbs.db
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reconstruct3d
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Pipeline offline de reconstrucción 3D desde video monocular: Structure-from-Motion incremental + Bundle Adjustment + densificación MVS + mallado CGAL, con front-ends de features intercambiables (SIFT, ORB, SuperPoint+LightGlue).
5
5
  Project-URL: Homepage, https://github.com/FernandoUs/Template-SLAM
6
6
  Project-URL: Repository, https://github.com/FernandoUs/Template-SLAM
@@ -0,0 +1,2 @@
1
+ jobs/
2
+ jobs.db
@@ -0,0 +1,27 @@
1
+ # Backend FastAPI de la demo. Instala la librería reconstruct3d con el extra
2
+ # 'api' y sirve uvicorn en :8000. Contexto de build = raíz del repo.
3
+ FROM python:3.12-slim
4
+
5
+ # Dependencias nativas de OpenCV (libGL/glib) que opencv-contrib-python necesita
6
+ # en runtime. Sin esto, importar cv2 falla con "libGL.so.1 not found".
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ libgl1 \
9
+ libglib2.0-0 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ WORKDIR /app
13
+
14
+ # hatch-vcs deriva la versión del tag git, pero aquí no copiamos .git. Le damos
15
+ # una versión fija de build para que la instalación no falle (es una imagen de
16
+ # demo, no el paquete publicado en PyPI).
17
+ ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
18
+
19
+ # Instalar solo la librería + backend. Copiamos lo mínimo para aprovechar caché.
20
+ COPY pyproject.toml README.md ./
21
+ COPY reconstruct3d ./reconstruct3d
22
+ RUN pip install --no-cache-dir ".[api]"
23
+
24
+ COPY backend ./backend
25
+
26
+ EXPOSE 8000
27
+ CMD ["uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -26,6 +26,8 @@ from fastapi.responses import FileResponse
26
26
 
27
27
  from reconstruct3d import Pipeline, ALL_STAGES
28
28
 
29
+ from backend import store
30
+
29
31
  HERE = os.path.dirname(os.path.abspath(__file__))
30
32
  JOBS_DIR = os.path.join(HERE, "jobs")
31
33
  os.makedirs(JOBS_DIR, exist_ok=True)
@@ -38,8 +40,12 @@ app.add_middleware(
38
40
  allow_headers=["*"],
39
41
  )
40
42
 
41
- # Un job a la vez: la reconstrucción es intensiva en CPU/memoria.
42
- _executor = ThreadPoolExecutor(max_workers=1)
43
+ # Procesamiento en paralelo/background: varios jobs a la vez. La reconstrucción
44
+ # es intensiva en CPU/memoria, así que el número de jobs concurrentes es acotado
45
+ # y configurable por env (R3D_MAX_WORKERS). Los que excedan el cupo quedan en
46
+ # cola ("queued") y arrancan cuando se libere un worker.
47
+ _MAX_WORKERS = max(1, int(os.environ.get("R3D_MAX_WORKERS", "2")))
48
+ _executor = ThreadPoolExecutor(max_workers=_MAX_WORKERS)
43
49
  _lock = threading.Lock()
44
50
 
45
51
 
@@ -47,19 +53,23 @@ _lock = threading.Lock()
47
53
  class Job:
48
54
  id: str
49
55
  stages: List[str]
56
+ name: str = "" # nombre del video subido (para mostrar en la UI)
50
57
  status: str = "queued" # queued | running | done | error
51
58
  current: Optional[str] = None
52
59
  progress: float = 0.0
60
+ detail: str = "" # avance fino de la etapa actual (p.ej. "densificando 213/379")
53
61
  events: List[dict] = field(default_factory=list)
54
62
  artifacts: Dict[str, str] = field(default_factory=dict) # stage -> filename
55
63
  error: Optional[str] = None
56
64
  out_dir: str = ""
57
65
  created: float = field(default_factory=time.time)
66
+ last_save: float = 0.0 # para throttlear escrituras a SQLite en eventos 'progress'
58
67
 
59
68
  def public(self) -> dict:
60
69
  return {
61
- "id": self.id, "status": self.status, "stages": self.stages,
62
- "current": self.current, "progress": round(self.progress, 3),
70
+ "id": self.id, "name": self.name, "status": self.status,
71
+ "stages": self.stages, "current": self.current,
72
+ "progress": round(self.progress, 3), "detail": self.detail,
63
73
  "events": self.events[-200:], "artifacts": self.artifacts,
64
74
  "error": self.error, "created": self.created,
65
75
  }
@@ -68,22 +78,65 @@ class Job:
68
78
  JOBS: Dict[str, Job] = {}
69
79
 
70
80
 
81
+ def _load_jobs_from_db():
82
+ """Restaura los jobs persistidos al arrancar el backend.
83
+
84
+ Los jobs que quedaron 'running'/'queued' al apagarse el servidor ya no tienen
85
+ hilo vivo: se marcan como interrumpidos (no se reanudan automáticamente)."""
86
+ for d in store.load_all():
87
+ job = Job(
88
+ id=d["id"], stages=d["stages"], name=d["name"] or "",
89
+ status=d["status"], current=d["current"], progress=d["progress"],
90
+ events=d["events"], artifacts=d["artifacts"], error=d["error"],
91
+ out_dir=d["out_dir"], created=d["created"] or time.time(),
92
+ )
93
+ if job.status in ("running", "queued"):
94
+ job.status = "error"
95
+ job.current = None
96
+ job.error = "interrumpido por reinicio del servidor"
97
+ store.save(job)
98
+ JOBS[job.id] = job
99
+
100
+
101
+ _load_jobs_from_db()
102
+
103
+
71
104
  def _record_event(job: Job, ev: dict):
72
105
  """Callback on_event de la Pipeline: actualiza estado y progreso del job."""
73
106
  with _lock:
74
- job.events.append(ev)
75
107
  stage, status = ev.get("stage"), ev.get("status")
108
+ ntotal = max(1, len(job.stages))
109
+
110
+ # Eventos de avance fino DENTRO de una etapa (no se guardan en el log para
111
+ # no inundarlo): mueven la barra de forma continua y la persistencia a
112
+ # SQLite se throttlea a ~1/s para no martillar el disco.
113
+ if status == "progress":
114
+ job.detail = ev.get("message", "")
115
+ if stage in job.stages and "frac" in ev:
116
+ base = job.stages.index(stage) / ntotal
117
+ job.progress = base + float(ev["frac"]) / ntotal
118
+ now = time.time()
119
+ if now - job.last_save >= 1.0:
120
+ job.last_save = now
121
+ store.save(job)
122
+ return
123
+
124
+ # Eventos de frontera de etapa (start/done): sí van al log.
125
+ job.events.append(ev)
76
126
  if status == "start" and stage in job.stages:
77
127
  job.current = stage
128
+ job.detail = ""
78
129
  done = sum(1 for e in job.events
79
130
  if e.get("status") == "done" and e.get("stage") in job.stages)
80
- job.progress = done / max(1, len(job.stages))
131
+ job.progress = done / ntotal
132
+ store.save(job) # refleja el avance en SQLite (sobrevive reinicios)
81
133
 
82
134
 
83
135
  def _run_job(job: Job, video_path: str, frontend: str, camera, jobs: int,
84
136
  config: dict):
85
137
  with _lock:
86
138
  job.status = "running"
139
+ store.save(job)
87
140
  try:
88
141
  pipe = Pipeline(job.out_dir, frontend=frontend, camera=camera, jobs=jobs,
89
142
  on_event=lambda ev: _record_event(job, ev))
@@ -93,10 +146,12 @@ def _run_job(job: Job, video_path: str, frontend: str, camera, jobs: int,
93
146
  job.status = "done"
94
147
  job.current = None
95
148
  job.progress = 1.0
149
+ store.save(job)
96
150
  except Exception as e: # noqa: BLE001
97
151
  with _lock:
98
152
  job.status = "error"
99
153
  job.error = str(e)
154
+ store.save(job)
100
155
 
101
156
 
102
157
  @app.get("/api/health")
@@ -130,12 +185,23 @@ async def create_job(
130
185
  with open(video_path, "wb") as f:
131
186
  shutil.copyfileobj(video.file, f)
132
187
 
133
- job = Job(id=job_id, stages=stages, out_dir=out_dir)
188
+ # Calibración: camera puede ser None (defaults), "auto" (intrínsecos derivados
189
+ # de la resolución del propio video, sin tablero), o un dict de intrínsecos.
190
+ camera = cfg.get("camera")
191
+ if camera == "auto":
192
+ from reconstruct3d.calibrate import auto_intrinsics
193
+ fm = float(cfg.get("focal_mult", 1.2))
194
+ camera = auto_intrinsics(video_path, focal_mult=fm)
195
+ print(f"[job {job_id}] calibración automática: {camera['_auto']}")
196
+
197
+ job = Job(id=job_id, stages=stages, out_dir=out_dir,
198
+ name=video.filename or f"input{suffix}")
134
199
  JOBS[job_id] = job
200
+ store.save(job)
135
201
 
136
202
  _executor.submit(
137
203
  _run_job, job, video_path,
138
- cfg.get("frontend", "sift"), cfg.get("camera"), int(cfg.get("jobs", 0)),
204
+ cfg.get("frontend", "sift"), camera, int(cfg.get("jobs", 0)),
139
205
  cfg.get("config", {}),
140
206
  )
141
207
  return {"job_id": job_id, "stages": stages}
@@ -170,6 +236,7 @@ def get_artifact(job_id: str, name: str):
170
236
  @app.delete("/api/jobs/{job_id}")
171
237
  def delete_job(job_id: str):
172
238
  job = JOBS.pop(job_id, None)
239
+ store.delete(job_id)
173
240
  if job and os.path.isdir(job.out_dir):
174
241
  shutil.rmtree(job.out_dir, ignore_errors=True)
175
242
  return {"deleted": bool(job)}
@@ -0,0 +1,87 @@
1
+ """Persistencia de jobs en SQLite.
2
+
3
+ Los jobs viven en memoria mientras corren, pero se reflejan en una base SQLite
4
+ para sobrevivir reinicios del backend. Guardamos el estado completo (incluidos
5
+ eventos y artefactos) como columnas, serializando las listas/dicts a JSON.
6
+
7
+ La conexión es única y compartida entre los hilos de los workers
8
+ (`check_same_thread=False`), protegida por un lock propio.
9
+ """
10
+ import json
11
+ import os
12
+ import sqlite3
13
+ import threading
14
+
15
+ _HERE = os.path.dirname(os.path.abspath(__file__))
16
+ DB_PATH = os.environ.get("R3D_DB", os.path.join(_HERE, "jobs.db"))
17
+
18
+ _conn = sqlite3.connect(DB_PATH, check_same_thread=False)
19
+ _conn.row_factory = sqlite3.Row
20
+ _conn.execute(
21
+ """
22
+ CREATE TABLE IF NOT EXISTS jobs (
23
+ id TEXT PRIMARY KEY,
24
+ name TEXT,
25
+ status TEXT,
26
+ current TEXT,
27
+ progress REAL,
28
+ stages TEXT, -- JSON
29
+ events TEXT, -- JSON
30
+ artifacts TEXT, -- JSON
31
+ error TEXT,
32
+ out_dir TEXT,
33
+ created REAL
34
+ )
35
+ """
36
+ )
37
+ _conn.commit()
38
+ _lock = threading.Lock()
39
+
40
+
41
+ def save(job) -> None:
42
+ """Inserta o actualiza un job (upsert por id)."""
43
+ with _lock:
44
+ _conn.execute(
45
+ """
46
+ INSERT INTO jobs
47
+ (id, name, status, current, progress, stages, events,
48
+ artifacts, error, out_dir, created)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
50
+ ON CONFLICT(id) DO UPDATE SET
51
+ name=excluded.name, status=excluded.status,
52
+ current=excluded.current, progress=excluded.progress,
53
+ stages=excluded.stages, events=excluded.events,
54
+ artifacts=excluded.artifacts, error=excluded.error,
55
+ out_dir=excluded.out_dir, created=excluded.created
56
+ """,
57
+ (
58
+ job.id, job.name, job.status, job.current, job.progress,
59
+ json.dumps(job.stages), json.dumps(job.events),
60
+ json.dumps(job.artifacts), job.error, job.out_dir, job.created,
61
+ ),
62
+ )
63
+ _conn.commit()
64
+
65
+
66
+ def delete(job_id: str) -> None:
67
+ with _lock:
68
+ _conn.execute("DELETE FROM jobs WHERE id = ?", (job_id,))
69
+ _conn.commit()
70
+
71
+
72
+ def load_all() -> list[dict]:
73
+ """Devuelve todos los jobs guardados como dicts (listas/dicts ya deserializados)."""
74
+ with _lock:
75
+ rows = _conn.execute("SELECT * FROM jobs ORDER BY created").fetchall()
76
+ out = []
77
+ for r in rows:
78
+ out.append({
79
+ "id": r["id"], "name": r["name"], "status": r["status"],
80
+ "current": r["current"], "progress": r["progress"] or 0.0,
81
+ "stages": json.loads(r["stages"] or "[]"),
82
+ "events": json.loads(r["events"] or "[]"),
83
+ "artifacts": json.loads(r["artifacts"] or "{}"),
84
+ "error": r["error"], "out_dir": r["out_dir"] or "",
85
+ "created": r["created"],
86
+ })
87
+ return out
@@ -0,0 +1,37 @@
1
+ # TEMPLATE-SLAM default configuration.
2
+ #
3
+ # Every magic number that used to live in the four scripts now lives here.
4
+ # Override on the CLI with --config <path/to/other.yaml>.
5
+
6
+ camera:
7
+ fx: 696.08
8
+ fy: 696.08
9
+ cx: 393.11
10
+ cy: 238.94
11
+ # Brown-Conrady (k1, k2, p1, p2, k3).
12
+ distortion: [0.3092, -0.7429, 0.0021, -0.0149, 0.6486]
13
+
14
+ frontend:
15
+ extractor: SIFT # SIFT (only one implemented for now)
16
+ matcher: BruteForce
17
+ ratio: 0.75 # Lowe's ratio test
18
+
19
+ extraction:
20
+ frame_skip: 5 # process 1 of every N frames from the video
21
+ window: 3 # pairwise E-matrix neighborhood radius (frames)
22
+ min_matches: 5 # below this, skip the pair
23
+
24
+ geometry:
25
+ essential:
26
+ prob: 0.999
27
+ threshold: 1.0 # px
28
+ pnp:
29
+ reprojection_error: 5.0
30
+ min_obs: 15 # need at least N 3D<->2D matches before solving PnP
31
+ min_inliers: 20 # accept the pose only if RANSAC keeps >= N inliers
32
+
33
+ io:
34
+ database_path: outputs/sfm_data.pkl
35
+ map_state_path: outputs/map_state.npy
36
+ initial_cloud_path: outputs/init_cloud.ply
37
+ tracked_cloud_path: outputs/tracked_cloud.ply
@@ -0,0 +1,45 @@
1
+ # iPhone — calibración promediada (estimada vía FOV + checkerboard).
2
+ # Fuente: config/iPhoneavg.yaml (formato ORB-SLAM3), convertido al
3
+ # layout que espera template_slam.
4
+ #
5
+ # Resolución del video al que corresponde: 540 x 960 (portrait).
6
+ # Si tu video tiene OTRA resolución, hay que re-calibrar o escalar
7
+ # fx, fy, cx, cy proporcionalmente.
8
+
9
+ camera:
10
+ fx: 750.6
11
+ fy: 750.6
12
+ cx: 229.3
13
+ cy: 435.1
14
+ # Distorsión en 0 — la calibración "real" estaba sobreajustada,
15
+ # así que en la versión promedio se desactivó.
16
+ distortion: [0.0, 0.0, 0.0, 0.0, 0.0]
17
+
18
+ frontend:
19
+ extractor: SIFT
20
+ matcher: BruteForce
21
+ ratio: 0.75
22
+
23
+ extraction:
24
+ # Para videos de iPhone (24 fps, mucho movimiento de mano) conviene
25
+ # un salto más alto que el default. Súbelo a 15-20 si sigue lento.
26
+ frame_skip: 20
27
+ window: 3
28
+ min_matches: 5
29
+
30
+ geometry:
31
+ essential:
32
+ prob: 0.999
33
+ threshold: 1.0
34
+ pnp:
35
+ reprojection_error: 5.0
36
+ min_obs: 15
37
+ min_inliers: 20
38
+
39
+ # Rutas namespaced con prefijo 'iphone_' para no pisar las salidas
40
+ # del default.yaml si corres ambas calibraciones en paralelo.
41
+ io:
42
+ database_path: outputs/iphone_sfm_data.pkl
43
+ map_state_path: outputs/iphone_map_state.npy
44
+ initial_cloud_path: outputs/iphone_init_cloud.ply
45
+ tracked_cloud_path: outputs/iphone_tracked_cloud.ply
@@ -0,0 +1,23 @@
1
+ # `data/` — Videos de entrada
2
+
3
+ Pon aquí los videos que quieras procesar con el pipeline. La carpeta
4
+ está en `.gitignore`, así que los archivos no se suben al repo (los
5
+ videos suelen pesar demasiado para git).
6
+
7
+ ## Uso
8
+
9
+ ```bash
10
+ python apps/extract_features.py --video data/mi_video.mp4
11
+ python apps/initialize_map.py
12
+ python apps/track.py
13
+ python apps/view.py --video data/mi_video.mp4
14
+ ```
15
+
16
+ ## Convención sugerida
17
+
18
+ * Un video por archivo, nombre descriptivo (`utec_aula.mp4`,
19
+ `iphone_calle.mp4`, ...).
20
+ * Si tienes varias cámaras con calibraciones distintas, crea un YAML
21
+ por cámara en `config/` (p. ej. `config/utec.yaml`, `config/iphone.yaml`)
22
+ y pásalo con `--config` en los cuatro pasos del pipeline.
23
+
@@ -0,0 +1,39 @@
1
+ # Demo completa con un solo comando:
2
+ #
3
+ # docker compose up --build
4
+ #
5
+ # Frontend: http://localhost:5173 (UI; sus llamadas /api van al backend)
6
+ # Backend: http://localhost:8000 (API FastAPI; /api/health para probar)
7
+ #
8
+ # Para apagar: docker compose down
9
+
10
+ services:
11
+ backend:
12
+ build:
13
+ context: .
14
+ dockerfile: backend/Dockerfile
15
+ ports:
16
+ - "8000:8000"
17
+ volumes:
18
+ # Código montado para hot-reload en desarrollo.
19
+ - ./backend:/app/backend
20
+ - ./reconstruct3d:/app/reconstruct3d
21
+ # Los jobs (videos subidos + nubes generadas) persisten en el host.
22
+ - ./backend/jobs:/app/backend/jobs
23
+ command: uvicorn backend.app:app --host 0.0.0.0 --port 8000 --reload
24
+
25
+ frontend:
26
+ build:
27
+ context: .
28
+ dockerfile: frontend/Dockerfile
29
+ ports:
30
+ - "5173:5173"
31
+ environment:
32
+ # Dentro de la red de Compose el backend es alcanzable por su nombre.
33
+ VITE_PROXY_TARGET: http://backend:8000
34
+ volumes:
35
+ # Código montado para hot-reload; node_modules se queda el del contenedor.
36
+ - ./frontend:/app
37
+ - /app/node_modules
38
+ depends_on:
39
+ - backend
@@ -0,0 +1,58 @@
1
+ """Ejemplo mínimo de uso de reconstruct3d como librería instalada.
2
+
3
+ Sirve para verificar que el paquete se instaló bien y funciona "en cualquier lado":
4
+
5
+ pip install reconstruct3d # (cuando esté publicado en PyPI)
6
+ python ejemplo_uso.py ruta/a/tu/video.mp4
7
+
8
+ O sin video, solo para comprobar que la importación funciona:
9
+
10
+ python ejemplo_uso.py --check
11
+ """
12
+ import sys
13
+
14
+ import reconstruct3d
15
+ from reconstruct3d import Pipeline
16
+
17
+
18
+ def check():
19
+ """Comprueba que el paquete importa y muestra qué hay disponible."""
20
+ print(f"reconstruct3d {reconstruct3d.__version__} importado correctamente ✅")
21
+ print("Etapas disponibles:", reconstruct3d.ALL_STAGES)
22
+ print("Front-ends: sift (default), orb, spglue (requiere extra 'spglue')")
23
+
24
+
25
+ def reconstruir(video: str):
26
+ """Corre la reconstrucción end-to-end sobre un video y reporta progreso."""
27
+
28
+ def on_event(ev):
29
+ # ev = {stage, status, message, ...}
30
+ print(f"[{ev['stage']:>8}] {ev['status']:<5} {ev.get('message', '')}")
31
+
32
+ pipe = Pipeline(
33
+ out_dir="outputs/ejemplo", # carpeta donde se escriben los artefactos
34
+ frontend="sift", # 'sift' | 'orb' | 'spglue'
35
+ camera=None, # o "camera.json" con tus intrínsecos
36
+ jobs=0, # 0 = auto (paraleliza extract/dense)
37
+ on_event=on_event,
38
+ )
39
+
40
+ # Etapas: extract -> init -> track -> ba -> dense -> mesh.
41
+ # Quita 'dense'/'mesh' si quieres algo más rápido (nube rala).
42
+ pipe.run(
43
+ video,
44
+ stages=["extract", "init", "track", "ba"],
45
+ config={"extract": {"k_skip": 15, "m_window": 4}},
46
+ )
47
+
48
+ print("\nArtefactos generados:")
49
+ for nombre, ruta in pipe.artifacts().items():
50
+ print(f" {nombre}: {ruta}")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ if len(sys.argv) < 2 or sys.argv[1] in ("--check", "-h", "--help"):
55
+ check()
56
+ print("\nUso: python ejemplo_uso.py ruta/a/video.mp4")
57
+ else:
58
+ reconstruir(sys.argv[1])
@@ -0,0 +1,14 @@
1
+ # Frontend Vite + React en modo desarrollo (hot reload). El proxy /api se
2
+ # redirige al backend vía VITE_PROXY_TARGET (lo fija docker-compose).
3
+ FROM node:20-slim
4
+
5
+ WORKDIR /app
6
+
7
+ # Instalar deps primero para cachear la capa de node_modules.
8
+ COPY frontend/package.json frontend/package-lock.json ./
9
+ RUN npm install
10
+
11
+ COPY frontend ./
12
+
13
+ EXPOSE 5173
14
+ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ }
7
+ }
8
+ }