3D-VIDEO / app.py
ginipick's picture
Update app.py
30b09aa verified
raw
history blame
18.5 kB
import os
import time
import glob
import json
import numpy as np
import trimesh
import argparse
from scipy.spatial.transform import Rotation
from PIL import Image, ImageDraw
import math
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)
def create_simple_rotation_animation(input_glb_path, output_glb_path, num_frames=30):
"""
원본 GLB νŒŒμΌμ— νšŒμ „ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•œ μƒˆλ‘œμš΄ GLB 파일 생성
"""
try:
# GLB 파일 λ‘œλ“œ
scene = trimesh.load(input_glb_path)
if isinstance(scene, trimesh.Scene):
# μ• λ‹ˆλ©”μ΄μ…˜ 적용 (첫 번째 ν”„λ ˆμž„λ§Œ μ‚¬μš©)
angle = math.pi / 4 # 45도 νšŒμ „
# μ”¬μ˜ λͺ¨λ“  λ©”μ‹œμ— νšŒμ „ 적용
for node_name, transform, geometry_name in scene.graph.nodes_geometry:
# 원본 μœ„μΉ˜ λ°±μ—…
original_transform = scene.graph[node_name][0]
# νšŒμ „ λ³€ν™˜ 계산
rotation = tf.rotation_matrix(angle, [0, 1, 0])
# μƒˆ λ³€ν™˜ = 원본 λ³€ν™˜ * νšŒμ „ λ³€ν™˜
new_transform = np.dot(original_transform, rotation)
# λ³€ν™˜ 적용
scene.graph[node_name] = new_transform
# νšŒμ „λœ GLB μ €μž₯
scene.export(output_glb_path)
print(f"Saved animated GLB to {output_glb_path}")
return output_glb_path
elif isinstance(scene, trimesh.Trimesh):
# 단일 λ©”μ‹œμΈ 경우
new_scene = trimesh.Scene()
# λ©”μ‹œμ— νšŒμ „ 적용
angle = math.pi / 4 # 45도 νšŒμ „
rotation = tf.rotation_matrix(angle, [0, 1, 0])
scene.apply_transform(rotation)
# νšŒμ „λœ λ©”μ‹œλ₯Ό 씬에 μΆ”κ°€
new_scene.add_geometry(scene)
# 씬을 GLB둜 μ €μž₯
new_scene.export(output_glb_path)
print(f"Saved animated GLB to {output_glb_path}")
return output_glb_path
else:
print(f"Unsupported format: {type(scene)}")
return None
except Exception as e:
print(f"Error creating animation: {str(e)}")
return None
def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30):
"""ν…μŠ€νŠΈ 기반의 κ°„λ‹¨ν•œ μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성 - λ Œλ”λ§ μ‹€νŒ¨ μ‹œ λŒ€μ²΄μš©"""
try:
# κ°„λ‹¨ν•œ ν”„λ ˆμž„ μ‹œν€€μŠ€ 생성
frames = []
num_frames = int(duration * fps)
if num_frames > 60: # λ„ˆλ¬΄ λ§Žμ€ ν”„λ ˆμž„μ€ νš¨μœ¨μ μ΄μ§€ μ•ŠμŒ
num_frames = 60
for i in range(num_frames):
t = i / (num_frames - 1) # 0~1 λ²”μœ„
angle = t * 360 # 전체 νšŒμ „
# μƒˆ 이미지 생성
img = Image.new('RGB', (640, 480), color=(240, 240, 240))
draw = ImageDraw.Draw(img)
# 정보 ν…μŠ€νŠΈ
draw.text((50, 50), f"Model: {os.path.basename(model_name)}", fill=(0, 0, 0))
draw.text((50, 100), f"Animation Type: {animation_type}", fill=(0, 0, 0))
draw.text((50, 150), f"Frame: {i+1}/{num_frames}", fill=(0, 0, 0))
# μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•μ— λ”°λ₯Έ μ‹œκ°μ  효과
center_x, center_y = 320, 240
if animation_type == 'rotate':
# νšŒμ „ν•˜λŠ” μ‚¬κ°ν˜•
radius = 100
x = center_x + radius * math.cos(math.radians(angle))
y = center_y + radius * math.sin(math.radians(angle))
draw.rectangle((x-40, y-40, x+40, y+40), outline=(0, 0, 0), fill=(255, 0, 0))
elif animation_type == 'float':
# μœ„μ•„λž˜λ‘œ μ›€μ§μ΄λŠ” 원
offset_y = 50 * math.sin(2 * math.pi * t)
draw.ellipse((center_x-50, center_y-50+offset_y, center_x+50, center_y+50+offset_y),
outline=(0, 0, 0), fill=(0, 0, 255))
elif animation_type == 'explode' or animation_type == 'assemble':
# λ°”κΉ₯μͺ½/μ•ˆμͺ½μœΌλ‘œ μ›€μ§μ΄λŠ” μ—¬λŸ¬ λ„ν˜•
scale = t if animation_type == 'explode' else 1 - t
for j in range(8):
angle_j = j * 45
dist = 120 * scale
x = center_x + dist * math.cos(math.radians(angle_j))
y = center_y + dist * math.sin(math.radians(angle_j))
if j % 3 == 0:
draw.rectangle((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(255, 0, 0))
elif j % 3 == 1:
draw.ellipse((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(0, 255, 0))
else:
draw.polygon([(x, y-20), (x+20, y+20), (x-20, y+20)], outline=(0, 0, 0), fill=(0, 0, 255))
elif animation_type == 'pulse':
# 크기가 λ³€ν•˜λŠ” 원
scale = 0.5 + 0.5 * math.sin(2 * math.pi * t)
radius = 100 * scale
draw.ellipse((center_x-radius, center_y-radius, center_x+radius, center_y+radius),
outline=(0, 0, 0), fill=(0, 255, 0))
elif animation_type == 'swing':
# 쒌우둜 μ›€μ§μ΄λŠ” μ‚Όκ°ν˜•
angle_offset = 30 * math.sin(2 * math.pi * t)
points = [
(center_x + 100 * math.cos(math.radians(angle_offset)), center_y - 80),
(center_x + 100 * math.cos(math.radians(120 + angle_offset)), center_y + 40),
(center_x + 100 * math.cos(math.radians(240 + angle_offset)), center_y + 40)
]
draw.polygon(points, outline=(0, 0, 0), fill=(255, 165, 0))
# ν”„λ ˆμž„ μΆ”κ°€
frames.append(img)
# GIF둜 μ €μž₯
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
optimize=False,
duration=int(1000 / fps),
loop=0
)
print(f"Created textual animation GIF at {output_path}")
return output_path
except Exception as e:
print(f"Error creating textual animation: {str(e)}")
return None
def create_glb_with_animation(input_glb_path, animation_type, duration=3.0, fps=30):
"""
μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•œ μƒˆλ‘œμš΄ GLB 파일 생성
"""
try:
base_filename = os.path.basename(input_glb_path).rsplit('.', 1)[0]
output_glb_path = os.path.join(LOG_PATH, f"animated_{base_filename}.glb")
# 원본 λͺ¨λΈ λ‘œλ“œ
scene = trimesh.load(input_glb_path)
print(f"Loaded GLB: {type(scene)}")
# μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•μ— 따라 처리
if animation_type == 'rotate':
# μ‹œκ³„λ°©ν–₯ 45도 νšŒμ „
angle = math.pi / 4
axis = [0, 1, 0] # YμΆ• (μˆ˜μ§μΆ•)
elif animation_type == 'float':
# 수직으둜 μ›€μ§μž„
angle = 0
axis = [0, 1, 0]
# YμΆ• λ°©ν–₯으둜 이동
if isinstance(scene, trimesh.Scene):
for node_name, transform, geometry_name in scene.graph.nodes_geometry:
# κ°€λ³κ²Œ μœ„λ‘œ 이동
translation = tf.translation_matrix([0, 0.2, 0])
original_transform = scene.graph[node_name][0]
new_transform = np.dot(original_transform, translation)
scene.graph[node_name] = new_transform
elif isinstance(scene, trimesh.Trimesh):
translation = tf.translation_matrix([0, 0.2, 0])
scene.apply_transform(translation)
elif animation_type == 'explode':
# 각 λΆ€λΆ„μ˜ μ€‘μ‹¬μ—μ„œ μ•½κ°„ λ©€μ–΄μ§€κ²Œ
angle = 0
axis = [0, 1, 0]
# μ˜€λΈŒμ νŠΈκ°€ μ—¬λŸ¬ 개일 경우 μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ 이동
if isinstance(scene, trimesh.Scene):
# μ”¬μ˜ 쀑심점 계산
all_vertices = []
for geometry_name, geometry in scene.geometry.items():
if hasattr(geometry, 'vertices') and len(geometry.vertices) > 0:
all_vertices.append(geometry.vertices)
if all_vertices:
all_points = np.vstack(all_vertices)
center = np.mean(all_points, axis=0)
for node_name, transform, geometry_name in scene.graph.nodes_geometry:
# 각 λΆ€λΆ„μ˜ 쀑심점 계산
geometry = scene.geometry[geometry_name]
if hasattr(geometry, 'centroid'):
part_center = geometry.centroid
# μ€‘μ‹¬μ—μ„œ 객체 λ°©ν–₯ 계산
direction = part_center - center
if np.linalg.norm(direction) > 0.001:
direction = direction / np.linalg.norm(direction)
# λ°©ν–₯으둜 이동
translation = tf.translation_matrix(direction * 0.2)
original_transform = scene.graph[node_name][0]
new_transform = np.dot(original_transform, translation)
scene.graph[node_name] = new_transform
elif animation_type == 'pulse':
# λͺ¨λΈμ„ μ•½κ°„ 킀움
scale_factor = 1.2
if isinstance(scene, trimesh.Scene):
# μ”¬μ˜ 쀑심점 계산
all_vertices = []
for geometry_name, geometry in scene.geometry.items():
if hasattr(geometry, 'vertices') and len(geometry.vertices) > 0:
all_vertices.append(geometry.vertices)
if all_vertices:
all_points = np.vstack(all_vertices)
center = np.mean(all_points, axis=0)
for node_name, transform, geometry_name in scene.graph.nodes_geometry:
# 쀑심 κΈ°μ€€ μŠ€μΌ€μΌλ§
translate_to_center = tf.translation_matrix(-center)
scale = np.eye(4)
scale[:3, :3] *= scale_factor
translate_back = tf.translation_matrix(center)
# λ³€ν™˜ 적용
original_transform = scene.graph[node_name][0]
new_transform = np.dot(translate_back, np.dot(scale, np.dot(translate_to_center, original_transform)))
scene.graph[node_name] = new_transform
elif isinstance(scene, trimesh.Trimesh):
# λ©”μ‹œ 쀑심 κΈ°μ€€ μŠ€μΌ€μΌλ§
center = scene.centroid
translate_to_center = tf.translation_matrix(-center)
scale = tf.scale_matrix(scale_factor)
translate_back = tf.translation_matrix(center)
scene.apply_transform(np.dot(translate_back, np.dot(scale, translate_to_center)))
else:
# 기본적으둜 νšŒμ „ 적용
angle = math.pi / 4
axis = [0, 1, 0]
# νšŒμ „ 적용 (float, explode, pulse μ•„λ‹Œ 경우)
if angle != 0:
if isinstance(scene, trimesh.Scene):
# μ”¬μ˜ λͺ¨λ“  뢀뢄에 νšŒμ „ 적용
for node_name, transform, geometry_name in scene.graph.nodes_geometry:
rotation = tf.rotation_matrix(angle, axis)
original_transform = scene.graph[node_name][0]
new_transform = np.dot(original_transform, rotation)
scene.graph[node_name] = new_transform
elif isinstance(scene, trimesh.Trimesh):
# 단일 λ©”μ‹œμ— νšŒμ „ 적용
rotation = tf.rotation_matrix(angle, axis)
scene.apply_transform(rotation)
# κ²°κ³Ό μ €μž₯
scene.export(output_glb_path)
print(f"Saved animated GLB to {output_glb_path}")
return output_glb_path
except Exception as e:
print(f"Error creating animated GLB: {str(e)}")
# 였λ₯˜ λ°œμƒ μ‹œ 원본 파일 볡사
try:
import shutil
output_path = os.path.join(LOG_PATH, f"copy_{os.path.basename(input_glb_path)}")
shutil.copy(input_glb_path, output_path)
print(f"Copied original GLB to {output_path}")
return output_path
except Exception as copy_error:
print(f"Error copying GLB: {copy_error}")
return input_glb_path # 원본 경둜 λ°˜ν™˜
@spaces.GPU
def process_3d_model(input_3d, animation_type, animation_duration, fps):
"""Process a 3D model and apply animation"""
print(f"Processing: {input_3d} with animation type: {animation_type}")
try:
# 1. μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— μ‹€μ œ μ• λ‹ˆλ©”μ΄μ…˜ 적용
animated_glb_path = create_glb_with_animation(
input_3d,
animation_type,
animation_duration,
fps
)
# 2. ν…μŠ€νŠΈ 기반 μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성 (λ°±μ—…μš©)
base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
animated_gif_path = create_textual_animation_gif(
text_gif_path,
os.path.basename(input_3d),
animation_type,
animation_duration,
fps
)
# 3. 메타데이터 생성
metadata = {
"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")
}
json_path = os.path.join(LOG_PATH, f'metadata_{base_filename}.json')
with open(json_path, 'w') as f:
json.dump(metadata, f, indent=4)
return animated_glb_path, animated_gif_path, json_path
except Exception as e:
error_msg = f"Error processing file: {str(e)}"
print(error_msg)
return error_msg, None, None
# Gradio μΈν„°νŽ˜μ΄μŠ€ μ„€μ •
with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
# 제λͺ© μ„Ήμ…˜
gr.Markdown("""
<h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
이 데λͺ¨λ₯Ό 톡해 정적인 3D λͺ¨λΈ(GLB 파일)에 λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
❗️❗️❗️**μ€‘μš”μ‚¬ν•­:**
- 이 데λͺ¨λŠ” μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•©λ‹ˆλ‹€.
- λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ μŠ€νƒ€μΌ μ€‘μ—μ„œ μ„ νƒν•˜μ„Έμš”: νšŒμ „, λΆ€μœ , 폭발, 쑰립, νŽ„μŠ€, μŠ€μœ™.
- κ²°κ³ΌλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB 파일과 미리보기용 GIF 파일둜 μ œκ³΅λ©λ‹ˆλ‹€.
""")
with gr.Row():
with gr.Column():
# μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ
input_3d = gr.Model3D(label="3D λͺ¨λΈ 파일 μ—…λ‘œλ“œ (GLB 포맷)")
with gr.Row():
animation_type = gr.Dropdown(
label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
value="rotate"
)
with gr.Row():
animation_duration = gr.Slider(
label="μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)",
minimum=1.0,
maximum=10.0,
value=3.0,
step=0.5
)
fps = gr.Slider(
label="μ΄ˆλ‹Ή ν”„λ ˆμž„ 수",
minimum=15,
maximum=60,
value=30,
step=1
)
submit_btn = gr.Button("λͺ¨λΈ 처리 및 μ• λ‹ˆλ©”μ΄μ…˜ 생성")
with gr.Column():
# 좜λ ₯ μ»΄ν¬λ„ŒνŠΈ
output_3d = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜ 적용된 3D λͺ¨λΈ")
output_gif = gr.Image(label="μ• λ‹ˆλ©”μ΄μ…˜ 미리보기 (GIF)")
output_json = gr.File(label="메타데이터 파일 λ‹€μš΄λ‘œλ“œ")
# μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
gr.Markdown("""
### μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
- **νšŒμ „(rotate)**: λͺ¨λΈμ΄ Y좕을 μ€‘μ‹¬μœΌλ‘œ νšŒμ „ν•©λ‹ˆλ‹€.
- **λΆ€μœ (float)**: λͺ¨λΈμ΄ μœ„μ•„λž˜λ‘œ λΆ€λ“œλŸ½κ²Œ λ– λ‹€λ‹™λ‹ˆλ‹€.
- **폭발(explode)**: λͺ¨λΈμ˜ 각 뢀뢄이 μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ νΌμ Έλ‚˜κ°‘λ‹ˆλ‹€.
- **쑰립(assemble)**: 폭발 μ• λ‹ˆλ©”μ΄μ…˜μ˜ λ°˜λŒ€ - λΆ€ν’ˆλ“€μ΄ ν•¨κ»˜ λͺ¨μž…λ‹ˆλ‹€.
- **νŽ„μŠ€(pulse)**: λͺ¨λΈμ΄ 크기가 μ»€μ‘Œλ‹€ μž‘μ•„μ‘Œλ‹€λ₯Ό λ°˜λ³΅ν•©λ‹ˆλ‹€.
- **μŠ€μœ™(swing)**: λͺ¨λΈμ΄ 쒌우둜 λΆ€λ“œλŸ½κ²Œ ν”λ“€λ¦½λ‹ˆλ‹€.
### 팁
- μ• λ‹ˆλ©”μ΄μ…˜ 길이와 FPSλ₯Ό μ‘°μ ˆν•˜μ—¬ μ›€μ§μž„μ˜ 속도와 λΆ€λ“œλŸ¬μ›€μ„ μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€.
- λ³΅μž‘ν•œ λͺ¨λΈμ€ 처리 μ‹œκ°„μ΄ 더 였래 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€.
- GIF λ―Έλ¦¬λ³΄κΈ°λŠ” λΉ λ₯Έ 참쑰용이며, κ³ ν’ˆμ§ˆ κ²°κ³Όλ₯Ό μœ„ν•΄μ„œλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB νŒŒμΌμ„ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”.
""")
# λ²„νŠΌ λ™μž‘ μ„€μ •
submit_btn.click(
fn=process_3d_model,
inputs=[input_3d, animation_type, animation_duration, fps],
outputs=[output_3d, output_gif, output_json]
)
# 예제 μ€€λΉ„
example_files = [[f] for f in glob.glob('./data/demo_glb/*.glb')]
if example_files:
gr.Examples(
examples=example_files,
inputs=[input_3d],
examples_per_page=10,
)
# μ•± μ‹€ν–‰
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)