File size: 5,872 Bytes
35fefbe
 
 
 
 
 
 
 
6f23c17
 
35fefbe
6f23c17
 
 
 
 
 
 
 
 
 
 
 
 
35fefbe
 
 
 
 
 
6ec7266
 
 
 
 
 
 
 
 
 
 
 
35fefbe
 
6ec7266
35fefbe
 
 
 
 
 
 
 
6ec7266
 
 
35fefbe
 
 
6ec7266
 
 
 
 
 
 
35fefbe
 
6ec7266
 
35fefbe
 
 
 
 
 
 
 
 
6ec7266
 
 
 
 
35fefbe
6ec7266
35fefbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f23c17
 
23e7f36
6f23c17
 
 
35fefbe
 
 
 
 
6f23c17
35fefbe
6f23c17
 
 
35fefbe
6f23c17
 
 
 
23e7f36
 
6f23c17
 
 
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
"""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)}


@spaces.GPU(duration=120)
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)