Spaces:
Paused
Paused
| import argparse, io, math, os, sys | |
| from pathlib import Path | |
| from typing import Tuple | |
| import numpy as np, requests | |
| from PIL import Image | |
| import trimesh, pyrender | |
| from trimesh.transformations import translation_matrix, rotation_matrix # NEW | |
| os.environ.setdefault("PYOPENGL_PLATFORM", "egl") | |
| def download_to_memory(url: str) -> bytes: | |
| resp = requests.get(url, timeout=30) | |
| resp.raise_for_status() | |
| return resp.content | |
| def load_mesh(data: bytes) -> trimesh.Trimesh: | |
| return trimesh.load(io.BytesIO(data), file_type="glb", force="mesh") | |
| def build_scene(mesh: trimesh.Trimesh) -> Tuple[pyrender.Scene, float]: | |
| tm_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False) | |
| scene = pyrender.Scene(bg_color=[1, 1, 1, 0]) | |
| scene.add(tm_mesh) | |
| bb = mesh.bounding_box_oriented.extents # fixed name | |
| if not np.all(bb): | |
| bb = mesh.extents | |
| radius = np.linalg.norm(bb) * 0.6 | |
| return scene, radius | |
| def add_lighting(scene: pyrender.Scene, radius: float) -> None: | |
| key = pyrender.PointLight(color=np.ones(3), intensity=40.0) | |
| fill = pyrender.PointLight(color=np.ones(3), intensity=20.0) | |
| back = pyrender.PointLight(color=np.ones(3), intensity=10.0) | |
| scene.add(key, pose=translation_matrix([ radius, radius, radius])) | |
| scene.add(fill, pose=translation_matrix([-radius, radius, radius])) | |
| scene.add(back, pose=translation_matrix([ 0, -radius, -radius])) | |
| def setup_camera(scene: pyrender.Scene, radius: float) -> None: | |
| cam = pyrender.PerspectiveCamera(yfov=np.radians(45.0)) | |
| cam_pose = translation_matrix([0, 0, radius * 2.5]) | |
| scene.add(cam, pose=cam_pose) | |
| def render_thumbnail(scene: pyrender.Scene, size: int) -> Image.Image: | |
| r = pyrender.OffscreenRenderer(viewport_width=size, viewport_height=size) | |
| try: | |
| color, _ = r.render(scene) | |
| finally: | |
| r.delete() | |
| return Image.fromarray(color) | |
| def generate_thumbnail(url: str, out_path: Path, size: int = 512) -> None: | |
| raw = download_to_memory(url) | |
| mesh = load_mesh(raw) | |
| # Get mesh dimensions before any transformations | |
| original_extents = mesh.extents | |
| longest_dimension = np.max(original_extents) | |
| # Print dimension info | |
| print(f"Mesh dimensions (X, Y, Z): {original_extents}") | |
| print(f"Longest dimension: {longest_dimension:.4f}") | |
| # Scaling constant - you can use this to normalize models to a target size | |
| target_size = 2.5 # Target longest dimension | |
| scale_factor = target_size / longest_dimension | |
| print(f"Scale factor to normalize to {target_size}: {scale_factor:.4f}") | |
| # Calculate radius BEFORE scaling for consistent camera/lighting positioning | |
| bb = mesh.bounding_box_oriented.extents | |
| if not np.all(bb): | |
| bb = mesh.extents | |
| fixed_radius = np.linalg.norm(bb) * 0.6 | |
| mesh.apply_translation(-mesh.bounding_box.centroid) | |
| # Apply the scaling transformation | |
| mesh.apply_scale(scale_factor) | |
| # Rotate 45 degrees to the left (around Y-axis) | |
| rotation = rotation_matrix(np.radians(30), [0.3, -0.5, 0]) | |
| mesh.apply_transform(rotation) | |
| # Build scene but use fixed radius for camera/lighting | |
| tm_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False) | |
| scene = pyrender.Scene(bg_color=[0.15, 0.15, 0.15, 1]) # Gray background | |
| scene.add(tm_mesh) | |
| add_lighting(scene, fixed_radius) | |
| setup_camera(scene, fixed_radius) | |
| img = render_thumbnail(scene, size) | |
| img.save(out_path, "PNG") | |
| print(f"Saved thumbnail → {out_path.resolve()}") | |
| def parse_args(argv): | |
| p = argparse.ArgumentParser(description="Render a GLB file to a PNG thumbnail") | |
| p.add_argument("url"), p.add_argument("output") | |
| p.add_argument("size", nargs="?", type=int, default=512) | |
| return p.parse_args(argv) | |
| if __name__ == "__main__": | |
| args = parse_args(sys.argv[1:]) | |
| generate_thumbnail(args.url, Path(args.output), args.size) | |