Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import subprocess | |
| import os | |
| import shutil | |
| import uuid | |
| import base64 | |
| import numpy as np | |
| from plyfile import PlyData, PlyElement | |
| from urllib.parse import quote | |
| STATIC_DIR = "/tmp/sharp_static" | |
| os.makedirs(STATIC_DIR, exist_ok=True) | |
| # 库文件路径(Docker 镜像中已下载) | |
| GAUSSIAN_SPLATS_JS = "/app/static/gaussian-splats-3d.module.js" | |
| def convert_to_binary_ply(input_path, output_path): | |
| """将 PLY 转为二进制格式,并统一属性为 float32""" | |
| plydata = PlyData.read(input_path) | |
| vert = plydata['vertex'] | |
| dtype = vert.data.dtype | |
| names = dtype.names | |
| new_dtype = [(name, np.float32) for name in names] | |
| old_data = vert.data | |
| new_data = np.zeros(old_data.shape, dtype=new_dtype) | |
| for name in names: | |
| new_data[name] = old_data[name].astype(np.float32) | |
| new_vert = PlyElement.describe(new_data, 'vertex') | |
| elements = [new_vert] | |
| for el in plydata.elements: | |
| if el.name != 'vertex': | |
| elements.append(el) | |
| PlyData(elements, text=False).write(output_path) | |
| def sharp_predict(image, enable_3d_preview): | |
| input_dir = "/tmp/sharp_input" | |
| output_dir = "/tmp/sharp_output" | |
| os.makedirs(input_dir, exist_ok=True) | |
| os.makedirs(output_dir, exist_ok=True) | |
| input_path = os.path.join(input_dir, "input.png") | |
| image.save(input_path) | |
| result = subprocess.run( | |
| ["sharp", "predict", "-i", input_dir, "-o", output_dir, | |
| "-c", "/app/checkpoints/sharp_2572gikvuh.pt"], | |
| cwd="/app/ml-sharp", | |
| capture_output=True, text=True, timeout=1800 | |
| ) | |
| if result.returncode != 0: | |
| raise gr.Error(f"SHARP 运行失败:{result.stderr}") | |
| ply_files = [f for f in os.listdir(output_dir) if f.endswith(".ply")] | |
| if not ply_files: | |
| raise gr.Error("未生成 .ply 文件") | |
| orig_ply = os.path.join(output_dir, ply_files[0]) | |
| unique_name = f"model_{uuid.uuid4().hex[:8]}.ply" | |
| dest_path = os.path.join(STATIC_DIR, unique_name) | |
| convert_to_binary_ply(orig_ply, dest_path) | |
| # 公网文件 URL(用于 SuperSplat 跳转) | |
| space_host = "satosakura-sharp.hf.space" # 请确认是你的域名 | |
| file_url = f"/file={dest_path}" | |
| full_url = f"https://{space_host}{file_url}" | |
| supersplat_url = f"https://playcanvas.com/supersplat/editor?content={quote(full_url)}" | |
| # 准备 HTML 说明(总是显示) | |
| info_html = f""" | |
| <div style="text-align: center; font-family: Arial; padding: 20px;"> | |
| <p style="font-size: 18px;">✅ 模型生成成功!</p> | |
| <p style="color: #888;">请使用下方的 <b>下载 .ply 文件</b> 获取模型。</p> | |
| <p> | |
| <a href="{supersplat_url}" target="_blank"> | |
| <button style="padding: 12px 24px; font-size: 16px; background: #4CAF50; color: white; border: none; border-radius: 8px; cursor: pointer;"> | |
| 🔍 在 SuperSplat 中查看(需公网访问) | |
| </button> | |
| </a> | |
| </p> | |
| </div> | |
| """ | |
| # 如果启用了 3D 预览,生成自包含 HTML 文件 | |
| viewer_html_path = None | |
| if enable_3d_preview: | |
| # 读取模型数据 | |
| with open(dest_path, "rb") as f: | |
| ply_bytes = f.read() | |
| ply_b64 = base64.b64encode(ply_bytes).decode("utf-8") | |
| # 读取渲染库 | |
| if os.path.exists(GAUSSIAN_SPLATS_JS): | |
| with open(GAUSSIAN_SPLATS_JS, "rb") as f: | |
| js_bytes = f.read() | |
| js_code = js_bytes.decode("utf-8") | |
| else: | |
| js_code = "" | |
| # 构建自包含 HTML | |
| viewer_html_content = f"""<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Gaussian Splatting Viewer</title> | |
| <style> | |
| body {{ margin: 0; overflow: hidden; font-family: Arial; }} | |
| #info {{ position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.5); padding: 5px 10px; border-radius: 5px; z-index: 10; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="info">加载中...</div> | |
| <script type="importmap"> | |
| {{ | |
| "imports": {{ | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| }} | |
| }} | |
| </script> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import {{ OrbitControls }} from 'three/addons/controls/OrbitControls.js'; | |
| const plyBase64 = "{ply_b64}"; | |
| const byteChars = atob(plyBase64); | |
| const byteNums = new Array(byteChars.length); | |
| for (let i = 0; i < byteChars.length; i++) {{ | |
| byteNums[i] = byteChars.charCodeAt(i); | |
| }} | |
| const byteArr = new Uint8Array(byteNums); | |
| const blob = new Blob([byteArr], {{ type: 'application/octet-stream' }}); | |
| const plyUrl = URL.createObjectURL(blob); | |
| // 内联的 GaussianSplats3D 代码 | |
| {js_code} | |
| const viewer = new GaussianSplats3D.Viewer({{ | |
| 'cameraUp': [0, -1, 0], | |
| 'initialCameraPosition': [0, 0, 5], | |
| 'initialCameraLookAt': [0, 0, 0], | |
| 'sharedMemoryForWorkers': false | |
| }}); | |
| viewer.init() | |
| .then(() => {{ | |
| document.getElementById('info').textContent = '正在加载模型...'; | |
| return viewer.addSplatScene(plyUrl, {{ | |
| 'splatAlphaRemovalThreshold': 5, | |
| 'showLoadingUI': true, | |
| 'position': [0, 0, 0], | |
| 'rotation': [1, 0, 0, 0], | |
| 'scale': [1, 1, 1] | |
| }}); | |
| }}) | |
| .then(() => {{ | |
| viewer.start(); | |
| document.getElementById('info').textContent = '模型已加载 | 左键旋转 | 滚轮缩放 | 右键平移'; | |
| }}) | |
| .catch((err) => {{ | |
| console.error(err); | |
| document.getElementById('info').textContent = '加载失败:' + err.message; | |
| }}); | |
| </script> | |
| </body> | |
| </html>""" | |
| # 保存自包含 HTML 文件 | |
| viewer_name = f"viewer_{uuid.uuid4().hex[:8]}.html" | |
| viewer_html_path = os.path.join(STATIC_DIR, viewer_name) | |
| with open(viewer_html_path, "w", encoding="utf-8") as f: | |
| f.write(viewer_html_content) | |
| # 更新提示信息 | |
| info_html += """ | |
| <p style="color: #2196F3; font-weight: bold;"> | |
| 📁 下方“下载交互式查看器”可下载自包含的 HTML 文件,<br> | |
| 双击即可在本地浏览器中实时旋转/缩放模型(利用本地 GPU)。 | |
| </p> | |
| """ | |
| # 返回:.ply 文件、说明文本、可能的查看器 HTML 文件 | |
| outputs = [dest_path, info_html] | |
| if viewer_html_path: | |
| outputs.append(viewer_html_path) | |
| else: | |
| outputs.append(None) # 占位,Gradio 支持 None 输出 | |
| return tuple(outputs) | |
| # 界面 | |
| with gr.Blocks(title="SHARP - 3DGS Dual Preview") as demo: | |
| gr.Markdown(""" | |
| # SHARP: Image to 3D Gaussian Splatting | |
| 上传一张图片,生成 3D 高斯泼溅模型。CPU 推理约需 10-20 分钟。 | |
| """) | |
| with gr.Row(): | |
| input_img = gr.Image(type="pil", label="上传图片") | |
| with gr.Column(): | |
| enable_3d = gr.Checkbox(label="生成交互式查看器(可下载 .html 文件)", value=True) | |
| output_file = gr.File(label="下载 .ply 模型文件") | |
| output_html = gr.HTML(label="操作面板") | |
| output_viewer = gr.File(label="下载交互式查看器 (.html)", visible=True) | |
| btn = gr.Button("开始生成", variant="primary") | |
| btn.click( | |
| sharp_predict, | |
| inputs=[input_img, enable_3d], | |
| outputs=[output_file, output_html, output_viewer] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| allowed_paths=[STATIC_DIR] | |
| ) |