HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/panomity.de/sharp/equirect_views.py
#!/usr/bin/env python3
"""Equirect → überlappende Perspektiv-Ansichten für COLMAP.

12 Ansichten bei Pitch 0° (60° FOV, 30°-Schritte) + 4 bei Pitch -35°
(75° FOV) pro Pano. Ausgabe 1280px, Dateiname pN_yYAW_pPITCH.jpg.
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
from PIL import Image

PANOS_DIR = Path("/home/panomity.de/vr.panomity.com/panos")
OUT_DIR = Path(sys.argv[1] if len(sys.argv) > 1 else "/home/panomity.de/gsplat-apartment/images2")

SIZE = 1280


def e2p(equi: np.ndarray, fov_deg: float, yaw_deg: float, pitch_deg: float, size: int) -> np.ndarray:
    """Equirectangular → perspektivische Ansicht (Pinhole, quadratisch)."""
    h, w, _ = equi.shape
    fov = np.deg2rad(fov_deg)
    yaw = np.deg2rad(yaw_deg)
    pitch = np.deg2rad(pitch_deg)

    f = (size / 2) / np.tan(fov / 2)
    xs = np.arange(size) - (size - 1) / 2
    ys = np.arange(size) - (size - 1) / 2
    px, py = np.meshgrid(xs, ys)

    # Kamerastrahlen (x rechts, y unten, z vorwärts)
    dirs = np.stack([px / f, py / f, np.ones_like(px)], axis=-1)
    dirs /= np.linalg.norm(dirs, axis=-1, keepdims=True)

    # Pitch (um x) dann Yaw (um y)
    cp, sp = np.cos(pitch), np.sin(pitch)
    rot_x = np.array([[1, 0, 0], [0, cp, -sp], [0, sp, cp]])
    cy, sy = np.cos(yaw), np.sin(yaw)
    rot_y = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]])
    dirs = dirs @ rot_x.T @ rot_y.T

    dx, dy, dz = dirs[..., 0], dirs[..., 1], dirs[..., 2]
    lon = np.arctan2(dx, dz)          # -pi..pi
    lat = np.arcsin(np.clip(dy, -1, 1))  # -pi/2..pi/2

    u = (lon / (2 * np.pi) + 0.5) * (w - 1)
    v = (lat / np.pi + 0.5) * (h - 1)

    u0 = np.floor(u).astype(int) % w
    u1 = (u0 + 1) % w
    v0 = np.clip(np.floor(v).astype(int), 0, h - 1)
    v1 = np.clip(v0 + 1, 0, h - 1)
    fu = (u - np.floor(u))[..., None]
    fv = (v - np.floor(v))[..., None]

    img = (equi[v0, u0] * (1 - fu) * (1 - fv)
           + equi[v0, u1] * fu * (1 - fv)
           + equi[v1, u0] * (1 - fu) * fv
           + equi[v1, u1] * fu * fv)
    return img.astype(np.uint8)


def main() -> None:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    # Einheitliches 60°-FOV, damit COLMAP eine einzige Pinhole-Kamera nutzen kann
    # (f = 640 / tan(30°) = 1108.51 bei 1280px).
    views = [(60.0, yaw, 0.0) for yaw in range(0, 360, 30)]
    views += [(60.0, yaw, -35.0) for yaw in range(0, 360, 90)]

    for tiles in sorted(PANOS_DIR.glob("p*.tiles"), key=lambda d: int(d.name[1:-6])):
        pano = tiles.name.replace(".tiles", "")
        eq_path = tiles / "equirect.jpg"
        if not eq_path.is_file():
            continue
        done = [OUT_DIR / f"{pano}_y{int(y):03d}_p{int(p):+03d}.jpg" for _, y, p in views]
        if all(d.is_file() for d in done):
            print(f"{pano}: vorhanden")
            continue
        equi = np.asarray(Image.open(eq_path).convert("RGB"))
        for fov, yaw, pitch in views:
            out = OUT_DIR / f"{pano}_y{int(yaw):03d}_p{int(pitch):+03d}.jpg"
            if out.is_file():
                continue
            img = e2p(equi, fov, yaw, pitch, SIZE)
            Image.fromarray(img).save(out, quality=90)
        print(f"{pano}: {len(views)} Ansichten")


if __name__ == "__main__":
    main()