import importlib.util
|
import json
|
import struct
|
import tempfile
|
import unittest
|
from argparse import Namespace
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
SCRIPT = Path(__file__).parents[1] / "run_cpu_dense_reconstruction.py"
|
SPEC = importlib.util.spec_from_file_location("cpu_dense_reconstruction", SCRIPT)
|
MODULE = importlib.util.module_from_spec(SPEC)
|
assert SPEC.loader is not None
|
SPEC.loader.exec_module(MODULE)
|
|
|
def write_test_glb(path: Path) -> None:
|
image = Image.new("RGB", (8, 8), (0, 0, 0))
|
for y in range(4, 8):
|
for x in range(8):
|
image.putpixel((x, y), (20, 180, 60))
|
image.save(path.parent / "atlas.png")
|
positions = struct.pack("<" + "f" * 18, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0)
|
texcoords = struct.pack("<" + "f" * 12, 0.1, 0.1, 0.8, 0.1, 0.1, 0.3, 0.1, 0.7, 0.8, 0.7, 0.1, 0.9)
|
indices = struct.pack("<IIIIII", 0, 1, 2, 3, 4, 5)
|
binary = positions + texcoords + indices
|
document = {
|
"asset": {"version": "2.0"},
|
"buffers": [{"byteLength": len(binary)}],
|
"bufferViews": [
|
{"buffer": 0, "byteOffset": 0, "byteLength": len(positions)},
|
{"buffer": 0, "byteOffset": len(positions), "byteLength": len(texcoords)},
|
{"buffer": 0, "byteOffset": len(positions) + len(texcoords), "byteLength": len(indices)},
|
],
|
"accessors": [
|
{"bufferView": 0, "componentType": 5126, "count": 6, "type": "VEC3"},
|
{"bufferView": 1, "componentType": 5126, "count": 6, "type": "VEC2"},
|
{"bufferView": 2, "componentType": 5125, "count": 6, "type": "SCALAR"},
|
],
|
"images": [{"uri": "atlas.png"}],
|
"textures": [{"source": 0}],
|
"materials": [{"pbrMetallicRoughness": {"baseColorTexture": {"index": 0}}}],
|
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "TEXCOORD_0": 1}, "indices": 2, "material": 0}]}],
|
}
|
json_chunk = json.dumps(document, separators=(",", ":")).encode("utf-8")
|
json_chunk += b" " * ((-len(json_chunk)) % 4)
|
output = bytearray(struct.pack("<4sII", b"glTF", 2, 0))
|
for kind, payload in ((b"JSON", json_chunk), (b"BIN\x00", binary)):
|
output.extend(struct.pack("<I4s", len(payload), kind))
|
output.extend(payload)
|
struct.pack_into("<I", output, 8, len(output))
|
path.write_bytes(output)
|
|
|
class ReliableTexturePreviewTests(unittest.TestCase):
|
def test_cuda_dense_log_requires_explicit_cuda_or_gpu_evidence(self) -> None:
|
MODULE.verify_cuda_dense_log("OpenMVS CUDA device 0 initialized")
|
MODULE.verify_cuda_dense_log("GPU depth maps are enabled")
|
with self.assertRaisesRegex(RuntimeError, "did not report"):
|
MODULE.verify_cuda_dense_log("OpenMVS x64 v2.4.0")
|
|
def test_openmvs_tool_log_uses_the_latest_timestamped_log(self) -> None:
|
with tempfile.TemporaryDirectory() as temporary:
|
directory = Path(temporary)
|
(directory / "DensifyPointCloud-older.log").write_text("CPU", encoding="utf-8")
|
(directory / "DensifyPointCloud-newer.log").write_text("CUDA device 0", encoding="utf-8")
|
self.assertEqual(MODULE.openmvs_tool_log(directory, "DensifyPointCloud"), "CUDA device 0")
|
|
def test_openmvs_failure_includes_native_log(self) -> None:
|
with tempfile.TemporaryDirectory() as temporary:
|
directory = Path(temporary)
|
(directory / "DensifyPointCloud-native.log").write_text("CUDA error (code 801)", encoding="utf-8")
|
error = MODULE.with_openmvs_log(RuntimeError("DensifyPointCloud.exe failed"), directory, "DensifyPointCloud")
|
self.assertIn("CUDA error (code 801)", str(error))
|
|
def test_external_tool_paths_are_absolute(self) -> None:
|
args = Namespace(input=Path("images"), sparse_model=Path("sparse"), output=Path("output"), openmvs_bin=Path("tools"))
|
MODULE.resolve_external_tool_paths(args)
|
self.assertTrue(all(getattr(args, name).is_absolute() for name in ("input", "sparse_model", "output", "openmvs_bin")))
|
|
def test_point_color_preview_reads_openmvs_records(self) -> None:
|
with tempfile.TemporaryDirectory() as temporary:
|
directory = Path(temporary)
|
dense = directory / "dense.ply"
|
mesh = directory / "mesh.ply"
|
destination = directory / "point-colour.glb"
|
dense.write_bytes(
|
b"ply\nformat binary_little_endian 1.0\nelement vertex 4\nproperty float32 x\nproperty float32 y\nproperty float32 z\nproperty uint8 red\nproperty uint8 green\nproperty uint8 blue\nproperty float32 nx\nproperty float32 ny\nproperty float32 nz\nproperty list uint8 uint32 view_indices\nproperty list uint8 float32 view_weights\nend_header\n"
|
+ b"".join(struct.pack("<fffBBBfffBB", *point, 0, 0) for point in ((0, 0, 0, 255, 0, 0, 0, 0, 1), (1, 0, 0, 0, 255, 0, 0, 0, 1), (0, 1, 0, 0, 0, 255, 0, 0, 1), (0, 0, 1, 255, 255, 255, 0, 0, 1)))
|
)
|
mesh.write_text(
|
"ply\nformat ascii 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nelement face 4\nproperty list uchar int vertex_indices\nend_header\n"
|
"0 0 0\n1 0 0\n0 1 0\n0 0 1\n3 0 1 2\n3 0 1 3\n3 0 2 3\n3 1 2 3\n",
|
encoding="ascii",
|
)
|
self.assertEqual(MODULE.build_point_color_preview_glb(dense, mesh, destination), 4)
|
self.assertTrue(destination.is_file())
|
|
def test_geometry_preview_keeps_all_mesh_faces(self) -> None:
|
with tempfile.TemporaryDirectory() as temporary:
|
directory = Path(temporary)
|
source = directory / "mesh.ply"
|
destination = directory / "geometry.glb"
|
source.write_text(
|
"ply\nformat ascii 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nelement face 4\nproperty list uchar int vertex_indices\nend_header\n"
|
"0 0 0\n1 0 0\n0 1 0\n0 0 1\n3 0 1 2\n3 0 1 3\n3 0 2 3\n3 1 2 3\n",
|
encoding="ascii",
|
)
|
self.assertEqual(MODULE.build_geometry_preview_glb(source, destination), 4)
|
self.assertTrue(destination.is_file())
|
|
def test_placeholder_colours_are_rejected(self) -> None:
|
self.assertFalse(MODULE.reliable_texture_pixel((0, 0, 0)))
|
self.assertFalse(MODULE.reliable_texture_pixel((255, 127, 39)))
|
self.assertTrue(MODULE.reliable_texture_pixel((20, 180, 60)))
|
|
def test_preview_keeps_only_faces_with_reliable_samples(self) -> None:
|
with tempfile.TemporaryDirectory() as temporary:
|
directory = Path(temporary)
|
source = directory / "source.glb"
|
destination = directory / "preview.glb"
|
report_path = directory / "report.json"
|
write_test_glb(source)
|
report = MODULE.build_filtered_preview_glb(source, destination, report_path)
|
self.assertEqual(report["input_faces"], 2)
|
self.assertEqual(report["kept_faces"], 1)
|
self.assertEqual(report["rejected_black_faces"], 1)
|
self.assertTrue(destination.is_file())
|
self.assertEqual(json.loads(report_path.read_text(encoding="utf-8"))["kept_faces"], 1)
|