reverse3d 0.9.0__tar.gz → 0.11.0__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.
- {reverse3d-0.9.0/reverse3d.egg-info → reverse3d-0.11.0}/PKG-INFO +24 -3
- {reverse3d-0.9.0 → reverse3d-0.11.0}/README.md +22 -2
- {reverse3d-0.9.0 → reverse3d-0.11.0}/pyproject.toml +1 -0
- reverse3d-0.11.0/reverse3d/_version.py +1 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/doctor.py +12 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/offline.py +65 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/recipes.py +85 -7
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reconstruction/photogrammetry.py +45 -3
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reconstruction/silhouette.py +123 -6
- reverse3d-0.11.0/reverse3d/reconstruction/texture.py +195 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/rendering/_blender_job.py +7 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0/reverse3d.egg-info}/PKG-INFO +24 -3
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d.egg-info/SOURCES.txt +2 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d.egg-info/requires.txt +1 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_color.py +15 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_silhouette.py +57 -0
- reverse3d-0.11.0/tests/test_texture.py +36 -0
- reverse3d-0.9.0/reverse3d/_version.py +0 -1
- {reverse3d-0.9.0 → reverse3d-0.11.0}/LICENSE +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/NOTICE +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/__main__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/_about.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/compact.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/mcp_server.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/prompts.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/skill/SKILL.md +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/agent/tools.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/autopilot.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/cli.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/config.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/errors.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/hardware.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/imaging.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/logging_utils.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/mesh_io.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/profiles.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/py.typed +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reconstruction/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reconstruction/depth_estimation.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reconstruction/pointcloud.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/rendering/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/rendering/blender_render.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/rendering/preview.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reverse_eng/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reverse_eng/geometry_fit.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reverse_eng/measure.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reverse_eng/mesh_cleaner.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/reverse_eng/parametric.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/scenes.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/security.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/setup_agents.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/userconfig.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/vision/__init__.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/vision/classifier.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/vision/feature_matcher.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/vision/mask_extractor.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/vision/segmenter.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d/watch.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d.egg-info/dependency_links.txt +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d.egg-info/entry_points.txt +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/reverse3d.egg-info/top_level.txt +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/setup.cfg +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_agent_tools.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_authorship.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_automation.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_cli.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_cli_automation.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_config.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_geometry_fit.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_heic.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_mesh.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_offline_autopilot.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_optional_extras.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_reconstruction.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_render.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_scenes.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_security.py +0 -0
- {reverse3d-0.9.0 → reverse3d-0.11.0}/tests/test_vision.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reverse3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: AI-agent-ready toolkit for 3D reverse engineering: image analysis, photogrammetry, mesh repair, primitive fitting and headless rendering.
|
|
5
5
|
Author-email: Voxel Enterprise <info@voxel.lat>
|
|
6
6
|
Maintainer-email: Voxel Enterprise <info@voxel.lat>
|
|
@@ -32,6 +32,7 @@ License-File: NOTICE
|
|
|
32
32
|
Requires-Dist: numpy>=1.26
|
|
33
33
|
Requires-Dist: scipy>=1.11
|
|
34
34
|
Requires-Dist: scikit-image>=0.22
|
|
35
|
+
Requires-Dist: xatlas>=0.0.9
|
|
35
36
|
Requires-Dist: opencv-python-headless>=4.9
|
|
36
37
|
Requires-Dist: pillow>=10.3
|
|
37
38
|
Requires-Dist: pillow-heif>=1.1
|
|
@@ -96,6 +97,7 @@ Desarrollado por **[Voxel Enterprise](https://voxel.lat)**
|
|
|
96
97
|
- [💰 Ahorro de tokens](#-ahorro-de-tokens)
|
|
97
98
|
- [🔌 Sin internet (modo offline)](#-sin-internet-modo-offline)
|
|
98
99
|
- [🤖 Piloto automático](#-piloto-automático)
|
|
100
|
+
- [🧠 Reverse3D Core (motor exclusivo)](#-reverse3d-core-motor-exclusivo)
|
|
99
101
|
- [Capacidades](#capacidades)
|
|
100
102
|
- [Herramientas externas opcionales](#herramientas-externas-opcionales)
|
|
101
103
|
- [Referencia de la CLI](#referencia-de-la-cli)
|
|
@@ -219,7 +221,7 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
|
|
|
219
221
|
| `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
|
|
220
222
|
| `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
|
|
221
223
|
| `--no-tools` | `-NoTools` | No instalar Blender ni COLMAP |
|
|
222
|
-
| *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación y
|
|
224
|
+
| *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación, profundidad y puntos con IA, unos 800 MB) para que todo funcione sin internet |
|
|
223
225
|
| `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
|
|
224
226
|
| `--with recon,depth` | `-With recon,depth` | Extras a elección |
|
|
225
227
|
| `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
|
|
@@ -358,7 +360,7 @@ reverse3d config set verbosity full # si alguna vez necesitas el detalle com
|
|
|
358
360
|
|
|
359
361
|
## 🔌 Sin internet (modo offline)
|
|
360
362
|
|
|
361
|
-
reverse3d **funciona sin conexión**. Todo el cálculo es local (OpenCV, COLMAP, Open3D, trimesh, Blender). Lo único que se descarga
|
|
363
|
+
reverse3d **funciona sin conexión**. Todo el cálculo es local (OpenCV, COLMAP, Open3D, trimesh, Blender). Lo único que se descarga son los modelos de IA local (sujeto, recorte, profundidad y los puntos ALIKED + LightGlue), y **el instalador los deja descargados**.
|
|
362
364
|
|
|
363
365
|
- **Cambio automático:** si el equipo no detecta internet, reverse3d pasa solo a modo offline. No tienes que hacer nada.
|
|
364
366
|
- **Comprobar:** `reverse3d offline status` dice si hay conexión y qué funciona sin ella. `reverse3d doctor` lo incluye en su informe.
|
|
@@ -407,6 +409,23 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
407
409
|
|
|
408
410
|
---
|
|
409
411
|
|
|
412
|
+
## 🧠 Reverse3D Core (motor exclusivo)
|
|
413
|
+
|
|
414
|
+
**Reverse3D Core** es el motor de reconstrucción propio de Voxel Enterprise. Es **propietario** (no forma parte de este repositorio de código abierto) y viene **incluido en reverse3d Studio**, que lo instala solo.
|
|
415
|
+
|
|
416
|
+
- **Cuando está instalado**, reverse3d lo usa automáticamente para fusionar las siluetas de los objetos. Aprende qué fotos son fiables, así que un brillo, un mal recorte o una foto mal ubicada **ya no borran el objeto**, y cada foto extra suma precisión.
|
|
417
|
+
- **Sin él**, reverse3d funciona igual con su método abierto.
|
|
418
|
+
- **Comprobarlo:** `reverse3d doctor` muestra `core: {available: true, version: …}`. El informe de cada trabajo indica qué motor se usó (`details.silhouette.engine`).
|
|
419
|
+
|
|
420
|
+
| Prueba interna (objeto de geometría conocida) | Método abierto | Reverse3D Core |
|
|
421
|
+
|---|---|---|
|
|
422
|
+
| Recortes perfectos | 0,943 | 0,941 |
|
|
423
|
+
| 15 % de recortes defectuosos | 0,000 (pierde el objeto) | **0,921** |
|
|
424
|
+
|
|
425
|
+
*(Coincidencia volumétrica, IoU: 1 = idéntico.)*
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
410
429
|
## Capacidades
|
|
411
430
|
|
|
412
431
|
| Área | Qué hace | Tecnología |
|
|
@@ -416,6 +435,8 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
416
435
|
| 🧱 **Plano → pieza** | Extrusión de contornos a sólido 3D con escala real | Shapely + trimesh |
|
|
417
436
|
| 📷 **Fotogrametría** | Fotos → SfM → MVS denso → nube → malla | COLMAP + Open3D |
|
|
418
437
|
| 🌊 **Profundidad con IA** | Una foto → mapa de profundidad → relieve 2.5D | Depth Anything V2 |
|
|
438
|
+
| 🎨 **Texturas** | Textura fotográfica 2048² desde las propias fotos (UV, visibilidad, sin costuras) en `model.glb` | xatlas + OpenCV |
|
|
439
|
+
| 🧠 **Puntos con IA** | Rescate automático de capturas difíciles con puntos y emparejamiento aprendidos | ALIKED + LightGlue (ONNX) |
|
|
419
440
|
| 🛠️ **Reparación de mallas** | Limpieza, restos sueltos, agujeros, normales, Taubin, decimación, alineación OBB/PCA, escala | trimesh |
|
|
420
441
|
| 📏 **Metrología** | Cotas AABB/OBB, área, volumen, centro de masa, estanqueidad | trimesh |
|
|
421
442
|
| 📐 **Primitivas** | Planos, cilindros y esferas con RANSAC (radios, ejes, normales) | NumPy |
|
|
@@ -28,6 +28,7 @@ Desarrollado por **[Voxel Enterprise](https://voxel.lat)**
|
|
|
28
28
|
- [💰 Ahorro de tokens](#-ahorro-de-tokens)
|
|
29
29
|
- [🔌 Sin internet (modo offline)](#-sin-internet-modo-offline)
|
|
30
30
|
- [🤖 Piloto automático](#-piloto-automático)
|
|
31
|
+
- [🧠 Reverse3D Core (motor exclusivo)](#-reverse3d-core-motor-exclusivo)
|
|
31
32
|
- [Capacidades](#capacidades)
|
|
32
33
|
- [Herramientas externas opcionales](#herramientas-externas-opcionales)
|
|
33
34
|
- [Referencia de la CLI](#referencia-de-la-cli)
|
|
@@ -151,7 +152,7 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
|
|
|
151
152
|
| `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
|
|
152
153
|
| `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
|
|
153
154
|
| `--no-tools` | `-NoTools` | No instalar Blender ni COLMAP |
|
|
154
|
-
| *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación y
|
|
155
|
+
| *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación, profundidad y puntos con IA, unos 800 MB) para que todo funcione sin internet |
|
|
155
156
|
| `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
|
|
156
157
|
| `--with recon,depth` | `-With recon,depth` | Extras a elección |
|
|
157
158
|
| `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
|
|
@@ -290,7 +291,7 @@ reverse3d config set verbosity full # si alguna vez necesitas el detalle com
|
|
|
290
291
|
|
|
291
292
|
## 🔌 Sin internet (modo offline)
|
|
292
293
|
|
|
293
|
-
reverse3d **funciona sin conexión**. Todo el cálculo es local (OpenCV, COLMAP, Open3D, trimesh, Blender). Lo único que se descarga
|
|
294
|
+
reverse3d **funciona sin conexión**. Todo el cálculo es local (OpenCV, COLMAP, Open3D, trimesh, Blender). Lo único que se descarga son los modelos de IA local (sujeto, recorte, profundidad y los puntos ALIKED + LightGlue), y **el instalador los deja descargados**.
|
|
294
295
|
|
|
295
296
|
- **Cambio automático:** si el equipo no detecta internet, reverse3d pasa solo a modo offline. No tienes que hacer nada.
|
|
296
297
|
- **Comprobar:** `reverse3d offline status` dice si hay conexión y qué funciona sin ella. `reverse3d doctor` lo incluye en su informe.
|
|
@@ -339,6 +340,23 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
339
340
|
|
|
340
341
|
---
|
|
341
342
|
|
|
343
|
+
## 🧠 Reverse3D Core (motor exclusivo)
|
|
344
|
+
|
|
345
|
+
**Reverse3D Core** es el motor de reconstrucción propio de Voxel Enterprise. Es **propietario** (no forma parte de este repositorio de código abierto) y viene **incluido en reverse3d Studio**, que lo instala solo.
|
|
346
|
+
|
|
347
|
+
- **Cuando está instalado**, reverse3d lo usa automáticamente para fusionar las siluetas de los objetos. Aprende qué fotos son fiables, así que un brillo, un mal recorte o una foto mal ubicada **ya no borran el objeto**, y cada foto extra suma precisión.
|
|
348
|
+
- **Sin él**, reverse3d funciona igual con su método abierto.
|
|
349
|
+
- **Comprobarlo:** `reverse3d doctor` muestra `core: {available: true, version: …}`. El informe de cada trabajo indica qué motor se usó (`details.silhouette.engine`).
|
|
350
|
+
|
|
351
|
+
| Prueba interna (objeto de geometría conocida) | Método abierto | Reverse3D Core |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| Recortes perfectos | 0,943 | 0,941 |
|
|
354
|
+
| 15 % de recortes defectuosos | 0,000 (pierde el objeto) | **0,921** |
|
|
355
|
+
|
|
356
|
+
*(Coincidencia volumétrica, IoU: 1 = idéntico.)*
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
342
360
|
## Capacidades
|
|
343
361
|
|
|
344
362
|
| Área | Qué hace | Tecnología |
|
|
@@ -348,6 +366,8 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
348
366
|
| 🧱 **Plano → pieza** | Extrusión de contornos a sólido 3D con escala real | Shapely + trimesh |
|
|
349
367
|
| 📷 **Fotogrametría** | Fotos → SfM → MVS denso → nube → malla | COLMAP + Open3D |
|
|
350
368
|
| 🌊 **Profundidad con IA** | Una foto → mapa de profundidad → relieve 2.5D | Depth Anything V2 |
|
|
369
|
+
| 🎨 **Texturas** | Textura fotográfica 2048² desde las propias fotos (UV, visibilidad, sin costuras) en `model.glb` | xatlas + OpenCV |
|
|
370
|
+
| 🧠 **Puntos con IA** | Rescate automático de capturas difíciles con puntos y emparejamiento aprendidos | ALIKED + LightGlue (ONNX) |
|
|
351
371
|
| 🛠️ **Reparación de mallas** | Limpieza, restos sueltos, agujeros, normales, Taubin, decimación, alineación OBB/PCA, escala | trimesh |
|
|
352
372
|
| 📏 **Metrología** | Cotas AABB/OBB, área, volumen, centro de masa, estanqueidad | trimesh |
|
|
353
373
|
| 📐 **Primitivas** | Planos, cilindros y esferas con RANSAC (radios, ejes, normales) | NumPy |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.0"
|
|
@@ -41,6 +41,16 @@ def _package_version(dist: str) -> str | None:
|
|
|
41
41
|
return "installed"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def core_status() -> dict[str, object]:
|
|
45
|
+
"""Whether reverse3d-core, the proprietary reconstruction engine, is active."""
|
|
46
|
+
from reverse3d.reconstruction.silhouette import core_engine
|
|
47
|
+
|
|
48
|
+
core = core_engine()
|
|
49
|
+
if core is None:
|
|
50
|
+
return {"available": False, "hint": "Included in reverse3d Studio."}
|
|
51
|
+
return {"available": True, "version": str(getattr(core, "__version__", "?"))}
|
|
52
|
+
|
|
53
|
+
|
|
44
54
|
def environment_report(settings: Settings, *, include_offline: bool = True) -> dict[str, object]:
|
|
45
55
|
"""Return a JSON-serialisable description of the runtime environment.
|
|
46
56
|
|
|
@@ -70,6 +80,8 @@ def environment_report(settings: Settings, *, include_offline: bool = True) -> d
|
|
|
70
80
|
"reverse3d": __version__,
|
|
71
81
|
"author": f"{AUTHOR} ({AUTHOR_URL})",
|
|
72
82
|
"official_source": OFFICIAL_REPOSITORY,
|
|
83
|
+
# Voxel Enterprise's proprietary engine (bundled with reverse3d Studio).
|
|
84
|
+
"core": core_status(),
|
|
73
85
|
"python": sys.version.split()[0],
|
|
74
86
|
"platform": platform.platform(),
|
|
75
87
|
"workspace": str(settings.workspace),
|
|
@@ -59,6 +59,55 @@ MODEL_REVISIONS: dict[str, str] = {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
#: Learned feature models COLMAP runs through ONNX (ALIKED, BSD-3; LightGlue,
|
|
63
|
+
#: Apache-2.0): (download URL, pinned SHA-256). Used to rescue hard photo sets.
|
|
64
|
+
COLMAP_MODELS: dict[str, tuple[str, str]] = {
|
|
65
|
+
"aliked-n16rot.onnx": (
|
|
66
|
+
"https://github.com/colmap/colmap/releases/download/3.13.0/aliked-n16rot.onnx",
|
|
67
|
+
"39c423d0a6f03d39ec89d3d1d61853765c2fb6a8b8381376c703e5758778a547",
|
|
68
|
+
),
|
|
69
|
+
"aliked-lightglue.onnx": (
|
|
70
|
+
"https://github.com/colmap/colmap/releases/download/3.13.0/aliked-lightglue.onnx",
|
|
71
|
+
"b9a5de7204648b18a8cf5dcac819f9d30de1a5961ef03756803c8b86c2dceb8d",
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def colmap_model_path(name: str) -> Path | None:
|
|
77
|
+
"""Local copy of a COLMAP ONNX model, if downloaded (filesystem check only)."""
|
|
78
|
+
path = models_dir() / "colmap" / name
|
|
79
|
+
return path if path.is_file() else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def download_colmap_models() -> dict[str, str]:
|
|
83
|
+
"""Download the learned-feature models, verifying their pinned SHA-256."""
|
|
84
|
+
import hashlib
|
|
85
|
+
import urllib.request
|
|
86
|
+
|
|
87
|
+
status: dict[str, str] = {}
|
|
88
|
+
folder = models_dir() / "colmap"
|
|
89
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
for name, (url, sha) in COLMAP_MODELS.items():
|
|
91
|
+
target = folder / name
|
|
92
|
+
if target.is_file() and hashlib.sha256(target.read_bytes()).hexdigest() == sha:
|
|
93
|
+
status[name] = "ready"
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
with urllib.request.urlopen(url, timeout=120) as response: # noqa: S310 # nosec B310 - pinned https URL, SHA-256 checked
|
|
97
|
+
data = response.read()
|
|
98
|
+
except Exception as exc: # network errors come in many shapes
|
|
99
|
+
status[name] = f"failed ({exc.__class__.__name__})"
|
|
100
|
+
continue
|
|
101
|
+
if hashlib.sha256(data).hexdigest() != sha:
|
|
102
|
+
status[name] = "failed (checksum mismatch)"
|
|
103
|
+
continue
|
|
104
|
+
tmp = target.with_suffix(".part")
|
|
105
|
+
tmp.write_bytes(data)
|
|
106
|
+
tmp.replace(target)
|
|
107
|
+
status[name] = "downloaded"
|
|
108
|
+
return status
|
|
109
|
+
|
|
110
|
+
|
|
62
111
|
def models_dir() -> Path:
|
|
63
112
|
"""Folder that holds the downloaded AI models (``~/.reverse3d/models``)."""
|
|
64
113
|
return home_dir() / "models"
|
|
@@ -263,6 +312,15 @@ def prepare(settings: Settings, *, include_non_commercial: bool = False) -> dict
|
|
|
263
312
|
}
|
|
264
313
|
)
|
|
265
314
|
|
|
315
|
+
if online or all(colmap_model_path(n) for n in COLMAP_MODELS):
|
|
316
|
+
for name, state in download_colmap_models().items():
|
|
317
|
+
results.append({"model": f"colmap/{name}", "status": state.split(" ")[0]})
|
|
318
|
+
else:
|
|
319
|
+
results += [
|
|
320
|
+
{"model": f"colmap/{n}", "status": "missing", "detail": "No internet connection."}
|
|
321
|
+
for n in COLMAP_MODELS
|
|
322
|
+
if not colmap_model_path(n)
|
|
323
|
+
]
|
|
266
324
|
ready = all(r["status"] in ("ready", "downloaded") for r in results)
|
|
267
325
|
return {
|
|
268
326
|
"ready": ready,
|
|
@@ -294,6 +352,7 @@ def offline_status(settings: Settings) -> dict[str, Any]:
|
|
|
294
352
|
"ai_segmentation": DEFAULT_SEGMENTER,
|
|
295
353
|
"ai_depth": _default_model(),
|
|
296
354
|
}
|
|
355
|
+
features_ready = all(colmap_model_path(n) for n in COLMAP_MODELS)
|
|
297
356
|
on_disk = {key: local_model_path(model) is not None for key, model in ai_models.items()}
|
|
298
357
|
internet = None if settings.offline != "auto" else has_internet()
|
|
299
358
|
|
|
@@ -310,6 +369,7 @@ def offline_status(settings: Settings) -> dict[str, Any]:
|
|
|
310
369
|
key: {"available": depth_installed, "offline": depth_installed and on_disk[key]}
|
|
311
370
|
for key in ai_models
|
|
312
371
|
},
|
|
372
|
+
"ai_features": {"available": True, "offline": features_ready},
|
|
313
373
|
}
|
|
314
374
|
missing = []
|
|
315
375
|
names = {
|
|
@@ -321,6 +381,11 @@ def offline_status(settings: Settings) -> dict[str, Any]:
|
|
|
321
381
|
missing.append(
|
|
322
382
|
"AI depth model not downloaded yet: run `reverse3d offline prepare` while online."
|
|
323
383
|
)
|
|
384
|
+
if not features_ready:
|
|
385
|
+
missing.append(
|
|
386
|
+
"AI feature models (ALIKED + LightGlue) not downloaded yet: "
|
|
387
|
+
"run `reverse3d offline prepare` while online."
|
|
388
|
+
)
|
|
324
389
|
for key in ("ai_subject", "ai_segmentation"):
|
|
325
390
|
if depth_installed and not on_disk[key]:
|
|
326
391
|
missing.append(
|
|
@@ -36,6 +36,7 @@ from reverse3d.config import Settings
|
|
|
36
36
|
from reverse3d.errors import (
|
|
37
37
|
CaptureQualityError,
|
|
38
38
|
DependencyMissingError,
|
|
39
|
+
ExternalToolError,
|
|
39
40
|
InputValidationError,
|
|
40
41
|
ProcessingError,
|
|
41
42
|
Reverse3DError,
|
|
@@ -161,7 +162,7 @@ def _clean(
|
|
|
161
162
|
import trimesh
|
|
162
163
|
|
|
163
164
|
glb = dst.with_name("model.glb")
|
|
164
|
-
trimesh.load(color_ply, process=False, force="mesh")
|
|
165
|
+
_export_glb(trimesh.load(color_ply, process=False, force="mesh"), glb)
|
|
165
166
|
run.outputs["model_color"] = str(color_ply)
|
|
166
167
|
run.outputs["model_glb"] = str(glb)
|
|
167
168
|
except Exception as exc: # colour is a bonus: never fail the model for it
|
|
@@ -169,6 +170,27 @@ def _clean(
|
|
|
169
170
|
return report.after
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
# SIFT features per photo when COLMAP only places the cameras.
|
|
174
|
+
_CAMERA_FEATURES = 4096
|
|
175
|
+
|
|
176
|
+
# Exhaustive matching stays practical for object captures up to this size.
|
|
177
|
+
_OBJECT_EXHAUSTIVE_LIMIT = 400
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _export_glb(mesh: Any, path: Path) -> None:
|
|
181
|
+
"""Write a GLB that stands up in any viewer: glTF is Y-up, our models are Z-up.
|
|
182
|
+
|
|
183
|
+
The geometry keeps its Z-up coordinates and a root node rotates it, so
|
|
184
|
+
Blender, game engines and web viewers show it upright.
|
|
185
|
+
"""
|
|
186
|
+
import trimesh
|
|
187
|
+
|
|
188
|
+
scene = trimesh.Scene()
|
|
189
|
+
up = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
|
|
190
|
+
scene.add_geometry(mesh, node_name="model", transform=up)
|
|
191
|
+
scene.export(path)
|
|
192
|
+
|
|
193
|
+
|
|
172
194
|
def _has_vertex_colors(path: Path) -> bool:
|
|
173
195
|
import trimesh
|
|
174
196
|
|
|
@@ -253,6 +275,30 @@ _DEFAULT_LOOK = {
|
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
|
|
278
|
+
def _texture(run: _Run, model: Path, photo: Any, to_world: list[list[float]], scale: float) -> None:
|
|
279
|
+
"""Bake a photo texture onto the final model: model.glb gets real detail."""
|
|
280
|
+
try:
|
|
281
|
+
import trimesh
|
|
282
|
+
|
|
283
|
+
from reverse3d.reconstruction.texture import bake_texture
|
|
284
|
+
|
|
285
|
+
mesh: Any = trimesh.load(model, process=False, force="mesh")
|
|
286
|
+
# The cleaned model is the carved hull scaled by ``scale``.
|
|
287
|
+
world = np.asarray(to_world) @ np.diag([1 / scale, 1 / scale, 1 / scale, 1.0])
|
|
288
|
+
textured = run.step(
|
|
289
|
+
"texture",
|
|
290
|
+
lambda: bake_texture(
|
|
291
|
+
mesh, Path(photo.images_dir), Path(photo.sparse_text_dir), to_world=world
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
glb = model.with_name("model.glb")
|
|
295
|
+
_export_glb(textured, glb)
|
|
296
|
+
run.outputs["model_glb"] = str(glb)
|
|
297
|
+
run.details["texture"] = {"size": 2048, "faces": len(textured.faces)}
|
|
298
|
+
except Exception as exc: # the texture is a bonus: never fail the model for it
|
|
299
|
+
logger.info("Texture skipped: %s", exc)
|
|
300
|
+
|
|
301
|
+
|
|
256
302
|
def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
257
303
|
from reverse3d.reconstruction.photogrammetry import (
|
|
258
304
|
PhotogrammetryOptions,
|
|
@@ -303,27 +349,51 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
303
349
|
# and the shape is carved from the silhouettes, which also works on plain,
|
|
304
350
|
# white or glossy surfaces. Open scenes keep classic dense reconstruction.
|
|
305
351
|
silhouette = scene in ("object", "character")
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
# An object is shot in several rings plus close-ups, never in one ordered
|
|
353
|
+
# sequence: match every photo with every other so all of them join one model
|
|
354
|
+
# (sequential matching split a 153-photo bottle into four pieces).
|
|
355
|
+
matcher = preset.matcher
|
|
356
|
+
if silhouette and matcher == "auto" and analysis.image_count <= _OBJECT_EXHAUSTIVE_LIMIT:
|
|
357
|
+
matcher = "exhaustive"
|
|
358
|
+
|
|
359
|
+
def photogrammetry(classic: bool, features: str = "sift") -> PhotogrammetryResult:
|
|
308
360
|
return run_photogrammetry(
|
|
309
361
|
src,
|
|
310
362
|
run.out / "reconstruction",
|
|
311
363
|
run.settings,
|
|
312
364
|
PhotogrammetryOptions(
|
|
313
365
|
quality=quality, # type: ignore[arg-type]
|
|
314
|
-
matcher=
|
|
366
|
+
matcher=matcher,
|
|
315
367
|
dense="auto" if classic else "off",
|
|
316
368
|
use_gpu=wants_gpu(run.settings),
|
|
317
369
|
build_mesh=classic,
|
|
318
370
|
poisson_depth=preset.poisson_depth,
|
|
319
371
|
density_quantile=preset.density_quantile,
|
|
372
|
+
features=features, # type: ignore[arg-type]
|
|
373
|
+
# Silhouette objects only need the cameras: fewer features keep
|
|
374
|
+
# exhaustive matching of a few hundred photos fast on any GPU.
|
|
375
|
+
max_features=None if classic else _CAMERA_FEATURES,
|
|
320
376
|
),
|
|
321
|
-
overwrite=opt.overwrite or not classic,
|
|
377
|
+
overwrite=opt.overwrite or not classic or features != "sift",
|
|
322
378
|
)
|
|
323
379
|
|
|
324
|
-
|
|
380
|
+
try:
|
|
381
|
+
photo = run.step("photogrammetry", lambda: photogrammetry(classic=not silhouette))
|
|
382
|
+
except ProcessingError as exc:
|
|
383
|
+
# Rescue for hard sets: learned AI features (ALIKED + LightGlue) place
|
|
384
|
+
# photos that SIFT cannot. Slower (CPU), so only when SIFT fell short.
|
|
385
|
+
logger.info("SIFT could not place the photos (%s); retrying with AI features", exc.message)
|
|
386
|
+
try:
|
|
387
|
+
photo = run.step(
|
|
388
|
+
"photogrammetry_ai",
|
|
389
|
+
lambda: photogrammetry(classic=not silhouette, features="aliked"),
|
|
390
|
+
)
|
|
391
|
+
except (ProcessingError, DependencyMissingError, ExternalToolError):
|
|
392
|
+
raise exc from None
|
|
393
|
+
run.warnings.append("Photos placed with AI features (ALIKED + LightGlue).")
|
|
325
394
|
mesh_path: str | None = photo.mesh_path
|
|
326
395
|
upright = False
|
|
396
|
+
hull_world: list[list[float]] | None = None
|
|
327
397
|
if silhouette:
|
|
328
398
|
from reverse3d.reconstruction.silhouette import visual_hull
|
|
329
399
|
from reverse3d.vision.segmenter import ObjectSegmenter
|
|
@@ -374,6 +444,7 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
374
444
|
run.details["silhouette"] = hull.to_dict()
|
|
375
445
|
mesh_path = hull.mesh_path
|
|
376
446
|
upright = True
|
|
447
|
+
hull_world = hull.to_world
|
|
377
448
|
if photo.registered_images >= 0.9 * max(photo.total_images, 1):
|
|
378
449
|
# COLMAP placed (almost) every photo: the quick pre-check's capture advice
|
|
379
450
|
# would only confuse.
|
|
@@ -422,6 +493,8 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
422
493
|
keep_largest=preset.keep_largest,
|
|
423
494
|
repair=opt.repair,
|
|
424
495
|
)
|
|
496
|
+
if upright and hull_world is not None:
|
|
497
|
+
_texture(run, model, photo, hull_world, opt.scale)
|
|
425
498
|
return _measure(run, model)
|
|
426
499
|
|
|
427
500
|
|
|
@@ -610,7 +683,12 @@ def run_recipe(
|
|
|
610
683
|
source = prepare_heic_input(source, out) # iPhone HEIC photos -> JPEG
|
|
611
684
|
stats = _RECIPES[recipe](run, source, opt)
|
|
612
685
|
if opt.render:
|
|
613
|
-
|
|
686
|
+
textured = run.details.get("texture") and run.outputs.get("model_glb")
|
|
687
|
+
_render(
|
|
688
|
+
run,
|
|
689
|
+
Path(textured or run.outputs.get("model_color") or run.outputs["model"]),
|
|
690
|
+
opt.overwrite,
|
|
691
|
+
)
|
|
614
692
|
|
|
615
693
|
summary: dict[str, Any] = {
|
|
616
694
|
"recipe": recipe,
|
|
@@ -94,6 +94,12 @@ class PhotogrammetryOptions:
|
|
|
94
94
|
poisson_depth: int | None = None
|
|
95
95
|
#: share of the least supported surface trimmed after Poisson.
|
|
96
96
|
density_quantile: float = 0.02
|
|
97
|
+
#: "sift" (fast, GPU) or "aliked": learned ALIKED features + LightGlue matching,
|
|
98
|
+
#: which places photos of hard sets SIFT cannot (CPU, much slower).
|
|
99
|
+
features: Literal["sift", "aliked"] = "sift"
|
|
100
|
+
#: SIFT features per photo; None uses the quality preset. Placing cameras
|
|
101
|
+
#: alone (silhouette objects) needs far fewer than a dense surface.
|
|
102
|
+
max_features: int | None = None
|
|
97
103
|
|
|
98
104
|
def validate(self) -> None:
|
|
99
105
|
if self.quality not in _PRESETS:
|
|
@@ -297,18 +303,43 @@ class ColmapPipeline:
|
|
|
297
303
|
"--ImageReader.single_camera",
|
|
298
304
|
"1" if options.single_camera else "0",
|
|
299
305
|
]
|
|
306
|
+
learned = options.features == "aliked"
|
|
307
|
+
if learned:
|
|
308
|
+
from reverse3d.offline import colmap_model_path, download_colmap_models
|
|
309
|
+
|
|
310
|
+
if not colmap_model_path("aliked-n16rot.onnx"):
|
|
311
|
+
download_colmap_models()
|
|
312
|
+
aliked = colmap_model_path("aliked-n16rot.onnx")
|
|
313
|
+
if aliked is None:
|
|
314
|
+
raise DependencyMissingError(
|
|
315
|
+
"The AI feature model (ALIKED) is not on this computer.",
|
|
316
|
+
hint="Connect to the internet once and run: reverse3d offline prepare",
|
|
317
|
+
)
|
|
318
|
+
# ONNX on the CPU: the CUDA runtime it needs is not bundled with COLMAP.
|
|
319
|
+
args += [
|
|
320
|
+
"--FeatureExtraction.type",
|
|
321
|
+
"ALIKED_N16ROT",
|
|
322
|
+
"--FeatureExtraction.use_gpu",
|
|
323
|
+
"0",
|
|
324
|
+
"--AlikedExtraction.n16rot_model_path",
|
|
325
|
+
str(aliked),
|
|
326
|
+
]
|
|
300
327
|
self._stage(
|
|
301
328
|
result,
|
|
302
329
|
"feature_extractor",
|
|
303
330
|
self._with_options(
|
|
304
331
|
"feature_extractor",
|
|
305
332
|
args,
|
|
306
|
-
{
|
|
333
|
+
{}
|
|
334
|
+
if learned
|
|
335
|
+
else {
|
|
307
336
|
("FeatureExtraction.use_gpu", "SiftExtraction.use_gpu"): gpu,
|
|
308
337
|
("FeatureExtraction.max_image_size", "SiftExtraction.max_image_size"): str(
|
|
309
338
|
preset["max_image_size"]
|
|
310
339
|
),
|
|
311
|
-
("SiftExtraction.max_num_features",): str(
|
|
340
|
+
("SiftExtraction.max_num_features",): str(
|
|
341
|
+
options.max_features or preset["max_features"]
|
|
342
|
+
),
|
|
312
343
|
},
|
|
313
344
|
),
|
|
314
345
|
)
|
|
@@ -324,8 +355,19 @@ class ColmapPipeline:
|
|
|
324
355
|
matcher = "exhaustive" if len(images) <= _EXHAUSTIVE_LIMIT else "sequential"
|
|
325
356
|
command = f"{matcher}_matcher"
|
|
326
357
|
extra: dict[tuple[str, ...], str] = {
|
|
327
|
-
("FeatureMatching.use_gpu", "SiftMatching.use_gpu"): gpu
|
|
358
|
+
("FeatureMatching.use_gpu", "SiftMatching.use_gpu"): "0" if learned else gpu
|
|
328
359
|
}
|
|
360
|
+
if learned:
|
|
361
|
+
from reverse3d.offline import colmap_model_path
|
|
362
|
+
|
|
363
|
+
glue = colmap_model_path("aliked-lightglue.onnx")
|
|
364
|
+
if glue is None:
|
|
365
|
+
raise DependencyMissingError(
|
|
366
|
+
"The AI matching model (LightGlue) is not on this computer.",
|
|
367
|
+
hint="Connect to the internet once and run: reverse3d offline prepare",
|
|
368
|
+
)
|
|
369
|
+
extra[("FeatureMatching.type",)] = "ALIKED_LIGHTGLUE"
|
|
370
|
+
extra[("AlikedMatching.lightglue_model_path",)] = str(glue)
|
|
329
371
|
if matcher == "spatial":
|
|
330
372
|
extra[("SpatialMatching.is_gps",)] = "1"
|
|
331
373
|
try:
|