ginipick commited on
Commit
8503628
Β·
verified Β·
1 Parent(s): a300b36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -117
app.py CHANGED
@@ -1,32 +1,56 @@
1
  """
2
- GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 – μ—…λ‘œλ“œν•œ 3D λͺ¨λΈμ„ 직접 λ Œλ”λ§ν•΄ GIFΒ·GLB둜 λ‚΄λ³΄λƒ…λ‹ˆλ‹€.
3
- (Gradio + Trimesh μ˜€ν”„μŠ€ν¬λ¦° λ Œλ”λ§)
4
-
5
- β€» μ£Όμš” λ³€κ²½
6
- 1. create_model_animation_gif : λͺ¨λΈμ„ μ‹€μ œ λ Œλ”λ§ν•΄ ν”„λ ˆμž„ 생성
7
- 2. process_3d_model λ‚΄λΆ€μ—μ„œ μƒˆ GIF ν•¨μˆ˜ μ‚¬μš©
8
- 3. μ˜ˆμ™Έ μ²˜λ¦¬Β·μ£Όμ„ 일뢀 보강
9
  """
10
 
11
- import os, io, time, glob, json, math
 
12
  import numpy as np
13
- import trimesh
14
  from PIL import Image
15
- import trimesh.transformations as tf
16
 
17
- os.environ["PYOPENGL_PLATFORM"] = "egl" # λͺ¨λ‹ˆν„° 없이 λ Œλ”λ§
 
 
 
 
 
18
 
19
  import gradio as gr
20
  import spaces
21
 
22
- # κ²°κ³Ό μ €μž₯ 디렉터리
23
  LOG_PATH = "./results/demo"
24
  os.makedirs(LOG_PATH, exist_ok=True)
25
 
26
-
27
- # ------------------------------------------------------------------
28
- # 1) μ‹€μ œ GLB λͺ¨λΈμ„ λ Œλ”λ§ν•΄ GIF 생성
29
- # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def create_model_animation_gif(
31
  output_path: str,
32
  input_glb_path: str,
@@ -35,20 +59,18 @@ def create_model_animation_gif(
35
  fps: int = 30,
36
  resolution=(640, 480),
37
  ):
38
- """
39
- μ—…λ‘œλ“œλœ GLB λͺ¨λΈμ„ μ˜€ν”„μŠ€ν¬λ¦°μœΌλ‘œ λ Œλ”λ§ν•΄ μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성
40
- """
41
- # (1) λͺ¨λΈ λ‘œλ“œ
42
- scene = trimesh.load(input_glb_path)
43
- if isinstance(scene, trimesh.Trimesh):
44
- scene = trimesh.Scene(scene)
45
-
46
- frames, num_frames = [], min(int(duration * fps), 60)
47
  for i in range(num_frames):
48
  t = i / (num_frames - 1)
 
49
 
50
- # (2) ν”„λ ˆμž„λ§ˆλ‹€ λ³€ν™˜ 적용
51
- scene_i = scene.copy()
52
  if animation_type == "rotate":
53
  M = tf.rotation_matrix(2 * math.pi * t, [0, 1, 0])
54
  elif animation_type == "float":
