Sempy32 commited on
Commit
a0ec681
·
verified ·
1 Parent(s): 1c31152

Fix ZeroGPU SAM3 runtime and request diagnostics

Browse files
Files changed (2) hide show
  1. README.md +4 -6
  2. app.py +120 -47
README.md CHANGED
@@ -1,14 +1,12 @@
1
  ---
2
  title: SAM3 Panoptic
3
- emoji: 🎯
4
- colorFrom: indigo
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.6.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: SAM 3 open-vocabulary panoptic concept segmentation API
11
  ---
12
 
13
- `api_panoptic(image, concepts, conf)` -> JSON detections with base64-PNG masks.
14
- Requires the `HF_TOKEN` Space secret to have access to gated `facebook/sam3`.
 
 
1
  ---
2
  title: SAM3 Panoptic
 
 
 
3
  sdk: gradio
4
  sdk_version: 6.6.0
5
  app_file: app.py
6
  pinned: false
7
+ short_description: SAM3 open-vocabulary panoptic concept segmentation API
8
  ---
9
 
10
+ `api_panoptic(image, concepts, confidence, mask_threshold)` returns JSON
11
+ detections with base64-encoded PNG masks. The Space requires an `HF_TOKEN`
12
+ secret with access to `facebook/sam3`.
app.py CHANGED
@@ -1,7 +1,11 @@
1
- """SAM 3 panoptic concept-segmentation API (ZeroGPU). Self-contained."""
 
 
 
2
  import base64
3
  import io
4
  import os
 
5
 
6
  import gradio as gr
7
  import numpy as np
@@ -10,69 +14,138 @@ import torch
10
  from PIL import Image
11
  from transformers import Sam3Model, Sam3Processor
12
 
 
13
  HF_TOKEN = os.environ.get("HF_TOKEN")
14
  MODEL_ID = "facebook/sam3"
 
15
 
16
- # Built at import on CPU; moved to CUDA inside the @spaces.GPU function.
17
  processor = Sam3Processor.from_pretrained(MODEL_ID, token=HF_TOKEN)
18
- model = Sam3Model.from_pretrained(MODEL_ID, token=HF_TOKEN)
19
  model.eval()
20
 
21
 
22
  def _encode_mask(mask_bool: np.ndarray) -> str:
23
- arr = (mask_bool.astype(np.uint8)) * 255
24
- buf = io.BytesIO()
25
- Image.fromarray(arr, mode="L").save(buf, format="PNG")
26
- return base64.b64encode(buf.getvalue()).decode("ascii")
 
 
27
 
 
 
 
 
 
 
28
 
29
- @spaces.GPU(duration=120)
 
30
  def api_panoptic(image, concepts, conf, mask_threshold=0.5):
31
  if image is None:
32
- return {"error": "no image provided"}
33
  image = image.convert("RGB")
34
- W, H = image.size
35
- concept_list = [c.strip() for c in (concepts or "").split(",") if c.strip()]
36
- device = "cuda"
37
- model.to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  detections = []
39
- for concept in concept_list:
40
- inputs = processor(images=image, text=concept, return_tensors="pt").to(device)
41
- with torch.no_grad():
42
- outputs = model(**inputs)
43
- target_sizes = (inputs["original_sizes"].tolist()
44
- if "original_sizes" in inputs else [[H, W]])
45
- res = processor.post_process_instance_segmentation(
46
- outputs, threshold=float(conf), mask_threshold=float(mask_threshold),
47
- target_sizes=target_sizes)[0]
48
- # NOTE (verify on live Space): expected keys masks/scores/boxes.
49
- masks, scores = res["masks"], res["scores"]
50
- boxes = res.get("boxes")
51
- for i in range(len(scores)):
52
- m = masks[i]
53
- m = m.cpu().numpy() if hasattr(m, "cpu") else np.asarray(m)
54
- mb = m > 0.5 if m.dtype != bool else m
55
- box = (boxes[i].cpu().numpy().tolist()
56
- if boxes is not None else [0, 0, 0, 0])
57
- detections.append({
58
- "label": concept, "score": float(scores[i]),
59
- "box": box, "mask_png_b64": _encode_mask(mb.astype(bool)),
60
- })
61
- return {"version": "3", "model": MODEL_ID, "width": W, "height": H,
62
- "detections": detections}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
 
65
  with gr.Blocks(title="SAM3 Panoptic") as demo:
66
- gr.Markdown("# SAM 3 Panoptic API\nUpload an image, enter comma-separated concepts.")
67
  with gr.Row():
68
- inp = gr.Image(type="pil", label="Image")
69
- out = gr.JSON(label="Detections")
70
- txt = gr.Textbox(label="Concepts (comma-separated)",
71
- value="person, car, road, sky, building, tree")
72
- conf = gr.Slider(0.0, 1.0, value=0.4, step=0.05, label="Confidence (lower = more detail)")
73
- mthr = gr.Slider(0.05, 0.95, value=0.5, step=0.05, label="Mask threshold")
74
- gr.Button("Segment").click(api_panoptic, [inp, txt, conf, mthr], out,
75
- api_name="api_panoptic")
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  if __name__ == "__main__":
78
- demo.queue().launch()
 
