reverse3d 0.8.0__tar.gz → 0.8.1__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.8.0/reverse3d.egg-info → reverse3d-0.8.1}/PKG-INFO +5 -4
- {reverse3d-0.8.0 → reverse3d-0.8.1}/README.md +4 -3
- reverse3d-0.8.1/reverse3d/_version.py +1 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/recipes.py +66 -41
- {reverse3d-0.8.0 → reverse3d-0.8.1/reverse3d.egg-info}/PKG-INFO +5 -4
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/SOURCES.txt +1 -0
- reverse3d-0.8.1/tests/test_color.py +49 -0
- reverse3d-0.8.0/reverse3d/_version.py +0 -1
- {reverse3d-0.8.0 → reverse3d-0.8.1}/LICENSE +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/NOTICE +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/pyproject.toml +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/__main__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/_about.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/compact.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/mcp_server.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/prompts.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/skill/SKILL.md +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/tools.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/autopilot.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/cli.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/config.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/doctor.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/errors.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/hardware.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/imaging.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/logging_utils.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/mesh_io.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/offline.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/profiles.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/py.typed +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/depth_estimation.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/photogrammetry.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/pointcloud.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/silhouette.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/_blender_job.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/blender_render.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/preview.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/geometry_fit.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/measure.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/mesh_cleaner.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/parametric.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/scenes.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/security.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/setup_agents.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/userconfig.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/__init__.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/classifier.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/feature_matcher.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/mask_extractor.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/segmenter.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/watch.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/dependency_links.txt +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/entry_points.txt +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/requires.txt +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/top_level.txt +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/setup.cfg +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_agent_tools.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_authorship.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_automation.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_cli.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_cli_automation.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_config.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_geometry_fit.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_heic.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_mesh.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_offline_autopilot.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_optional_extras.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_reconstruction.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_render.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_scenes.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_security.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_silhouette.py +0 -0
- {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_vision.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reverse3d
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
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>
|
|
@@ -215,10 +215,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
|
|
|
215
215
|
|
|
216
216
|
| bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
|
|
217
217
|
|---------------------|----------------------------|--------|
|
|
218
|
-
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA
|
|
219
|
-
| `--lite` | `-Lite` | Todo
|
|
220
|
-
| `--minimal` | `-Minimal` |
|
|
218
|
+
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
|
|
219
|
+
| `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
|
|
220
|
+
| `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
|
|
221
221
|
| `--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 profundidad, unos 750 MB) para que todo funcione sin internet |
|
|
222
223
|
| `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
|
|
223
224
|
| `--with recon,depth` | `-With recon,depth` | Extras a elección |
|
|
224
225
|
| `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
|
|
@@ -147,10 +147,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
|
|
|
147
147
|
|
|
148
148
|
| bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
|
|
149
149
|
|---------------------|----------------------------|--------|
|
|
150
|
-
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA
|
|
151
|
-
| `--lite` | `-Lite` | Todo
|
|
152
|
-
| `--minimal` | `-Minimal` |
|
|
150
|
+
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
|
|
151
|
+
| `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
|
|
152
|
+
| `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
|
|
153
153
|
| `--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 profundidad, unos 750 MB) para que todo funcione sin internet |
|
|
154
155
|
| `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
|
|
155
156
|
| `--with recon,depth` | `-With recon,depth` | Extras a elección |
|
|
156
157
|
| `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.1"
|
|
@@ -30,6 +30,8 @@ from dataclasses import dataclass, field
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
from typing import Any, Literal, TypeVar
|
|
32
32
|
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
33
35
|
from reverse3d.config import Settings
|
|
34
36
|
from reverse3d.errors import (
|
|
35
37
|
CaptureQualityError,
|
|
@@ -135,29 +137,70 @@ def _clean(
|
|
|
135
137
|
parts, and tiny debris is already dropped by the area filter."""
|
|
136
138
|
from reverse3d.reverse_eng.mesh_cleaner import CleanOptions, clean_mesh_file
|
|
137
139
|
|
|
140
|
+
options = CleanOptions(
|
|
141
|
+
keep_largest_only=keep_largest,
|
|
142
|
+
fill_holes=fill_holes,
|
|
143
|
+
align=align,
|
|
144
|
+
ground=align != "none",
|
|
145
|
+
scale=scale,
|
|
146
|
+
max_hole_edges=2000 if repair == "strong" else 200,
|
|
147
|
+
)
|
|
138
148
|
report = run.step(
|
|
139
149
|
"clean",
|
|
140
|
-
lambda: clean_mesh_file(
|
|
141
|
-
src,
|
|
142
|
-
dst,
|
|
143
|
-
run.settings,
|
|
144
|
-
CleanOptions(
|
|
145
|
-
keep_largest_only=keep_largest,
|
|
146
|
-
fill_holes=fill_holes,
|
|
147
|
-
align=align,
|
|
148
|
-
ground=align != "none",
|
|
149
|
-
scale=scale,
|
|
150
|
-
max_hole_edges=2000 if repair == "strong" else 200,
|
|
151
|
-
),
|
|
152
|
-
overwrite=overwrite,
|
|
153
|
-
),
|
|
150
|
+
lambda: clean_mesh_file(src, dst, run.settings, options, overwrite=overwrite),
|
|
154
151
|
)
|
|
155
152
|
run.warnings.extend(report.warnings)
|
|
156
153
|
run.outputs["model"] = str(dst)
|
|
157
154
|
run.details["clean"] = report.to_dict()
|
|
155
|
+
if _has_vertex_colors(src):
|
|
156
|
+
# Same cleaning on the coloured mesh (deterministic, colours follow the
|
|
157
|
+
# vertices): model_color.ply for renders and model.glb for games and viewers.
|
|
158
|
+
try:
|
|
159
|
+
color_ply = dst.with_name("model_color.ply")
|
|
160
|
+
clean_mesh_file(src, color_ply, run.settings, options, overwrite=True)
|
|
161
|
+
import trimesh
|
|
162
|
+
|
|
163
|
+
glb = dst.with_name("model.glb")
|
|
164
|
+
trimesh.load(color_ply, process=False, force="mesh").export(glb)
|
|
165
|
+
run.outputs["model_color"] = str(color_ply)
|
|
166
|
+
run.outputs["model_glb"] = str(glb)
|
|
167
|
+
except Exception as exc: # colour is a bonus: never fail the model for it
|
|
168
|
+
logger.info("Coloured model skipped: %s", exc)
|
|
158
169
|
return report.after
|
|
159
170
|
|
|
160
171
|
|
|
172
|
+
def _has_vertex_colors(path: Path) -> bool:
|
|
173
|
+
import trimesh
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
mesh: Any = trimesh.load(path, process=False, force="mesh")
|
|
177
|
+
except Exception:
|
|
178
|
+
return False
|
|
179
|
+
colors = getattr(mesh.visual, "vertex_colors", None)
|
|
180
|
+
if colors is None or len(colors) != len(mesh.vertices):
|
|
181
|
+
return False
|
|
182
|
+
rgb = np.asarray(colors)[:, :3]
|
|
183
|
+
return float(np.max(np.std(rgb, axis=0))) > 1.0 # not just a uniform default colour
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _color_from_cloud(mesh_path: Path, cloud_path: Path, out: Path) -> Path | None:
|
|
187
|
+
"""Paint a mesh with the colours of the point cloud it was built from."""
|
|
188
|
+
import trimesh
|
|
189
|
+
from scipy.spatial import cKDTree
|
|
190
|
+
|
|
191
|
+
cloud: Any = trimesh.load(cloud_path, process=False)
|
|
192
|
+
colors = getattr(cloud, "colors", None)
|
|
193
|
+
if colors is None or len(colors) != len(getattr(cloud, "vertices", [])):
|
|
194
|
+
return None
|
|
195
|
+
mesh: Any = trimesh.load(mesh_path, process=False, force="mesh")
|
|
196
|
+
_, idx = cKDTree(np.asarray(cloud.vertices)).query(np.asarray(mesh.vertices), k=4)
|
|
197
|
+
rgb = np.asarray(colors)[idx][..., :3].astype(np.float64).mean(axis=1)
|
|
198
|
+
rgba = np.column_stack([rgb, np.full(len(rgb), 255.0)]).clip(0, 255).astype(np.uint8)
|
|
199
|
+
mesh.visual = trimesh.visual.ColorVisuals(mesh, vertex_colors=rgba)
|
|
200
|
+
mesh.export(out)
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
|
|
161
204
|
def _render(run: _Run, mesh: Path, overwrite: bool) -> None:
|
|
162
205
|
from reverse3d.rendering.blender_render import RenderOptions, preset_options, render_mesh
|
|
163
206
|
|
|
@@ -210,28 +253,6 @@ _DEFAULT_LOOK = {
|
|
|
210
253
|
}
|
|
211
254
|
|
|
212
255
|
|
|
213
|
-
def _color_outputs(run: _Run, colored: Path, model: Path, scale: float) -> None:
|
|
214
|
-
"""Give the final model the photo colours of the carved hull: model.glb (games,
|
|
215
|
-
web, viewers) and model_color.ply (renders)."""
|
|
216
|
-
import numpy as np
|
|
217
|
-
import trimesh
|
|
218
|
-
from scipy.spatial import cKDTree
|
|
219
|
-
|
|
220
|
-
source: Any = trimesh.load(colored, process=False, force="mesh")
|
|
221
|
-
target: Any = trimesh.load(model, process=False, force="mesh")
|
|
222
|
-
colors = getattr(source.visual, "vertex_colors", None)
|
|
223
|
-
if colors is None or len(colors) != len(source.vertices):
|
|
224
|
-
return
|
|
225
|
-
_, nearest = cKDTree(np.asarray(source.vertices) * scale).query(np.asarray(target.vertices))
|
|
226
|
-
target.visual = trimesh.visual.ColorVisuals(target, vertex_colors=np.asarray(colors)[nearest])
|
|
227
|
-
glb = run.out / "model.glb"
|
|
228
|
-
ply = run.out / "model_color.ply"
|
|
229
|
-
target.export(glb)
|
|
230
|
-
target.export(ply)
|
|
231
|
-
run.outputs["model_glb"] = str(glb)
|
|
232
|
-
run.outputs["model_color"] = str(ply)
|
|
233
|
-
|
|
234
|
-
|
|
235
256
|
def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
236
257
|
from reverse3d.reconstruction.photogrammetry import (
|
|
237
258
|
PhotogrammetryOptions,
|
|
@@ -352,6 +373,15 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
352
373
|
"Point cloud created but no mesh could be built.",
|
|
353
374
|
hint='Install the meshing extra: pip install "reverse3d[recon]" (Python ≤ 3.12).',
|
|
354
375
|
)
|
|
376
|
+
cloud = photo.dense_points_path or photo.sparse_points_path
|
|
377
|
+
if not upright and cloud:
|
|
378
|
+
try:
|
|
379
|
+
colored = _color_from_cloud(
|
|
380
|
+
Path(mesh_path), Path(cloud), run.out / "reconstruction" / "mesh_color.ply"
|
|
381
|
+
)
|
|
382
|
+
mesh_path = str(colored) if colored else mesh_path
|
|
383
|
+
except Exception as exc: # colour is a bonus
|
|
384
|
+
logger.info("Colouring from the point cloud skipped: %s", exc)
|
|
355
385
|
model = run.out / "model.stl"
|
|
356
386
|
_clean(
|
|
357
387
|
run,
|
|
@@ -364,11 +394,6 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
364
394
|
keep_largest=preset.keep_largest,
|
|
365
395
|
repair=opt.repair,
|
|
366
396
|
)
|
|
367
|
-
if upright:
|
|
368
|
-
try:
|
|
369
|
-
_color_outputs(run, Path(mesh_path), model, opt.scale)
|
|
370
|
-
except Exception as exc: # colour is a bonus: never fail the model for it
|
|
371
|
-
logger.info("Colour transfer skipped: %s", exc)
|
|
372
397
|
return _measure(run, model)
|
|
373
398
|
|
|
374
399
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reverse3d
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
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>
|
|
@@ -215,10 +215,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
|
|
|
215
215
|
|
|
216
216
|
| bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
|
|
217
217
|
|---------------------|----------------------------|--------|
|
|
218
|
-
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA
|
|
219
|
-
| `--lite` | `-Lite` | Todo
|
|
220
|
-
| `--minimal` | `-Minimal` |
|
|
218
|
+
| *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
|
|
219
|
+
| `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
|
|
220
|
+
| `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
|
|
221
221
|
| `--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 profundidad, unos 750 MB) para que todo funcione sin internet |
|
|
222
223
|
| `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
|
|
223
224
|
| `--with recon,depth` | `-With recon,depth` | Extras a elección |
|
|
224
225
|
| `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import trimesh
|
|
7
|
+
|
|
8
|
+
from reverse3d.config import Settings
|
|
9
|
+
from reverse3d.recipes import _clean, _color_from_cloud, _has_vertex_colors, _Run
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _two_tone_cloud(path: Path) -> Path:
|
|
13
|
+
"""Points on a sphere: red on top, blue at the bottom (like a COLMAP fused.ply)."""
|
|
14
|
+
pts = trimesh.creation.icosphere(subdivisions=5).vertices
|
|
15
|
+
rgb = np.where(pts[:, 2:3] > 0, [220, 30, 30], [30, 30, 220])
|
|
16
|
+
trimesh.PointCloud(pts, colors=np.column_stack([rgb, np.full(len(pts), 255)])).export(path)
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_mesh_takes_the_colours_of_its_point_cloud(tmp_path: Path) -> None:
|
|
21
|
+
plain = tmp_path / "mesh.ply"
|
|
22
|
+
trimesh.creation.icosphere(subdivisions=3).export(plain)
|
|
23
|
+
assert not _has_vertex_colors(plain)
|
|
24
|
+
colored = _color_from_cloud(plain, _two_tone_cloud(tmp_path / "cloud.ply"), tmp_path / "c.ply")
|
|
25
|
+
assert colored is not None and _has_vertex_colors(colored)
|
|
26
|
+
mesh = trimesh.load(colored, process=False)
|
|
27
|
+
top = mesh.vertices[:, 2] > 0.3
|
|
28
|
+
assert mesh.visual.vertex_colors[top, 0].mean() > 150 # red on top
|
|
29
|
+
assert mesh.visual.vertex_colors[~top & (mesh.vertices[:, 2] < -0.3), 2].mean() > 150
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cleaning_a_coloured_mesh_also_writes_glb(tmp_path: Path, settings: Settings) -> None:
|
|
33
|
+
plain = tmp_path / "mesh.ply"
|
|
34
|
+
trimesh.creation.icosphere(subdivisions=3).export(plain)
|
|
35
|
+
colored = _color_from_cloud(plain, _two_tone_cloud(tmp_path / "cloud.ply"), tmp_path / "c.ply")
|
|
36
|
+
assert colored is not None
|
|
37
|
+
run = _Run(recipe="photos_to_model", settings=settings, out=tmp_path)
|
|
38
|
+
_clean(run, colored, tmp_path / "model.stl", scale=1.0, overwrite=True)
|
|
39
|
+
assert {"model", "model_color", "model_glb"} <= set(run.outputs)
|
|
40
|
+
glb = trimesh.load(run.outputs["model_glb"], force="mesh")
|
|
41
|
+
assert len(np.unique(np.asarray(glb.visual.vertex_colors)[:, :3], axis=0)) > 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_plain_meshes_get_no_colour_outputs(tmp_path: Path, settings: Settings) -> None:
|
|
45
|
+
plain = tmp_path / "mesh.ply"
|
|
46
|
+
trimesh.creation.icosphere(subdivisions=3).export(plain)
|
|
47
|
+
run = _Run(recipe="mesh_report", settings=settings, out=tmp_path)
|
|
48
|
+
_clean(run, plain, tmp_path / "model.stl", scale=1.0, overwrite=True)
|
|
49
|
+
assert "model_glb" not in run.outputs
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|