reverse3d 0.11.3__tar.gz → 0.13.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.11.3/reverse3d.egg-info → reverse3d-0.13.0}/PKG-INFO +3 -1
- {reverse3d-0.11.3 → reverse3d-0.13.0}/README.md +2 -0
- reverse3d-0.13.0/reverse3d/_version.py +1 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/recipes.py +37 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/silhouette.py +48 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0/reverse3d.egg-info}/PKG-INFO +3 -1
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_silhouette.py +65 -0
- reverse3d-0.11.3/reverse3d/_version.py +0 -1
- {reverse3d-0.11.3 → reverse3d-0.13.0}/LICENSE +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/NOTICE +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/pyproject.toml +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/__main__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/_about.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/compact.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/mcp_server.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/prompts.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/skill/SKILL.md +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/agent/tools.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/autopilot.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/cli.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/config.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/doctor.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/errors.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/hardware.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/imaging.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/logging_utils.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/mesh_io.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/offline.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/profiles.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/py.typed +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/depth_estimation.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/photogrammetry.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/pointcloud.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reconstruction/texture.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/rendering/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/rendering/_blender_job.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/rendering/blender_render.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/rendering/preview.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reverse_eng/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reverse_eng/geometry_fit.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reverse_eng/measure.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reverse_eng/mesh_cleaner.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/reverse_eng/parametric.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/scenes.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/security.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/setup_agents.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/userconfig.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/vision/__init__.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/vision/classifier.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/vision/feature_matcher.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/vision/mask_extractor.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/vision/segmenter.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d/watch.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d.egg-info/SOURCES.txt +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d.egg-info/dependency_links.txt +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d.egg-info/entry_points.txt +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d.egg-info/requires.txt +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/reverse3d.egg-info/top_level.txt +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/setup.cfg +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_agent_tools.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_authorship.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_automation.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_cli.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_cli_automation.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_color.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_config.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_geometry_fit.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_heic.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_mesh.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_offline_autopilot.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_optional_extras.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_reconstruction.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_render.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_scenes.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_security.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.0}/tests/test_texture.py +0 -0
- {reverse3d-0.11.3 → reverse3d-0.13.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.13.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>
|
|
@@ -421,6 +421,8 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
421
421
|
|---|---|---|
|
|
422
422
|
| Recortes perfectos | 0,943 | 0,941 |
|
|
423
423
|
| 15 % de recortes defectuosos | 0,000 (pierde el objeto) | **0,921** |
|
|
424
|
+
| Concavidad invisible a las siluetas (error en la ranura) | 0,154 | **0,091** (refinamiento por fotos, Core 0.3) |
|
|
425
|
+
| Mapa de confianza y consejos de captura | — | ✅ Core 0.4 (`model_quality.glb`) |
|
|
424
426
|
|
|
425
427
|
*(Coincidencia volumétrica, IoU: 1 = idéntico.)*
|
|
426
428
|
|
|
@@ -352,6 +352,8 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
352
352
|
|---|---|---|
|
|
353
353
|
| Recortes perfectos | 0,943 | 0,941 |
|
|
354
354
|
| 15 % de recortes defectuosos | 0,000 (pierde el objeto) | **0,921** |
|
|
355
|
+
| Concavidad invisible a las siluetas (error en la ranura) | 0,154 | **0,091** (refinamiento por fotos, Core 0.3) |
|
|
356
|
+
| Mapa de confianza y consejos de captura | — | ✅ Core 0.4 (`model_quality.glb`) |
|
|
355
357
|
|
|
356
358
|
*(Coincidencia volumétrica, IoU: 1 = idéntico.)*
|
|
357
359
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.13.0"
|
|
@@ -177,6 +177,42 @@ _CAMERA_FEATURES = 4096
|
|
|
177
177
|
_OBJECT_EXHAUSTIVE_LIMIT = 400
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
_WHERE = {
|
|
181
|
+
"top": "from above",
|
|
182
|
+
"around": "around the object at mid height",
|
|
183
|
+
"low": "from a low angle",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _quality_outputs(run: _Run, hull: Any) -> None:
|
|
188
|
+
"""Confidence map (model_quality.glb) and capture advice from reverse3d-core."""
|
|
189
|
+
quality = getattr(hull, "quality", None) or {}
|
|
190
|
+
if hull.quality_path:
|
|
191
|
+
try:
|
|
192
|
+
import trimesh
|
|
193
|
+
|
|
194
|
+
glb = run.out / "model_quality.glb"
|
|
195
|
+
_export_glb(trimesh.load(hull.quality_path, process=False, force="mesh"), glb)
|
|
196
|
+
run.outputs["model_quality"] = str(glb)
|
|
197
|
+
except Exception as exc: # the map is a bonus: never fail the model for it
|
|
198
|
+
logger.info("Confidence map skipped: %s", exc)
|
|
199
|
+
covered = quality.get("covered")
|
|
200
|
+
if covered is None or covered >= _WELL_COVERED:
|
|
201
|
+
return
|
|
202
|
+
for advice in quality.get("advice", [])[:2]:
|
|
203
|
+
where = _WHERE.get(str(advice.get("elevation")), "around the object")
|
|
204
|
+
photo = Path(str(advice.get("near_photo", ""))).stem
|
|
205
|
+
share = round(100 * float(advice.get("share", 0.0)))
|
|
206
|
+
run.warnings.append(
|
|
207
|
+
f"{share}% of the model is weakly supported by the photos: take more photos "
|
|
208
|
+
f"{where}, near where you took {photo}."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Share of the surface the photos must support well for no capture advice.
|
|
213
|
+
_WELL_COVERED = 0.95
|
|
214
|
+
|
|
215
|
+
|
|
180
216
|
def _export_glb(mesh: Any, path: Path) -> None:
|
|
181
217
|
"""Write a GLB that stands up in any viewer: glTF is Y-up, our models are Z-up.
|
|
182
218
|
|
|
@@ -442,6 +478,7 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
|
|
|
442
478
|
mesh_path = photo.mesh_path
|
|
443
479
|
else:
|
|
444
480
|
run.details["silhouette"] = hull.to_dict()
|
|
481
|
+
_quality_outputs(run, hull)
|
|
445
482
|
if hull.unreliable:
|
|
446
483
|
names = ", ".join(Path(n).stem for n in hull.unreliable[:5])
|
|
447
484
|
more = "…" if len(hull.unreliable) > 5 else ""
|
|
@@ -92,6 +92,12 @@ class HullResult:
|
|
|
92
92
|
engine: str = "reverse3d"
|
|
93
93
|
#: photos reverse3d-core judged inconsistent with the rest (moved object, bad cut-out)
|
|
94
94
|
unreliable: list[str] = field(default_factory=list)
|
|
95
|
+
#: vertices reverse3d-core moved by photo consistency (0 without it)
|
|
96
|
+
refined_vertices: int = 0
|
|
97
|
+
#: reverse3d-core quality agent: share of the surface well supported and advice
|
|
98
|
+
quality: dict[str, object] = field(default_factory=dict)
|
|
99
|
+
#: confidence map (red -> green vertex colours), same coordinates as the model
|
|
100
|
+
quality_path: str | None = None
|
|
95
101
|
|
|
96
102
|
def to_dict(self) -> dict[str, object]:
|
|
97
103
|
return asdict(self)
|
|
@@ -367,6 +373,12 @@ def _complete_base(
|
|
|
367
373
|
return out.reshape(volume.shape)
|
|
368
374
|
|
|
369
375
|
|
|
376
|
+
def _outward_share(mesh: trimesh.Trimesh) -> float:
|
|
377
|
+
"""Share of vertex normals pointing away from the centroid (about 1 when outward)."""
|
|
378
|
+
rel = np.asarray(mesh.vertices) - np.asarray(mesh.centroid)
|
|
379
|
+
return float(np.mean((rel * np.asarray(mesh.vertex_normals)).sum(axis=1) > 0))
|
|
380
|
+
|
|
381
|
+
|
|
370
382
|
def _coarse_grabcut(image: np.ndarray, rect: tuple[int, int, int, int]) -> np.ndarray:
|
|
371
383
|
"""GrabCut at half resolution (4x fewer pixels): enough for a first silhouette."""
|
|
372
384
|
h, w = image.shape[:2]
|
|
@@ -963,6 +975,22 @@ def visual_hull(
|
|
|
963
975
|
mesh = trimesh.Trimesh(verts, faces, process=True)
|
|
964
976
|
trimesh.smoothing.filter_taubin(mesh, lamb=0.5, nu=-0.53, iterations=15)
|
|
965
977
|
mesh.fix_normals()
|
|
978
|
+
# fix_normals cannot tell outside from inside on a mesh that is not perfectly
|
|
979
|
+
# closed: make the normals point away from the object's centre.
|
|
980
|
+
if _outward_share(mesh) < 0.5:
|
|
981
|
+
mesh.invert()
|
|
982
|
+
refined_vertices = 0
|
|
983
|
+
refine_mesh = getattr(core, "refine_mesh", None) if core_cameras is not None else None
|
|
984
|
+
if refine_mesh is not None:
|
|
985
|
+
# reverse3d-core >= 0.3: photo consistency carves what silhouettes cannot see.
|
|
986
|
+
try:
|
|
987
|
+
mesh, stats = refine_mesh(
|
|
988
|
+
mesh, core_cameras, [small for small, _ in images], 0.5 * (2 * radius / (n - 1))
|
|
989
|
+
)
|
|
990
|
+
refined_vertices = int(stats.get("moved", 0))
|
|
991
|
+
logger.info("Photo consistency refined %d vertices", refined_vertices)
|
|
992
|
+
except Exception as exc: # never fail a job because of the optional engine
|
|
993
|
+
logger.warning("reverse3d-core refinement failed (%s); keeping the hull", exc)
|
|
966
994
|
# Real colours from the photos (kept by PLY export; the model gets them as GLB).
|
|
967
995
|
mesh.visual = trimesh.visual.ColorVisuals(
|
|
968
996
|
mesh, vertex_colors=_vertex_colors(mesh, views, images, masks)
|
|
@@ -971,6 +999,17 @@ def visual_hull(
|
|
|
971
999
|
# Stand it upright: +Z along the table normal (or the mean camera "up").
|
|
972
1000
|
up = plane[0] if plane is not None else np.mean(cameras - center, axis=0)
|
|
973
1001
|
up = up / np.linalg.norm(up)
|
|
1002
|
+
quality: dict[str, object] = {}
|
|
1003
|
+
quality_colors: np.ndarray | None = None
|
|
1004
|
+
assess_quality = getattr(core, "assess_quality", None) if core_cameras is not None else None
|
|
1005
|
+
if assess_quality is not None:
|
|
1006
|
+
# reverse3d-core >= 0.4: how well the photos support each part, and advice.
|
|
1007
|
+
try:
|
|
1008
|
+
report = assess_quality(mesh, core_cameras, up, 0.5 * (2 * radius / (n - 1)))
|
|
1009
|
+
quality_colors = np.asarray(report["colors"])
|
|
1010
|
+
quality = {"covered": float(report["covered"]), "advice": list(report["advice"])}
|
|
1011
|
+
except Exception as exc: # never fail a job because of the optional engine
|
|
1012
|
+
logger.warning("reverse3d-core quality check failed (%s)", exc)
|
|
974
1013
|
side = np.cross([0.0, 1.0, 0.0] if abs(up[1]) < 0.9 else [1.0, 0.0, 0.0], up)
|
|
975
1014
|
side /= np.linalg.norm(side)
|
|
976
1015
|
basis = np.stack([side, np.cross(up, side), up])
|
|
@@ -983,6 +1022,12 @@ def visual_hull(
|
|
|
983
1022
|
|
|
984
1023
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
985
1024
|
mesh.export(output_path)
|
|
1025
|
+
quality_path: str | None = None
|
|
1026
|
+
if quality_colors is not None and len(quality_colors) == len(mesh.vertices):
|
|
1027
|
+
heat = trimesh.Trimesh(mesh.vertices, mesh.faces, process=False)
|
|
1028
|
+
heat.visual = trimesh.visual.ColorVisuals(heat, vertex_colors=quality_colors)
|
|
1029
|
+
quality_path = str(output_path.with_name(output_path.stem + "_quality.ply"))
|
|
1030
|
+
heat.export(quality_path)
|
|
986
1031
|
logger.info("Silhouette model: %d views, %d faces", len(views), len(mesh.faces))
|
|
987
1032
|
return HullResult(
|
|
988
1033
|
str(output_path),
|
|
@@ -1001,4 +1046,7 @@ def visual_hull(
|
|
|
1001
1046
|
]
|
|
1002
1047
|
if core_weights is not None and engine != "reverse3d"
|
|
1003
1048
|
else [],
|
|
1049
|
+
refined_vertices,
|
|
1050
|
+
quality,
|
|
1051
|
+
quality_path,
|
|
1004
1052
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reverse3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.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>
|
|
@@ -421,6 +421,8 @@ Si las fotos están borrosas o falta una herramienta, **no reintenta en vano**:
|
|
|
421
421
|
|---|---|---|
|
|
422
422
|
| Recortes perfectos | 0,943 | 0,941 |
|
|
423
423
|
| 15 % de recortes defectuosos | 0,000 (pierde el objeto) | **0,921** |
|
|
424
|
+
| Concavidad invisible a las siluetas (error en la ranura) | 0,154 | **0,091** (refinamiento por fotos, Core 0.3) |
|
|
425
|
+
| Mapa de confianza y consejos de captura | — | ✅ Core 0.4 (`model_quality.glb`) |
|
|
424
426
|
|
|
425
427
|
*(Coincidencia volumétrica, IoU: 1 = idéntico.)*
|
|
426
428
|
|
|
@@ -421,3 +421,68 @@ def test_legs_stay_legs_when_the_base_is_completed() -> None:
|
|
|
421
421
|
out = _complete_base(volume, grid, np.array([0.0, 0.0, 1.0]), 0.0, voxel).reshape(-1) > 0.5
|
|
422
422
|
between = (np.abs(x) < 0.4) & (np.abs(y) < 0.4) & (z < 0.5)
|
|
423
423
|
assert not out[between].any()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class _RefiningCore(_FakeCore):
|
|
427
|
+
"""A fake reverse3d-core >= 0.3 that also refines the mesh by photo consistency."""
|
|
428
|
+
|
|
429
|
+
def refine_mesh(self, mesh, cameras, images, step): # type: ignore[no-untyped-def]
|
|
430
|
+
assert len(cameras) == len(images) and step > 0
|
|
431
|
+
return mesh, {"moved": 42, "vertices": len(mesh.vertices), "mean_offset": 0.0}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_the_core_refines_the_hull_by_photo_consistency(
|
|
435
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
436
|
+
) -> None:
|
|
437
|
+
from reverse3d.reconstruction import silhouette
|
|
438
|
+
|
|
439
|
+
monkeypatch.setattr(silhouette, "core_engine", lambda: _RefiningCore())
|
|
440
|
+
images, text = _scene(tmp_path)
|
|
441
|
+
result = visual_hull(images, text, tmp_path / "hull.ply", resolution=48, refinements=1)
|
|
442
|
+
assert result.refined_vertices == 42
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class _QualityCore(_FakeCore):
|
|
446
|
+
"""A fake reverse3d-core >= 0.4 with the quality agent."""
|
|
447
|
+
|
|
448
|
+
def assess_quality(self, mesh, cameras, up, step): # type: ignore[no-untyped-def]
|
|
449
|
+
n = len(mesh.vertices)
|
|
450
|
+
colors = np.tile(np.array([[74, 222, 128, 255]], np.uint8), (n, 1))
|
|
451
|
+
advice = [{"elevation": "top", "share": 0.08, "near_photo": "img_03.png"}]
|
|
452
|
+
return {"confidence": np.ones(n), "colors": colors, "covered": 0.82, "advice": advice}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_the_core_quality_agent_writes_a_confidence_map(
|
|
456
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
457
|
+
) -> None:
|
|
458
|
+
from reverse3d.reconstruction import silhouette
|
|
459
|
+
|
|
460
|
+
monkeypatch.setattr(silhouette, "core_engine", lambda: _QualityCore())
|
|
461
|
+
images, text = _scene(tmp_path)
|
|
462
|
+
result = visual_hull(images, text, tmp_path / "hull.ply", resolution=48, refinements=1)
|
|
463
|
+
assert result.quality["covered"] == pytest.approx(0.82)
|
|
464
|
+
assert result.quality_path is not None
|
|
465
|
+
heat = trimesh.load(result.quality_path, process=False)
|
|
466
|
+
assert len(heat.vertices) == len(trimesh.load(result.mesh_path, process=False).vertices)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_capture_advice_becomes_a_warning_only_when_coverage_is_low(tmp_path: Path) -> None:
|
|
470
|
+
from types import SimpleNamespace
|
|
471
|
+
|
|
472
|
+
from reverse3d.config import Settings
|
|
473
|
+
from reverse3d.recipes import _quality_outputs, _Run
|
|
474
|
+
|
|
475
|
+
run = _Run(recipe="photos_to_model", settings=Settings(workspace=tmp_path), out=tmp_path)
|
|
476
|
+
advice = [{"elevation": "top", "share": 0.08, "near_photo": "IMG_5026.jpg"}]
|
|
477
|
+
_quality_outputs(
|
|
478
|
+
run, SimpleNamespace(quality={"covered": 0.82, "advice": advice}, quality_path=None)
|
|
479
|
+
)
|
|
480
|
+
assert run.warnings == [
|
|
481
|
+
"8% of the model is weakly supported by the photos: take more photos from above, "
|
|
482
|
+
"near where you took IMG_5026."
|
|
483
|
+
]
|
|
484
|
+
run.warnings.clear()
|
|
485
|
+
_quality_outputs(
|
|
486
|
+
run, SimpleNamespace(quality={"covered": 0.98, "advice": advice}, quality_path=None)
|
|
487
|
+
)
|
|
488
|
+
assert run.warnings == []
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.11.3"
|
|
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
|
|
File without changes
|
|
File without changes
|