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.
- reconstruct3d-0.1.2/.dockerignore +18 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.gitignore +1 -1
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/PKG-INFO +1 -1
- reconstruct3d-0.1.2/backend/.gitignore +2 -0
- reconstruct3d-0.1.2/backend/Dockerfile +27 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/backend/app.py +75 -8
- reconstruct3d-0.1.2/backend/store.py +87 -0
- reconstruct3d-0.1.2/config/default.yaml +37 -0
- reconstruct3d-0.1.2/config/iphone.yaml +45 -0
- reconstruct3d-0.1.2/data/README.md +23 -0
- reconstruct3d-0.1.2/docker-compose.yml +39 -0
- reconstruct3d-0.1.2/examples/ejemplo_uso.py +58 -0
- reconstruct3d-0.1.2/frontend/Dockerfile +14 -0
- reconstruct3d-0.1.2/frontend/jsconfig.json +8 -0
- reconstruct3d-0.1.2/frontend/package-lock.json +3432 -0
- reconstruct3d-0.1.2/frontend/package.json +34 -0
- reconstruct3d-0.1.2/frontend/postcss.config.js +6 -0
- reconstruct3d-0.1.2/frontend/src/App.jsx +291 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/Viewer.jsx +1 -1
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/api.js +14 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/badge.jsx +25 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/button.jsx +35 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/card.jsx +34 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/checkbox.jsx +24 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/input.jsx +17 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/label.jsx +14 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/progress.jsx +19 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/scroll-area.jsx +21 -0
- reconstruct3d-0.1.2/frontend/src/components/ui/select.jsx +69 -0
- reconstruct3d-0.1.2/frontend/src/index.css +43 -0
- reconstruct3d-0.1.2/frontend/src/lib/utils.js +8 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/src/main.jsx +1 -1
- reconstruct3d-0.1.2/frontend/tailwind.config.js +31 -0
- reconstruct3d-0.1.2/frontend/vite.config.js +24 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/__init__.py +8 -1
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/api.py +6 -3
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/calibrate.py +36 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/core.py +28 -6
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/dense_mvs.py +60 -19
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/track_sfm.py +12 -1
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/uv.lock +1 -2
- reconstruct3d-0.1.0/backend/.gitignore +0 -1
- reconstruct3d-0.1.0/frontend/package-lock.json +0 -1687
- reconstruct3d-0.1.0/frontend/package.json +0 -20
- reconstruct3d-0.1.0/frontend/src/App.jsx +0 -196
- reconstruct3d-0.1.0/frontend/src/styles.css +0 -77
- reconstruct3d-0.1.0/frontend/vite.config.js +0 -13
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.github/workflows/publish.yml +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/.python-version +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/CLAUDE.md +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/LICENSE +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/README.md +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/backend/__init__.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/camera.example.json +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/camera.json +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/data/.gitkeep +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/docs.md +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/.gitignore +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/frontend/index.html +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/pipeline.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/pyproject.toml +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/bundle_adjust.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/.gitignore +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/CMakeLists.txt +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cgal_mesh/mesh_reconstruct.cpp +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/chunked.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/cli.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/init_sfm.py +0 -0
- {reconstruct3d-0.1.0 → reconstruct3d-0.1.2}/reconstruct3d/mesh.py +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reconstruct3d
|
|
3
|
-
Version: 0.1.
|
|
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,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
|
-
#
|
|
42
|
-
|
|
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, "
|
|
62
|
-
"
|
|
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 /
|
|
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
|
-
|
|
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"),
|
|
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"]
|