AccessPath / tools /render_accessibility_turntable.py
anonymous-accesspath's picture
Audited anonymous-review AccessPath release
2f382c4 verified
Raw
History Blame Contribute Delete
50 kB
#!/usr/bin/env python3
"""Render one accessibility geometry mesh into a compact turntable review set.
Open3D is imported only after the render environment has been configured. This
keeps argument/manifest helpers usable on CPU-only review machines and makes a
software fallback an explicit, auditable user choice.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, MutableMapping, Sequence
import imageio.v2 as imageio
import numpy as np
from PIL import Image, ImageDraw, ImageOps
REQUESTED_RENDERER = "Open3D/EGL"
RENDERER_IMPLEMENTATION = "open3d.visualization.rendering.OffscreenRenderer"
PRODUCTION_RENDER_DEVICE = "physical_nvidia_gpu"
DOMINANT_COMPONENT_MIN_FACE_FRACTION = 0.90
GL_VENDOR = 0x1F00
GL_RENDERER = 0x1F01
GL_VERSION = 0x1F02
GL_STRING_NAMES = {
GL_VENDOR: "GL_VENDOR",
GL_RENDERER: "GL_RENDERER",
GL_VERSION: "GL_VERSION",
}
SOFTWARE_OPENGL_MARKERS = (
"llvmpipe",
"softpipe",
"swrast",
"software",
"swiftshader",
"lavapipe",
)
NVIDIA_EGL_VENDOR_JSON = Path(
"/usr/share/glvnd/egl_vendor.d/10_nvidia.json"
)
OUTPUT_NAMES = {
"video": "turntable.mp4",
"gif": "turntable.gif",
"poster": "turntable_poster.png",
"multiview": "multiview.jpg",
}
def parse_color(value: str) -> tuple[float, float, float, float]:
"""Parse an RGB/RGBA command-line color in the inclusive [0, 1] range."""
values = [
float(item.strip())
for item in value.replace(":", ",").replace(";", ",").split(",")
if item.strip()
]
if len(values) == 3:
values.append(1.0)
if len(values) != 4:
raise argparse.ArgumentTypeError("expected three or four comma-separated numbers")
if any(not math.isfinite(item) or item < 0.0 or item > 1.0 for item in values):
raise argparse.ArgumentTypeError("background components must be between 0 and 1")
return tuple(values) # type: ignore[return-value]
def parse_yaws(value: str) -> tuple[float, ...]:
"""Parse the yaw angles used in the multiview contact sheet."""
try:
values = tuple(float(item.strip()) for item in value.split(",") if item.strip())
except ValueError as error:
raise argparse.ArgumentTypeError("multiview yaws must be comma-separated numbers") from error
if not values:
raise argparse.ArgumentTypeError("at least one multiview yaw is required")
if len(values) > 12:
raise argparse.ArgumentTypeError("at most 12 multiview yaws are supported")
if any(not math.isfinite(item) for item in values):
raise argparse.ArgumentTypeError("multiview yaws must be finite")
return values
def build_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--mesh",
"--ply",
dest="mesh",
required=True,
help="Input triangle mesh. --ply is retained as a convenient alias.",
)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--sample-id", default=None)
parser.add_argument("--category", default="unknown")
parser.add_argument("--width", type=int, default=960)
parser.add_argument("--height", type=int, default=720)
parser.add_argument("--frames", type=int, default=96)
parser.add_argument("--fps", type=int, default=24)
parser.add_argument("--fov", type=float, default=32.0)
parser.add_argument("--elevation", type=float, default=18.0)
parser.add_argument("--distance-scale", type=float, default=1.25)
parser.add_argument("--shader", default="defaultUnlit")
parser.add_argument(
"--vertex-colors-srgb",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Treat byte RGB vertex colors sampled from photographs as sRGB. "
"Enabled by default to avoid linear-color whitening."
),
)
parser.add_argument("--background", type=parse_color, default=parse_color("0.82,0.82,0.80,1"))
parser.add_argument("--smooth-iterations", type=int, default=0)
parser.add_argument(
"--orbit-mode",
choices=("full_360", "front_arc"),
default="front_arc",
help=(
"full_360 is a conventional object turntable. front_arc keeps an "
"open-world scene in camera-facing review views and avoids treating "
"its unobserved back side as reconstructed evidence."
),
)
parser.add_argument(
"--orbit-span",
type=float,
default=140.0,
help="Total yaw span in degrees for --orbit-mode front_arc.",
)
parser.add_argument(
"--multiview-yaws",
type=parse_yaws,
default=parse_yaws("-60,-30,0,30,60"),
help=(
"Comma-separated camera-facing yaw angles. In front_arc mode every "
"angle must stay within half of --orbit-span."
),
)
parser.add_argument(
"--require-vertex-colors",
action="store_true",
help=(
"Reject meshes without a complete finite RGB value for every rendered "
"vertex instead of silently producing an untextured white surface."
),
)
parser.add_argument(
"--cpu-fallback",
action="store_true",
# Retained only for low-level renderer diagnostics and unit tests. It is
# intentionally absent from normal CLI help and every production
# orchestrator; production rendering is physical-NVIDIA-GPU only.
help=argparse.SUPPRESS,
)
return parser
def validate_arguments(args: argparse.Namespace) -> None:
if args.width < 64 or args.height < 64:
raise ValueError("--width and --height must each be at least 64")
if args.frames < 2:
raise ValueError("--frames must be at least 2")
if args.fps < 1:
raise ValueError("--fps must be at least 1")
if not math.isfinite(args.fov) or not (5.0 <= args.fov < 170.0):
raise ValueError("--fov must satisfy 5 <= fov < 170")
if not math.isfinite(args.elevation):
raise ValueError("--elevation must be finite")
if not math.isfinite(args.distance_scale) or args.distance_scale <= 0:
raise ValueError("--distance-scale must be positive")
if args.smooth_iterations < 0:
raise ValueError("--smooth-iterations must be non-negative")
if not (20.0 <= args.orbit_span <= 240.0):
raise ValueError("--orbit-span must satisfy 20 <= span <= 240")
if args.orbit_mode == "front_arc":
half_span = 0.5 * args.orbit_span
if any(abs(yaw) > half_span + 1e-6 for yaw in args.multiview_yaws):
raise ValueError(
"front_arc multiview yaws must stay within half of --orbit-span"
)
def _visibility_state(value: str | None) -> str:
if value is None:
return "unspecified"
if not value.strip() or value.strip().lower() in {"-1", "none", "void", "nodevfiles"}:
return "hidden_or_disabled"
return "configured_visible_set"
def inspect_render_environment(
environment: Mapping[str, str] | None = None,
*,
cpu_fallback: bool = False,
) -> dict[str, Any]:
"""Return environment evidence without claiming that Open3D used a GPU.
CUDA visibility and EGL configuration are useful diagnostics, but neither
proves which physical renderer Filament selected. The returned status is
intentionally conservative.
"""
env = os.environ if environment is None else environment
cuda_visible = env.get("CUDA_VISIBLE_DEVICES")
egl_platform = env.get("EGL_PLATFORM")
software_value = env.get("LIBGL_ALWAYS_SOFTWARE")
software_requested = cpu_fallback or software_value == "1"
return {
"slurm": {
"job_id": env.get("SLURM_JOB_ID"),
"job_gpus": env.get("SLURM_JOB_GPUS"),
"gpu_allocation_present": bool(env.get("SLURM_JOB_ID")),
},
"egl": {
"platform": egl_platform,
"platform_configured": egl_platform is not None,
"visibility": "configured" if egl_platform is not None else "unspecified",
"device_id": env.get("EGL_DEVICE_ID"),
"gpu_use": "not_proven",
},
"cuda": {
"visible_devices": cuda_visible,
"visibility": _visibility_state(cuda_visible),
"used_by_renderer": "not_proven",
},
"software_rendering": {
"requested": software_requested,
"libgl_always_software": software_value,
"mesa_loader_driver_override": env.get("MESA_LOADER_DRIVER_OVERRIDE"),
"gallium_driver": env.get("GALLIUM_DRIVER"),
},
"hardware_acceleration": {
"status": (
"software_fallback_requested_backend_unverified"
if software_requested
else "hardware_backend_unverified"
),
"reason": (
"Open3D OffscreenRenderer does not expose reliable physical-device "
"identity through this render path; environment visibility alone is "
"not hardware proof."
),
},
}
def require_slurm_gpu_environment(
environment: Mapping[str, str] | None = None,
) -> dict[str, Any]:
"""Fail closed unless rendering runs inside a visible Slurm GPU allocation."""
env = os.environ if environment is None else environment
job_id = str(env.get("SLURM_JOB_ID") or "").strip()
cuda_visible = str(env.get("CUDA_VISIBLE_DEVICES") or "").strip()
if not job_id:
raise RuntimeError(
"GPU rendering is required by default and must run inside a Slurm "
"allocation. Use sbatch/srun, or explicitly pass --cpu-fallback "
"for a diagnostic software render."
)
if not cuda_visible or cuda_visible.lower() in {"-1", "none", "void", "nodevfiles"}:
raise RuntimeError(
"The Slurm render job has no visible CUDA device; refusing an "
"implicit CPU/software fallback."
)
return {
"policy": "slurm_gpu_required_unless_cpu_fallback_explicit",
"slurm_job_id": job_id,
"cuda_visible_devices": cuda_visible,
"gpu_allocation_contract_satisfied": True,
}
def configure_render_environment(
cpu_fallback: bool,
environment: MutableMapping[str, str] | None = None,
) -> dict[str, Any]:
"""Configure EGL before importing Open3D and return auditable evidence."""
env = os.environ if environment is None else environment
original = {
key: env.get(key)
for key in (
"EGL_PLATFORM",
"CUDA_VISIBLE_DEVICES",
"__EGL_VENDOR_LIBRARY_FILENAMES",
"LIBGL_ALWAYS_SOFTWARE",
"MESA_LOADER_DRIVER_OVERRIDE",
"GALLIUM_DRIVER",
)
}
env.setdefault("EGL_PLATFORM", "surfaceless")
gpu_contract = None
if cpu_fallback:
env["LIBGL_ALWAYS_SOFTWARE"] = "1"
env.setdefault("MESA_LOADER_DRIVER_OVERRIDE", "llvmpipe")
env.setdefault("GALLIUM_DRIVER", "llvmpipe")
else:
gpu_contract = require_slurm_gpu_environment(env)
if NVIDIA_EGL_VENDOR_JSON.is_file():
env.setdefault(
"__EGL_VENDOR_LIBRARY_FILENAMES",
str(NVIDIA_EGL_VENDOR_JSON),
)
evidence = inspect_render_environment(env, cpu_fallback=cpu_fallback)
evidence["configuration"] = {
"original": original,
"egl_platform_defaulted_by_tool": original["EGL_PLATFORM"] is None,
"nvidia_egl_vendor_defaulted_by_tool": (
not cpu_fallback
and original["__EGL_VENDOR_LIBRARY_FILENAMES"] is None
and env.get("__EGL_VENDOR_LIBRARY_FILENAMES")
== str(NVIDIA_EGL_VENDOR_JSON)
),
"cpu_fallback_explicit": cpu_fallback,
"gpu_contract": gpu_contract,
}
return evidence
def _load_gl_get_string() -> tuple[Any, str]:
"""Resolve ``glGetString`` without importing an OpenGL Python package.
Open3D wheels do not expose Filament's selected renderer through their
Python API. Once ``OffscreenRenderer`` has created its EGL context, the GL
dispatch function is sufficient to query the active context directly.
"""
import ctypes
import ctypes.util
attempts: list[str] = []
candidates: list[tuple[str, str | None]] = [("process", None)]
for library_name in ("GL", "OpenGL", "GLESv2"):
library_path = ctypes.util.find_library(library_name)
if library_path:
candidates.append((library_name, library_path))
for label, library_path in candidates:
try:
library = ctypes.CDLL(library_path)
gl_get_string = getattr(library, "glGetString")
except (AttributeError, OSError) as error:
attempts.append(f"{label}: {error}")
continue
gl_get_string.argtypes = [ctypes.c_uint]
gl_get_string.restype = ctypes.c_char_p
return gl_get_string, f"ctypes:{label}"
# Some EGL/GLES deployments expose GL entry points only through
# eglGetProcAddress. Keep this as a final standards-based resolution path.
egl_path = ctypes.util.find_library("EGL")
if egl_path:
try:
egl = ctypes.CDLL(egl_path)
egl_get_proc_address = egl.eglGetProcAddress
egl_get_proc_address.argtypes = [ctypes.c_char_p]
egl_get_proc_address.restype = ctypes.c_void_p
address = egl_get_proc_address(b"glGetString")
if address:
prototype = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_uint)
return prototype(address), "ctypes:EGL.eglGetProcAddress"
attempts.append("EGL.eglGetProcAddress: returned NULL")
except (AttributeError, OSError) as error:
attempts.append(f"EGL: {error}")
detail = "; ".join(attempts) or "no OpenGL libraries were discoverable"
raise RuntimeError(f"Could not resolve glGetString ({detail})")
def _decode_gl_string(value: Any) -> str | None:
"""Convert a ``glGetString`` result into a non-empty Python string."""
if value is None:
return None
if isinstance(value, bytes):
decoded = value.decode("utf-8", errors="replace")
elif isinstance(value, str):
decoded = value
else:
decoded = str(value)
decoded = decoded.strip()
return decoded or None
def query_current_opengl_context(
gl_get_string: Any | None = None,
*,
query_source: str | None = None,
) -> dict[str, Any]:
"""Query the OpenGL strings for the context current on this thread.
The optional callable is an injection seam for CPU-only unit tests. In
production this function is called immediately after constructing Open3D's
``OffscreenRenderer``.
"""
evidence: dict[str, Any] = {
"query_api": "glGetString",
"query_source": query_source,
"query_succeeded": False,
"GL_VENDOR": None,
"GL_RENDERER": None,
"GL_VERSION": None,
"software_renderer_detected": False,
"software_markers": [],
"verification_status": "active_context_unverified",
"error": None,
}
try:
if gl_get_string is None:
gl_get_string, resolved_source = _load_gl_get_string()
evidence["query_source"] = resolved_source
elif evidence["query_source"] is None:
evidence["query_source"] = "injected_gl_get_string"
errors: list[str] = []
for enum_value, field_name in GL_STRING_NAMES.items():
try:
evidence[field_name] = _decode_gl_string(gl_get_string(enum_value))
except Exception as error: # ctypes failures must become fail-closed evidence.
errors.append(f"{field_name}: {type(error).__name__}: {error}")
missing = [name for name in GL_STRING_NAMES.values() if not evidence[name]]
if missing:
errors.append("NULL or empty values: " + ", ".join(missing))
evidence["query_succeeded"] = not errors
if errors:
evidence["error"] = "; ".join(errors)
except Exception as error:
evidence["error"] = f"{type(error).__name__}: {error}"
combined = " ".join(
str(evidence[name] or "").lower() for name in GL_STRING_NAMES.values()
)
markers = [marker for marker in SOFTWARE_OPENGL_MARKERS if marker in combined]
evidence["software_markers"] = markers
evidence["software_renderer_detected"] = bool(markers)
if evidence["query_succeeded"]:
evidence["verification_status"] = (
"active_software_context_verified"
if markers
else "active_non_software_context_verified"
)
return evidence
def inspect_process_graphics_backend(
*,
maps_text: str | None = None,
fd_targets: Sequence[str] | None = None,
environment: Mapping[str, str] | None = None,
) -> dict[str, Any]:
"""Inspect process-wide EGL libraries and open GPU device handles.
Filament owns its OpenGL context on an internal render thread, so
``glGetString`` from the Python thread may legitimately return NULL. In
that case a process-wide NVIDIA EGL binding plus an open physical
``/dev/nvidiaN`` handle is stronger evidence than CUDA visibility alone.
Injection seams keep this policy fully unit-testable without a GPU.
"""
env = os.environ if environment is None else environment
errors: list[str] = []
if maps_text is None:
try:
maps_text = Path("/proc/self/maps").read_text(
encoding="utf-8",
errors="replace",
)
except OSError as error:
maps_text = ""
errors.append(f"maps: {type(error).__name__}: {error}")
if fd_targets is None:
discovered_targets: list[str] = []
try:
for entry in Path("/proc/self/fd").iterdir():
try:
discovered_targets.append(os.readlink(entry))
except OSError:
continue
except OSError as error:
errors.append(f"fd: {type(error).__name__}: {error}")
fd_targets = discovered_targets
mapped_paths = sorted(
{
line.rsplit(None, 1)[-1]
for line in maps_text.splitlines()
if line.strip() and "/" in line.rsplit(None, 1)[-1]
}
)
nvidia_egl_libraries = [
path
for path in mapped_paths
if (
"libegl_nvidia" in path.lower()
or "libnvidia-eglcore" in path.lower()
)
]
software_library_markers = sorted(
{
marker
for path in mapped_paths
for marker in (
"libegl_mesa",
"llvmpipe",
"softpipe",
"swrast",
"lavapipe",
"swiftshader",
)
if marker in path.lower()
}
)
gpu_device_fds = sorted(
{target for target in fd_targets if target.startswith("/dev/nvidia")}
)
physical_gpu_fds = [
target
for target in gpu_device_fds
if re.fullmatch(r"/dev/nvidia\d+", target)
]
nvidia_backend_verified = bool(
nvidia_egl_libraries
and physical_gpu_fds
and not software_library_markers
)
return {
"inspection_api": "/proc/self/maps + /proc/self/fd",
"nvidia_egl_libraries": nvidia_egl_libraries,
"software_library_markers": software_library_markers,
"gpu_device_fds": gpu_device_fds,
"physical_gpu_fds": physical_gpu_fds,
"forced_egl_vendor_json": env.get(
"__EGL_VENDOR_LIBRARY_FILENAMES"
),
"nvidia_process_backend_verified": nvidia_backend_verified,
"verification_status": (
"nvidia_egl_and_physical_device_fd_verified"
if nvidia_backend_verified
else "process_graphics_backend_unverified"
),
"errors": errors,
}
def enforce_opengl_context_policy(
opengl_context: Mapping[str, Any],
*,
cpu_fallback: bool,
process_backend: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Apply the fail-closed GPU policy to actual active-context evidence."""
query_succeeded = bool(opengl_context.get("query_succeeded"))
software_detected = bool(opengl_context.get("software_renderer_detected"))
renderer_name = opengl_context.get("GL_RENDERER")
nvidia_process_verified = bool(
process_backend
and process_backend.get("nvidia_process_backend_verified")
)
if cpu_fallback:
if not query_succeeded:
status = "cpu_fallback_requested_context_unverified"
reason = (
"Explicit --cpu-fallback permits diagnostic rendering even though "
"the active OpenGL strings could not be verified."
)
elif software_detected:
status = "software_fallback_active_context_verified"
reason = (
"Explicit --cpu-fallback was requested and the active OpenGL "
"context identifies a software renderer."
)
else:
status = "cpu_fallback_requested_non_software_context_verified"
reason = (
"Explicit --cpu-fallback was requested, but the active OpenGL "
"strings do not identify a known software renderer."
)
return {
"status": status,
"reason": reason,
"active_context_verified": query_succeeded,
"software_renderer_detected": software_detected,
"physical_gpu_identity_proven": False,
}
if software_detected:
markers = ", ".join(
str(value) for value in opengl_context.get("software_markers", [])
)
raise RuntimeError(
"The active OpenGL context is a software OpenGL renderer "
f"(GL_RENDERER={renderer_name!r}; markers={markers}); refusing the "
"default GPU render. Use --cpu-fallback to opt in explicitly."
)
if nvidia_process_verified:
physical_gpu_fds = list(
process_backend.get("physical_gpu_fds", [])
if process_backend is not None
else []
)
return {
"status": "active_nvidia_egl_process_backend_verified",
"reason": (
(
"The active OpenGL context contains no software-renderer "
"marker, and the renderer process loaded NVIDIA EGL without "
"a known software library and opened a physical NVIDIA GPU "
"device."
)
if query_succeeded
else (
"Filament's render-thread GL strings were not visible to "
"Python, but the renderer process loaded NVIDIA EGL without "
"a known software library and opened a physical NVIDIA GPU "
"device."
)
),
"active_context_verified": query_succeeded,
"process_backend_verified": True,
"software_renderer_detected": False,
"physical_gpu_identity_proven": True,
"physical_gpu_fds": physical_gpu_fds,
}
if not query_succeeded:
detail = str(opengl_context.get("error") or "no GL strings were returned")
raise RuntimeError(
"Could not verify the active OpenGL context after creating "
f"OffscreenRenderer; refusing the default GPU render ({detail}). "
"Use --cpu-fallback only for an explicit diagnostic software render."
)
raise RuntimeError(
"The active OpenGL strings contain no known software marker, but the "
"renderer process did not prove an NVIDIA EGL backend plus a physical "
"/dev/nvidiaN device; refusing the default GPU render. Use "
"--cpu-fallback only for an explicit diagnostic software render."
)
def _merge_runtime_graphics_evidence(
environment_evidence: Mapping[str, Any],
render_result: Mapping[str, Any],
) -> dict[str, Any]:
"""Combine pre-import environment facts with post-context GL evidence."""
merged = dict(environment_evidence)
opengl_context = render_result.get("opengl_context")
process_backend = render_result.get("process_graphics_backend")
hardware_acceleration = render_result.get("hardware_acceleration")
if isinstance(opengl_context, Mapping):
merged["opengl_context"] = dict(opengl_context)
if isinstance(process_backend, Mapping):
merged["process_graphics_backend"] = dict(process_backend)
if isinstance(hardware_acceleration, Mapping):
merged["hardware_acceleration"] = dict(hardware_acceleration)
return merged
def relative_path(path: Path, anchor: Path) -> str:
"""Return a portable relative path, even for an input outside ``anchor``."""
return Path(os.path.relpath(path.resolve(), anchor.resolve())).as_posix()
def file_record(path: Path, anchor: Path) -> dict[str, Any]:
import hashlib
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return {
"path": relative_path(path, anchor),
"sha256": digest.hexdigest(),
"bytes": path.stat().st_size,
}
def build_render_manifest(
*,
mesh_path: Path,
output_dir: Path,
sample_id: str,
category: str,
render_settings: Mapping[str, Any],
render_result: Mapping[str, Any],
environment_evidence: Mapping[str, Any],
cpu_fallback: bool,
) -> dict[str, Any]:
"""Build a manifest whose filesystem paths are all relative."""
runtime_evidence = _merge_runtime_graphics_evidence(
environment_evidence,
render_result,
)
opengl_context = dict(runtime_evidence.get("opengl_context", {}))
process_backend = dict(
runtime_evidence.get("process_graphics_backend", {})
)
outputs = {
role: file_record(output_dir / filename, output_dir)
for role, filename in OUTPUT_NAMES.items()
}
return {
"schema_version": "accessibilityamodal_turntable_v2",
"created_at_utc": datetime.now(timezone.utc).isoformat(),
"sample_id": sample_id,
"category": category,
"requested_renderer": REQUESTED_RENDERER,
"renderer": {
"requested": REQUESTED_RENDERER,
"implementation": RENDERER_IMPLEMENTATION,
"production_render_device": PRODUCTION_RENDER_DEVICE,
"production_gpu_fail_closed": True,
"requested_execution": (
"cpu_fallback_explicit"
if cpu_fallback
else "slurm_gpu_required_egl"
),
"cpu_fallback_explicit": cpu_fallback,
"gpu_required_by_default": True,
"render_call_succeeded": True,
"hardware_acceleration_status": runtime_evidence[
"hardware_acceleration"
]["status"],
"opengl_context": opengl_context,
"process_graphics_backend": process_backend,
},
"environment_evidence": runtime_evidence,
"input": {
"mesh": file_record(mesh_path, output_dir),
},
"outputs": outputs,
"render_settings": dict(render_settings),
"mesh_summary": {
"vertices": int(render_result["vertices"]),
"renderable_vertices": int(
render_result.get("renderable_vertices", render_result["vertices"])
),
"unreferenced_vertices_excluded_from_camera_fit": int(
render_result.get(
"unreferenced_vertices_excluded_from_camera_fit",
0,
)
),
"faces": int(render_result["faces"]),
"camera_framing": dict(render_result.get("camera_framing", {})),
"camera_motion": dict(render_result.get("camera", {})),
"vertex_colors": dict(render_result.get("vertex_colors", {})),
},
"scene_review": dict(render_result.get("orbit", {})),
"limitations": [
"GPU mode rejects known software GL strings; when Filament keeps its context on a render thread, NVIDIA EGL mappings plus a physical /dev/nvidiaN handle provide process-backend proof.",
"When one connected surface contains at least 90% of faces, camera framing prioritizes it; detached islands remain in the mesh but may fall outside a close-up view.",
"front_arc shows the observed scene-facing hemisphere and intentionally does not claim an inferred back side.",
"This rotating geometry is review evidence, not metric accessibility or navigation certification.",
],
"path_policy": "All filesystem paths in this manifest are relative to manifest.json.",
}
def _camera_pose(
center: np.ndarray,
camera_distance: float,
yaw_degrees: float,
elevation_degrees: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
yaw = math.radians(yaw_degrees)
elevation = math.radians(elevation_degrees)
# ``camera_distance`` already includes the requested framing margin. Keep
# that margin in one place so --distance-scale is not applied twice.
distance = max(camera_distance, 1e-3)
eye = center + np.array(
[
distance * math.sin(yaw) * math.cos(elevation),
-distance * math.cos(yaw) * math.cos(elevation),
distance * math.sin(elevation),
],
dtype=np.float64,
)
return eye, center, np.array([0.0, 0.0, 1.0], dtype=np.float64)
def _orbit_yaws(
frames: int,
*,
mode: str,
span_degrees: float,
) -> list[float]:
"""Return deterministic yaw positions for object or open-scene review."""
if frames < 2:
raise ValueError("orbit requires at least two frames")
if mode == "full_360":
return [360.0 * index / frames for index in range(frames)]
if mode != "front_arc":
raise ValueError(f"Unsupported orbit mode: {mode}")
half_span = 0.5 * float(span_degrees)
# Begin at the input-facing view, sweep right, return through the front,
# sweep left, and finish near the front. This loops without a 360-degree
# discontinuity and never claims an unobserved back-side reconstruction.
return [
half_span * math.sin(2.0 * math.pi * index / frames)
for index in range(frames)
]
def _fitted_camera_distance(
bbox_extent: np.ndarray,
vertical_fov_degrees: float,
aspect_ratio: float,
) -> float:
radius = max(float(np.linalg.norm(bbox_extent)) * 0.5, 1e-3)
vertical_fov = math.radians(vertical_fov_degrees)
horizontal_fov = 2.0 * math.atan(math.tan(vertical_fov * 0.5) * aspect_ratio)
limiting_fov = min(vertical_fov, horizontal_fov)
return radius / max(math.sin(limiting_fov * 0.5), 1e-3)
def _fitted_camera_distance_for_view(
vertices: np.ndarray,
center: np.ndarray,
*,
yaw_degrees: float,
elevation_degrees: float,
vertical_fov_degrees: float,
aspect_ratio: float,
distance_scale: float,
) -> float:
"""Fit an elongated surface for one view without a global-sphere zoom-out."""
yaw = math.radians(yaw_degrees)
elevation = math.radians(elevation_degrees)
eye_direction = np.array(
[
math.sin(yaw) * math.cos(elevation),
-math.cos(yaw) * math.cos(elevation),
math.sin(elevation),
],
dtype=np.float64,
)
forward = -eye_direction
world_up = np.array([0.0, 0.0, 1.0], dtype=np.float64)
right = np.cross(forward, world_up)
right_norm = float(np.linalg.norm(right))
if right_norm < 1e-8:
right = np.array([1.0, 0.0, 0.0], dtype=np.float64)
else:
right /= right_norm
view_up = np.cross(right, forward)
view_up /= max(float(np.linalg.norm(view_up)), 1e-8)
relative = np.asarray(vertices, dtype=np.float64) - center
projected_x = np.abs(relative @ right)
projected_y = np.abs(relative @ view_up)
toward_camera = relative @ eye_direction
vertical_half_fov = math.radians(vertical_fov_degrees) * 0.5
horizontal_half_fov = math.atan(
math.tan(vertical_half_fov) * aspect_ratio
)
required_x = toward_camera + projected_x / max(
math.tan(horizontal_half_fov),
1e-6,
)
required_y = toward_camera + projected_y / max(
math.tan(vertical_half_fov),
1e-6,
)
fitted = max(float(np.max(required_x)), float(np.max(required_y)), 1e-3)
return fitted * distance_scale
def _fixed_camera_distance_for_views(
vertices: np.ndarray,
center: np.ndarray,
*,
yaw_degrees: Sequence[float],
elevation_degrees: float,
vertical_fov_degrees: float,
aspect_ratio: float,
distance_scale: float,
) -> tuple[float, dict[str, Any]]:
"""Fit every requested view once and use the largest distance for all frames."""
unique_yaws = tuple(dict.fromkeys(float(yaw) for yaw in yaw_degrees))
if not unique_yaws:
raise ValueError("fixed camera fitting requires at least one yaw")
if any(not math.isfinite(yaw) for yaw in unique_yaws):
raise ValueError("fixed camera fitting requires finite yaws")
required_distances = [
_fitted_camera_distance_for_view(
vertices,
center,
yaw_degrees=yaw,
elevation_degrees=elevation_degrees,
vertical_fov_degrees=vertical_fov_degrees,
aspect_ratio=aspect_ratio,
distance_scale=distance_scale,
)
for yaw in unique_yaws
]
fixed_distance = max(required_distances)
return fixed_distance, {
"distance_policy": "fixed_max_over_orbit_and_multiview",
"fixed_distance": float(fixed_distance),
"fit_yaw_count": len(unique_yaws),
"fit_yaw_min": float(min(unique_yaws)),
"fit_yaw_max": float(max(unique_yaws)),
"per_view_required_distance_min": float(min(required_distances)),
"per_view_required_distance_max": float(max(required_distances)),
}
def _summarize_vertex_colors(
vertex_colors: np.ndarray,
vertex_count: int,
*,
required: bool,
) -> dict[str, Any]:
"""Return auditable texture evidence without inferring visual quality."""
colors = np.asarray(vertex_colors, dtype=np.float64)
complete = (
vertex_count > 0
and colors.ndim == 2
and colors.shape == (vertex_count, 3)
)
finite = bool(complete and np.isfinite(colors).all())
summary: dict[str, Any] = {
"required": bool(required),
"present": bool(complete),
"finite": finite,
"colored_vertices": int(colors.shape[0]) if colors.ndim == 2 else 0,
"coverage_fraction": 1.0 if complete else 0.0,
}
if finite:
summary.update(
{
"channel_mean": [float(value) for value in np.mean(colors, axis=0)],
"channel_std": [float(value) for value in np.std(colors, axis=0)],
"channel_min": [float(value) for value in np.min(colors, axis=0)],
"channel_max": [float(value) for value in np.max(colors, axis=0)],
}
)
return summary
def _referenced_vertex_indices(
triangles: np.ndarray,
vertex_count: int,
) -> np.ndarray:
"""Return the vertices that can contribute pixels to a triangle render.
Some reconstruction meshes retain point-cloud vertices that are not
referenced by any triangle. Those vertices are invisible, and a single
far-away orphan must not expand the camera bounds and shrink the actual
surface to a few pixels.
"""
faces = np.asarray(triangles, dtype=np.int64)
if faces.ndim != 2 or faces.shape[1] != 3 or faces.size == 0:
raise ValueError("camera framing requires at least one triangular face")
referenced = np.unique(faces.reshape(-1))
if int(referenced[0]) < 0 or int(referenced[-1]) >= vertex_count:
raise ValueError("triangle index is outside the mesh vertex array")
return referenced
def _camera_framing_vertex_indices(
triangles: np.ndarray,
triangle_component_labels: np.ndarray,
*,
dominant_component_min_face_fraction: float = DOMINANT_COMPONENT_MIN_FACE_FRACTION,
) -> tuple[np.ndarray, dict[str, Any]]:
"""Choose a review framing surface without letting tiny islands zoom out.
A strongly dominant connected triangle surface represents the primary
accessibility target in these reconstructions. Small detached islands can
still be rendered, but they should not make the target unreadably small.
When no component owns at least 90% of faces, all triangle vertices remain
in the camera fit so legitimate multi-part geometry is not guessed away.
"""
faces = np.asarray(triangles, dtype=np.int64)
labels = np.asarray(triangle_component_labels, dtype=np.int64).reshape(-1)
referenced = _referenced_vertex_indices(faces, int(faces.max()) + 1)
if labels.shape[0] != faces.shape[0] or np.any(labels < 0):
raise ValueError("triangle component labels must match triangular faces")
if not (0.0 < dominant_component_min_face_fraction <= 1.0):
raise ValueError("dominant component face fraction must be in (0, 1]")
counts = np.bincount(labels)
dominant_label = int(np.argmax(counts))
dominant_faces = int(counts[dominant_label])
face_count = int(faces.shape[0])
dominant_fraction = dominant_faces / face_count
use_dominant = dominant_fraction >= dominant_component_min_face_fraction
if use_dominant:
framing = np.unique(faces[labels == dominant_label].reshape(-1))
scope = "dominant_connected_surface"
else:
framing = referenced
scope = "all_connected_surfaces"
return framing, {
"scope": scope,
"framing_faces": dominant_faces if use_dominant else face_count,
"framing_vertices": int(len(framing)),
"dominant_component_faces": dominant_faces,
"dominant_component_face_fraction": float(dominant_fraction),
"component_count": int(len(counts)),
"minimum_dominant_face_fraction": float(
dominant_component_min_face_fraction
),
}
def _contact_sheet(images: Sequence[np.ndarray], labels: Sequence[str], path: Path) -> None:
columns = 2 if len(images) > 1 else 1
rows = math.ceil(len(images) / columns)
panel_width = 480
panel_height = 390
label_height = 34
canvas = Image.new(
"RGB",
(panel_width * columns, panel_height * rows),
(235, 235, 232),
)
for index, (array, label) in enumerate(zip(images, labels)):
image = Image.fromarray(array).convert("RGB")
body = ImageOps.contain(image, (panel_width, panel_height - label_height))
panel = Image.new("RGB", (panel_width, panel_height), "white")
draw = ImageDraw.Draw(panel)
draw.rectangle((0, 0, panel_width, label_height), fill=(242, 242, 239))
draw.text((12, 11), label, fill=(20, 20, 20))
panel.paste(
body,
(
(panel_width - body.width) // 2,
label_height + (panel_height - label_height - body.height) // 2,
),
)
canvas.paste(panel, ((index % columns) * panel_width, (index // columns) * panel_height))
canvas.save(path, quality=94, subsampling=0)
def render_outputs(
mesh_path: Path,
output_dir: Path,
*,
width: int,
height: int,
frames: int,
fps: int,
fov: float,
elevation: float,
distance_scale: float,
shader: str,
background: Sequence[float],
smooth_iterations: int,
multiview_yaws: Sequence[float],
orbit_mode: str = "front_arc",
orbit_span: float = 140.0,
require_vertex_colors: bool = False,
vertex_colors_srgb: bool = True,
cpu_fallback: bool = False,
) -> dict[str, Any]:
"""Render all outputs through Open3D's OffscreenRenderer path."""
import open3d as o3d
from open3d.visualization import rendering
mesh = o3d.io.read_triangle_mesh(str(mesh_path))
if len(mesh.vertices) == 0 or len(mesh.triangles) == 0:
raise ValueError(f"Input is not a non-empty triangle mesh: {mesh_path}")
source_vertex_count = len(mesh.vertices)
referenced_indices = _referenced_vertex_indices(
np.asarray(mesh.triangles, dtype=np.int64),
source_vertex_count,
)
unreferenced_vertex_count = source_vertex_count - len(referenced_indices)
# Open3D's scene bounds can include vertices that have no incident face,
# even though rasterization cannot display them. Remove only these
# topologically inert points before both rendering and camera fitting.
if unreferenced_vertex_count:
mesh.remove_unreferenced_vertices()
# Geometry produced from image-camera coordinates is easier to inspect after
# mapping x/right, z/up, and -y/depth into a conventional scene frame.
vertices = np.asarray(mesh.vertices, dtype=np.float64)
mesh.vertices = o3d.utility.Vector3dVector(
np.stack([vertices[:, 0], vertices[:, 2], -vertices[:, 1]], axis=1)
)
if smooth_iterations:
mesh = mesh.filter_smooth_laplacian(number_of_iterations=smooth_iterations)
mesh.compute_vertex_normals()
vertex_color_summary = _summarize_vertex_colors(
np.asarray(mesh.vertex_colors, dtype=np.float64),
len(mesh.vertices),
required=require_vertex_colors,
)
if require_vertex_colors and not (
vertex_color_summary["present"] and vertex_color_summary["finite"]
):
raise ValueError(
"Input mesh does not provide complete finite vertex colors required "
f"for texture-preserving rendering: {mesh_path}"
)
transformed_vertices = np.asarray(mesh.vertices, dtype=np.float64)
triangle_component_labels, _, _ = mesh.cluster_connected_triangles()
framing_indices, framing_summary = _camera_framing_vertex_indices(
np.asarray(mesh.triangles, dtype=np.int64),
np.asarray(triangle_component_labels, dtype=np.int64),
)
framing_vertices = transformed_vertices[framing_indices]
framing_min = np.min(framing_vertices, axis=0)
framing_max = np.max(framing_vertices, axis=0)
center = (framing_min + framing_max) * 0.5
orbit_yaws = _orbit_yaws(
frames,
mode=orbit_mode,
span_degrees=orbit_span,
)
fixed_camera_distance, camera_summary = _fixed_camera_distance_for_views(
framing_vertices,
center,
yaw_degrees=tuple(orbit_yaws) + tuple(multiview_yaws),
elevation_degrees=elevation,
vertical_fov_degrees=fov,
aspect_ratio=width / height,
distance_scale=distance_scale,
)
output_dir.mkdir(parents=True, exist_ok=True)
renderer = rendering.OffscreenRenderer(width, height)
try:
opengl_context = query_current_opengl_context()
process_graphics_backend = inspect_process_graphics_backend()
hardware_acceleration = enforce_opengl_context_policy(
opengl_context,
cpu_fallback=cpu_fallback,
process_backend=process_graphics_backend,
)
renderer.scene.set_background(list(background))
renderer.scene.scene.set_sun_light(
[0.35, -0.5, -0.75],
[1.0, 0.97, 0.92],
65000,
)
renderer.scene.scene.enable_sun_light(True)
renderer.scene.show_axes(False)
material = rendering.MaterialRecord()
material.shader = shader
material.sRGB_color = bool(vertex_colors_srgb)
material.base_color = [1.0, 1.0, 1.0, 1.0]
material.base_roughness = 0.68
material.base_reflectance = 0.18
renderer.scene.add_geometry("accessibility_geometry", mesh, material)
def render_yaw(yaw: float) -> np.ndarray:
eye, look_at, up = _camera_pose(
center,
fixed_camera_distance,
yaw,
elevation,
)
renderer.setup_camera(fov, look_at, eye, up)
array = np.asarray(renderer.render_to_image())
if array.ndim != 3 or array.shape[2] not in (3, 4):
raise RuntimeError(f"Unexpected Open3D render shape: {array.shape}")
return array[..., :3].astype(np.uint8, copy=False)
video_frames = [render_yaw(yaw) for yaw in orbit_yaws]
imageio.mimsave(
output_dir / OUTPUT_NAMES["video"],
video_frames,
fps=fps,
quality=8,
macro_block_size=1,
)
imageio.mimsave(
output_dir / OUTPUT_NAMES["gif"],
video_frames,
duration=1000.0 / fps,
loop=0,
)
Image.fromarray(video_frames[0]).save(output_dir / OUTPUT_NAMES["poster"])
views = [render_yaw(yaw) for yaw in multiview_yaws]
labels = [f"Geometry view {yaw:g} deg" for yaw in multiview_yaws]
_contact_sheet(views, labels, output_dir / OUTPUT_NAMES["multiview"])
finally:
release = getattr(renderer, "release_resources", None)
if callable(release):
release()
return {
"vertices": source_vertex_count,
"renderable_vertices": len(mesh.vertices),
"unreferenced_vertices_excluded_from_camera_fit": unreferenced_vertex_count,
"faces": len(mesh.triangles),
"camera_framing": framing_summary,
"camera": camera_summary,
"vertex_colors": vertex_color_summary,
"opengl_context": opengl_context,
"process_graphics_backend": process_graphics_backend,
"hardware_acceleration": hardware_acceleration,
"orbit": {
"mode": orbit_mode,
"span_degrees": float(orbit_span),
"yaw_min": float(min(orbit_yaws)),
"yaw_max": float(max(orbit_yaws)),
"multiview_yaws": [float(yaw) for yaw in multiview_yaws],
"open_world_scene_review": orbit_mode == "front_arc",
},
}
def write_json(path: Path, payload: Mapping[str, Any]) -> None:
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def main(argv: Sequence[str] | None = None) -> int:
parser = build_argument_parser()
args = parser.parse_args(argv)
validate_arguments(args)
mesh_path = Path(args.mesh).expanduser().resolve()
if not mesh_path.is_file():
raise FileNotFoundError(mesh_path)
output_dir = Path(args.output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
sample_id = str(args.sample_id or mesh_path.stem)
category = str(args.category).strip().lower() or "unknown"
environment_evidence = configure_render_environment(args.cpu_fallback)
render_settings = {
"resolution": [args.width, args.height],
"frames": args.frames,
"fps": args.fps,
"fov_degrees": args.fov,
"elevation_degrees": args.elevation,
"distance_scale": args.distance_scale,
"camera_fit": "fixed_max_projected_primary_surface_v4",
"camera_distance_policy": "fixed_max_over_orbit_and_multiview",
"shader": args.shader,
"background": list(args.background),
"smooth_iterations": args.smooth_iterations,
"require_vertex_colors": args.require_vertex_colors,
"vertex_colors_srgb": args.vertex_colors_srgb,
"multiview_yaws_degrees": list(args.multiview_yaws),
"orbit_mode": args.orbit_mode,
"orbit_span_degrees": args.orbit_span,
"scene_interpretation": (
"open_world_accessibility_surface_review"
if args.orbit_mode == "front_arc"
else "full_object_turntable"
),
}
result = render_outputs(
mesh_path,
output_dir,
width=args.width,
height=args.height,
frames=args.frames,
fps=args.fps,
fov=args.fov,
elevation=args.elevation,
distance_scale=args.distance_scale,
shader=args.shader,
background=args.background,
smooth_iterations=args.smooth_iterations,
multiview_yaws=args.multiview_yaws,
orbit_mode=args.orbit_mode,
orbit_span=args.orbit_span,
require_vertex_colors=args.require_vertex_colors,
vertex_colors_srgb=args.vertex_colors_srgb,
cpu_fallback=args.cpu_fallback,
)
manifest = build_render_manifest(
mesh_path=mesh_path,
output_dir=output_dir,
sample_id=sample_id,
category=category,
render_settings=render_settings,
render_result=result,
environment_evidence=environment_evidence,
cpu_fallback=args.cpu_fallback,
)
write_json(output_dir / "manifest.json", manifest)
print(
json.dumps(
{
"status": "rendered",
"sample_id": sample_id,
"output_dir": str(output_dir),
"cpu_fallback_explicit": args.cpu_fallback,
"hardware_acceleration_status": manifest["renderer"][
"hardware_acceleration_status"
],
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())