"""实测 deepseek-flash 的 grounding:能否给出可用的「放大区域」归一化坐标。 流程:截一张真实图框整体图 → 问 VLM「哪些区域需要放大」→ 解析它给的归一化坐标 → 映射成 CAD 世界坐标(WCS)→ 按坐标放大截图,供目视验证 grounding 是否可用。 用法: python verify_grounding.py [--frames 1] [--out reports/_probe] 前置:CAD 在线(127.0.0.1:8080 监听)、DEEPSEEK_API_KEY 可用。 """ import argparse import base64 import json import time from pathlib import Path from dwg_review import config from dwg_review.findings import extract_json_block from dwg_review.llm_client import LlmClient, downsample_image_b64 from dwg_review.mcp_client import result_images from dwg_review.phases import planning from dwg_review.context import ReviewContext _PROMPT = ( "这是某工程图纸中一个图框的截图。请判断:图中哪些区域**细节不清、需要放大**才能看清?\n" "用**归一化坐标**给出想放大的区域:x0,y0,x1,y1 取值 0~1," "图片左上角为 (0,0)、右下角为 (1,1)。\n" "最多给 3 个区域,按重要性排序。\n" '只输出 JSON,不要解释:{"regions":[{"bbox":[x0,y0,x1,y1],"reason":"..."}]}' ) def _save(b64, path): path.write_bytes(base64.b64decode(b64)) return path def _norm_to_wcs(win, x0, y0, x1, y1): """归一化坐标(左上原点)→ WCS window。""" dx = win["max_x"] - win["min_x"] dy = win["max_y"] - win["min_y"] return { "min_x": win["min_x"] + x0 * dx, "max_x": win["min_x"] + x1 * dx, "min_y": win["max_y"] - y1 * dy, # 图片 y 向下,WCS y 向上 "max_y": win["max_y"] - y0 * dy, } def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("--frames", type=int, default=1) ap.add_argument("--out", default="reports/_probe") args = ap.parse_args(argv) ctx = ReviewContext.create() plan = planning.plan(ctx) frames = plan.get("frames", [])[: args.frames] if not frames: print("未识别到图框") return 1 out_dir = Path(config.PROJECT_ROOT) / args.out out_dir.mkdir(parents=True, exist_ok=True) frame = frames[0] win = { k: float(v) for k, v in zip(("min_x", "min_y", "max_x", "max_y"), frame["bbox"]) } print("图框 window:", win) ctx.tools.call("zoom_to_window", dict(win, margin=0.02)) time.sleep(config.RENDER_WAIT) shot = ctx.tools.call("get_viewport_screenshot", {}) imgs = result_images(shot) if not imgs: print("无截图") return 1 b64, mime = imgs[0] nb, nm = downsample_image_b64(b64, mime) print("整体图:", _save(b64, out_dir / "overall.png")) llm = LlmClient() print("模型:", llm.model) messages = [ {"role": "system", "content": "你是资深工程图纸审图助手。"}, { "role": "user", "content": [ {"type": "text", "text": _PROMPT}, LlmClient.image_content(nb, nm), ], }, ] resp = llm.chat(messages) text = resp.choices[0].message.content or "" print("=== VLM 原始响应 ===") print(text) block = extract_json_block(text) if not block: print("[X] 未解析出 JSON") return 1 data = json.loads(block) regions = data.get("regions") or [] print(f"=== 解析到 {len(regions)} 个区域 ===") for i, r in enumerate(regions, 1): bbox = r.get("bbox") if not (isinstance(bbox, list) and len(bbox) == 4): print(f" 区域 {i}: bbox 非法 {bbox}") continue x0, y0, x1, y1 = (float(v) for v in bbox) sub = _norm_to_wcs(win, x0, y0, x1, y1) print(f" 区域 {i}: bbox={bbox} reason={r.get('reason')!r}") print(f" -> WCS {sub}") ctx.tools.call("zoom_to_window", dict(sub, margin=0.05)) time.sleep(config.RENDER_WAIT) s2 = ctx.tools.call("get_viewport_screenshot", {}) im2 = result_images(s2) if im2: print(" -> 已保存", _save(im2[0][0], out_dir / f"region_{i}.png")) print("\n截图目录:", out_dir) return 0 if __name__ == "__main__": raise SystemExit(main())