Files
cad-agent/verify_heatmap.py
T
gjm 9b8a2329df feat(heatmap): 实体分布热力图(占用网格 + 聚类 + 可视化)
- 新增 dwg_review/heatmap.py:entity_boxes / build_grid(counts+coverage)/ occupied_cells /
  cluster_cells(4-连通,可选膨胀合并)/ grid_to_windows / grid_stats / render_overlay
- drop_oversized:过滤 bbox 占满图框的实体(实测每框恰 1 个,即图框块),否则热力图全满
- 新增 verify_heatmap.py:取实体 bbox(分页)-> 建网格 -> 聚类 -> 渲染 PNG,并缓存 bbox JSON 供离线调参
- 新增 tests/test_heatmap.py(22 项)
2026-10-15 22:51:05 +08:00

126 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""实体分布热力图验证:取图框内实体 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())