multimodalart HF Staff commited on
Commit
4e2fb2e
·
verified ·
1 Parent(s): 4b73118

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ 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/ex_closer_object.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/ex_depth_difference.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,53 @@
1
  ---
2
- title: Gpd Spatial Reasoning
3
- emoji: 📚
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.30.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: GPD Spatial Reasoning
3
+ emoji: 🧭
4
+ colorFrom: pink
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.30.0
 
8
  app_file: app.py
9
+ short_description: Spatial-reasoning VLM distilled with routed 3D privilege
10
+ python_version: "3.12"
11
  ---
12
 
13
+ # 🧭 GPD · Distilling Routed 3D Privilege for Spatial Reasoning
14
+
15
+ This Space runs **GPD-4B**, an RGB-only vision-language model fine-tuned from
16
+ [Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) with
17
+ **GPD** (*Geometry-Privileged Distillation*): on-policy self-distillation in which a
18
+ teacher version of the model sees question-routed textual 3D cues (depth / semantic /
19
+ bird's-eye-view) plus the reference answer, while the deployed student stays **RGB-only**.
20
+ The privileged KL on incorrect trajectories augments GRPO.
21
+
22
+ Upload a single image, ask a 3D spatial-reasoning question — observer-shift spatial
23
+ imagination, directional relations between objects, depth / distance estimation in meters —
24
+ and the model thinks step by step, then wraps its final answer in `<answer>` tags.
25
+
26
+ ## Model
27
+
28
+ - **Checkpoint:** [xinyili0624/GPD-4B](https://huggingface.co/xinyili0624/GPD-4B) (Apache-2.0)
29
+ - **2B variant:** [xinyili0624/GPD-2B](https://huggingface.co/xinyili0624/GPD-2B)
30
+ - **Paper:** [Distilling Routed 3D Privilege for Spatial Reasoning in Vision-Language Models](https://huggingface.co/papers/2610.12355)
31
+ - **Code:** [github.com/ZJU-REAL/GPD](https://github.com/ZJU-REAL/GPD)
32
+ - **Training data:** [xinyili0624/GPD-15k](https://huggingface.co/datasets/xinyili0624/GPD-15k) (VSI 10k + SPAR 4k + MindCube 1k)
33
+
34
+ On Qwen3-VL-4B, GPD reaches **57.1** on VSI-Bench and **37.6** average across
35
+ MindCube, SPAR-Bench, MMSI-Bench, and ViewSpatial.
36
+
37
+ ## Examples
38
+
39
+ The bundled example images and questions are taken from the authors'
40
+ [GPD-15k](https://huggingface.co/datasets/xinyili0624/GPD-15k) dataset
41
+ (Apache-2.0), specifically its SPAR single-image scenes, which are rendered with
42
+ bounding-box / point annotations marking the objects the question refers to.
43
+
44
+ ## Citation
45
+
46
+ ```bibtex
47
+ @article{li2026gpd,
48
+ title={Distilling Routed 3D Privilege for Spatial Reasoning in Vision-Language Models},
49
+ author={Li, Hongxing and Li, Yixin and Li, Dingming and Wang, Zixuan and Yan, Yuchen and Zhang, Wenqi and Lu, Weiming and Shen, Yongliang},
50
+ journal={arXiv preprint arXiv:2610.12355},
51
+ year={2026}
52
+ }
53
+ ```
app.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3
+
4
+ import spaces # MUST be the first import after the env var, before torch
5
+ import re
6
+ import threading
7
+
8
+ import gradio as gr
9
+ import torch
10
+ from transformers import AutoProcessor, Qwen3VLForConditionalGeneration, TextIteratorStreamer
11
+
12
+ MODEL_ID = "xinyili0624/GPD-4B"
13
+
14
+ DEFAULT_SYSTEM_PROMPT = (
15
+ "You are a helpful assistant for 3D spatial reasoning. Think step by step using "
16
+ "<think></think> tags, then give your final answer inside <answer> </answer> tags."
17
+ )
18
+
19
+ processor = AutoProcessor.from_pretrained(MODEL_ID)
20
+ model = (
21
+ Qwen3VLForConditionalGeneration.from_pretrained(
22
+ MODEL_ID,
23
+ torch_dtype=torch.bfloat16,
24
+ attn_implementation="sdpa",
25
+ )
26
+ .to("cuda")
27
+ .eval()
28
+ )
29
+
30
+ ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL)
31
+
32
+
33
+ def _extract_answer(text: str) -> str:
34
+ """Return the content of the last <answer>...</answer> tag (or its best effort)."""
35
+ matches = ANSWER_RE.findall(text)
36
+ if matches:
37
+ return matches[-1].strip()
38
+ return ""
39
+
40
+
41
+ @spaces.GPU(duration=90)
42
+ def answer(
43
+ image,
44
+ question: str,
45
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
46
+ max_new_tokens: int = 1024,
47
+ temperature: float = 0.0,
48
+ top_p: float = 0.95,
49
+ ):
50
+ """Answer a spatial-reasoning question about an image with GPD-4B.
51
+
52
+ GPD (Geometry-Privileged Distillation) is an RGB-only vision-language model
53
+ fine-tuned from Qwen3-VL-4B-Instruct for single-image 3D spatial reasoning
54
+ (observer shifts, directional relations, depth/distance estimation). It
55
+ thinks step by step between <think> and </think> tags and wraps its
56
+ final answer in <answer> </answer> tags.
57
+
58
+ Args:
59
+ image: input image (PIL Image) or None for a text-only question.
60
+ question: the spatial question to ask about the image.
61
+ system_prompt: system prompt guiding the reasoning format.
62
+ max_new_tokens: maximum number of generated tokens.
63
+ temperature: sampling temperature; 0 = deterministic greedy decoding.
64
+ top_p: nucleus sampling probability (used when temperature > 0).
65
+
66
+ Yields:
67
+ Tuple of (full_generated_text, extracted_final_answer), streamed live.
68
+ """
69
+ question = (question or "").strip()
70
+ if not question:
71
+ yield "Please enter a question.", ""
72
+ return
73
+
74
+ content = []
75
+ if image is not None:
76
+ content.append({"type": "image", "image": image})
77
+ content.append({"type": "text", "text": question})
78
+ messages = [
79
+ {"role": "system", "content": system_prompt},
80
+ {"role": "user", "content": content},
81
+ ]
82
+
83
+ prompt_text = processor.apply_chat_template(
84
+ messages, tokenize=False, add_generation_prompt=True
85
+ )
86
+ images = [image] if image is not None else None
87
+ inputs = processor(
88
+ text=[prompt_text], images=images, return_tensors="pt"
89
+ ).to(model.device)
90
+
91
+ gen_kwargs = {
92
+ "max_new_tokens": int(max_new_tokens),
93
+ "do_sample": float(temperature) > 0.0,
94
+ }
95
+ if gen_kwargs["do_sample"]:
96
+ gen_kwargs["temperature"] = float(temperature)
97
+ gen_kwargs["top_p"] = float(top_p)
98
+
99
+ streamer = TextIteratorStreamer(
100
+ processor.tokenizer, skip_prompt=True, skip_special_tokens=True
101
+ )
102
+ gen_kwargs["streamer"] = streamer
103
+
104
+ generation_thread = threading.Thread(
105
+ target=lambda: model.generate(**inputs, **gen_kwargs), daemon=True
106
+ )
107
+ generation_thread.start()
108
+
109
+ full_text = ""
110
+ for chunk in streamer:
111
+ full_text += chunk
112
+ yield full_text, _extract_answer(full_text)
113
+
114
+ generation_thread.join()
115
+
116
+ final_answer = _extract_answer(full_text)
117
+ if not final_answer:
118
+ tail = full_text.strip().splitlines()[-1] if full_text.strip() else ""
119
+ final_answer = tail.strip()
120
+ yield full_text, final_answer
121
+
122
+
123
+ EXAMPLES = [
124
+ [
125
+ "examples/ex_distance_to_chair.jpg",
126
+ (
127
+ "From the person's position who took the photo, what is the distance "
128
+ "to the center of chair (red point)? Express it in meters. Calculate "
129
+ "or judge based on the 3D center points of these objects. Enter a "
130
+ "single numeric response and no other details.\n"
131
+ "Think step by step and provide your reasoning inside <think> </think> tags. "
132
+ "Then give your final answer inside <answer> </answer> tags with a "
133
+ "single number or value."
134
+ ),
135
+ ],
136
+ [
137
+ "examples/ex_depth_difference.jpg",
138
+ (
139
+ "The depth of pillow (red point) is 2.5. Use this information to "
140
+ "find the absolute depth difference between plant (green point) and "
141
+ "table (blue point). Answer in meters. Calculate or judge based on "
142
+ "the 3D center points of these objects. Please give a response in "
143
+ "the form of a single number.\n"
144
+ "Think step by step and provide your reasoning inside <think> </think> tags. "
145
+ "Then give your final answer inside <answer> </answer> tags with a "
146
+ "single number or value."
147
+ ),
148
+ ],
149
+ [
150
+ "examples/ex_closer_object.jpg",
151
+ (
152
+ "From the red point laundry basket, which object, container (green "
153
+ "point) or box (blue point), is closer? Calculate or judge based on "
154
+ "the 3D center points of these objects. Choose the correct response "
155
+ "from the given choices.\n"
156
+ "A. box (blue point)\n"
157
+ "B. container (green point)\n"
158
+ "Your answer can only include one of options A, B.\n"
159
+ "Think step by step and provide your reasoning inside <think> </think> tags. "
160
+ "Then give your final answer inside <answer> </answer> tags. For "
161
+ "example: <answer>A</answer>"
162
+ ),
163
+ ],
164
+ [
165
+ "examples/ex_directional_relation.jpg",
166
+ (
167
+ "Analyze the image and determine the directional relationship "
168
+ "between the monitor (red bbox) and the whiteboard (blue bbox) as "
169
+ "seen by the observer. Calculate or judge based on the 3D center "
170
+ "points of these objects.\n"
171
+ "The options describe the spatial relationship between two objects "
172
+ "in terms of left-right (left, right, or empty if "
173
+ "indistinguishable), above-below (above, below, or empty if "
174
+ "indistinguishable), and farther-closer (farther, closer, or empty "
175
+ "if indistinguishable).\n"
176
+ "Select the right response from the available choices.\n"
177
+ "A. left, , closer\n"
178
+ "B. right, , farther\n"
179
+ "C. , above, closer\n"
180
+ "D. right, below, \n"
181
+ "Your answer can only include one of options A, B, C or D.\n"
182
+ "Think step by step and provide your reasoning inside <think> </think> tags. "
183
+ "Then give your final answer inside <answer> </answer> tags. For "
184
+ "example: <answer>A</answer>"
185
+ ),
186
+ ],
187
+ [
188
+ "examples/ex_observer_shift.jpg",
189
+ (
190
+ "From the observer's original viewpoint, where is dining table "
191
+ "(red bbox)? How does this relationship shift once the observer "
192
+ "moves to chair's 3D center (green bbox) and faces oven (blue "
193
+ "bbox)? Calculate or judge based on the 3D center points of these "
194
+ "objects. Please use the world coordinate system to determine the "
195
+ "up and down position relationship of objects.\n"
196
+ "For multiple-choice questions, consider only the state after the "
197
+ "observer has moved.\n"
198
+ "The options describe the spatial relationship between object and "
199
+ "observer in terms of left-right (left, right, or empty if "
200
+ "indistinguishable), above-below (above, below, or empty if "
201
+ "indistinguishable), and front-behind (front, behind, or empty if "
202
+ "indistinguishable).\n"
203
+ "Choose the correct response from the given choices.\n"
204
+ "A. right, below, front\n"
205
+ "B. right, , behind\n"
206
+ "C. left, above, behind\n"
207
+ "D. right, below, \n"
208
+ "Your answer can only include one of options A, B, C or D.\n"
209
+ "Think step by step and provide your reasoning inside <think> </think> tags. "
210
+ "Then give your final answer inside <answer> </answer> tags. For "
211
+ "example: <answer>A</answer>"
212
+ ),
213
+ ],
214
+ ]
215
+
216
+ CSS = """
217
+ #col-container { max-width: 1100px; margin: 0 auto; }
218
+ .dark .gradio-container { color: var(--body-text-color); }
219
+ """
220
+
221
+ with gr.Blocks(title="GPD · Spatial Reasoning") as demo:
222
+ gr.Markdown(
223
+ """
224
+ # 🧭 GPD · Distilling Routed 3D Privilege for Spatial Reasoning
225
+ **GPD-4B** is an RGB-only vision-language model (fine-tuned from
226
+ [Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct)) that answers 3D
227
+ spatial-reasoning questions about a single image — observer-shift, directional relations,
228
+ and depth/distance estimation — thinking step by step and wrapping the final answer in
229
+ `<answer>` tags.
230
+
231
+ [Paper](https://huggingface.co/papers/2610.12355) ·
232
+ [Code](https://github.com/ZJU-REAL/GPD) ·
233
+ [GPD-4B](https://huggingface.co/xinyili0624/GPD-4B) ·
234
+ [GPD-2B](https://huggingface.co/xinyili0624/GPD-2B) ·
235
+ [GPD-15k Data](https://huggingface.co/datasets/xinyili0624/GPD-15k)
236
+ """
237
+ )
238
+ with gr.Column(elem_id="col-container"):
239
+ with gr.Row():
240
+ with gr.Column(scale=1):
241
+ image_input = gr.Image(
242
+ type="pil", label="Image", height=420, sources=["upload", "clipboard"]
243
+ )
244
+ with gr.Column(scale=1):
245
+ question_input = gr.Textbox(
246
+ label="Question",
247
+ lines=10,
248
+ placeholder=(
249
+ "Ask a spatial question about the image, e.g. "
250
+ "'What is closer to the camera, the chair or the table?'"
251
+ ),
252
+ )
253
+ submit_btn = gr.Button("Answer", variant="primary")
254
+
255
+ answer_output = gr.Textbox(
256
+ label="Final answer",
257
+ lines=1,
258
+ buttons=["copy"],
259
+ )
260
+ reasoning_output = gr.Textbox(
261
+ label="Full reasoning",
262
+ lines=12,
263
+ buttons=["copy"],
264
+ )
265
+
266
+ with gr.Accordion("Advanced settings", open=False):
267
+ system_prompt_input = gr.Textbox(
268
+ label="System prompt",
269
+ value=DEFAULT_SYSTEM_PROMPT,
270
+ lines=2,
271
+ )
272
+ with gr.Row():
273
+ max_new_tokens_input = gr.Slider(
274
+ label="Max new tokens", minimum=64, maximum=3072, value=1024, step=64
275
+ )
276
+ temperature_input = gr.Slider(
277
+ label="Temperature (0 = deterministic)", minimum=0.0, maximum=1.5, value=0.0, step=0.05
278
+ )
279
+ top_p_input = gr.Slider(
280
+ label="Top-p (used when temperature > 0)", minimum=0.05, maximum=1.0, value=0.95, step=0.05
281
+ )
282
+
283
+ gr.Examples(
284
+ examples=EXAMPLES,
285
+ inputs=[image_input, question_input],
286
+ outputs=[answer_output, reasoning_output],
287
+ fn=answer,
288
+ cache_examples=True,
289
+ cache_mode="lazy",
290
+ label="Examples (from the authors' GPD-15k dataset — SPAR scenes)",
291
+ )
292
+
293
+ submit_btn.click(
294
+ answer,
295
+ inputs=[image_input, question_input, system_prompt_input, max_new_tokens_input, temperature_input, top_p_input],
296
+ outputs=[answer_output, reasoning_output],
297
+ api_name="answer",
298
+ )
299
+
300
+ if __name__ == "__main__":
301
+ demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)
examples/ex_closer_object.jpg ADDED

Git LFS Details

  • SHA256: e7195c822a252b24bc8afee61bfcc775594fbc1833c64037c3e94e143162d679
  • Pointer size: 131 Bytes
  • Size of remote file: 121 kB
examples/ex_depth_difference.jpg ADDED

Git LFS Details

  • SHA256: 7f9a0c5a7e2ae405bd25aeb3c88388b0ece1145cb1a3d2b79fd2b955892a2bf0
  • Pointer size: 131 Bytes
  • Size of remote file: 102 kB
examples/ex_directional_relation.jpg ADDED
examples/ex_distance_to_chair.jpg ADDED
examples/ex_observer_shift.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ transformers==4.57.3
2
+ accelerate
3
+ pillow