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_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)
|