Spaces:
Running on Zero
Running on Zero
| """Florence-2 scene tagger (ZeroGPU): proposes class names from an image. | |
| The first implementation used only the Florence ``<OD>`` task, which is too | |
| sparse for street-level panoptic work because object detection misses broad | |
| surface/stuff classes. This endpoint now combines object detection, dense | |
| region captions, and detailed caption text into one open-vocabulary concept | |
| list for SAM3. | |
| """ | |
| import os | |
| from collections import Counter | |
| import re | |
| import gradio as gr | |
| import spaces | |
| import torch | |
| from transformers import AutoProcessor, Florence2ForConditionalGeneration | |
| MODEL_ID = "florence-community/Florence-2-large" | |
| # Built at import on CPU; moved to CUDA inside the @spaces.GPU function. | |
| processor = AutoProcessor.from_pretrained(MODEL_ID) | |
| model = Florence2ForConditionalGeneration.from_pretrained(MODEL_ID) | |
| model.eval() | |
| TASKS = ("<OD>", "<DENSE_REGION_CAPTION>", "<MORE_DETAILED_CAPTION>") | |
| DROP_WORDS = { | |
| "a", "an", "the", "this", "that", "these", "those", "there", "here", | |
| "image", "photo", "picture", "view", "scene", "background", "foreground", | |
| "left", "right", "top", "bottom", "front", "back", "side", "area", "part", | |
| "visible", "large", "small", "several", "multiple", "many", "some", | |
| "is", "are", "was", "were", "be", "being", "been", "appears", "appear", | |
| "of", "in", "it", "its", "to", "for", "by", "as", "at", "from", "into", | |
| "parked", "parking", "surrounded", "including", "taken", "shining", | |
| "brightly", "different", "models", "colors", "color", "angle", "high", | |
| "panoramic", "modern", "curved", "few", "blue", "red", "white", | |
| } | |
| KEEP_PHRASES = { | |
| "parking lot", | |
| } | |
| DROP_LABELS = { | |
| "roof", "floor", "floors", "balcony", "balconies", "sun", "cloud", "clouds", | |
| "sky", "brightly", "angle", "high angle", | |
| } | |
| CAPTION_SPLIT = re.compile( | |
| r"[,.;:]|\\bwith\\b|\\band\\b|\\bnext to\\b|\\bin front of\\b|\\bbehind\\b|\\bon\\b|\\balong\\b|\\bnear\\b|\\bsurrounded by\\b", | |
| flags=re.IGNORECASE, | |
| ) | |
| def _clean_label(value: str) -> str: | |
| text = str(value).strip().lower() | |
| text = re.sub(r"[_/\\-]+", " ", text) | |
| text = re.sub(r"[^a-z0-9\\s]+", " ", text) | |
| for phrase in KEEP_PHRASES: | |
| if phrase in text: | |
| return phrase | |
| words = [w for w in text.split() if w and w not in DROP_WORDS] | |
| if not words: | |
| return "" | |
| if len(words) == 1: | |
| word = words[0] | |
| if len(word) > 3 and word.endswith("ies"): | |
| word = word[:-3] + "y" | |
| elif len(word) > 3 and word.endswith("s") and not word.endswith("ss"): | |
| word = word[:-1] | |
| words = [word] | |
| if len(words) > 5: | |
| words = words[-5:] | |
| label = " ".join(words) | |
| return "" if label in DROP_LABELS else label | |
| def _labels_from_caption(text: str) -> list[str]: | |
| labels: list[str] = [] | |
| for chunk in CAPTION_SPLIT.split(str(text)): | |
| clean = _clean_label(chunk) | |
| if not clean: | |
| continue | |
| words = clean.split() | |
| candidates = [clean] | |
| if len(words) >= 3: | |
| candidates.append(" ".join(words[-2:])) | |
| candidates.append(words[-1]) | |
| for candidate in candidates: | |
| candidate = _clean_label(candidate) | |
| if candidate and len(candidate) > 2 and candidate not in labels: | |
| labels.append(candidate) | |
| break | |
| return labels | |
| def _extract_labels(parsed) -> list[str]: | |
| labels: list[str] = [] | |
| def walk(obj, key_hint: str = ""): | |
| if isinstance(obj, dict): | |
| for key, value in obj.items(): | |
| k = str(key).lower() | |
| if k in {"label", "labels", "caption", "captions", "text", "description", "descriptions"}: | |
| walk(value, k) | |
| else: | |
| walk(value, key_hint) | |
| elif isinstance(obj, (list, tuple)): | |
| for item in obj: | |
| walk(item, key_hint) | |
| elif isinstance(obj, str): | |
| if key_hint in {"label", "labels"}: | |
| clean = _clean_label(obj) | |
| if clean: | |
| labels.append(clean) | |
| else: | |
| labels.extend(_labels_from_caption(obj)) | |
| walk(parsed) | |
| return labels | |
| def _run_florence_task(image, task: str, num_beams: int) -> dict: | |
| device = "cuda" | |
| model.to(device) | |
| inputs = processor(text=task, images=image, return_tensors="pt").to(device) | |
| with torch.no_grad(): | |
| gen = model.generate(**inputs, max_new_tokens=1536, num_beams=int(num_beams)) | |
| text = processor.batch_decode(gen, skip_special_tokens=False)[0] | |
| parsed = processor.post_process_generation(text, task=task, image_size=image.size) | |
| return {"task": task, "text": text, "parsed": parsed, "labels": _extract_labels(parsed)} | |
| def api_autotag(image, max_tags, num_beams=3): | |
| if image is None: | |
| return {"error": "no image provided"} | |
| image = image.convert("RGB") | |
| task_results = [_run_florence_task(image, task, int(num_beams)) for task in TASKS] | |
| labels = [] | |
| for result in task_results: | |
| labels.extend(result["labels"]) | |
| counts = Counter(label for label in labels if label) | |
| tags = [{"name": n, "count": c} for n, c in counts.most_common(int(max_tags))] | |
| return {"model": MODEL_ID, "tasks": task_results, "tags": tags, "labels": [t["name"] for t in tags]} | |
| with gr.Blocks(title="SAM3 AutoTag") as demo: | |
| gr.Markdown("# Florence-2 AutoTag\nUpload an image; returns multi-task scene class names for SAM3.") | |
| with gr.Row(): | |
| inp = gr.Image(type="pil", label="Image") | |
| out = gr.JSON(label="Tags") | |
| mt = gr.Slider(1, 50, value=20, step=1, label="Max tags") | |
| nb = gr.Slider(1, 8, value=3, step=1, label="Beams (higher = more precise)") | |
| gr.Button("Tag").click(api_autotag, [inp, mt, nb], out, api_name="api_autotag") | |
| if __name__ == "__main__": | |
| demo.queue().launch(show_error=True) | |