Claude Sonnet 4.5 Claude Fable 5 commited on
Commit
f9d98a5
Β·
unverified Β·
1 Parent(s): 1f00166

feat: SAM3 concept segmentation replaces SAM2

Browse files

reconstruct_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>

Files changed (2) hide show
  1. app.py +63 -40
  2. 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
- SAM2 auto-detection + 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,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"SAM2: {'loaded' if SAM2_GENERATOR is not None else 'missing'}")
140
- lines.append(f"SAM3D: {'loaded' if SAM3D is not None else 'lazy (loads on first reconstruct)'}")
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
- Automatically detect the largest object with SAM2 and reconstruct it
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="Detecting objects (SAM2)...")
166
  image_np = np.asarray(image)
167
- masks = SAM2_GENERATOR.generate(image_np)
168
- if not masks:
169
- return None, image_np, "⚠️ No objects detected"
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" {len(masks)} masks ({time.time()-t0:.0f}s)")
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"βœ“ {len(masks)} objects detected, {n_faces:,} faces ({int(time.time()-t0)}s)"
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
- Automatically detects objects and reconstructs the largest one in 3D.
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
- btn = gr.Button("πŸš€ Detect & Reconstruct", variant="primary", size="lg")
 
 
 
216
  with gr.Column():
217
- preview = gr.Image(label="Detected Object", type="numpy", interactive=False)
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], outputs=[output_model, preview, status])
 
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