Upload scripts/reconstruct.py
Browse files- scripts/reconstruct.py +112 -0
scripts/reconstruct.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
CLI script: reconstruct a mesh from a point-cloud file using NKSR.
|
| 4 |
+
|
| 5 |
+
Usage
|
| 6 |
+
-----
|
| 7 |
+
python reconstruct.py input.ply output.ply --detail 1.0 --mise-iter 1
|
| 8 |
+
python reconstruct.py input.ply output.ply --chunk-size 50.0 --no-normals
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
# Allow running from repo root without installing
|
| 18 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 19 |
+
|
| 20 |
+
from nksr_wrapper import NKSRMeshReconstructor, load_point_cloud, save_mesh
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main() -> None:
|
| 24 |
+
parser = argparse.ArgumentParser(
|
| 25 |
+
description="NKSR point-cloud → mesh reconstruction"
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument("input", type=Path, help="Input PLY or PCD file")
|
| 28 |
+
parser.add_argument("output", type=Path, help="Output mesh file (PLY/OBJ/GLB)")
|
| 29 |
+
parser.add_argument("--device", default="cuda:0", help="PyTorch device")
|
| 30 |
+
parser.add_argument("--config", default="ks", help="NKSR model config (ks/snet/snet-wonormal)")
|
| 31 |
+
parser.add_argument("--detail", type=float, default=1.0, help="Detail level 0.0-1.0")
|
| 32 |
+
parser.add_argument("--voxel-size", type=float, default=None, help="Override voxel size")
|
| 33 |
+
parser.add_argument("--chunk-size", type=float, default=-1.0, help="Chunk size for large scenes")
|
| 34 |
+
parser.add_argument("--mise-iter", type=int, default=1, help="MISE iterations")
|
| 35 |
+
parser.add_argument("--no-normals", action="store_true", help="Ignore normals in file; estimate them")
|
| 36 |
+
parser.add_argument("--estimate-normals", action="store_true", help="Estimate normals if file lacks them")
|
| 37 |
+
parser.add_argument("--sensor", type=Path, default=None, help="Optional NPY file with sensor positions")
|
| 38 |
+
parser.add_argument("--colors", type=Path, default=None, help="Optional NPY file with per-point RGB colors")
|
| 39 |
+
parser.add_argument("--solver-iter", type=int, default=2000, help="PCG solver max iterations")
|
| 40 |
+
parser.add_argument("--solver-tol", type=float, default=1e-5, help="PCG solver tolerance")
|
| 41 |
+
parser.add_argument("--verbose", action="store_true", help="Print extra progress info")
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
+
|
| 44 |
+
if not args.input.exists():
|
| 45 |
+
parser.error(f"Input file not found: {args.input}")
|
| 46 |
+
|
| 47 |
+
# ---- load point cloud -----------------------------------------------
|
| 48 |
+
print(f"Loading point cloud from {args.input} ...")
|
| 49 |
+
points, normals = load_point_cloud(
|
| 50 |
+
args.input,
|
| 51 |
+
estimate_normals=args.estimate_normals or args.no_normals,
|
| 52 |
+
)
|
| 53 |
+
print(f" Loaded {len(points)} points")
|
| 54 |
+
if normals is not None:
|
| 55 |
+
print(f" Normals present: {normals.shape}")
|
| 56 |
+
elif not args.no_normals:
|
| 57 |
+
print(" No normals found in file — will estimate on-the-fly")
|
| 58 |
+
|
| 59 |
+
if args.no_normals:
|
| 60 |
+
normals = None
|
| 61 |
+
print(" --no-normals set: normals will be estimated")
|
| 62 |
+
|
| 63 |
+
# ---- optional extras ------------------------------------------------
|
| 64 |
+
sensor = None
|
| 65 |
+
if args.sensor:
|
| 66 |
+
import numpy as np
|
| 67 |
+
sensor = np.load(args.sensor)
|
| 68 |
+
print(f" Sensor positions loaded: {sensor.shape}")
|
| 69 |
+
|
| 70 |
+
colors = None
|
| 71 |
+
if args.colors:
|
| 72 |
+
import numpy as np
|
| 73 |
+
colors = np.load(args.colors)
|
| 74 |
+
print(f" Per-point colors loaded: {colors.shape}")
|
| 75 |
+
|
| 76 |
+
# ---- reconstruct ----------------------------------------------------
|
| 77 |
+
print("\nInitialising NKSR reconstructor ...")
|
| 78 |
+
if not torch.cuda.is_available() and args.device.startswith("cuda"):
|
| 79 |
+
print("WARNING: CUDA not available, falling back to CPU (very slow)")
|
| 80 |
+
args.device = "cpu"
|
| 81 |
+
|
| 82 |
+
recon = NKSRMeshReconstructor(
|
| 83 |
+
device=args.device,
|
| 84 |
+
config=args.config,
|
| 85 |
+
chunk_tmp_device="cpu" if args.chunk_size > 0 else None,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
print("Reconstructing mesh ...")
|
| 89 |
+
mesh = recon.reconstruct(
|
| 90 |
+
points=points,
|
| 91 |
+
normals=normals,
|
| 92 |
+
sensor_positions=sensor,
|
| 93 |
+
colors=colors,
|
| 94 |
+
detail_level=args.detail,
|
| 95 |
+
voxel_size=args.voxel_size,
|
| 96 |
+
chunk_size=args.chunk_size,
|
| 97 |
+
mise_iter=args.mise_iter,
|
| 98 |
+
solver_max_iter=args.solver_iter,
|
| 99 |
+
solver_tol=args.solver_tol,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# ---- save -----------------------------------------------------------
|
| 103 |
+
print(f"\nSaving mesh to {args.output} ...")
|
| 104 |
+
save_mesh(args.output, mesh.vertices, mesh.faces, mesh.vertex_colors)
|
| 105 |
+
print(f" Vertices: {len(mesh.vertices):,} | Faces: {len(mesh.faces):,}")
|
| 106 |
+
if mesh.vertex_colors is not None:
|
| 107 |
+
print(" Vertex colors included")
|
| 108 |
+
print("Done.")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
main()
|