sharp / app.py
satosakura's picture
Update app.py
60f23a1 verified
Raw
History Blame Contribute Delete
7.99 kB
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]
)