Spaces:
Running on Zero
Running on Zero
Claude Sonnet 4.5 Claude Fable 5 commited on
feat: SAM3 concept segmentation replaces SAM2
Browse filesreconstruct_objects(image, prompt) β SAM3 (facebook/sam3, transformers)
segments the object described by a short noun phrase and the best-scoring
instance goes to SAM 3D Objects. Default prompt 'object' keeps the
zero-config UX. Drops the sam2 runtime install and iopath.
Models load lazily inside @spaces.GPU with global caches β both SAM3D's
and SAM2's constructors crashed under ZeroGPU's startup CUDA emulation
(device-mixing tensor ops), so module-level cuda loading is not usable here.
Requires the space token to have accepted the gated facebook/sam3 license.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app.py +63 -40
- requirements.txt +3 -3
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
SAM 3D Objects MCP Server
|
| 3 |
-
Image β 3D Object (GLB)
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
torch is provided by ZeroGPU. kaolin and pytorch3d are replaced by pure-python
|
| 8 |
stubs covering exactly the surface the pipeline touches β texture baking, mesh
|
|
@@ -56,7 +56,6 @@ def _pip(*args):
|
|
| 56 |
print("=== Runtime installs (need torch present) ===")
|
| 57 |
# utils3d pinned to the commit MoGe expects β newer commits dropped points_to_normals
|
| 58 |
_pip("--no-deps", "git+https://github.com/EasternJournalist/utils3d.git@3913c65d81e05e47b9f367250cf8c0f7462a0900")
|
| 59 |
-
_pip("--no-deps", "sam2>=1.1.0")
|
| 60 |
_pip("--no-deps", "git+https://github.com/microsoft/MoGe.git@a8c37341bc0325ca99b9d57981cc3bb2bd3e255b")
|
| 61 |
# no prebuilt gsplat wheels beyond torch 2.4 β the PyPI sdist installs in JIT
|
| 62 |
# mode; kernels never compile here because rendering paths are disabled
|
|
@@ -80,35 +79,53 @@ if hf_ckpt.exists() and not local_ckpt.exists():
|
|
| 80 |
local_ckpt.symlink_to(hf_ckpt)
|
| 81 |
CONFIG_PATH = str(local_ckpt / "pipeline.yaml")
|
| 82 |
|
| 83 |
-
print("Loading models (ZeroGPU CUDA emulation)...")
|
| 84 |
-
from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
|
| 85 |
-
from sam3d_inference import SAM3DInference
|
| 86 |
-
|
| 87 |
-
# build on CPU first: SAM2's init runs tensor ops that mix devices under
|
| 88 |
-
# CUDA emulation; a plain .to("cuda") placement afterwards is supported
|
| 89 |
-
SAM2_GENERATOR = SAM2AutomaticMaskGenerator.from_pretrained(
|
| 90 |
-
"facebook/sam2-hiera-small", device="cpu")
|
| 91 |
-
SAM2_GENERATOR.predictor.model.to("cuda")
|
| 92 |
-
print("β SAM2 loaded")
|
| 93 |
-
|
| 94 |
-
try:
|
| 95 |
-
SAM3D = SAM3DInference(CONFIG_PATH)
|
| 96 |
-
print("β SAM 3D Objects loaded (startup)")
|
| 97 |
-
except Exception:
|
| 98 |
-
import traceback
|
| 99 |
-
traceback.print_exc()
|
| 100 |
-
SAM3D = None
|
| 101 |
-
print("! SAM3D startup load failed under CUDA emulation β lazy-loading in GPU context instead")
|
| 102 |
print("=== Startup complete ===")
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def _get_sam3d():
|
| 106 |
global SAM3D
|
| 107 |
if SAM3D is None:
|
|
|
|
| 108 |
SAM3D = SAM3DInference(CONFIG_PATH)
|
| 109 |
return SAM3D
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def _export_result(result, out_dir):
|
| 113 |
"""Export the pipeline result: prefer the ready-made GLB mesh, fall back
|
| 114 |
to the raw gaussian splat PLY."""
|
|
@@ -136,43 +153,45 @@ def diagnose():
|
|
| 136 |
lines = [f"torch={torch.__version__}", f"cuda={torch.cuda.is_available()}"]
|
| 137 |
if torch.cuda.is_available():
|
| 138 |
lines.append(f"gpu={torch.cuda.get_device_name()}")
|
| 139 |
-
lines.append(f"
|
| 140 |
-
lines.append(f"SAM3D: {'loaded' if SAM3D is not None else '
|
| 141 |
lines.append(f"config: {Path(CONFIG_PATH).exists()}")
|
| 142 |
return "\n".join(lines)
|
| 143 |
|
| 144 |
|
| 145 |
@spaces.GPU(duration=180)
|
| 146 |
-
def reconstruct_objects(image: np.ndarray, progress=gr.Progress()):
|
| 147 |
"""
|
| 148 |
-
|
| 149 |
-
in 3D with SAM 3D Objects.
|
| 150 |
|
| 151 |
Args:
|
| 152 |
image: Input RGB image
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
Returns:
|
| 155 |
tuple: (model_path, preview_image, status)
|
| 156 |
"""
|
| 157 |
if image is None:
|
| 158 |
return None, None, "β No image provided"
|
|
|
|
| 159 |
try:
|
| 160 |
import time
|
| 161 |
import torch
|
| 162 |
t0 = time.time()
|
| 163 |
print(f"GPU: {torch.cuda.get_device_name()}")
|
| 164 |
|
| 165 |
-
progress(0.05, desc="
|
| 166 |
image_np = np.asarray(image)
|
| 167 |
-
|
| 168 |
-
if
|
| 169 |
-
return None, image_np, "β οΈ No
|
| 170 |
-
masks = sorted(masks, key=lambda x: x["area"], reverse=True)
|
| 171 |
-
best_mask = masks[0]["segmentation"]
|
| 172 |
|
| 173 |
preview = image_np.copy()
|
| 174 |
preview[best_mask] = (preview[best_mask] * 0.5 + np.array([0, 255, 0]) * 0.5).astype(np.uint8)
|
| 175 |
-
print(f" {
|
| 176 |
|
| 177 |
progress(0.25, desc="Reconstructing 3D (SAM 3D Objects, ~1-2 min)...")
|
| 178 |
result = _get_sam3d()(image=image_np, mask=best_mask, seed=42)
|
|
@@ -192,7 +211,7 @@ def reconstruct_objects(image: np.ndarray, progress=gr.Progress()):
|
|
| 192 |
n_faces = len(trimesh.load(model_path, force="mesh").faces)
|
| 193 |
except Exception:
|
| 194 |
n_faces = 0
|
| 195 |
-
return model_path, preview, f"β {
|
| 196 |
except Exception:
|
| 197 |
import traceback
|
| 198 |
tb = traceback.format_exc()
|
|
@@ -203,23 +222,27 @@ def reconstruct_objects(image: np.ndarray, progress=gr.Progress()):
|
|
| 203 |
with gr.Blocks(title="SAM 3D Objects MCP") as demo:
|
| 204 |
gr.Markdown("""
|
| 205 |
# π¦ SAM 3D Objects MCP Server
|
| 206 |
-
**Image β 3D Object (GLB)**
|
| 207 |
|
| 208 |
-
|
| 209 |
""")
|
| 210 |
|
| 211 |
with gr.Tab("Reconstruct"):
|
| 212 |
with gr.Row():
|
| 213 |
with gr.Column():
|
| 214 |
input_image = gr.Image(label="Input Image", type="numpy")
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
| 216 |
with gr.Column():
|
| 217 |
-
preview = gr.Image(label="
|
| 218 |
status = gr.Textbox(label="Status")
|
| 219 |
with gr.Row():
|
| 220 |
output_model = gr.Model3D(label="3D Preview")
|
| 221 |
output_file = gr.File(label="Download")
|
| 222 |
-
btn.click(reconstruct_objects, inputs=[input_image
|
|
|
|
| 223 |
output_model.change(lambda x: x, inputs=[output_model], outputs=[output_file])
|
| 224 |
|
| 225 |
with gr.Tab("Diagnose"):
|
|
|
|
| 1 |
"""
|
| 2 |
SAM 3D Objects MCP Server
|
| 3 |
+
Image (+ text prompt) β 3D Object (GLB)
|
| 4 |
|
| 5 |
+
SAM3 concept segmentation + SAM 3D Objects reconstruction on ZeroGPU.
|
| 6 |
|
| 7 |
torch is provided by ZeroGPU. kaolin and pytorch3d are replaced by pure-python
|
| 8 |
stubs covering exactly the surface the pipeline touches β texture baking, mesh
|
|
|
|
| 56 |
print("=== Runtime installs (need torch present) ===")
|
| 57 |
# utils3d pinned to the commit MoGe expects β newer commits dropped points_to_normals
|
| 58 |
_pip("--no-deps", "git+https://github.com/EasternJournalist/utils3d.git@3913c65d81e05e47b9f367250cf8c0f7462a0900")
|
|
|
|
| 59 |
_pip("--no-deps", "git+https://github.com/microsoft/MoGe.git@a8c37341bc0325ca99b9d57981cc3bb2bd3e255b")
|
| 60 |
# no prebuilt gsplat wheels beyond torch 2.4 β the PyPI sdist installs in JIT
|
| 61 |
# mode; kernels never compile here because rendering paths are disabled
|
|
|
|
| 79 |
local_ckpt.symlink_to(hf_ckpt)
|
| 80 |
CONFIG_PATH = str(local_ckpt / "pipeline.yaml")
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
print("=== Startup complete ===")
|
| 83 |
|
| 84 |
+
# Model construction needs a real GPU (the constructors run device-mixing
|
| 85 |
+
# tensor ops that ZeroGPU's startup CUDA emulation rejects), so models load
|
| 86 |
+
# inside @spaces.GPU and are cached for reuse.
|
| 87 |
+
SAM3 = None
|
| 88 |
+
SAM3D = None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _get_sam3():
|
| 92 |
+
global SAM3
|
| 93 |
+
if SAM3 is None:
|
| 94 |
+
import torch
|
| 95 |
+
from transformers import Sam3Model, Sam3Processor
|
| 96 |
+
model = Sam3Model.from_pretrained("facebook/sam3").to("cuda").eval()
|
| 97 |
+
processor = Sam3Processor.from_pretrained("facebook/sam3")
|
| 98 |
+
SAM3 = (model, processor)
|
| 99 |
+
return SAM3
|
| 100 |
+
|
| 101 |
|
| 102 |
def _get_sam3d():
|
| 103 |
global SAM3D
|
| 104 |
if SAM3D is None:
|
| 105 |
+
from sam3d_inference import SAM3DInference
|
| 106 |
SAM3D = SAM3DInference(CONFIG_PATH)
|
| 107 |
return SAM3D
|
| 108 |
|
| 109 |
|
| 110 |
+
def _segment(image_np, prompt):
|
| 111 |
+
"""SAM3 concept segmentation: returns the best-scoring mask for the
|
| 112 |
+
prompt as a bool array, plus the number of instances found."""
|
| 113 |
+
import torch
|
| 114 |
+
model, processor = _get_sam3()
|
| 115 |
+
inputs = processor(images=image_np, text=prompt, return_tensors="pt").to(model.device)
|
| 116 |
+
with torch.no_grad():
|
| 117 |
+
outputs = model(**inputs)
|
| 118 |
+
results = processor.post_process_instance_segmentation(
|
| 119 |
+
outputs, threshold=0.5, mask_threshold=0.5,
|
| 120 |
+
target_sizes=inputs.get("original_sizes").tolist(),
|
| 121 |
+
)[0]
|
| 122 |
+
masks = results["masks"]
|
| 123 |
+
if len(masks) == 0:
|
| 124 |
+
return None, 0
|
| 125 |
+
best = int(torch.as_tensor(results["scores"]).argmax())
|
| 126 |
+
return masks[best].cpu().numpy().astype(bool), len(masks)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
def _export_result(result, out_dir):
|
| 130 |
"""Export the pipeline result: prefer the ready-made GLB mesh, fall back
|
| 131 |
to the raw gaussian splat PLY."""
|
|
|
|
| 153 |
lines = [f"torch={torch.__version__}", f"cuda={torch.cuda.is_available()}"]
|
| 154 |
if torch.cuda.is_available():
|
| 155 |
lines.append(f"gpu={torch.cuda.get_device_name()}")
|
| 156 |
+
lines.append(f"SAM3: {'loaded' if SAM3 is not None else 'loads on first reconstruct'}")
|
| 157 |
+
lines.append(f"SAM3D: {'loaded' if SAM3D is not None else 'loads on first reconstruct'}")
|
| 158 |
lines.append(f"config: {Path(CONFIG_PATH).exists()}")
|
| 159 |
return "\n".join(lines)
|
| 160 |
|
| 161 |
|
| 162 |
@spaces.GPU(duration=180)
|
| 163 |
+
def reconstruct_objects(image: np.ndarray, prompt: str = "object", progress=gr.Progress()):
|
| 164 |
"""
|
| 165 |
+
Segment the object described by the text prompt with SAM3 and
|
| 166 |
+
reconstruct it in 3D with SAM 3D Objects.
|
| 167 |
|
| 168 |
Args:
|
| 169 |
image: Input RGB image
|
| 170 |
+
prompt: Short noun phrase describing what to reconstruct
|
| 171 |
+
(e.g. "the chair"). Default "object" picks the most
|
| 172 |
+
prominent object.
|
| 173 |
|
| 174 |
Returns:
|
| 175 |
tuple: (model_path, preview_image, status)
|
| 176 |
"""
|
| 177 |
if image is None:
|
| 178 |
return None, None, "β No image provided"
|
| 179 |
+
prompt = (prompt or "object").strip()
|
| 180 |
try:
|
| 181 |
import time
|
| 182 |
import torch
|
| 183 |
t0 = time.time()
|
| 184 |
print(f"GPU: {torch.cuda.get_device_name()}")
|
| 185 |
|
| 186 |
+
progress(0.05, desc=f"Segmenting '{prompt}' (SAM3)...")
|
| 187 |
image_np = np.asarray(image)
|
| 188 |
+
best_mask, n_masks = _segment(image_np, prompt)
|
| 189 |
+
if best_mask is None:
|
| 190 |
+
return None, image_np, f"β οΈ No '{prompt}' found in image"
|
|
|
|
|
|
|
| 191 |
|
| 192 |
preview = image_np.copy()
|
| 193 |
preview[best_mask] = (preview[best_mask] * 0.5 + np.array([0, 255, 0]) * 0.5).astype(np.uint8)
|
| 194 |
+
print(f" {n_masks} instances of '{prompt}' ({time.time()-t0:.0f}s)")
|
| 195 |
|
| 196 |
progress(0.25, desc="Reconstructing 3D (SAM 3D Objects, ~1-2 min)...")
|
| 197 |
result = _get_sam3d()(image=image_np, mask=best_mask, seed=42)
|
|
|
|
| 211 |
n_faces = len(trimesh.load(model_path, force="mesh").faces)
|
| 212 |
except Exception:
|
| 213 |
n_faces = 0
|
| 214 |
+
return model_path, preview, f"β {n_masks} Γ '{prompt}' found, {n_faces:,} faces ({int(time.time()-t0)}s)"
|
| 215 |
except Exception:
|
| 216 |
import traceback
|
| 217 |
tb = traceback.format_exc()
|
|
|
|
| 222 |
with gr.Blocks(title="SAM 3D Objects MCP") as demo:
|
| 223 |
gr.Markdown("""
|
| 224 |
# π¦ SAM 3D Objects MCP Server
|
| 225 |
+
**Image (+ text prompt) β 3D Object (GLB)**
|
| 226 |
|
| 227 |
+
SAM3 segments the object you describe, SAM 3D Objects reconstructs it in 3D.
|
| 228 |
""")
|
| 229 |
|
| 230 |
with gr.Tab("Reconstruct"):
|
| 231 |
with gr.Row():
|
| 232 |
with gr.Column():
|
| 233 |
input_image = gr.Image(label="Input Image", type="numpy")
|
| 234 |
+
input_prompt = gr.Textbox(
|
| 235 |
+
label="What to reconstruct", value="object",
|
| 236 |
+
placeholder="e.g. the chair, yellow car, laptop")
|
| 237 |
+
btn = gr.Button("π Segment & Reconstruct", variant="primary", size="lg")
|
| 238 |
with gr.Column():
|
| 239 |
+
preview = gr.Image(label="Segmented Object", type="numpy", interactive=False)
|
| 240 |
status = gr.Textbox(label="Status")
|
| 241 |
with gr.Row():
|
| 242 |
output_model = gr.Model3D(label="3D Preview")
|
| 243 |
output_file = gr.File(label="Download")
|
| 244 |
+
btn.click(reconstruct_objects, inputs=[input_image, input_prompt],
|
| 245 |
+
outputs=[output_model, preview, status])
|
| 246 |
output_model.change(lambda x: x, inputs=[output_model], outputs=[output_file])
|
| 247 |
|
| 248 |
with gr.Tab("Diagnose"):
|
requirements.txt
CHANGED
|
@@ -24,6 +24,9 @@ huggingface_hub
|
|
| 24 |
timm
|
| 25 |
astor
|
| 26 |
|
|
|
|
|
|
|
|
|
|
| 27 |
# 3D geometry / mesh processing
|
| 28 |
trimesh
|
| 29 |
plyfile
|
|
@@ -35,6 +38,3 @@ igraph
|
|
| 35 |
|
| 36 |
# sparse convolution β standalone binary (cumm runtime), independent of torch version
|
| 37 |
spconv-cu124==2.3.8
|
| 38 |
-
|
| 39 |
-
# sam2 runtime dependency
|
| 40 |
-
iopath
|
|
|
|
| 24 |
timm
|
| 25 |
astor
|
| 26 |
|
| 27 |
+
# SAM3 segmentation
|
| 28 |
+
transformers
|
| 29 |
+
|
| 30 |
# 3D geometry / mesh processing
|
| 31 |
trimesh
|
| 32 |
plyfile
|
|
|
|
| 38 |
|
| 39 |
# sparse convolution β standalone binary (cumm runtime), independent of torch version
|
| 40 |
spconv-cu124==2.3.8
|
|
|
|
|
|
|
|
|