3D-VIDEO / app.py
ginipick's picture
Update app.py
a285b9c verified
raw
history blame
10 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_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
@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:
# ν…μŠ€νŠΈ 기반 μ• λ‹ˆλ©”μ΄μ…˜ 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
)
# 원본 GLB 파일 볡사
copy_glb_path = os.path.join(LOG_PATH, f'copy_{base_filename}.glb')
import shutil
try:
shutil.copy(input_3d, copy_glb_path)
animated_glb_path = copy_glb_path
print(f"Copied original GLB to {copy_glb_path}")
except Exception as e:
print(f"Error copying GLB: {e}")
animated_glb_path = input_3d
# 메타데이터 생성
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)