chaoxu commited on
Commit
d26c5a6
1 Parent(s): 818de80
Files changed (4) hide show
  1. .gitignore +2 -0
  2. Dockerfile +30 -0
  3. launch.py +428 -0
  4. style.css +13 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ examples_data/
2
+ examples_display/
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
4
+ ENV TZ=America/Los_Angeles
5
+ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
6
+
7
+ RUN pip3 install --no-cache-dir \
8
+ numpy \
9
+ uuid \
10
+ json \
11
+ trimesh \
12
+ Pillow \
13
+ gradio==4.39.0
14
+
15
+ # Set up a new user named "user" with user ID 1000
16
+ RUN useradd -m -u 1000 user
17
+ # Switch to the "user" user
18
+ USER user
19
+ # Set home to the user's home directory
20
+ ENV HOME=/home/user \
21
+ PATH=/home/user/.local/bin:$PATH \
22
+ PYTHONPATH=$HOME/app \
23
+ PYTHONUNBUFFERED=1 \
24
+ SYSTEM=spaces
25
+ # Set the working directory to the user's home directory
26
+ WORKDIR $HOME/app
27
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
28
+ COPY --chown=user . $HOME/app
29
+
30
+ CMD ["python3", "launch.py"]
launch.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import numpy as np
5
+ import gradio as gr
6
+ import trimesh
7
+ import zipfile
8
+ import subprocess
9
+ from datetime import datetime
10
+ from functools import partial
11
+ from PIL import Image, ImageChops
12
+ from huggingface_hub import snapshot_download
13
+
14
+
15
+ is_local_run = os.path.exists("../SpaRP_API")
16
+
17
+ code_dir = snapshot_download("sudo-ai/SpaRP_API", token=os.environ['HF_TOKEN']) if not is_local_run else "../SpaRP_API"
18
+
19
+ if not is_local_run:
20
+ zip_file_path = f'{code_dir}/examples.zip'
21
+ # Unzipping the file into the current directory
22
+ with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
23
+ zip_ref.extractall(os.getcwd())
24
+
25
+
26
+ os.system(f"pip install {code_dir}/gradio_model3dcolor-0.0.1-py3-none-any.whl")
27
+ os.system(f"pip install {code_dir}/gradio_model3dnormal-0.0.1-py3-none-any.whl")
28
+ from gradio_model3dcolor import Model3DColor
29
+ from gradio_model3dnormal import Model3DNormal
30
+
31
+ with open(f'{code_dir}/api.json', 'r') as file:
32
+ api_dict = json.load(file)
33
+ SEGM_i_CALL = api_dict["SEGM_i_CALL"]
34
+ SEGM_CALL = api_dict["SEGM_CALL"]
35
+ UNPOSED_CALL = api_dict["UNPOSED_CALL"]
36
+ MESH_CALL = api_dict["MESH_CALL"]
37
+
38
+
39
+ _TITLE = (
40
+ """SpaRP: Fast 3D Object Reconstruction and Pose Estimation from Sparse Views"""
41
+ )
42
+ _DESCRIPTION = (
43
+ """Try SpaRP to reconstruct 3D textured mesh from one or a few unposed images!"""
44
+ )
45
+ _PR = """
46
+ <div>
47
+ <b><em>Check out <a href="https://www.sudo.ai/3dgen">Hillbot (sudoAI)</a> for more details and advanced features.</em></b>
48
+ </div>
49
+ """
50
+
51
+
52
+ STYLE = """
53
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
54
+ <style>
55
+ .alert, .alert div, .alert b {
56
+ color: black !important;
57
+ }
58
+ </style>
59
+ """
60
+ # info (info-circle-fill), cursor (hand-index-thumb), wait (hourglass-split), done (check-circle)
61
+ ICONS = {
62
+ "info": """<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0d6efd" class="bi bi-info-circle-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
63
+ <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
64
+ </svg>""",
65
+ "cursor": """<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0dcaf0" class="bi bi-hand-index-thumb-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
66
+ <path d="M8.5 1.75v2.716l.047-.002c.312-.012.742-.016 1.051.046.28.056.543.18.738.288.273.152.456.385.56.642l.132-.012c.312-.024.794-.038 1.158.108.37.148.689.487.88.716.075.09.141.175.195.248h.582a2 2 0 0 1 1.99 2.199l-.272 2.715a3.5 3.5 0 0 1-.444 1.389l-1.395 2.441A1.5 1.5 0 0 1 12.42 16H6.118a1.5 1.5 0 0 1-1.342-.83l-1.215-2.43L1.07 8.589a1.517 1.517 0 0 1 2.373-1.852L5 8.293V1.75a1.75 1.75 0 0 1 3.5 0z"/>
67
+ </svg>""",
68
+ "wait": """<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" class="bi bi-hourglass-split flex-shrink-0 me-2" viewBox="0 0 16 16">
69
+ <path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
70
+ </svg>""",
71
+ "done": """<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#198754" class="bi bi-check-circle-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
72
+ <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
73
+ </svg>""",
74
+ }
75
+
76
+ icons2alert = {
77
+ "info": "primary", # blue
78
+ "cursor": "info", # light blue
79
+ "wait": "secondary", # gray
80
+ "done": "success", # green
81
+ }
82
+
83
+
84
+ def message(text, icon_type="info"):
85
+ return f"""{STYLE} <div class="alert alert-{icons2alert[icon_type]} d-flex align-items-center" role="alert"> {ICONS[icon_type]}
86
+ <div>
87
+ {text}
88
+ </div>
89
+ </div>"""
90
+
91
+
92
+ def create_tmp_dir():
93
+ tmp_dir = (
94
+ "../demo_exp/"
95
+ + datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
96
+ + "_"
97
+ + str(uuid.uuid4())[:4]
98
+ )
99
+ os.makedirs(tmp_dir, exist_ok=True)
100
+ print("create tmp_exp_dir", tmp_dir)
101
+ return tmp_dir
102
+
103
+
104
+ def preprocess_imgs(tmp_dir, input_img, idx=None):
105
+ if isinstance(input_img, list) and idx is None:
106
+ for i, img_tuple in enumerate(input_img):
107
+ Image.open(img_tuple[0]).save(f"{tmp_dir}/input_{i}.png")
108
+ os.system(SEGM_i_CALL.replace("{tmp_dir}", tmp_dir).replace("{i}", str(i)))
109
+ return [Image.open(f"{tmp_dir}/seg_{i}.png") for i in range(len(input_img))]
110
+
111
+ if idx is not None:
112
+ print("image idx:", int(idx))
113
+ input_img = Image.open(input_img[int(idx)][0])
114
+ input_img.save(f"{tmp_dir}/input.png")
115
+ os.system(SEGM_CALL.replace("{tmp_dir}", tmp_dir))
116
+ processed_img = Image.open(f"{tmp_dir}/seg.png")
117
+ return processed_img.resize((320, 320), Image.Resampling.LANCZOS)
118
+
119
+
120
+ def ply_to_glb(ply_path):
121
+ script_path = "../utils/ply2glb.py"
122
+ result = subprocess.run(
123
+ ["python", script_path, "--", ply_path],
124
+ capture_output=True,
125
+ text=True,
126
+ )
127
+
128
+ print("Output of blender script:")
129
+ print(result.stdout)
130
+
131
+ glb_path = ply_path.replace(".ply", ".glb")
132
+ return glb_path
133
+
134
+
135
+ def mesh_gen(tmp_dir, use_seg):
136
+ os.system(UNPOSED_CALL.replace("{tmp_dir}", tmp_dir).replace("{use_seg}", str(use_seg)))
137
+ os.system(MESH_CALL.replace("{tmp_dir}", tmp_dir))
138
+
139
+ mesh = trimesh.load_mesh(f"{tmp_dir}/mesh.ply")
140
+ vertex_normals = mesh.vertex_normals
141
+ colors = (-vertex_normals + 1) / 2.0
142
+ colors = (colors * 255).astype(np.uint8) # Convert to 8-bit color
143
+ mesh.visual.vertex_colors = colors
144
+ mesh.export(f"{tmp_dir}/mesh_normal.ply", file_type="ply")
145
+
146
+ color_path = ply_to_glb(f"{tmp_dir}/mesh.ply")
147
+ normal_path = ply_to_glb(f"{tmp_dir}/mesh_normal.ply")
148
+
149
+ return color_path, normal_path
150
+
151
+
152
+ def feed_example_to_gallery(img):
153
+ for display_img in display_imgs:
154
+ display_img = display_img[0]
155
+ diff = ImageChops.difference(img, display_img)
156
+ if not diff.getbbox(): # two images are the same
157
+ img_id = display_img.filename
158
+ data_dir = os.path.join(data_folder, str(img_id))
159
+ data_fns = os.listdir(data_dir)
160
+ data_fns.sort()
161
+ data_imgs = []
162
+ for data_fn in data_fns:
163
+ file_path = os.path.join(data_dir, data_fn)
164
+ img = Image.open(file_path)
165
+ data_imgs.append(img)
166
+ return data_imgs
167
+ return [img]
168
+
169
+
170
+ custom_theme = gr.themes.Soft(primary_hue="blue").set(
171
+ button_secondary_background_fill="*neutral_100",
172
+ button_secondary_background_fill_hover="*neutral_200",
173
+ )
174
+
175
+ # Gradio blocks
176
+ with gr.Blocks(title=_TITLE, css="style.css", theme=custom_theme) as demo:
177
+ tmp_dir_unposed = gr.State("./demo_exp/placeholder")
178
+ display_folder = os.path.join(os.path.dirname(__file__), "examples_display")
179
+ display_fns = os.listdir(display_folder)
180
+ display_fns.sort()
181
+ display_imgs = []
182
+ for i, display_fn in enumerate(display_fns):
183
+ file_path = os.path.join(display_folder, display_fn)
184
+ img = Image.open(file_path)
185
+ img.filename = i
186
+ display_imgs.append([img])
187
+ data_folder = os.path.join(os.path.dirname(__file__), "examples_data")
188
+
189
+ # UI
190
+ with gr.Row():
191
+ gr.Markdown("# " + _TITLE)
192
+ with gr.Row():
193
+ gr.Markdown("### " + _DESCRIPTION)
194
+ with gr.Row():
195
+ gr.Markdown(_PR)
196
+ with gr.Row():
197
+ guide_text = gr.HTML(
198
+ message("Input image(s) of object that you want to generate mesh with.")
199
+ )
200
+ with gr.Row(variant="panel"):
201
+ with gr.Column():
202
+ with gr.Row():
203
+ with gr.Column(scale=5):
204
+ input_gallery = gr.Gallery(
205
+ label="Input Images",
206
+ show_label=False,
207
+ columns=[3],
208
+ rows=[2],
209
+ object_fit="contain",
210
+ height=400,
211
+ )
212
+ input_image = gr.Image(
213
+ type="pil",
214
+ image_mode="RGBA",
215
+ visible=False,
216
+ )
217
+ with gr.Column(scale=5):
218
+ processed_gallery = gr.Gallery(
219
+ label="Background Removal",
220
+ columns=[3],
221
+ rows=[2],
222
+ object_fit="contain",
223
+ height=400,
224
+ interactive=False,
225
+ )
226
+ with gr.Row():
227
+ with gr.Column(scale=5):
228
+ example = gr.Examples(
229
+ examples=display_imgs,
230
+ inputs=[input_image],
231
+ outputs=[input_gallery],
232
+ fn=feed_example_to_gallery,
233
+ label="Image Examples (Click one of the images below to start)",
234
+ examples_per_page=10,
235
+ run_on_click=True,
236
+ )
237
+ with gr.Column(scale=5):
238
+ with gr.Row():
239
+ bg_removed_checkbox = gr.Checkbox(
240
+ value=True,
241
+ label="Use background removed images (uncheck to use original)",
242
+ interactive=True,
243
+ )
244
+ with gr.Row():
245
+ run_btn = gr.Button(
246
+ "Generate",
247
+ variant="primary",
248
+ interactive=False,
249
+ )
250
+ with gr.Row():
251
+ with gr.Column(scale=5):
252
+ mesh_output = Model3DColor(
253
+ label="Generated Mesh (color)",
254
+ elem_id="mesh-out",
255
+ height=400,
256
+ )
257
+ with gr.Column(scale=5):
258
+ mesh_output_normal = Model3DNormal(
259
+ label="Generated Mesh (normal)",
260
+ elem_id="mesh-normal-out",
261
+ height=400,
262
+ )
263
+
264
+ # Callbacks
265
+ disable_button = lambda: gr.Button(interactive=False)
266
+ enable_button = lambda: gr.Button(interactive=True)
267
+ update_guide = lambda GUIDE_TEXT, icon_type="info": gr.HTML(
268
+ value=message(GUIDE_TEXT, icon_type)
269
+ )
270
+
271
+ def is_cleared(content):
272
+ if content:
273
+ raise ValueError # gr.Error(visible=False) doesn't work, trick for not showing error message
274
+
275
+ def not_cleared(content):
276
+ if not content:
277
+ raise ValueError # gr.Error(visible=False) doesn't work, trick for not showing error message
278
+
279
+ # Upload event listener for input gallery
280
+ input_gallery.upload(
281
+ fn=disable_button,
282
+ outputs=[run_btn],
283
+ queue=False,
284
+ ).success(
285
+ fn=create_tmp_dir,
286
+ outputs=[tmp_dir_unposed],
287
+ queue=False,
288
+ ).success(
289
+ fn=partial(
290
+ update_guide, "Removing background of the input image(s)...", "wait"
291
+ ),
292
+ outputs=[guide_text],
293
+ queue=False,
294
+ ).success(
295
+ fn=preprocess_imgs,
296
+ inputs=[tmp_dir_unposed, input_gallery],
297
+ outputs=[processed_gallery],
298
+ queue=True,
299
+ ).success(
300
+ fn=partial(update_guide, "Click <b>Generate</b> to generate mesh.", "cursor"),
301
+ outputs=[guide_text],
302
+ queue=False,
303
+ ).success(
304
+ fn=enable_button,
305
+ outputs=[run_btn],
306
+ queue=False,
307
+ )
308
+
309
+ # Clear event listener for input gallery
310
+ input_gallery.change(
311
+ fn=is_cleared,
312
+ inputs=[input_gallery],
313
+ queue=False,
314
+ ).success(
315
+ fn=disable_button,
316
+ outputs=[run_btn],
317
+ queue=False,
318
+ ).success(
319
+ fn=lambda: None,
320
+ outputs=[input_image],
321
+ queue=False,
322
+ ).success(
323
+ fn=lambda: None,
324
+ outputs=[processed_gallery],
325
+ queue=False,
326
+ ).success(
327
+ fn=lambda: None,
328
+ outputs=[mesh_output],
329
+ queue=False,
330
+ ).success(
331
+ fn=lambda: None,
332
+ outputs=[mesh_output_normal],
333
+ queue=False,
334
+ ).success(
335
+ fn=partial(
336
+ update_guide,
337
+ "Input image(s) of object that you want to generate mesh with.",
338
+ "info",
339
+ ),
340
+ outputs=[guide_text],
341
+ queue=False,
342
+ )
343
+
344
+ # Change event listener for input image
345
+ input_image.change(
346
+ fn=not_cleared,
347
+ inputs=[input_image],
348
+ queue=False,
349
+ ).success(
350
+ fn=disable_button,
351
+ outputs=run_btn,
352
+ queue=False,
353
+ ).success(
354
+ fn=lambda: None,
355
+ outputs=[mesh_output],
356
+ queue=False,
357
+ ).success(
358
+ fn=lambda: None,
359
+ outputs=[mesh_output_normal],
360
+ queue=False,
361
+ ).success(
362
+ fn=create_tmp_dir,
363
+ outputs=tmp_dir_unposed,
364
+ queue=False,
365
+ ).success(
366
+ fn=partial(
367
+ update_guide, "Removing background of the input image(s)...", "wait"
368
+ ),
369
+ outputs=[guide_text],
370
+ queue=False,
371
+ ).success(
372
+ fn=preprocess_imgs,
373
+ inputs=[tmp_dir_unposed, input_gallery],
374
+ outputs=[processed_gallery],
375
+ queue=True,
376
+ ).success(
377
+ fn=partial(update_guide, "Click <b>Generate</b> to generate mesh.", "cursor"),
378
+ outputs=[guide_text],
379
+ queue=False,
380
+ ).success(
381
+ fn=enable_button,
382
+ outputs=run_btn,
383
+ queue=False,
384
+ )
385
+
386
+ # Click event listener for run button
387
+ run_btn.click(
388
+ fn=disable_button,
389
+ outputs=[run_btn],
390
+ queue=False,
391
+ ).success(
392
+ fn=lambda: None,
393
+ outputs=[mesh_output],
394
+ queue=False,
395
+ ).success(
396
+ fn=lambda: None,
397
+ outputs=[mesh_output_normal],
398
+ queue=False,
399
+ ).success(
400
+ fn=partial(update_guide, "Generating the mesh...", "wait"),
401
+ outputs=[guide_text],
402
+ queue=False,
403
+ ).success(
404
+ fn=mesh_gen,
405
+ inputs=[tmp_dir_unposed, bg_removed_checkbox],
406
+ outputs=[mesh_output, mesh_output_normal],
407
+ queue=True,
408
+ ).success(
409
+ fn=partial(
410
+ update_guide,
411
+ "Successfully generated the mesh. (It might take a few seconds to load the mesh)",
412
+ "done",
413
+ ),
414
+ outputs=[guide_text],
415
+ queue=False,
416
+ ).success(
417
+ fn=enable_button,
418
+ outputs=[run_btn],
419
+ queue=False,
420
+ )
421
+
422
+ demo.queue().launch(
423
+ debug=False,
424
+ share=False,
425
+ inline=False,
426
+ show_api=False,
427
+ server_name="0.0.0.0",
428
+ )
style.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .centered {
2
+ text-align: center; /* Horizontally center the content */
3
+ }
4
+
5
+ .centered img {
6
+ display: block; /* Make the image a block element */
7
+ margin: 0 auto; /* Center the block element (the image) horizontally */
8
+ height: 100px;
9
+ }
10
+
11
+ .tab_at_top button.selected{
12
+ font-size: 24px !important;
13
+ }