Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +49 -7
- __pycache__/app.cpython-314.pyc +0 -0
- app.py +191 -0
- requirements.txt +6 -0
README.md
CHANGED
|
@@ -1,13 +1,55 @@
|
|
| 1 |
---
|
| 2 |
-
title: Segment Everything
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Segment Everything API
|
| 3 |
+
emoji: π§©
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
python_version: "3.12"
|
| 10 |
+
short_description: SAM 2.1 masks as a decodable label-map PNG
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Segment Everything API
|
| 14 |
+
|
| 15 |
+
SAM 2.1 automatic mask generation, returned as **data instead of a picture**.
|
| 16 |
+
|
| 17 |
+
Most segmentation demos hand back a colour-blended overlay, which cannot be
|
| 18 |
+
decoded into masks again. This Space returns:
|
| 19 |
+
|
| 20 |
+
| Output | What it is |
|
| 21 |
+
|---|---|
|
| 22 |
+
| `labels.png` | Lossless, untagged PNG. Each pixel carries its segment id as `R + G*256`; `0` means unsegmented. |
|
| 23 |
+
| `manifest` | `{width, height, count, model, regions: [{id, area, bbox}]}` |
|
| 24 |
+
| `preview` | A tinted image for eyeballing only β never parse it. |
|
| 25 |
+
|
| 26 |
+
A client decodes `labels.png` once into a per-pixel label array and then does
|
| 27 |
+
hit-testing, hover highlighting and selection entirely locally: **one call per
|
| 28 |
+
image, no round trip per interaction**.
|
| 29 |
+
|
| 30 |
+
## API
|
| 31 |
+
|
| 32 |
+
```python
|
| 33 |
+
from gradio_client import Client, handle_file
|
| 34 |
+
|
| 35 |
+
client = Client("Snake4y5h/segment-everything-api")
|
| 36 |
+
labels, manifest, preview = client.predict(
|
| 37 |
+
image=handle_file("art.png"),
|
| 38 |
+
points_per_crop=32, # 8 coarse β¦ 64 fine
|
| 39 |
+
min_area_frac=0.0002, # drop specks
|
| 40 |
+
pred_iou_thresh=0.88,
|
| 41 |
+
stability_score_thresh=0.95,
|
| 42 |
+
api_name="/segment",
|
| 43 |
+
)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Decoding the label map (JavaScript):
|
| 47 |
+
|
| 48 |
+
```js
|
| 49 |
+
const bmp = await createImageBitmap(pngBlob, { colorSpaceConversion: 'none' });
|
| 50 |
+
// draw to a canvas, then per pixel: id = data[p] + (data[p + 1] << 8)
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Note `colorSpaceConversion: 'none'` β colour management would corrupt the ids.
|
| 54 |
+
|
| 55 |
+
Long edge is capped at 2048px; the manifest reports the size actually segmented.
|
__pycache__/app.cpython-314.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
segment-everything-api β one call in, every mask out.
|
| 3 |
+
|
| 4 |
+
SAM 2.1 automatic mask generation, returned as a **label map** rather than a
|
| 5 |
+
picture of one. The reply is a lossless PNG where each segment is a distinct
|
| 6 |
+
index encoded in the red and green channels (`id = R + G*256`, 0 = unsegmented),
|
| 7 |
+
plus a JSON manifest of each segment's area and bounding box.
|
| 8 |
+
|
| 9 |
+
That shape is the point: a client can decode the PNG straight into a per-pixel
|
| 10 |
+
label array and do hit-testing, hover highlighting and selection locally, with no
|
| 11 |
+
further round trips. Overlay screenshots β what most segmentation demos return β
|
| 12 |
+
can't be decoded back into masks, so they're offered only as a human preview.
|
| 13 |
+
|
| 14 |
+
API: /segment(image, points_per_crop, min_area_frac, pred_iou_thresh,
|
| 15 |
+
stability_score_thresh) -> (labels.png, manifest, preview)
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
import tempfile
|
| 20 |
+
|
| 21 |
+
import spaces # must precede torch β it patches torch.cuda before CUDA init
|
| 22 |
+
import gradio as gr
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
from PIL import Image
|
| 26 |
+
from scipy import ndimage
|
| 27 |
+
from transformers import pipeline
|
| 28 |
+
|
| 29 |
+
MODEL_ID = os.environ.get("SAM_MODEL", "facebook/sam2.1-hiera-large")
|
| 30 |
+
MAX_EDGE = 2048 # bounds VRAM and the returned payload
|
| 31 |
+
MAX_SEGMENTS = 65535 # what the two-channel label encoding holds
|
| 32 |
+
|
| 33 |
+
generator = pipeline("mask-generation", model=MODEL_ID, device=0)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _fit(image: Image.Image) -> Image.Image:
|
| 37 |
+
"""Downscale so the long edge is at most MAX_EDGE; leave smaller art alone."""
|
| 38 |
+
w, h = image.size
|
| 39 |
+
scale = MAX_EDGE / max(w, h)
|
| 40 |
+
if scale >= 1:
|
| 41 |
+
return image
|
| 42 |
+
return image.resize((max(1, round(w * scale)), max(1, round(h * scale))), Image.LANCZOS)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _paint_labels(masks, height: int, width: int, min_area: int):
|
| 46 |
+
"""
|
| 47 |
+
Flatten possibly-overlapping masks into one label map.
|
| 48 |
+
|
| 49 |
+
Painted largest-first, so where masks overlap the SMALLER one wins β a big
|
| 50 |
+
background mask never buries the little parts a user actually wants to click.
|
| 51 |
+
Areas and boxes are measured on the finished map, not on the input masks, so
|
| 52 |
+
they describe what's really there after the overwrites.
|
| 53 |
+
"""
|
| 54 |
+
order = sorted(range(len(masks)), key=lambda i: -int(masks[i].sum()))
|
| 55 |
+
raw = np.zeros((height, width), dtype=np.int32)
|
| 56 |
+
for slot, i in enumerate(order[:MAX_SEGMENTS], start=1):
|
| 57 |
+
raw[np.asarray(masks[i], dtype=bool)] = slot
|
| 58 |
+
|
| 59 |
+
# Drop whatever ended up too small (or fully covered), then renumber densely.
|
| 60 |
+
areas = np.bincount(raw.ravel(), minlength=int(raw.max()) + 1)
|
| 61 |
+
keep = [i for i in range(1, len(areas)) if areas[i] >= min_area]
|
| 62 |
+
remap = np.zeros(len(areas), dtype=np.int32)
|
| 63 |
+
for new_id, old_id in enumerate(keep, start=1):
|
| 64 |
+
remap[old_id] = new_id
|
| 65 |
+
labels = remap[raw]
|
| 66 |
+
|
| 67 |
+
boxes = ndimage.find_objects(labels)
|
| 68 |
+
final_areas = np.bincount(labels.ravel(), minlength=len(keep) + 1)
|
| 69 |
+
regions = []
|
| 70 |
+
for new_id in range(1, len(keep) + 1):
|
| 71 |
+
box = boxes[new_id - 1]
|
| 72 |
+
if box is None:
|
| 73 |
+
continue
|
| 74 |
+
ys, xs = box
|
| 75 |
+
regions.append(
|
| 76 |
+
{
|
| 77 |
+
"id": new_id,
|
| 78 |
+
"area": int(final_areas[new_id]),
|
| 79 |
+
"bbox": [int(xs.start), int(ys.start), int(xs.stop), int(ys.stop)],
|
| 80 |
+
}
|
| 81 |
+
)
|
| 82 |
+
return labels, regions
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _encode(labels: np.ndarray) -> str:
|
| 86 |
+
"""Label ids into R + G*256 of a lossless, untagged PNG."""
|
| 87 |
+
height, width = labels.shape
|
| 88 |
+
rgb = np.zeros((height, width, 3), dtype=np.uint8)
|
| 89 |
+
rgb[..., 0] = (labels & 0xFF).astype(np.uint8)
|
| 90 |
+
rgb[..., 1] = ((labels >> 8) & 0xFF).astype(np.uint8)
|
| 91 |
+
path = os.path.join(tempfile.mkdtemp(), "labels.png")
|
| 92 |
+
Image.fromarray(rgb, mode="RGB").save(path, format="PNG", optimize=True)
|
| 93 |
+
return path
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _preview(image: np.ndarray, labels: np.ndarray) -> np.ndarray:
|
| 97 |
+
"""A human-readable tint of the label map. Never parse this β use the PNG."""
|
| 98 |
+
rng = np.random.default_rng(7)
|
| 99 |
+
palette = rng.integers(40, 235, size=(int(labels.max()) + 1, 3), dtype=np.uint8)
|
| 100 |
+
palette[0] = (0, 0, 0)
|
| 101 |
+
tint = palette[labels]
|
| 102 |
+
return ((image.astype(np.uint16) + tint.astype(np.uint16) * 2) // 3).astype(np.uint8)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@spaces.GPU(duration=120)
|
| 106 |
+
def _run(pixels: np.ndarray, points_per_crop: int, pred_iou_thresh: float, stability: float):
|
| 107 |
+
return generator(
|
| 108 |
+
Image.fromarray(pixels),
|
| 109 |
+
points_per_crop=int(points_per_crop),
|
| 110 |
+
points_per_batch=64,
|
| 111 |
+
pred_iou_thresh=float(pred_iou_thresh),
|
| 112 |
+
stability_score_thresh=float(stability),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def segment(
|
| 117 |
+
image: Image.Image,
|
| 118 |
+
points_per_crop: int = 32,
|
| 119 |
+
min_area_frac: float = 0.0002,
|
| 120 |
+
pred_iou_thresh: float = 0.88,
|
| 121 |
+
stability_score_thresh: float = 0.95,
|
| 122 |
+
):
|
| 123 |
+
"""
|
| 124 |
+
Segment every object in an image and return the result as a label map.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
image: The image to segment.
|
| 128 |
+
points_per_crop: Grid density of the automatic prompt sampler β higher
|
| 129 |
+
finds smaller parts and costs more time (16 coarse β¦ 64 fine).
|
| 130 |
+
min_area_frac: Drop segments smaller than this fraction of the frame.
|
| 131 |
+
pred_iou_thresh: Minimum predicted mask quality to keep a segment.
|
| 132 |
+
stability_score_thresh: Minimum mask stability to keep a segment.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
labels.png β a lossless PNG whose pixels carry the segment id as
|
| 136 |
+
`R + G*256`, with 0 meaning unsegmented;
|
| 137 |
+
manifest β {width, height, count, model, regions:[{id, area, bbox}]};
|
| 138 |
+
preview β a tinted image for eyeballing only, not for parsing.
|
| 139 |
+
"""
|
| 140 |
+
if image is None:
|
| 141 |
+
raise gr.Error("No image supplied.")
|
| 142 |
+
image = _fit(image.convert("RGB"))
|
| 143 |
+
pixels = np.asarray(image)
|
| 144 |
+
height, width = pixels.shape[:2]
|
| 145 |
+
|
| 146 |
+
outputs = _run(pixels, points_per_crop, pred_iou_thresh, stability_score_thresh)
|
| 147 |
+
masks = outputs.get("masks", []) if isinstance(outputs, dict) else []
|
| 148 |
+
min_area = max(1, int(min_area_frac * width * height))
|
| 149 |
+
labels, regions = _paint_labels(masks, height, width, min_area)
|
| 150 |
+
|
| 151 |
+
manifest = {
|
| 152 |
+
"width": width,
|
| 153 |
+
"height": height,
|
| 154 |
+
"count": len(regions),
|
| 155 |
+
"model": MODEL_ID,
|
| 156 |
+
"regions": regions,
|
| 157 |
+
}
|
| 158 |
+
return _encode(labels), manifest, _preview(pixels, labels)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
with gr.Blocks(title="Segment Everything API") as demo:
|
| 162 |
+
gr.Markdown(
|
| 163 |
+
"# Segment Everything API\n"
|
| 164 |
+
"SAM 2.1 automatic mask generation returned as a **label map PNG** "
|
| 165 |
+
"(`id = R + G*256`, 0 = unsegmented) plus a JSON manifest β so a client can "
|
| 166 |
+
"decode the masks and do its own hit-testing offline. Built for API use; "
|
| 167 |
+
"the preview is only for eyeballing."
|
| 168 |
+
)
|
| 169 |
+
with gr.Row():
|
| 170 |
+
with gr.Column():
|
| 171 |
+
image_in = gr.Image(label="Image", type="pil")
|
| 172 |
+
points = gr.Slider(8, 64, value=32, step=8, label="Points per crop (detail)")
|
| 173 |
+
min_area = gr.Slider(
|
| 174 |
+
0.0, 0.01, value=0.0002, step=0.0001, label="Min segment area (fraction)"
|
| 175 |
+
)
|
| 176 |
+
iou = gr.Slider(0.5, 0.99, value=0.88, step=0.01, label="Predicted IoU threshold")
|
| 177 |
+
stability = gr.Slider(0.5, 0.99, value=0.95, step=0.01, label="Stability threshold")
|
| 178 |
+
run = gr.Button("Segment", variant="primary")
|
| 179 |
+
with gr.Column():
|
| 180 |
+
preview_out = gr.Image(label="Preview (not machine-readable)")
|
| 181 |
+
labels_out = gr.File(label="labels.png")
|
| 182 |
+
manifest_out = gr.JSON(label="manifest")
|
| 183 |
+
|
| 184 |
+
run.click(
|
| 185 |
+
fn=segment,
|
| 186 |
+
inputs=[image_in, points, min_area, iou, stability],
|
| 187 |
+
outputs=[labels_out, manifest_out, preview_out],
|
| 188 |
+
api_name="segment",
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
demo.launch(mcp_server=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers==5.15.1
|
| 2 |
+
torchvision
|
| 3 |
+
accelerate
|
| 4 |
+
scipy
|
| 5 |
+
pillow
|
| 6 |
+
numpy
|