@@ -63,160 +85,124 @@ def create_model_animation_gif(
63
  M = tf.rotation_matrix(math.pi / 6 * math.sin(2 * math.pi * t), [0, 0, 1])
64
  else:
65
  M = np.eye(4)
 
66
 
67
- scene_i.apply_transform(M)
 
 
 
 
 
68
 
69
- # (3) μ˜€ν”„μŠ€ν¬λ¦° λ Œλ” β†’ PNG λ°”μ΄νŠΈ
70
- png_bytes = scene_i.save_image(resolution=resolution, visible=True)
71
- frame = Image.open(io.BytesIO(png_bytes)).convert("RGB")
72
  frames.append(frame)
73
 
74
- # (4) GIF μ €μž₯
75
  frames[0].save(
76
  output_path,
77
  save_all=True,
78
  append_images=frames[1:],
79
- optimize=False,
80
  duration=int(1000 / fps),
81
  loop=0,
82
  )
83
- print(f"Created model animation GIF at {output_path}")
84
  return output_path
85
 
86
-
87
- # ------------------------------------------------------------------
88
- # 2) GLB νŒŒμΌμ— λ‹¨μˆœ λ³€ν™˜ μ μš©ν•΄ μ €μž₯
89
- # ------------------------------------------------------------------
90
  def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"):
91
- """
92
- μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— 단일 λ³€ν™˜(νšŒμ „Β·μ΄λ™Β·μŠ€μΌ€μΌ)을 μ μš©ν•΄ μƒˆ GLB둜 μ €μž₯
93
- """
94
  try:
95
- scene = trimesh.load(input_glb_path)
96
- if not isinstance(scene, trimesh.Scene):
97
- scene = trimesh.Scene(scene)
98
 
99
- # λ³€ν™˜ 맀트릭슀 κ²°μ •
100
  if animation_type == "rotate":
101
- transform = tf.rotation_matrix(math.pi / 4, [0, 1, 0])
102
  elif animation_type == "float":
103
- transform = tf.translation_matrix([0, 0.5, 0])
104
  elif animation_type == "pulse":
105
- transform = tf.scale_matrix(1.2)
106
  elif animation_type == "explode":
107
- transform = tf.translation_matrix([0.5, 0, 0])
108
  elif animation_type == "assemble":
109
- transform = tf.translation_matrix([-0.5, 0, 0])
110
  elif animation_type == "swing":
111
- transform = tf.rotation_matrix(math.pi / 8, [0, 0, 1])
112
  else:
113
- transform = np.eye(4)
114
 
115
- scene.apply_transform(transform)
116
- scene.export(output_glb_path)
117
- print(f"Exported modified GLB to {output_glb_path}")
118
  return output_glb_path
119
-
120
  except Exception as e:
121
- print("Error modifying GLB:", e)
122
- # μ‹€νŒ¨ μ‹œ 원본 볡사
123
- import shutil
124
-
125
  shutil.copy(input_glb_path, output_glb_path)
126
- print(f"Copied original GLB to {output_glb_path}")
127
  return output_glb_path
128
 
129
-
130
- # ------------------------------------------------------------------
131
- # 3) Gradioμ—μ„œ ν˜ΈμΆœν•  메인 νŒŒμ΄ν”„λΌμΈ
132
- # ------------------------------------------------------------------
133
  @spaces.GPU
134
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
135
- """
136
- β‘  GLB λ³€ν™˜ β†’ β‘‘ GIF λ Œλ” β†’ β‘’ 메타데이터 JSON μ €μž₯
137
- """
138
  try:
139
  base = os.path.splitext(os.path.basename(input_3d))[0]
140
- animated_glb = os.path.join(LOG_PATH, f"animated_{base}.glb")
141
- animated_gif = os.path.join(LOG_PATH, f"preview_{base}.gif")
142
- json_path = os.path.join(LOG_PATH, f"metadata_{base}.json")
143
-
144
- # 1) GLB λ³€ν˜•
145
- modify_glb_file(input_3d, animated_glb, animation_type)
146
 
147
- # 2) μ‹€μ œ λͺ¨λΈ 기반 GIF
148
  create_model_animation_gif(
149
- animated_gif,
150
- input_3d,
151
- animation_type,
152
- animation_duration,
153
- fps,
154
  )
155
 
156
- # 3) 메타데이터 기둝
157
- metadata = dict(
158
  animation_type=animation_type,
159
  duration=animation_duration,
160
  fps=fps,
161
  original_model=os.path.basename(input_3d),
162
  created_at=time.strftime("%Y-%m-%d %H:%M:%S"),
163
  )
164
- with open(json_path, "w") as f:
165
- json.dump(metadata, f, indent=4)
166
 
167
- return animated_glb, animated_gif, json_path
168
 
169
  except Exception as e:
170
- # 치λͺ…적 였λ₯˜ μ‹œ: 원본 λͺ¨λΈ + 빈 GIF
171
- print("Error in process_3d_model:", e)
172
- fallback_gif = os.path.join(LOG_PATH, "error.gif")
173
- Image.new("RGB", (640, 480), (255, 0, 0)).save(fallback_gif)
174
- return input_3d, fallback_gif, None
175
 
176
-
177
- # ------------------------------------------------------------------
178
- # 4) Gradio UI
179
- # ------------------------------------------------------------------
180
  with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
181
  gr.Markdown(
182
  """
183
  <h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
184
- 정적인 3D λͺ¨λΈ(GLB)에 νšŒμ „Β·λΆ€μœ Β·ν­λ°œ λ“± μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•©λ‹ˆλ‹€.
185
  """
186
  )
187
-
188
  with gr.Row():
189
  with gr.Column():
190
- input_3d = gr.Model3D(label="3D λͺ¨λΈ μ—…λ‘œλ“œ (GLB)")
191
- animation_type = gr.Dropdown(
192
  label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
193
  choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
194
  value="rotate",
195
  )
196
- animation_duration = gr.Slider(
197
- label="μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)", minimum=1.0, maximum=10.0, value=3.0, step=0.5
198
- )
199
- fps = gr.Slider(label="FPS", minimum=15, maximum=60, value=30, step=1)
200
- submit_btn = gr.Button("μ• λ‹ˆλ©”μ΄μ…˜ 생성")
201
-
202
  with gr.Column():
203
- output_3d = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜λœ GLB")
204
- output_gif = gr.Image(label="미리보기 GIF")
205
- output_json = gr.File(label="메타데이터 JSON")
206
 
207
- submit_btn.click(
208
  fn=process_3d_model,
209
- inputs=[input_3d, animation_type, animation_duration, fps],
210
- outputs=[output_3d, output_gif, output_json],
211
  )
212
 
213
- # 예제 GLB 폴더가 μžˆλ‹€λ©΄ μžλ™ 등둝
214
- example_glbs = [[f] for f in glob.glob("./data/demo_glb/*.glb")]
215
- if example_glbs:
216
- gr.Examples(examples=example_glbs, inputs=[input_3d])
217
 
218
- # ------------------------------------------------------------------
219
- # 5) μ•± μ‹€ν–‰
220
- # ------------------------------------------------------------------
221
  if __name__ == "__main__":
222
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  """
2
+ GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기
3
+ ────────────────────────────────────────────────────────
4
+ μ—…λ‘œλ“œν•œ GLB 3D λͺ¨λΈμ— νšŒμ „Β·λΆ€μœ Β·ν­λ°œ λ“± μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•΄
5
+ β‘  λ³€ν˜•λœ GLB β‘‘ μ‹€μ œ λͺ¨λΈμ„ λ Œλ”λ§ν•œ GIF β‘’ 메타데이터 JSON 을 λŒλ €μ€λ‹ˆλ‹€.
6
+ β€’ headless μ„œλ²„ λŒ€μ‘: EGL + pyglet.headless + trimesh β†’ μ‹€νŒ¨ μ‹œ pyrender 폴백
7
+ β€’ μ΅œλŒ€ 60 fpsΒ·60 ν”„λ ˆμž„λ‘œ μ œν•œ (데λͺ¨ λͺ©μ )
 
8
  """
9
 
10
+ # ──────────────────── 1. 곡톡 λͺ¨λ“ˆ ────────────────────
11
+ import os, io, time, glob, json, math, shutil
12
  import numpy as np
 
13
  from PIL import Image
 
14
 
15
+ import pyglet
16
+ pyglet.options["headless"] = True # λ°˜λ“œμ‹œ trimesh 전에!
17
+ os.environ["PYOPENGL_PLATFORM"] = "egl" # Off-screen GL
18
+
19
+ import trimesh
20
+ import trimesh.transformations as tf
21
 
22
  import gradio as gr
23
  import spaces
24
 
 
25
  LOG_PATH = "./results/demo"
26
  os.makedirs(LOG_PATH, exist_ok=True)
27
 
28
+ # ──────────────────── 2. λ Œλ” μœ ν‹Έ ────────────────────
29
+ def _render_with_trimesh(scene: trimesh.Scene, res):
30
+ png = scene.save_image(resolution=res, visible=True)
31
+ if png is None:
32
+ raise RuntimeError("trimesh.save_image returned None")
33
+ return Image.open(io.BytesIO(png)).convert("RGB")
34
+
35
+ def _render_with_pyrender(mesh_or_scene, res):
36
+ import pyrender # μ§€μ—° μž„ν¬νŠΈ
37
+ if isinstance(mesh_or_scene, trimesh.Scene):
38
+ mesh = trimesh.util.concatenate(mesh_or_scene.dump())
39
+ else:
40
+ mesh = mesh_or_scene
41
+ mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
42
+ scn = pyrender.Scene()
43
+ scn.add(mesh)
44
+ cam = pyrender.PerspectiveCamera(yfov=np.pi / 3)
45
+ scn.add(cam, pose=tf.translation_matrix([0, 0, 3]))
46
+ light = pyrender.DirectionalLight(intensity=3.0)
47
+ scn.add(light, pose=tf.translation_matrix([0, 5, 5]))
48
+ r = pyrender.OffscreenRenderer(*res)
49
+ color, _ = r.render(scn, flags=pyrender.RenderFlags.RGBA)
50
+ r.delete()
51
+ return Image.fromarray(color[..., :3])
52
+
53
+ # ──────────────────── 3. GIF 생성 ────────────────────
54
  def create_model_animation_gif(
55
  output_path: str,
56
  input_glb_path: str,
 
59
  fps: int = 30,
60
  resolution=(640, 480),
61
  ):
62
+ """GLB λͺ¨λΈμ„ μ‹€μ œ λ Œλ”λ§ν•˜μ—¬ μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성"""
63
+ base = trimesh.load(input_glb_path)
64
+ if isinstance(base, trimesh.Trimesh):
65
+ base = trimesh.Scene(base)
66
+
67
+ num_frames = min(int(duration * fps), 60)
68
+ frames = []
 
 
69
  for i in range(num_frames):
70
  t = i / (num_frames - 1)
71
+ scene = base.copy()
72
 
73
+ # λ³€ν™˜ ν–‰λ ¬
 
74
  if animation_type == "rotate":
75
  M = tf.rotation_matrix(2 * math.pi * t, [0, 1, 0])
76
  elif animation_type == "float":
 
85
  M = tf.rotation_matrix(math.pi / 6 * math.sin(2 * math.pi * t), [0, 0, 1])
86
  else:
87
  M = np.eye(4)
88
+ scene.apply_transform(M)
89
 
90
+ # β‘  trimesh β†’ β‘‘ pyrender 폴백
91
+ try:
92
+ frame = _render_with_trimesh(scene, resolution)
93
+ except Exception as e:
94
+ print("trimesh λ Œλ” μ‹€νŒ¨, pyrender 폴백:", e)
95
+ frame = _render_with_pyrender(scene, resolution)
96
 
 
 
 
97
  frames.append(frame)
98
 
 
99
  frames[0].save(
100
  output_path,
101
  save_all=True,
102
  append_images=frames[1:],
 
103
  duration=int(1000 / fps),
104
  loop=0,
105
  )
106
+ print("GIF saved:", output_path)
107
  return output_path
108
 
109
+ # ──────────────────── 4. GLB λ³€ν˜• ────────────────────
 
 
 
110
  def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"):
111
+ """단일 λ³€ν™˜μ„ μ μš©ν•œ μƒˆ GLB μ €μž₯ (μ‹€μ œ μ• λ‹ˆλ©”μ΄μ…˜μ€ μ•„λ‹˜)"""
 
 
112
  try:
113
+ scn = trimesh.load(input_glb_path)
114
+ if not isinstance(scn, trimesh.Scene):
115
+ scn = trimesh.Scene(scn)
116
 
 
117
  if animation_type == "rotate":
118
+ T = tf.rotation_matrix(math.pi / 4, [0, 1, 0])
119
  elif animation_type == "float":
120
+ T = tf.translation_matrix([0, 0.5, 0])
121
  elif animation_type == "pulse":
122
+ T = tf.scale_matrix(1.2)
123
  elif animation_type == "explode":
124
+ T = tf.translation_matrix([0.5, 0, 0])
125
  elif animation_type == "assemble":
126
+ T = tf.translation_matrix([-0.5, 0, 0])
127
  elif animation_type == "swing":
128
+ T = tf.rotation_matrix(math.pi / 8, [0, 0, 1])
129
  else:
130
+ T = np.eye(4)
131
 
132
+ scn.apply_transform(T)
133
+ scn.export(output_glb_path)
 
134
  return output_glb_path
 
135
  except Exception as e:
136
+ print("GLB λ³€ν˜• μ‹€νŒ¨, 원본 볡사:", e)
 
 
 
137
  shutil.copy(input_glb_path, output_glb_path)
 
138
  return output_glb_path
139
 
140
+ # ──────────────────── 5. Gradio νŒŒμ΄ν”„λΌμΈ ────────────────────
 
 
 
141
  @spaces.GPU
142
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
143
+ """전체 νŒŒμ΄ν”„λΌμΈ: GLB λ³€ν˜• β†’ GIF λ Œλ” β†’ JSON μž‘μ„±"""
 
 
144
  try:
145
  base = os.path.splitext(os.path.basename(input_3d))[0]
146
+ glb_out = os.path.join(LOG_PATH, f"animated_{base}.glb")
147
+ gif_out = os.path.join(LOG_PATH, f"preview_{base}.gif")
148
+ json_out = os.path.join(LOG_PATH, f"metadata_{base}.json")
 
 
 
149
 
150
+ modify_glb_file(input_3d, glb_out, animation_type)
151
  create_model_animation_gif(
152
+ gif_out, input_3d, animation_type, animation_duration, fps
 
 
 
 
153
  )
154
 
155
+ meta = dict(
 
156
  animation_type=animation_type,
157
  duration=animation_duration,
158
  fps=fps,
159
  original_model=os.path.basename(input_3d),
160
  created_at=time.strftime("%Y-%m-%d %H:%M:%S"),
161
  )
162
+ with open(json_out, "w") as f:
163
+ json.dump(meta, f, indent=4)
164
 
165
+ return glb_out, gif_out, json_out
166
 
167
  except Exception as e:
168
+ print("process_3d_model μ‹€νŒ¨:", e)
169
+ err_gif = os.path.join(LOG_PATH, "error.gif")
170
+ Image.new("RGB", (640, 480), (255, 0, 0)).save(err_gif)
171
+ return input_3d, err_gif, None
 
172
 
173
+ # ──────────────────── 6. Gradio UI ────────────────────
 
 
 
174
  with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
175
  gr.Markdown(
176
  """
177
  <h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
178
+ μ—…λ‘œλ“œν•œ GLB λͺ¨λΈμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•΄ λ³€ν˜•λœ GLBΒ·GIFΒ·JSON을 μ œκ³΅ν•©λ‹ˆλ‹€.
179
  """
180
  )
 
181
  with gr.Row():
182
  with gr.Column():
183
+ inp = gr.Model3D(label="3D λͺ¨λΈ μ—…λ‘œλ“œ (GLB)")
184
+ typ = gr.Dropdown(
185
  label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
186
  choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
187
  value="rotate",
188
  )
189
+ dur = gr.Slider("μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)", 1.0, 10.0, 3.0, 0.5)
190
+ fps = gr.Slider("FPS", 15, 60, 30, 1)
191
+ btn = gr.Button("μ• λ‹ˆλ©”μ΄μ…˜ 생성")
 
 
 
192
  with gr.Column():
193
+ out_glb = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜λœ GLB")
194
+ out_gif = gr.Image(label="미리보기 GIF")
195
+ out_json = gr.File(label="메타데이터 JSON")
196
 
197
+ btn.click(
198
  fn=process_3d_model,
199
+ inputs=[inp, typ, dur, fps],
200
+ outputs=[out_glb, out_gif, out_json],
201
  )
202
 
203
+ ex = [[f] for f in glob.glob("./data/demo_glb/*.glb")]
204
+ if ex:
205
+ gr.Examples(examples=ex, inputs=[inp])
 
206
 
 
 
 
207
  if __name__ == "__main__":
208
  demo.launch(server_name="0.0.0.0", server_port=7860)