Files
cad-agent/tests/test_heatmap.py
T
gjm 9c4ab62dc1 feat(tiling): CAD 实测标定 R_text/R_line + 瓦片粒度内容过滤 + 默认参数定稿
Stage B2(CAD 实测标定)与收尾:

- 新增 verify_text_resolution.py:临时加字(字高 200 图形单位 = 1:100 纸面 2mm)+
  平行线对,按目标像素换算 zoom 窗口 → 截图 → 问 VLM;测完 delete_entities 清理不保存。
- 标定结论:
  * **可读下限与字体强相关**——离线合成(Consolas 抗锯齿填充字体)12px;
    真实 CAD 单线体 .shx(描边字体)6px 即可读对(3/3,两轮)。
  * 线条「1 条 vs 2 条」:2/3/4px 失败,5/8/12px 成功 → R_line ≈ 5px。
  * **必须用 ACI 7 白字**:实测红字(ACI 1)12px 因对比度不足完全读不出。
- config 定稿:TEXT_READABLE_PX=8(折中)、LINE_READABLE_PX=6、
  EVIDENCE_MAX_IMAGES=12(原 5 会把规划结果截到只剩 4 张瓦片)。
- 新增 heatmap.occupied_boxes:占用格的**世界 bbox**。
- tiling.plan_regions 新增 keep 谓词;evidence._plan_tiles 按占用格剔除**空瓦片**
  (簇 bbox 是包围盒,L 形/斜向分布含大片空白,实测有效)。
- 测试:test_heatmap 补 occupied_boxes;test_tiling 补 keep 过滤;全量 286 项全绿。
- 真实图纸端到端验证:A0 图框出 12 张(1 整体 + 11 瓦片),单框审查 1m38s 通过。
- 记忆库同步:工具语义坑(type_filter 必须传列表 / layer_filter 是通配字符串 /
  add_* 返回句柄在 created 键)、标定结论与默认值。
2026-10-15 23:22:15 +08:00

183 lines
5.7 KiB
Python

