multimodalart HF Staff commited on
Commit
b3c8668
Β·
verified Β·
1 Parent(s): 586d125

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/car_fruit_stand.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/pikachu_mario.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/sign_bianca_buda.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/throne_dystopia.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,35 @@
1
  ---
2
  title: Structured Defect Grounding
3
- emoji: 🐨
4
- colorFrom: blue
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.19.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: Structured Defect Grounding
3
+ emoji: πŸ”
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.19.0
 
8
  app_file: app.py
9
+ short_description: Ground localized defects in text-to-image generations
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  pinned: false
13
  ---
14
 
15
+ # Structured Defect Grounding (SDG)
16
+
17
+ Interactive demo of the **SDG Detector** ([`P1n3/sdg-detector-grpo`](https://huggingface.co/P1n3/sdg-detector-grpo)),
18
+ a Qwen3-VL-4B vision-language model trained with SFT + GRPO for the paper
19
+ [*Where, What, Why, and Importance: Structured Defect Grounding for
20
+ Text-to-Image Feedback*](https://huggingface.co/papers/2606.06113).
21
+
22
+ Given a generated image (and optionally the caption it was generated from), the
23
+ detector predicts a **structured set of localized defects**, each grounded as a
24
+ tuple of *(where, what, why, importance)*:
25
+
26
+ - **where** β€” a bounding box localizing the problematic region,
27
+ - **what** β€” the defect type: πŸ”΄ *artifact* (visual defect) or πŸ”΅ *misalignment*
28
+ (image doesn't match the caption),
29
+ - **why** β€” a natural-language explanation of the issue,
30
+ - **importance** β€” a 1–100 severity score.
31
+
32
+ The demo overlays the predicted boxes on the image and renders a structured
33
+ feedback table plus the detector's raw reasoning.
34
+
35
+ Model license: `cc-by-nc-4.0` (research / non-commercial use only).
app.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3
+
4
+ import spaces # noqa: E402 must precede torch / transformers
5
+
6
+ import json
7
+ import re
8
+ import html
9
+ import tempfile
10
+
11
+ import torch
12
+ import gradio as gr
13
+ from PIL import Image, ImageDraw, ImageFont
14
+ from transformers import AutoProcessor, AutoModelForImageTextToText
15
+
16
+ MODEL_ID = "P1n3/sdg-detector-grpo"
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Model (loaded once at module scope, moved to CUDA eagerly for ZeroGPU)
20
+ # ---------------------------------------------------------------------------
21
+ processor = AutoProcessor.from_pretrained(MODEL_ID)
22
+ model = AutoModelForImageTextToText.from_pretrained(
23
+ MODEL_ID,
24
+ dtype=torch.bfloat16,
25
+ attn_implementation="sdpa",
26
+ ).to("cuda")
27
+ model.eval()
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Prompt (the SDG detector's structured-grounding question template)
31
+ # ---------------------------------------------------------------------------
32
+ QUESTION_TEMPLATE = """You are an AI image quality evaluator. You will be given **one image** to analyze.
33
+
34
+ ### Definitions
35
+
36
+ **Misalignment**: Areas where the image content does NOT match the text caption, including:
37
+ - Missing objects: Objects mentioned in caption but not present in image
38
+ - Extra objects: Objects present in image but not mentioned in caption
39
+ - Wrong attributes: Incorrect color, size, material, count, or other properties
40
+ - Wrong spatial relationships: Incorrect positions, orientations, or arrangements
41
+
42
+ **Artifact**: Visual defects in the generated image, including:
43
+ - Distorted anatomy: Malformed hands, extra/missing limbs, wrong number of fingers
44
+ - Duplicated/missing parts: Repeated or absent body parts, objects
45
+ - Warped geometry: Perspective errors, impossible shapes
46
+ - Texture issues: Melted, smeared, or overly smooth textures
47
+ - Unnatural edges: Jagged, broken, or blurry boundaries
48
+ - Garbled text: Unreadable or malformed text/letters
49
+ - Lighting inconsistencies: Wrong shadows, reflections, or light sources
50
+
51
+ Text Caption: {caption}
52
+
53
+ **Goal**: Produce a detailed analysis of the image quality and output bounding boxes for all detected issues.
54
+
55
+ ### Strict Output Rules
56
+ Output **TWO blocks in this exact order**:
57
+ 1) `<think>` - Your detailed analysis
58
+ 2) `<answer>` - JSON list of bounding boxes
59
+
60
+ ### Answer Format (for <answer>)
61
+ Return a JSON list:
62
+ [
63
+ {{"box_2d": [x0, y0, x1, y1], "label": "misalignment"|"artifact", "description": "brief description of the issue", "importance": 1-100}}
64
+ ]
65
+
66
+ Bounding box coordinates are in normalized 0-1000 space: [x0, y0, x1, y1].
67
+ The "importance" is an integer from 1 (minor) to 100 (severe) rating how much the defect hurts image quality.
68
+ If there are no issues, output an empty list.
69
+
70
+ Now analyze the image and produce your output:
71
+ """
72
+
73
+ ARTIFACT_COLOR = (239, 68, 68) # red
74
+ MISALIGNMENT_COLOR = (37, 99, 235) # blue
75
+
76
+
77
+ def _load_font(size):
78
+ for path in (
79
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
80
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
81
+ ):
82
+ try:
83
+ return ImageFont.truetype(path, size)
84
+ except Exception:
85
+ continue
86
+ return ImageFont.load_default()
87
+
88
+
89
+ def parse_answer(response: str):
90
+ """Extract the structured defect list from the model's <answer> block."""
91
+ m = re.search(r"<answer>\s*(.*?)\s*</answer>", response, re.DOTALL)
92
+ payload = m.group(1) if m else response
93
+ start, end = payload.find("["), payload.rfind("]")
94
+ if start == -1 or end == -1 or end <= start:
95
+ return []
96
+ try:
97
+ data = json.loads(payload[start:end + 1])
98
+ except Exception:
99
+ return []
100
+ if not isinstance(data, list):
101
+ return []
102
+
103
+ defects = []
104
+ for item in data:
105
+ if not isinstance(item, dict):
106
+ continue
107
+ box = item.get("box_2d")
108
+ if not (isinstance(box, list) and len(box) == 4):
109
+ continue
110
+ try:
111
+ box = [float(v) for v in box]
112
+ except Exception:
113
+ continue
114
+ label = str(item.get("label", "")).lower()
115
+ if label not in ("artifact", "misalignment"):
116
+ label = "artifact"
117
+ desc = item.get("description") or item.get("desc") or ""
118
+ imp = item.get("importance")
119
+ try:
120
+ imp = int(imp)
121
+ except Exception:
122
+ imp = None
123
+ defects.append({
124
+ "box_2d": box,
125
+ "label": label,
126
+ "description": str(desc),
127
+ "importance": imp,
128
+ })
129
+ return defects
130
+
131
+
132
+ def extract_think(response: str) -> str:
133
+ m = re.search(r"<think>\s*(.*?)\s*</think>", response, re.DOTALL)
134
+ return m.group(1).strip() if m else ""
135
+
136
+
137
+ def draw_defects(image: Image.Image, defects):
138
+ """Overlay defect bounding boxes (coords normalized 0-1000, Qwen order)."""
139
+ image = image.convert("RGB").copy()
140
+ w, h = image.size
141
+ draw = ImageDraw.Draw(image)
142
+ line_w = max(3, round(min(w, h) / 250))
143
+ font = _load_font(max(16, round(min(w, h) / 45)))
144
+
145
+ for idx, d in enumerate(defects, start=1):
146
+ x0, y0, x1, y1 = d["box_2d"]
147
+ px0, py0 = x0 / 1000 * w, y0 / 1000 * h
148
+ px1, py1 = x1 / 1000 * w, y1 / 1000 * h
149
+ if px1 < px0:
150
+ px0, px1 = px1, px0
151
+ if py1 < py0:
152
+ py0, py1 = py1, py0
153
+ color = ARTIFACT_COLOR if d["label"] == "artifact" else MISALIGNMENT_COLOR
154
+ draw.rectangle([px0, py0, px1, py1], outline=color, width=line_w)
155
+
156
+ imp = d["importance"]
157
+ tag = f"{idx}" + (f" \u00b7 {imp}" if imp is not None else "")
158
+ bbox = draw.textbbox((0, 0), tag, font=font)
159
+ tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
160
+ ly = max(0, py0 - th - 4)
161
+ draw.rectangle([px0, ly, px0 + tw + 8, ly + th + 4], fill=color)
162
+ draw.text((px0 + 4, ly + 2), tag, fill=(255, 255, 255), font=font)
163
+
164
+ return image
165
+
166
+
167
+ def defects_to_markdown(defects):
168
+ if not defects:
169
+ return ("### βœ… No defects detected\n\n"
170
+ "The SDG detector did not ground any localized artifacts or "
171
+ "caption misalignments in this image.")
172
+ n_art = sum(1 for d in defects if d["label"] == "artifact")
173
+ n_mis = sum(1 for d in defects if d["label"] == "misalignment")
174
+ lines = [
175
+ f"### Found {len(defects)} defect(s) β€” "
176
+ f"πŸ”΄ {n_art} artifact, πŸ”΅ {n_mis} misalignment\n",
177
+ "| # | Type | Importance | Where / What / Why |",
178
+ "|---|------|-----------|--------------------|",
179
+ ]
180
+ for idx, d in enumerate(defects, start=1):
181
+ emoji = "πŸ”΄" if d["label"] == "artifact" else "πŸ”΅"
182
+ imp = d["importance"] if d["importance"] is not None else "β€”"
183
+ box = ", ".join(str(int(v)) for v in d["box_2d"])
184
+ desc = html.escape(d["description"]).replace("\n", " ").replace("|", "\\|")
185
+ lines.append(
186
+ f"| {idx} | {emoji} {d['label']} | {imp} | "
187
+ f"{desc} <br><sub>box (0–1000): [{box}]</sub> |"
188
+ )
189
+ return "\n".join(lines)
190
+
191
+
192
+ @spaces.GPU(duration=90)
193
+ def detect(image, caption, max_new_tokens=1024):
194
+ """Detect and ground structured defects in a text-to-image generation.
195
+
196
+ Args:
197
+ image: the generated image to inspect for defects.
198
+ caption: the text prompt the image was generated from (used to spot
199
+ caption/image misalignments). Optional.
200
+ max_new_tokens: generation budget for the detector's reasoning + answer.
201
+
202
+ Returns:
203
+ The image annotated with defect boxes, a structured defect table, and
204
+ the detector's raw reasoning.
205
+ """
206
+ if image is None:
207
+ raise gr.Error("Please provide an image to analyze.")
208
+ caption = (caption or "").strip()
209
+
210
+ messages = [
211
+ {
212
+ "role": "user",
213
+ "content": [
214
+ {"type": "image", "image": image},
215
+ {"type": "text", "text": QUESTION_TEMPLATE.format(caption=caption)},
216
+ ],
217
+ }
218
+ ]
219
+ inputs = processor.apply_chat_template(
220
+ messages,
221
+ tokenize=True,
222
+ add_generation_prompt=True,
223
+ return_dict=True,
224
+ return_tensors="pt",
225
+ ).to(model.device)
226
+
227
+ with torch.inference_mode():
228
+ generated = model.generate(
229
+ **inputs,
230
+ max_new_tokens=int(max_new_tokens),
231
+ do_sample=False,
232
+ )
233
+ trimmed = generated[:, inputs["input_ids"].shape[1]:]
234
+ response = processor.batch_decode(
235
+ trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
236
+ )[0]
237
+
238
+ defects = parse_answer(response)
239
+ annotated = draw_defects(image, defects)
240
+ table = defects_to_markdown(defects)
241
+ think = extract_think(response)
242
+ reasoning = think if think else response.strip()
243
+ return annotated, table, reasoning
244
+
245
+
246
+ CSS = """
247
+ #col-container { max-width: 1200px; margin: 0 auto; }
248
+ .dark .gradio-container { color: var(--body-text-color); }
249
+ """
250
+
251
+ INTRO = """# πŸ” Structured Defect Grounding (SDG)
252
+
253
+ Detect **localized defects** in text-to-image generations and get instance-level
254
+ feedback β€” *where* the defect is, *what* type it is (πŸ”΄ artifact / πŸ”΅ misalignment),
255
+ *why* it's wrong, and *how important* it is (1–100).
256
+
257
+ Powered by the **SDG Detector** ([`P1n3/sdg-detector-grpo`](https://huggingface.co/P1n3/sdg-detector-grpo)),
258
+ a Qwen3-VL-4B model trained with SFT + GRPO from the paper
259
+ [*Where, What, Why, and Importance: Structured Defect Grounding for Text-to-Image Feedback*](https://huggingface.co/papers/2606.06113).
260
+
261
+ Upload a generated image and (optionally) the caption it was generated from,
262
+ then press **Analyze**.
263
+ """
264
+
265
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="Structured Defect Grounding") as demo:
266
+ with gr.Column(elem_id="col-container"):
267
+ gr.Markdown(INTRO)
268
+ with gr.Row():
269
+ with gr.Column(scale=1):
270
+ image_in = gr.Image(type="pil", label="Generated image", height=380)
271
+ caption_in = gr.Textbox(
272
+ label="Caption / prompt (optional)",
273
+ placeholder="The text prompt the image was generated from…",
274
+ lines=2,
275
+ )
276
+ run = gr.Button("Analyze", variant="primary")
277
+ with gr.Accordion("Advanced settings", open=False):
278
+ max_tokens = gr.Slider(
279
+ 256, 2048, value=1024, step=64,
280
+ label="Max new tokens",
281
+ info="Generation budget for the detector's reasoning + answer.",
282
+ )
283
+ with gr.Column(scale=1):
284
+ image_out = gr.Image(type="pil", label="Detected defects", height=380)
285
+ table_out = gr.Markdown(label="Structured feedback")
286
+ with gr.Accordion("Detector reasoning (<think>)", open=False):
287
+ reasoning_out = gr.Textbox(label="Raw reasoning", lines=8, show_copy_button=True)
288
+
289
+ gr.Examples(
290
+ examples=[
291
+ ["examples/throne_dystopia.png", "a jung male sitting down on a throne in a dystopian world, digital art, epic"],
292
+ ["examples/sign_bianca_buda.png", "A sign that says Bianca Buda"],
293
+ ["examples/pikachu_mario.png", "Pikachu Ninja turtle Mewtwo super Mario"],
294
+ ["examples/car_fruit_stand.png", "a car driving through a fruit stand, movie action scene, fruits flying everywhere"],
295
+ ],
296
+ inputs=[image_in, caption_in],
297
+ outputs=[image_out, table_out, reasoning_out],
298
+ fn=detect,
299
+ cache_examples=True,
300
+ cache_mode="lazy",
301
+ )
302
+
303
+ run.click(
304
+ detect,
305
+ inputs=[image_in, caption_in, max_tokens],
306
+ outputs=[image_out, table_out, reasoning_out],
307
+ api_name="detect",
308
+ )
309
+
310
+ if __name__ == "__main__":
311
+ demo.launch(mcp_server=True)
examples/car_fruit_stand.png ADDED

Git LFS Details

  • SHA256: 12efaf219931ed3d3e97a6d9e18ed79fdd6a5a4063333a4ddcad0413448c448b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
examples/pikachu_mario.png ADDED

Git LFS Details

  • SHA256: 6b134995e1047e2915eb68c16f2cf34887a816df89dc3cedb3729f7f72103be6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.92 MB
examples/sign_bianca_buda.png ADDED

Git LFS Details

  • SHA256: 7b592aa9a2d624ac7b4df8ff0f25eb9009d1d2b82a163351ff27eb28957b9a4d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.19 MB
examples/throne_dystopia.png ADDED

Git LFS Details

  • SHA256: 0db59015dbc8b506ef72d8a5b615cbd2b03d2c1921949bbe938c7835a9dc452e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ transformers>=5.10.0
2
+ accelerate
3
+ torchvision
4
+ pillow