File size: 9,184 Bytes
e7334c8 0f11cd9 95ca774 af8b4a0 95ca774 d81f6c9 7afaf9e e7334c8 0f11cd9 e7334c8 af8b4a0 e7334c8 7afaf9e 1d8163a af8b4a0 4330d70 af8b4a0 4330d70 af8b4a0 f8e7037 33aa4bb f8e7037 33aa4bb af8b4a0 f8e7037 33aa4bb f8e7037 33aa4bb af8b4a0 f8e7037 af8b4a0 579e65b af8b4a0 a07c563 579e65b aaa1b00 4330d70 e7334c8 4330d70 e7334c8 f8e7037 95ca774 f8e7037 95ca774 f8e7037 a07c563 aaa1b00 579e65b e7334c8 1d8163a e7334c8 16a4c7b 33aa4bb af8b4a0 354d431 33aa4bb e7334c8 33aa4bb e7334c8 05f7921 e7334c8 f8e7037 59822ae f8e7037 59822ae f8e7037 aaa1b00 579e65b aaa1b00 579e65b aaa1b00 f8e7037 05f7921 f8e7037 af8b4a0 0f11cd9 e7334c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | import gradio as gr
import spaces, torch, os, json
from PIL import Image
from typing import Union
import numpy as np
from samv2_handler import (
load_sam_image_model,
run_sam_im_inference,
load_sam_video_model,
run_sam_video_inference,
logger,
)
from toolbox.mask_encoding import b64_mask_decode
torch.autocast(device_type="cuda", dtype=torch.bfloat16).__enter__()
if torch.cuda.get_device_properties(0).major >= 8:
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
@spaces.GPU
def load_im_model(variant):
return load_sam_image_model(variant=variant, device="cuda")
@spaces.GPU
def load_vid_model(variant):
return load_sam_video_model(variant=variant, device="cuda")
@spaces.GPU
@torch.inference_mode()
@torch.autocast(device_type="cuda", dtype=torch.bfloat16)
def process_image(
im: Image.Image,
variant: str,
bboxes: Union[list, str] = None,
points: Union[list, str] = None,
point_labels: Union[list, str] = None,
) -> list[str]:
"""SAM2 image segmentation: segment objects in an image from box and/or point prompts and return their masks.
Provide prompts as bounding boxes and/or points (points require point_labels). Returns a JSON list of segmentation masks, one mask per prompt -- one mask per bounding box, or a single mask for a points-only prompt -- in the same order as the input prompts. Each mask is a base64-encoded 1-bit (binary) PNG string; when decoded the PNG has the same width and height as the original input image, with pixel value 1 inside the object and 0 elsewhere.
Args:
im: The RGB image to segment, as a Pillow Image.
variant: SAM2 model variant to load; one of "tiny", "small", "base_plus", or "large".
bboxes: Object bounding boxes to segment, as a JSON list of dicts [{"x0":..., "y0":..., "x1":..., "y1":...}, ...] in ABSOLUTE PIXEL coordinates of the input image (top-left corner x0/y0, bottom-right corner x1/y1) -- the albumentations "pascal_voc" format; either bboxes or points must be provided.
points: Object points to segment, as a JSON list of dicts [{"x":..., "y":...}, ...] in ABSOLUTE PIXEL coordinates of the input image; requires point_labels.
point_labels: JSON list of integers, one per point, where 1 marks a foreground (include) point and 0 a background (exclude) point.
"""
# input validation
has_bboxes = type(bboxes) != type(None) and bboxes != ""
has_points = type(points) != type(None) and points != ""
has_point_labels = type(point_labels) != type(None) and point_labels != ""
assert has_bboxes or has_points, f"either bboxes or points must be provided."
if has_points:
assert has_point_labels, f"point_labels is required if points are provided."
bboxes = json.loads(bboxes) if isinstance(bboxes, str) and has_bboxes else bboxes
points = json.loads(points) if isinstance(points, str) and has_points else points
point_labels = (
json.loads(point_labels)
if isinstance(point_labels, str) and has_point_labels
else point_labels
)
if has_points:
assert len(points) == len(
point_labels
), f"{len(points)} points provided but there are {len(point_labels)} labels."
model = load_im_model(variant=variant)
return run_sam_im_inference(
model,
image=im,
bboxes=bboxes,
points=points,
point_labels=point_labels,
b64_encode_mask=True,
)
@spaces.GPU(
duration=120
) # user must have 2-minute of inference time left at the time of calling
@torch.inference_mode()
@torch.autocast(device_type="cuda", dtype=torch.bfloat16)
def process_video(
video_path: str,
variant: str,
masks: Union[list, str],
drop_masks: bool = False,
ref_frame_idx: int = 0,
async_frame_load: bool = True,
) -> list[dict]:
"""SAM2 video segmentation: track objects through a video given their segmentation masks on a reference frame.
Seed the objects to track by supplying their masks on the reference frame (ref_frame_idx); SAM2 then propagates each object forward -- and, when ref_frame_idx is nonzero, also backward -- through every frame. Returns a JSON list of per-frame detections, one entry per tracked object per frame in which it appears (frames where an object is absent are skipped). Each detection is a dict with: "frame" (integer frame index); "track_id" (integer object id, stable across frames); "x", "y", "w", "h" (the object's bounding box as top-left-x, top-left-y, width, height, each NORMALIZED to 0.0-1.0 by dividing by the frame width or height -- this is the albumentations "coco" layout [x_min, y_min, width, height] but normalized to 0-1 rather than absolute pixels, and it is NOT [x_min, y_min, x_max, y_max]; box formats documented at https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats ); "conf" (always 1); and, unless drop_masks is true, "mask_b64" (a base64-encoded 1-bit PNG string the same width and height as the video frame, 1 inside the object and 0 elsewhere).
Args:
video_path: Filesystem path to the input video file.
variant: SAM2 model variant to load; one of "tiny", "small", "base_plus", or "large".
masks: JSON list of base64-encoded 1-bit PNG masks for the reference frame, one per object to track, e.g. ["b'iVBORw0KGgo...'", ...]; the b'...' literal wrapper is accepted and stripped.
drop_masks: When true, omit the "mask_b64" field from every detection so only bounding-box information is returned.
ref_frame_idx: Frame index the provided masks correspond to; a nonzero value triggers bidirectional tracking (forward and backward from this frame).
async_frame_load: When true, load video frames in parallel with propagation to reduce inference time.
"""
model = load_vid_model(variant=variant)
masks = json.loads(masks) if isinstance(masks, str) else masks
logger.debug(f"masks---\n{masks}")
masks = [
m[2:-1].encode() if m.startswith("b'") and m.endswith("'") else m for m in masks
] # expect the b'' literal to be included
masks = np.array([b64_mask_decode(m).astype(np.uint8) for m in masks])
logger.debug(f"masks---\n{masks}")
return run_sam_video_inference(
model,
video_path=video_path,
masks=masks,
device="cuda",
do_tidy_up=True,
drop_mask=drop_masks,
async_frame_load=async_frame_load,
ref_frame_idx=ref_frame_idx,
)
with gr.Blocks() as demo:
with gr.Tab("Images"):
gr.Interface(
fn=process_image,
inputs=[
gr.Image(label="Input Image", type="pil"),
gr.Dropdown(
label="Model Variant",
choices=["tiny", "small", "base_plus", "large"],
),
gr.Textbox(
label="Bounding Boxes",
value=None,
lines=5,
placeholder='JSON list of dicts: [{"x0":..., "y0":..., "x1":..., "y1":...}, ...]',
),
gr.Textbox(
label="Points",
lines=3,
placeholder='JSON list of dicts: [{"x":..., "y":...}, ...]',
),
gr.Textbox(label="Points' Labels", placeholder="JSON List of Integars"),
],
outputs=gr.JSON(label="Output JSON"),
title="SAM2 for Images",
api_name="process_image",
)
with gr.Tab("Videos"):
gr.Interface(
fn=process_video,
inputs=[
gr.Video(label="Input Video"),
gr.Dropdown(
label="Model Variant",
choices=["tiny", "small", "base_plus", "large"],
),
gr.Textbox(
label="Masks for Objects of Interest in the First Frame",
value=None,
lines=5,
placeholder="""
JSON list of base64 encoded masks, e.g.: ["b'iVBORw0KGgoAAAANSUhEUgAABDgAAAeAAQAAAAADGtqnAAAXz...'",...]
""",
),
gr.Checkbox(
label="Drop Masks",
info="remove base64 encoded masks from result JSON",
value=True,
),
gr.Number(
label="Reference Frame Index",
info="frame index for the provided object masks",
value=0,
precision=0,
),
gr.Checkbox(
label="async frame load",
info="start inference in parallel to frame loading",
),
],
outputs=gr.JSON(label="Output JSON"),
title="SAM2 for Videos",
api_name="process_video",
)
# Model weights are auto-downloaded from the HuggingFace Hub on first use.
demo.launch(
mcp_server=True, app_kwargs={"docs_url": "/docs"} # add FastAPI Swagger API Docs
)
|