3D-VIDEO / app.py
ginipick's picture
Update app.py
a300b36 verified
raw
history blame
8 kB
"""
GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ โ€“ ์—…๋กœ๋“œํ•œ 3D ๋ชจ๋ธ์„ ์ง์ ‘ ๋ Œ๋”๋งํ•ด GIFยทGLB๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.
(Gradio + Trimesh ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง)
โ€ป ์ฃผ์š” ๋ณ€๊ฒฝ
1. create_model_animation_gif : ๋ชจ๋ธ์„ ์‹ค์ œ ๋ Œ๋”๋งํ•ด ํ”„๋ ˆ์ž„ ์ƒ์„ฑ
2. process_3d_model ๋‚ด๋ถ€์—์„œ ์ƒˆ GIF ํ•จ์ˆ˜ ์‚ฌ์šฉ
3. ์˜ˆ์™ธ ์ฒ˜๋ฆฌยท์ฃผ์„ ์ผ๋ถ€ ๋ณด๊ฐ•
"""
import os, io, time, glob, json, math
import numpy as np
import trimesh
from PIL import Image
import trimesh.transformations as tf
os.environ["PYOPENGL_PLATFORM"] = "egl" # ๋ชจ๋‹ˆํ„ฐ ์—†์ด ๋ Œ๋”๋ง
import gradio as gr
import spaces
# ๊ฒฐ๊ณผ ์ €์žฅ ๋””๋ ‰ํ„ฐ๋ฆฌ
LOG_PATH = "./results/demo"
os.makedirs(LOG_PATH, exist_ok=True)
# ------------------------------------------------------------------
# 1) ์‹ค์ œ GLB ๋ชจ๋ธ์„ ๋ Œ๋”๋งํ•ด GIF ์ƒ์„ฑ
# ------------------------------------------------------------------
def create_model_animation_gif(
output_path: str,
input_glb_path: str,
animation_type: str,
duration: float = 3.0,
fps: int = 30,
resolution=(640, 480),
):
"""
์—…๋กœ๋“œ๋œ GLB ๋ชจ๋ธ์„ ์˜คํ”„์Šคํฌ๋ฆฐ์œผ๋กœ ๋ Œ๋”๋งํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜ GIF ์ƒ์„ฑ
"""
# (1) ๋ชจ๋ธ ๋กœ๋“œ
scene = trimesh.load(input_glb_path)
if isinstance(scene, trimesh.Trimesh):
scene = trimesh.Scene(scene)
frames, num_frames = [], min(int(duration * fps), 60)
for i in range(num_frames):
t = i / (num_frames - 1)
# (2) ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋ณ€ํ™˜ ์ ์šฉ
scene_i = scene.copy()
if animation_type == "rotate":
M = tf.rotation_matrix(2 * math.pi * t, [0, 1, 0])
elif animation_type == "float":
M = tf.translation_matrix([0, 0.5 * math.sin(2 * math.pi * t), 0])
elif animation_type == "pulse":
M = tf.scale_matrix(0.8 + 0.4 * math.sin(2 * math.pi * t))
elif animation_type == "explode":
M = tf.translation_matrix([0.5 * t, 0, 0])
elif animation_type == "assemble":
M = tf.translation_matrix([0.5 * (1 - t), 0, 0])
elif animation_type == "swing":
M = tf.rotation_matrix(math.pi / 6 * math.sin(2 * math.pi * t), [0, 0, 1])
else:
M = np.eye(4)
scene_i.apply_transform(M)
# (3) ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋” โ†’ PNG ๋ฐ”์ดํŠธ
png_bytes = scene_i.save_image(resolution=resolution, visible=True)
frame = Image.open(io.BytesIO(png_bytes)).convert("RGB")
frames.append(frame)
# (4) GIF ์ €์žฅ
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
optimize=False,
duration=int(1000 / fps),
loop=0,
)
print(f"Created model animation GIF at {output_path}")
return output_path
# ------------------------------------------------------------------
# 2) GLB ํŒŒ์ผ์— ๋‹จ์ˆœ ๋ณ€ํ™˜ ์ ์šฉํ•ด ์ €์žฅ
# ------------------------------------------------------------------
def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"):
"""
์—…๋กœ๋“œ๋œ GLB ํŒŒ์ผ์— ๋‹จ์ผ ๋ณ€ํ™˜(ํšŒ์ „ยท์ด๋™ยท์Šค์ผ€์ผ)์„ ์ ์šฉํ•ด ์ƒˆ GLB๋กœ ์ €์žฅ
"""
try:
scene = trimesh.load(input_glb_path)
if not isinstance(scene, trimesh.Scene):
scene = trimesh.Scene(scene)
# ๋ณ€ํ™˜ ๋งคํŠธ๋ฆญ์Šค ๊ฒฐ์ •
if animation_type == "rotate":
transform = tf.rotation_matrix(math.pi / 4, [0, 1, 0])
elif animation_type == "float":
transform = tf.translation_matrix([0, 0.5, 0])
elif animation_type == "pulse":
transform = tf.scale_matrix(1.2)
elif animation_type == "explode":
transform = tf.translation_matrix([0.5, 0, 0])
elif animation_type == "assemble":
transform = tf.translation_matrix([-0.5, 0, 0])
elif animation_type == "swing":
transform = tf.rotation_matrix(math.pi / 8, [0, 0, 1])
else:
transform = np.eye(4)
scene.apply_transform(transform)
scene.export(output_glb_path)
print(f"Exported modified GLB to {output_glb_path}")
return output_glb_path
except Exception as e:
print("Error modifying GLB:", e)
# ์‹คํŒจ ์‹œ ์›๋ณธ ๋ณต์‚ฌ
import shutil
shutil.copy(input_glb_path, output_glb_path)
print(f"Copied original GLB to {output_glb_path}")
return output_glb_path
# ------------------------------------------------------------------
# 3) Gradio์—์„œ ํ˜ธ์ถœํ•  ๋ฉ”์ธ ํŒŒ์ดํ”„๋ผ์ธ
# ------------------------------------------------------------------
@spaces.GPU
def process_3d_model(input_3d, animation_type, animation_duration, fps):
"""
โ‘  GLB ๋ณ€ํ™˜ โ†’ โ‘ก GIF ๋ Œ๋” โ†’ โ‘ข ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON ์ €์žฅ
"""
try:
base = os.path.splitext(os.path.basename(input_3d))[0]
animated_glb = os.path.join(LOG_PATH, f"animated_{base}.glb")
animated_gif = os.path.join(LOG_PATH, f"preview_{base}.gif")
json_path = os.path.join(LOG_PATH, f"metadata_{base}.json")
# 1) GLB ๋ณ€ํ˜•
modify_glb_file(input_3d, animated_glb, animation_type)
# 2) ์‹ค์ œ ๋ชจ๋ธ ๊ธฐ๋ฐ˜ GIF
create_model_animation_gif(
animated_gif,
input_3d,
animation_type,
animation_duration,
fps,
)
# 3) ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ธฐ๋ก
metadata = dict(
animation_type=animation_type,
duration=animation_duration,
fps=fps,
original_model=os.path.basename(input_3d),
created_at=time.strftime("%Y-%m-%d %H:%M:%S"),
)
with open(json_path, "w") as f:
json.dump(metadata, f, indent=4)
return animated_glb, animated_gif, json_path
except Exception as e:
# ์น˜๋ช…์  ์˜ค๋ฅ˜ ์‹œ: ์›๋ณธ ๋ชจ๋ธ + ๋นˆ GIF
print("Error in process_3d_model:", e)
fallback_gif = os.path.join(LOG_PATH, "error.gif")
Image.new("RGB", (640, 480), (255, 0, 0)).save(fallback_gif)
return input_3d, fallback_gif, None
# ------------------------------------------------------------------
# 4) Gradio UI
# ------------------------------------------------------------------
with gr.Blocks(title="GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ") as demo:
gr.Markdown(
"""
<h2><b>GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ - 3D ๋ชจ๋ธ ์›€์ง์ž„ ํšจ๊ณผ</b></h2>
์ •์ ์ธ 3D ๋ชจ๋ธ(GLB)์— ํšŒ์ „ยท๋ถ€์œ ยทํญ๋ฐœ ๋“ฑ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
"""
)
with gr.Row():
with gr.Column():
input_3d = gr.Model3D(label="3D ๋ชจ๋ธ ์—…๋กœ๋“œ (GLB)")
animation_type = gr.Dropdown(
label="์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•",
choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
value="rotate",
)
animation_duration = gr.Slider(
label="์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธธ์ด (์ดˆ)", minimum=1.0, maximum=10.0, value=3.0, step=0.5
)
fps = gr.Slider(label="FPS", minimum=15, maximum=60, value=30, step=1)
submit_btn = gr.Button("์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ")
with gr.Column():
output_3d = gr.Model3D(label="์• ๋‹ˆ๋ฉ”์ด์…˜๋œ GLB")
output_gif = gr.Image(label="๋ฏธ๋ฆฌ๋ณด๊ธฐ GIF")
output_json = gr.File(label="๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON")
submit_btn.click(
fn=process_3d_model,
inputs=[input_3d, animation_type, animation_duration, fps],
outputs=[output_3d, output_gif, output_json],
)
# ์˜ˆ์ œ GLB ํด๋”๊ฐ€ ์žˆ๋‹ค๋ฉด ์ž๋™ ๋“ฑ๋ก
example_glbs = [[f] for f in glob.glob("./data/demo_glb/*.glb")]
if example_glbs:
gr.Examples(examples=example_glbs, inputs=[input_3d])
# ------------------------------------------------------------------
# 5) ์•ฑ ์‹คํ–‰
# ------------------------------------------------------------------
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)