Snake4y5h commited on
Commit
1a73df3
Β·
verified Β·
1 Parent(s): 029df1a

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +49 -7
  2. __pycache__/app.cpython-314.pyc +0 -0
  3. app.py +191 -0
  4. requirements.txt +6 -0
README.md CHANGED
@@ -1,13 +1,55 @@
1
  ---
2
- title: Segment Everything Api
3
- emoji: πŸ“Š
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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