"""`dwg_review.heatmap` 单元测试(纯函数,不依赖 CAD / LLM / Pillow)。"""
from dwg_review import heatmap as hm
WIN = {"min_x": 0.0, "min_y": 0.0, "max_x": 100.0, "max_y": 100.0}
def _grid(boxes, cell=50.0):
return hm.build_grid(WIN, boxes, cell)
# --- norm_box / entity_boxes ------------------------------------------------
def test_norm_box_valid():
assert hm.norm_box([0, 0, 10, 20]) == (0.0, 0.0, 10.0, 20.0)
def test_norm_box_swaps_reversed():
assert hm.norm_box([10, 20, 0, 0]) == (0.0, 0.0, 10.0, 20.0)
def test_norm_box_invalid():
assert hm.norm_box([1, 2, 3]) is None
assert hm.norm_box("x") is None
assert hm.norm_box(None) is None
assert hm.norm_box([1, 2, 3, "a"]) is None
assert hm.norm_box([0, 0, float("nan"), 1]) is None
def test_entity_boxes_accepts_single_envelope():
env = {"items": [{"bbox": [0, 0, 1, 1]}, {"bbox": [2, 2, 3, 3]}], "total": 2}
assert hm.entity_boxes(env) == [(0.0, 0.0, 1.0, 1.0), (2.0, 2.0, 3.0, 3.0)]
def test_entity_boxes_accepts_iterable_and_skips_invalid():
envs = [
{"items": [{"bbox": [0, 0, 1, 1]}, {"nope": 1}, "junk"]},
{"items": "not-a-list"},
None,
{"items": [{"bbox": [5, 5, 6, 6]}]},
]
assert hm.entity_boxes(envs) == [(0.0, 0.0, 1.0, 1.0), (5.0, 5.0, 6.0, 6.0)]
def test_entity_boxes_empty():
assert hm.entity_boxes(None) == []
assert hm.entity_boxes({}) == []
assert hm.entity_boxes({"items": []}) == []
# --- box helpers ------------------------------------------------------------
def test_box_area_and_union():
assert hm.box_area((0, 0, 10, 5)) == 50.0
assert hm.box_area((10, 0, 0, 5)) == 0.0 # 退化(未归一)
assert hm.union_box([(0, 0, 10, 10), (5, 5, 20, 30)]) == (0.0, 0.0, 20.0, 30.0)
assert hm.union_box([]) is None
def test_div_to_cell_uses_long_edge():
assert hm.div_to_cell(WIN, 4) == 25.0
# --- build_grid -------------------------------------------------------------
def test_build_grid_shape_and_left_half():
g = _grid([(0, 0, 50, 100)])
assert (g["cols"], g["rows"]) == (2, 2)
assert (g["cell_w"], g["cell_h"]) == (50.0, 50.0)
# 左半格两行各命中一次,右半格不命中(恰好落在 x=50 边界上不算越界)
assert [row[0] for row in g["counts"]] == [1, 1]
assert [row[1] for row in g["counts"]] == [0, 0]
assert g["coverage"][0][0] == 1.0
assert g["coverage"][1][0] == 1.0
def test_build_grid_row0_is_top():
g = _grid([(0, 60, 10, 70)])
assert g["counts"][0][0] == 1
assert g["counts"][1][0] == 0
def test_build_grid_degenerate_horizontal_line_counted():
# 高度为 0 的线(bbox 退化)也要能被计入,且横跨两列
g = _grid([(0, 25, 100, 25)])
assert g["counts"][1] == [1, 1]
assert g["counts"][0] == [0, 0]
def test_build_grid_ignores_boxes_outside_window():
g = _grid([(200, 200, 300, 300)])
assert sum(sum(row) for row in g["counts"]) == 0
def test_build_grid_invalid_window():
g = hm.build_grid({"min_x": 0, "min_y": 0, "max_x": 0, "max_y": 10}, [], 5)
assert g["cols"] == 0 and g["rows"] == 0 and g["counts"] == []
# --- occupied / cluster -----------------------------------------------------
def test_occupied_cells_row_major():
g = _grid([(0, 0, 50, 100)])
assert hm.occupied_cells(g) == [(0, 0), (1, 0)]
def test_cluster_cells_separate_diagonal():
g = _grid([(5, 5, 15, 15), (85, 85, 95, 95)])
assert sorted(hm.occupied_cells(g)) == [(0, 1), (1, 0)]
clusters = hm.cluster_cells(g)
assert len(clusters) == 2 # 对角相邻不算连通
boxes = sorted(cl["bbox"] for cl in clusters)
assert boxes == [(0.0, 0.0, 50.0, 50.0), (50.0, 50.0, 100.0, 100.0)]
def test_cluster_cells_dilate_merges_nearby():
g = _grid([(5, 5, 15, 15), (85, 85, 95, 95)])
clusters = hm.cluster_cells(g, dilate=1)
assert len(clusters) == 1
# 两格合并后 bbox 为两真实占用格的并集(膨胀出来的空格不参与)
assert clusters[0]["bbox"] == (0.0, 0.0, 100.0, 100.0)
def test_cluster_cells_sorted_by_count_desc():
# 左下格被 3 个实体命中,右上格 1 个
g = _grid([(0, 0, 10, 10), (0, 20, 10, 30), (0, 40, 10, 50), (90, 90, 100, 100)])
clusters = hm.cluster_cells(g)
assert clusters[0]["count"] == 3
assert clusters[0]["bbox"] == (0.0, 0.0, 50.0, 50.0)
def test_cluster_cells_empty_grid():
assert hm.cluster_cells(_grid([])) == []
# --- windows / stats --------------------------------------------------------
def test_grid_to_windows_applies_margin():
clusters = [{"bbox": (0.0, 0.0, 100.0, 50.0), "cells": [], "count": 1}]
(win,) = hm.grid_to_windows(clusters, margin=0.1)
assert win == {"min_x": -10.0, "min_y": -5.0, "max_x": 110.0, "max_y": 55.0}
def test_grid_to_windows_preserves_order():
clusters = [
{"bbox": (0.0, 0.0, 1.0, 1.0), "cells": [], "count": 9},
{"bbox": (2.0, 2.0, 3.0, 3.0), "cells": [], "count": 1},
]
wins = hm.grid_to_windows(clusters)
assert [w["min_x"] for w in wins] == [0.0, 2.0]
def test_grid_stats():
g = _grid([(0, 0, 50, 100)])
clusters = hm.cluster_cells(g)
st = hm.grid_stats(g, clusters)
assert st["rows"] == 2 and st["cols"] == 2
assert st["occupied"] == 2
assert st["occupied_ratio"] == 0.5
assert st["max_count"] == 1
assert st["clusters"] == 1
assert st["max_cluster_cells"] == 2
def test_grid_stats_without_clusters():
st = hm.grid_stats(_grid([]))
assert st["occupied"] == 0
assert "clusters" not in st
def test_occupied_boxes_returns_cell_world_boxes():
# 左下角格(row 1 / col 0)→ 世界 (0,0)-(50,50)
assert hm.occupied_boxes(_grid([(0, 0, 50, 50)])) == [(0.0, 0.0, 50.0, 50.0)]
def test_occupied_boxes_empty():
assert hm.occupied_boxes(_grid([])) == []