Spaces:
Running on Zero
Running on Zero
Download camera_io.py from LittleFrog/LVSPM: direct link, hf CLI and curl.
- Browser
- Download file 4.55 kB
-
https://huggingface.co/spaces/LittleFrog/LVSPM/resolve/main/camera_io.py
- Command line
-
hf download hf://spaces/LittleFrog/LVSPM/camera_io.py
-
curl -L -o camera_io.py https://huggingface.co/spaces/LittleFrog/LVSPM/resolve/main/camera_io.py
4.55 kB
| """Camera downloads in Nerfstudio/OpenGL and COLMAP/OpenCV conventions.""" | |
| import json | |
| from pathlib import Path | |
| from zipfile import ZIP_DEFLATED, ZipFile | |
| import numpy as np | |
| import trimesh | |
| from scipy.spatial.transform import Rotation | |
| def export_cameras(root: Path, c2w: np.ndarray, intrinsics: np.ndarray, | |
| image_names: list[str], width: int, height: int) -> tuple[str, str]: | |
| """Export poses for the processed images without changing the world frame.""" | |
| if len(c2w) != len(intrinsics) or len(c2w) != len(image_names): | |
| raise ValueError("Camera and image counts differ") | |
| if not np.isfinite(c2w).all() or not np.isfinite(intrinsics).all(): | |
| raise ValueError("Cameras must be finite") | |
| frames, cameras, images = [], [], [] | |
| flip = np.diag([1.0, -1.0, -1.0, 1.0]) | |
| for index, (pose, k, name) in enumerate(zip(c2w, intrinsics, image_names), 1): | |
| if Path(name).name != name or any(ch.isspace() for ch in name): | |
| raise ValueError("Export filenames must be simple basenames") | |
| fx, fy, cx, cy = k[0, 0], k[1, 1], k[0, 2], k[1, 2] | |
| frames.append({ | |
| "file_path": f"images/{name}", | |
| "transform_matrix": (pose @ flip).tolist(), | |
| "fl_x": float(fx), "fl_y": float(fy), | |
| "cx": float(cx), "cy": float(cy), "w": width, "h": height, | |
| }) | |
| cameras.append(f"{index} PINHOLE {width} {height} {fx:.12g} {fy:.12g} {cx:.12g} {cy:.12g}\n") | |
| w2c = np.linalg.inv(pose) | |
| xyzw = Rotation.from_matrix(w2c[:3, :3]).as_quat() | |
| qwxyz = xyzw[[3, 0, 1, 2]] | |
| values = " ".join(f"{value:.12g}" for value in np.r_[qwxyz, w2c[:3, 3]]) | |
| # COLMAP requires a second (empty here) observations line per image. | |
| images.append(f"{index} {values} {index} {name}\n\n") | |
| transforms = root / "transforms.json" | |
| transforms.write_text(json.dumps({"camera_model": "OPENCV", "k1": 0.0, | |
| "k2": 0.0, "p1": 0.0, "p2": 0.0, "frames": frames}, indent=2) + "\n") | |
| sparse = root / "sparse" / "0" | |
| sparse.mkdir(parents=True, exist_ok=True) | |
| (sparse / "cameras.txt").write_text("# CAMERA_ID MODEL WIDTH HEIGHT PARAMS[]\n" + "".join(cameras)) | |
| (sparse / "images.txt").write_text("# IMAGE_ID QW QX QY QZ TX TY TZ CAMERA_ID NAME\n# Next line: POINTS2D[] (empty)\n" + "".join(images)) | |
| (sparse / "points3D.txt").write_text("# Camera-only model; no reconstructed 3D points.\n") | |
| outputs = [] | |
| for filename, metadata in [("nerfstudio.zip", [transforms]), | |
| ("colmap.zip", list(sparse.glob("*.txt")))]: | |
| archive = root / filename | |
| with ZipFile(archive, "w", compression=ZIP_DEFLATED) as handle: | |
| for path in metadata + [root / "images" / name for name in image_names]: | |
| handle.write(path, path.relative_to(root).as_posix()) | |
| outputs.append(str(archive)) | |
| return tuple(outputs) | |
| def camera_scene(root: Path, c2w: np.ndarray, intrinsics: np.ndarray, | |
| width: int, height: int, trajectory: np.ndarray | None = None) -> str: | |
| """Create a GLB of camera frusta and optional render path (no inferred mesh).""" | |
| scene = trimesh.Scene() | |
| centers = c2w[:, :3, 3] | |
| span = max(float(np.linalg.norm(np.ptp(centers, axis=0))), 0.1) | |
| depth, radius = span * 0.045, span * 0.0015 | |
| def line(a, b, color): | |
| if np.linalg.norm(b - a) < 1e-9: | |
| return | |
| mesh = trimesh.creation.cylinder(radius=radius, segment=np.stack([a, b]), sections=6) | |
| mesh.visual.face_colors = color | |
| scene.add_geometry(mesh) | |
| for index, (pose, k) in enumerate(zip(c2w, intrinsics)): | |
| pixels = np.array([[0, 0, 1], [width, 0, 1], [width, height, 1], [0, height, 1]]) | |
| corners = (np.linalg.inv(k) @ pixels.T).T * depth | |
| corners = corners @ pose[:3, :3].T + pose[:3, 3] | |
| color = [55, int(115 + 105 * index / max(len(c2w) - 1, 1)), 245, 255] | |
| for corner in corners: | |
| line(pose[:3, 3], corner, color) | |
| for left, right in zip(corners, np.roll(corners, -1, axis=0)): | |
| line(left, right, color) | |
| if trajectory is not None: | |
| for left, right in zip(trajectory[:-1, :3, 3], trajectory[1:, :3, 3]): | |
| line(left, right, [255, 165, 45, 255]) | |
| # Display camera coordinates in a Y-up viewer; exported poses stay untouched. | |
| scene.apply_transform(np.diag([1.0, -1.0, -1.0, 1.0])) | |
| output = root / ("cameras.glb" if trajectory is None else "cameras_with_path.glb") | |
| scene.export(str(output)) | |
| return str(output) | |