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_depth_to_splat.py
#!/usr/bin/env python3
"""Dichter Szenen-Splat aus Equirect + DepthAnything-Tiefenkarte.

Projiziert jeden Pixel des 360°-Bildes per Kugelkoordinaten und der
(Disparitäts-)Tiefe in einen farbigen 3D-Splat. Ergebnis: eine dichte,
lückenlose Oberfläche der Szene vom Aufnahmepunkt aus.

Verbesserungen gegen Unschärfe & schwarze Löcher:
- höhere Auflösung + kleinere Splats (schärfer)
- Kantenbehandlung: an Tiefensprüngen (Türöffnungen) werden gedehnte
  "Gummiband"-Splats verworfen statt schwarz/verschmiert darzustellen
- Kappung unzuverlässiger Ferntiefen (Fenster/Himmel)

Ausgabe: antimatter15 .splat (32 Bytes/Splat), passend zum Web-Viewer.
"""
from __future__ import annotations

import argparse
from pathlib import Path

import numpy as np
from PIL import Image


def build(pano_dir: Path, out: Path, width: int, depth_scale: float,
          near_disp: float = 0.05, gap: float = 1.25,
          far_cap: float = 7.5, edge_ratio: float = 0.18) -> None:
    eq_path = pano_dir / "equirect.jpg"
    dm_path = pano_dir / "dmap.png"
    if not eq_path.is_file() or not dm_path.is_file():
        raise SystemExit(f"equirect/dmap fehlt in {pano_dir}")

    W = width
    H = W // 2
    color = Image.open(eq_path).convert("RGB").resize((W, H), Image.LANCZOS)
    depth = Image.open(dm_path).convert("L").resize((W, H), Image.BILINEAR)
    col = np.asarray(color, dtype=np.float32) / 255.0
    disp = np.asarray(depth, dtype=np.float32) / 255.0      # 0..1, hell=nah

    us = (np.arange(W) + 0.5) / W
    vs = (np.arange(H) + 0.5) / H
    lon = (us - 0.5) * 2.0 * np.pi
    lat = (0.5 - vs) * np.pi
    lon_g, lat_g = np.meshgrid(lon, lat)

    cos_lat = np.cos(lat_g)
    dir_x = cos_lat * np.sin(lon_g)
    dir_y = -np.sin(lat_g)
    dir_z = cos_lat * np.cos(lon_g)

    dist = depth_scale / (disp + near_disp)
    dist = np.clip(dist, 0.1, far_cap)

    pos = np.stack([dir_x * dist, dir_y * dist, dir_z * dist], axis=-1)

    dtheta = 2.0 * np.pi / W
    scale = (dist * dtheta * gap).astype(np.float32)

    # Kantenmaske: große relative Disparitätssprünge zu Nachbarn = Tiefenkante
    # (Türrahmen/Verdeckung). Solche Pixel erzeugen gedehnte Splats → verwerfen.
    d = disp
    grad = np.zeros_like(d)
    grad[:, 1:]  = np.maximum(grad[:, 1:],  np.abs(d[:, 1:]  - d[:, :-1]))
    grad[:, :-1] = np.maximum(grad[:, :-1], np.abs(d[:, :-1] - d[:, 1:]))
    grad[1:, :]  = np.maximum(grad[1:, :],  np.abs(d[1:, :]  - d[:-1, :]))
    grad[:-1, :] = np.maximum(grad[:-1, :], np.abs(d[:-1, :] - d[1:, :]))
    keep = (grad < edge_ratio)
    # Ganz ferne (unzuverlässige) Pixel ebenfalls verwerfen.
    keep &= (disp > 0.06)

    keep_flat = keep.reshape(-1)
    n = int(keep_flat.sum())
    idx = np.where(keep_flat)[0]

    positions = pos.reshape(-1, 3)[idx].astype(np.float32)
    scales = np.repeat(scale.reshape(-1, 1)[idx], 3, axis=1).astype(np.float32)
    rgb = col.reshape(-1, 3)[idx] * 255.0
    rgba = np.concatenate([rgb, np.full((n, 1), 255.0)], axis=1)
    rgba = np.clip(rgba, 0, 255).astype(np.uint8)
    quat = np.tile(np.array([255, 128, 128, 128], dtype=np.uint8), (n, 1))

    rec = np.zeros(n, dtype=[("pos", np.float32, 3), ("scale", np.float32, 3),
                            ("rgba", np.uint8, 4), ("quat", np.uint8, 4)])
    rec["pos"] = positions
    rec["scale"] = scales
    rec["rgba"] = rgba
    rec["quat"] = quat
    out.parent.mkdir(parents=True, exist_ok=True)
    rec.tofile(out)
    total = W * H
    print(f"{out}: {n}/{total} Splats ({n*32/1e6:.1f} MB), "
          f"{100*n/total:.0f}% behalten, Distanz {dist.min():.2f}–{dist.max():.2f}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("pano_dir")
    ap.add_argument("out")
    ap.add_argument("--width", type=int, default=1600)
    ap.add_argument("--depth-scale", type=float, default=1.6)
    ap.add_argument("--gap", type=float, default=1.25)
    ap.add_argument("--far-cap", type=float, default=7.5)
    args = ap.parse_args()
    build(Path(args.pano_dir), Path(args.out), args.width, args.depth_scale,
          gap=args.gap, far_cap=args.far_cap)