feat(tiling): 自适应瓦片规划器 + VLM 分辨率离线标定
背景:逐图框审查固定「整体 + 2x2 象限」截图,大图幅(A0/A1)读不清细节。 改为按**实体分布**自适应决定截几张、截哪里。 - 新增 dwg_review/tiling.py(纯函数): SHEET_SIZES_MM / frame_world_size / frame_scale(sheet_scale>0 用之,否则由 bbox/纸张反算,否则 default)/ effective_px(与 downsample 同规则)/ world_per_px / is_text_type(标准名 + 自定义类名含 TEXT/LABEL/DIM/NOTE/TAG/ ANNO/TITLE)/ cluster_text_flags / tiles_for_box / subdivide / plan_regions (文字簇优先、空白区不出图、max_tiles 截断、窗口夹到图框范围) - 新增 verify_vlm_resolution_offline.py:PIL 合成类图纸图(精确像素字高 + 可控线间距/线宽/虚线)→ 问 VLM 强约束 JSON → 定 R_text / R_line。 实测:R_text=12px(黑底;白底 10px);R_line=6px。 另:实测该模型**无法可靠计数**(6 条线在 2~10px 间距下答 4/10/4/5/4/4), 故线条判据改用「1 条 vs 2 条」二选一。 - config 新增 TILING_ENABLED / TEXT_HEIGHT_MM / TEXT_READABLE_PX=12 / LINE_READABLE_PX=6 / TILE_MAX_COUNT / TILE_OVERLAP / HEATMAP_CELL_DIV / HEATMAP_CLUSTER_DILATE / HEATMAP_MAX_AREA_FRAC;EVIDENCE_DETAIL_TILES 与 EVIDENCE_MAX_IMAGES 保留作回退路径参数 - llm_client 新增 image_size_b64(测量绘图区实际像素,决定单张覆盖范围) - phases/evidence.capture_images 支持瓦片规划:先截整体图并测像素 → plan_regions 成功用规划窗口,否则退回 sub_windows(行为零变化); 末尾始终复位整体视口。collect_frame 传实体/文字 bbox;任一信封 total>returned(截断)→ 退回固定策略,避免误删有内容的瓦片 - 测试:tests/test_tiling.py(28 项)、tests/test_evidence.py 新增规划路径与 降级用例、test_config / test_llm_client 补新常量与 image_size_b64; 全量 283 项全绿
This commit is contained in:
1 parent
9b8a2329df
commit
cba05b0c65
9 files changed
+1246
-10
No files matched your search
@@ -10,7 +10,8 @@
|
||||
import math
|
||||
import time
|
||||
|
||||
from .. import config, evidence_model
|
||||
from .. import config, evidence_model, heatmap, tiling
|
||||
from ..llm_client import image_size_b64
|
||||
from ..mcp_client import result_images, result_json
|
||||
|
||||
_LABEL_OVERALL = "overall"
|
||||
@@ -66,10 +67,80 @@ def _zoom_and_shot(ctx, window, margin=0.02):
|
||||
return images[0]
|
||||
|
||||
|
||||
def capture_images(ctx, window, *, tiles=0, max_images=None):
|
||||
"""采集视觉证据:整体一张 + 局部 tiles 张。
|
||||
def _is_truncated(*envelopes):
|
||||
"""任一信封 `total > len(items)` 即视为**被截断**(数据不全)。"""
|
||||
for env in envelopes:
|
||||
if not isinstance(env, dict):
|
||||
continue
|
||||
items = env.get("items")
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
try:
|
||||
total = int(env.get("total") or len(items))
|
||||
except (TypeError, ValueError):
|
||||
total = len(items)
|
||||
if total > len(items):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _text_boxes(entities, texts, dimensions):
|
||||
"""收集「文字类」实体 bbox。
|
||||
|
||||
标准文字 / 标注信封(`get_text_content` / `get_dimensions`)全收;
|
||||
再加**类名含 text/label/dim 等关键词**的自定义实体(`tiling.is_text_type`)。
|
||||
"""
|
||||
boxes = list(heatmap.entity_boxes(texts))
|
||||
boxes.extend(heatmap.entity_boxes(dimensions))
|
||||
items = entities.get("items") if isinstance(entities, dict) else None
|
||||
for it in items or []:
|
||||
if isinstance(it, dict) and tiling.is_text_type(it.get("type")):
|
||||
b = heatmap.norm_box(it.get("bbox"))
|
||||
if b is not None:
|
||||
boxes.append(b)
|
||||
return boxes
|
||||
|
||||
|
||||
def _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px):
|
||||
"""尝试按实体分布规划瓦片窗口;任何条件不满足都返回 [](调用方退回固定策略)。
|
||||
|
||||
降级条件:规划被关闭 / 实体数据被截断 / 缺图框 / 无实体 / 拿不到绘图区像素 /
|
||||
比例未知(`frame_scale` 返 0)。
|
||||
"""
|
||||
if not config.TILING_ENABLED or truncated or not frame or not boxes or not view_px:
|
||||
return []
|
||||
scale = tiling.frame_scale(frame)
|
||||
s_text = tiling.world_per_px(scale, config.TEXT_HEIGHT_MM, config.TEXT_READABLE_PX)
|
||||
# 线条区只需看清走向:同以「最小关注特征尺寸」(纸面 TEXT_HEIGHT_MM)为基准,
|
||||
# 但像素门槛更低(R_line < R_text)→ 覆盖范围更大、瓦片更少。
|
||||
s_line = tiling.world_per_px(scale, config.TEXT_HEIGHT_MM, config.LINE_READABLE_PX)
|
||||
if s_text <= 0 or s_line <= 0:
|
||||
return []
|
||||
eff = tiling.effective_px(view_px, config.IMAGE_MAX_EDGE)
|
||||
if eff[0] <= 0 or eff[1] <= 0:
|
||||
return []
|
||||
|
||||
cell = heatmap.div_to_cell(window, config.HEATMAP_CELL_DIV)
|
||||
grid = heatmap.build_grid(window, boxes, cell)
|
||||
clusters = heatmap.cluster_cells(grid, dilate=config.HEATMAP_CLUSTER_DILATE)
|
||||
if not clusters:
|
||||
return []
|
||||
flags = tiling.cluster_text_flags(clusters, text_boxes)
|
||||
text_idx = {i for i, is_text in enumerate(flags) if is_text}
|
||||
return tiling.plan_regions(
|
||||
frame, clusters,
|
||||
text_clusters=text_idx, s_text=s_text, s_line=s_line, view_px=eff,
|
||||
max_tiles=config.TILE_MAX_COUNT, overlap=config.TILE_OVERLAP,
|
||||
)
|
||||
|
||||
|
||||
def capture_images(ctx, window, *, tiles=0, max_images=None, frame=None,
|
||||
boxes=None, text_boxes=None, truncated=False):
|
||||
"""采集视觉证据:整体一张 + 局部若干张(自适应瓦片优先,否则等分象限回退)。
|
||||
|
||||
- 整体 label="overall",局部 label="tile-1"..;行优先。
|
||||
- 局部窗口优先由 `_plan_tiles` 按**实体分布**给出(空白区不出图);
|
||||
不可用时退回 `sub_windows(window, tiles)`(行为与旧版完全一致)。
|
||||
- 结束时**复位到整体视口**,保证后续流程从整幅视图起手。
|
||||
- 受 max_images(默认 config.EVIDENCE_MAX_IMAGES)截断。
|
||||
"""
|
||||
@@ -81,11 +152,17 @@ def capture_images(ctx, window, *, tiles=0, max_images=None):
|
||||
if shot and len(images) < max_images:
|
||||
images.append(evidence_model.make_image(shot[0], shot[1], label))
|
||||
|
||||
# 整体
|
||||
_add(_zoom_and_shot(ctx, window), _LABEL_OVERALL)
|
||||
# 整体图:既是证据,也用于测量绘图区**实际像素**(决定单张截图的世界覆盖范围)
|
||||
overall = _zoom_and_shot(ctx, window)
|
||||
view_px = image_size_b64(overall[0]) if overall else None
|
||||
_add(overall, _LABEL_OVERALL)
|
||||
|
||||
# 局部(等分象限)
|
||||
for i, sub in enumerate(sub_windows(window, tiles), 1):
|
||||
# 局部:优先自适应瓦片,失败退回等分象限
|
||||
regions = _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px)
|
||||
if not regions:
|
||||
regions = sub_windows(window, tiles)
|
||||
|
||||
for i, sub in enumerate(regions, 1):
|
||||
if len(images) >= max_images:
|
||||
break
|
||||
_add(_zoom_and_shot(ctx, sub), f"tile-{i}")
|
||||
@@ -115,8 +192,19 @@ def collect_frame(ctx, index, frame):
|
||||
ctx.tools.call("get_polylines_info", {"window": win, "limit": limit}), {}
|
||||
)
|
||||
|
||||
# b. 视觉:整体 + 可选局部(多图)
|
||||
images = capture_images(ctx, win, tiles=config.EVIDENCE_DETAIL_TILES)
|
||||
# b. 视觉:整体 + 局部(优先按实体分布自适应瓦片;数据截断则退回固定策略)
|
||||
truncated = _is_truncated(entities, texts, dimensions, blocks, polylines)
|
||||
boxes = heatmap.drop_oversized(
|
||||
heatmap.entity_boxes(entities), win, config.HEATMAP_MAX_AREA_FRAC
|
||||
)
|
||||
images = capture_images(
|
||||
ctx, win,
|
||||
tiles=config.EVIDENCE_DETAIL_TILES,
|
||||
frame=frame,
|
||||
boxes=boxes,
|
||||
text_boxes=_text_boxes(entities, texts, dimensions),
|
||||
truncated=truncated,
|
||||
)
|
||||
|
||||
return {
|
||||
"source_type": evidence_model.source_type_of(frame),
|
||||
|
||||
Reference in new issue
Block a user