Spaces:
Paused
Paused
File size: 3,916 Bytes
07c2969 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
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)
|