1
+ """SAM3 open-vocabulary panoptic segmentation API for Hugging Face Spaces."""
2
+
3
+ from __future__ import annotations
4
+
5
  import base64
6
  import io
7
  import os
8
+ import traceback
9
 
10
  import gradio as gr
11
  import numpy as np
 
14
  from PIL import Image
15
  from transformers import Sam3Model, Sam3Processor
16
 
17
+
18
  HF_TOKEN = os.environ.get("HF_TOKEN")
19
  MODEL_ID = "facebook/sam3"
20
+ DEVICE = "cuda"
21
 
22
+ # ZeroGPU emulates CUDA during startup and swaps in a real GPU for @spaces.GPU.
23
  processor = Sam3Processor.from_pretrained(MODEL_ID, token=HF_TOKEN)
24
+ model = Sam3Model.from_pretrained(MODEL_ID, token=HF_TOKEN).to(DEVICE)
25
  model.eval()
26
 
27
 
28
  def _encode_mask(mask_bool: np.ndarray) -> str:
29
+ buffer = io.BytesIO()
30
+ Image.fromarray(mask_bool.astype(np.uint8) * 255, mode="L").save(
31
+ buffer, format="PNG"
32
+ )
33
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
34
+
35
 
36
+ def _gpu_duration(image, concepts, conf, mask_threshold=0.5) -> int:
37
+ del image, conf, mask_threshold
38
+ concept_count = len(
39
+ [value for value in str(concepts or "").split(",") if value.strip()]
40
+ )
41
+ return min(120, max(30, concept_count * 20))
42
 
43
+
44
+ @spaces.GPU(duration=_gpu_duration)
45
  def api_panoptic(image, concepts, conf, mask_threshold=0.5):
46
  if image is None:
47
+ return {"error": "no image provided", "detections": []}
48
  image = image.convert("RGB")
49
+ width, height = image.size
50
+ concept_list = [
51
+ value.strip() for value in str(concepts or "").split(",") if value.strip()
52
+ ]
53
+ if not concept_list:
54
+ return {
55
+ "version": "4",
56
+ "model": MODEL_ID,
57
+ "width": width,
58
+ "height": height,
59
+ "detections": [],
60
+ }
61
+
62
+ print(
63
+ f"SAM3 request: size={width}x{height} concepts={len(concept_list)}",
64
+ flush=True,
65
+ )
66
  detections = []
67
+ try:
68
+ for concept in concept_list:
69
+ inputs = processor(
70
+ images=image, text=concept, return_tensors="pt"
71
+ ).to(DEVICE)
72
+ with torch.inference_mode():
73
+ outputs = model(**inputs)
74
+ target_sizes = (
75
+ inputs["original_sizes"].tolist()
76
+ if "original_sizes" in inputs
77
+ else [[height, width]]
78
+ )
79
+ result = processor.post_process_instance_segmentation(
80
+ outputs,
81
+ threshold=float(conf),
82
+ mask_threshold=float(mask_threshold),
83
+ target_sizes=target_sizes,
84
+ )[0]
85
+ masks = result["masks"]
86
+ scores = result["scores"]
87
+ boxes = result.get("boxes")
88
+ for index in range(len(scores)):
89
+ mask = masks[index]
90
+ mask_array = (
91
+ mask.detach().cpu().numpy()
92
+ if hasattr(mask, "detach")
93
+ else np.asarray(mask)
94
+ )
95
+ mask_bool = (
96
+ mask_array
97
+ if mask_array.dtype == bool
98
+ else mask_array > 0.5
99
+ )
100
+ box = (
101
+ boxes[index].detach().cpu().numpy().tolist()
102
+ if boxes is not None
103
+ else [0, 0, 0, 0]
104
+ )
105
+ detections.append(
106
+ {
107
+ "label": concept,
108
+ "score": float(scores[index]),
109
+ "box": box,
110
+ "mask_png_b64": _encode_mask(mask_bool),
111
+ }
112
+ )
113
+ except Exception:
114
+ traceback.print_exc()
115
+ raise
116
+
117
+ print(f"SAM3 response: detections={len(detections)}", flush=True)
118
+ return {
119
+ "version": "4",
120
+ "model": MODEL_ID,
121
+ "width": width,
122
+ "height": height,
123
+ "detections": detections,
124
+ }
125
 
126
 
127
  with gr.Blocks(title="SAM3 Panoptic") as demo:
128
+ gr.Markdown("# SAM3 Panoptic API")
129
  with gr.Row():
130
+ input_image = gr.Image(type="pil", label="Image")
131
+ output_json = gr.JSON(label="Detections")
132
+ concept_text = gr.Textbox(
133
+ label="Concepts",
134
+ value="person, car, road, building, tree",
135
+ )
136
+ confidence = gr.Slider(
137
+ 0.0, 1.0, value=0.4, step=0.05, label="Confidence"
138
+ )
139
+ mask_threshold = gr.Slider(
140
+ 0.05, 0.95, value=0.5, step=0.05, label="Mask threshold"
141
+ )
142
+ gr.Button("Segment").click(
143
+ api_panoptic,
144
+ [input_image, concept_text, confidence, mask_threshold],
145
+ output_json,
146
+ api_name="api_panoptic",
147
+ )
148
+
149
 
150
  if __name__ == "__main__":
151
+ demo.queue(default_concurrency_limit=1).launch()