Files
cad-agent/verify_heatmap.py
T

125 lines
4.9 KiB
Python
Raw Normal View History

"""实体分布热力图验证:取图框内实体 bbox → 建占用网格 → 聚类 → 渲染 PNG 供人工目视。
不消耗 VLM token(纯几何统计 + 本地渲染)。
用法:
python verify_heatmap.py [--frames 3] [--div 32,64] [--dilate 0] [--out reports/_probe]
前置:CAD 在线(127.0.0.1:8080 监听)。不需要 DEEPSEEK_API_KEY。
同时把每个图框的实体 bbox 缓存为 JSON,便于后续**离线**反复调聚类 / 切分参数。
"""
import argparse
import json
from pathlib import Path
from dwg_review import config
from dwg_review import heatmap
from dwg_review.context import ReviewContext
from dwg_review.mcp_client import result_json
from dwg_review.phases import planning
_PAGE = 1000 # query_entities 的 limit 上限
_MAX_PAGES = 20 # 兜底,避免异常分页死循环
def fetch_boxes(tools, window):
"""分页取回 window 内全部实体的 bbox。
返回 (boxes, total, fetched);total 为工具报告的总数,fetched 为实际取回条数。
"""
boxes, offset, total = [], 0, 0
for _ in range(_MAX_PAGES):
env = result_json(
tools.call("query_entities", {"window": window, "limit": _PAGE, "offset": offset}),
{},
)
items = env.get("items") or []
boxes.extend(heatmap.entity_boxes(env))
total = int(env.get("total") or len(items))
offset += len(items)
if not items or offset >= total:
break
return boxes, total, offset
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--frames", type=int, default=3, help="最多分析的图框数(0=全部)")
ap.add_argument("--div", default="32", help="网格划分(长边切几份),逗号分隔可多档对比")
ap.add_argument("--dilate", type=int, default=0, help="聚类前膨胀格数(合并相近内容)")
ap.add_argument("--max-area-frac", type=float, default=0.5,
help="丢弃 bbox 面积超过图框面积该比例的实体(默认 0.5,去图框边框/图签块)")
ap.add_argument("--out", default="reports/_probe")
args = ap.parse_args(argv)
divs = [int(x) for x in str(args.div).split(",") if x.strip()]
ctx = ReviewContext.create(need_llm=False)
plan = planning.plan(ctx)
frames = plan.get("frames", [])
if args.frames > 0:
frames = 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)
for i, frame in enumerate(frames, 1):
bbox = frame.get("bbox")
if not bbox or len(bbox) != 4:
print(f"[图框 {i}] 缺少有效 bbox,跳过")
continue
win = {
k: float(v)
for k, v in zip(("min_x", "min_y", "max_x", "max_y"), bbox)
}
print(f"\n[图框 {i}] sheet={frame.get('sheet_size')} scale={frame.get('sheet_scale')} "
f"bbox={bbox}")
boxes, total, fetched = fetch_boxes(ctx.tools, win)
kept = heatmap.drop_oversized(boxes, win, args.max_area_frac)
dropped = len(boxes) - len(kept)
print(f" 实体:工具报告 {total} 条,取回 {fetched} 条,有效 bbox {len(boxes)} 个;"
f"丢弃超大实体(图框边框/图签块等){dropped} 个 -> 参与分析 {len(kept)} 个")
boxes = kept
# 缓存 bbox,便于离线调参
cache = out_dir / f"boxes_frame{i}.json"
cache.write_text(
json.dumps({"frame": frame, "window": win, "boxes": boxes},
ensure_ascii=False),
encoding="utf-8",
)
for div in divs:
cell = heatmap.div_to_cell(win, div)
grid = heatmap.build_grid(win, boxes, cell)
clusters = heatmap.cluster_cells(grid, dilate=args.dilate)
st = heatmap.grid_stats(grid, clusters)
print(f" [div={div}] 格子 {st['cols']}x{st['rows']}(cell={cell:.1f})"
f" 占用格 {st['occupied']} ({st['occupied_ratio']:.0%})"
f" 最大格计数 {st['max_count']}")
print(f" 簇 {st['clusters']} 个,最大簇 {st['max_cluster_cells']} 格 / "
f"{st['max_cluster_count']} 个实体")
png = out_dir / f"heatmap_frame{i}_div{div}.png"
heatmap.render_overlay(png, win, boxes, grid, clusters)
print(f" -> {png.name}")
# 前几个簇的窗口(后续瓦片规划直接复用)
for j, cl in enumerate(clusters[:5], 1):
x1, y1, x2, y2 = cl["bbox"]
print(f" 簇{j}: {len(cl['cells'])} 格 / {cl['count']} 实体 "
f"bbox=({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}) "
f"尺寸={x2 - x1:.0f}x{y2 - y1:.0f}")
print(f"\n输出目录: {out_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())