feat(review): 截屏前最大化绘图区 + 空瓦片过滤覆盖回退路径
新增 MCP 工具 set_drawing_area(mode="max"|"restore"):最大化主窗口 + CLEANSCREENON (隐藏功能区/命令行/面板)。实测绘图区 2110x462 -> 3310x1084(面积 3.68x), restore 精确回位(窗口矩形/最大化态/绘图区尺寸全部还原)。 坑:CLEANSCREENOFF 是异步命令,AutoCAD 会按「进入干净屏幕时的布局」重设窗口几何, 把同步的 SetWindowPlacement 覆盖掉(表现为窗口停在最大化)。解法是插件内 SetTimer 延迟 700ms 再压回;实测窗口在 0.5~1.0s 之间收敛,故调用方等待取 1.5s。 取证层: - 新增 maximized_drawing_area(ctx) 上下文管理器,__main__ 用它包住整个取证循环 (多图框只进出一次);受 DRAWING_AREA_MAXIMIZE 控制,工具缺失/调用失败静默降级, restore 失败不掩盖业务异常。 - 内容过滤统一到规划与回退两条路径,共用同一谓词;过滤后为空则退回原集合。 - 判定改用「实体 bbox」而非「占用格」:格子量化会把跨界内容算进相邻格,反而更粗 (实测实体 bbox 正好压象限边界时 4 个象限全判有内容)。heatmap.occupied_boxes 随之删除。 实测(真实图纸 A0/A1/A2 三图框):截图张数 12/8/4 -> 8/5/4;pytest 296 passed。 新增 verify_drawing_area.py 作为该工具的验收脚本。
This commit is contained in:
1 parent
9c4ab62dc1
commit
1e5bff268a
15 files changed
+657
-50
No files matched your search
+17
-14
@@ -64,20 +64,23 @@ def run_review(ctx, args):
|
||||
|
||||
print("[2/4] 取证:逐图框采集(串行) ...")
|
||||
evidences = []
|
||||
for idx, frame in enumerate(frames, 1):
|
||||
print(
|
||||
f" - 图框 {idx}/{len(frames)} kind={frame.get('kind')} "
|
||||
f"sheet={frame.get('sheet_size')}"
|
||||
)
|
||||
ev = evidence_phase.collect(ctx, idx, frame)
|
||||
if ev is None:
|
||||
print(" [跳过] 缺少有效包围盒")
|
||||
continue
|
||||
evidences.append(ev)
|
||||
print(
|
||||
f" 图元 {(ev.get('entities') or {}).get('total')} 条,"
|
||||
f"截图 {len(evidence_images(ev))} 张"
|
||||
)
|
||||
# 截屏前把绘图区最大化(窗口铺满 + 干净屏幕),采集完在 finally 里还原;
|
||||
# 旧版插件没有该工具时静默降级。
|
||||
with evidence_phase.maximized_drawing_area(ctx):
|
||||
for idx, frame in enumerate(frames, 1):
|
||||
print(
|
||||
f" - 图框 {idx}/{len(frames)} kind={frame.get('kind')} "
|
||||
f"sheet={frame.get('sheet_size')}"
|
||||
)
|
||||
ev = evidence_phase.collect(ctx, idx, frame)
|
||||
if ev is None:
|
||||
print(" [跳过] 缺少有效包围盒")
|
||||
continue
|
||||
evidences.append(ev)
|
||||
print(
|
||||
f" 图元 {(ev.get('entities') or {}).get('total')} 条,"
|
||||
f"截图 {len(evidence_images(ev))} 张"
|
||||
)
|
||||
|
||||
if args.plan_only:
|
||||
path = _dump_evidence(evidences)
|
||||
|
||||
@@ -155,6 +155,19 @@ HEATMAP_CLUSTER_DILATE = int(os.environ.get("HEATMAP_CLUSTER_DILATE", "0"))
|
||||
# 丢弃 bbox 面积超过图框该比例的实体(去图框边框/图签块)
|
||||
HEATMAP_MAX_AREA_FRAC = float(os.environ.get("HEATMAP_MAX_AREA_FRAC", "0.5"))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 绘图区最大化(截屏前把 CAD 窗口铺满屏幕 + 进入干净屏幕)
|
||||
# ---------------------------------------------------------------------------
|
||||
# 是否在取证前最大化绘图区。实测绘图区面积 2110x462 → 3310x1084(3.68x),
|
||||
# 同样的视觉 token 预算下细节更清楚。工具不存在(旧版插件)时**静默降级**。
|
||||
DRAWING_AREA_MAXIMIZE = _read_bool("DRAWING_AREA_MAXIMIZE", True)
|
||||
# 进入干净屏幕后等待 CAD 渲染 / 重排窗口的秒数
|
||||
DRAWING_AREA_ENTER_WAIT = float(os.environ.get("DRAWING_AREA_ENTER_WAIT", "1.5"))
|
||||
# 还原后等待的秒数。插件内部是「700ms 延迟 + SetWindowPlacement」(CLEANSCREENOFF 是
|
||||
# 异步命令,会把窗口几何改回它记住的布局,必须等它跑完再压回),实测窗口在 0.5~1.0s
|
||||
# 之间收敛,故取 1.5s 留余量——太小会在界面还没回位时就开始下一步。
|
||||
DRAWING_AREA_RESTORE_WAIT = float(os.environ.get("DRAWING_AREA_RESTORE_WAIT", "1.5"))
|
||||
|
||||
|
||||
def has_api_key() -> bool:
|
||||
return bool(API_KEY.strip())
|
||||
@@ -246,15 +246,6 @@ def _cell_box(grid, r, c):
|
||||
return (x1, y_top - ch, x1 + cw, y_top)
|
||||
|
||||
|
||||
def occupied_boxes(grid, min_count=1):
|
||||
"""占用格的**世界 bbox** 列表。
|
||||
|
||||
供瓦片规划做「内容过滤」:簇 bbox 是**包围盒**,对 L 形/斜向分布会含大片空白,
|
||||
故瓦片切分后还要按这些格子判断该瓦片是否真有内容。
|
||||
"""
|
||||
return [_cell_box(grid, r, c) for r, c in occupied_cells(grid, min_count)]
|
||||
|
||||
|
||||
def grid_to_windows(clusters, margin=0.0):
|
||||
"""把簇 bbox 转成 zoom 窗口列表(保持输入顺序,即实体计数降序)。"""
|
||||
out = []
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(未来在对应分支产出**同形** evidence,下游无需改动)。
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import math
|
||||
import time
|
||||
|
||||
@@ -16,6 +17,44 @@ from ..mcp_client import result_images, result_json
|
||||
|
||||
_LABEL_OVERALL = "overall"
|
||||
|
||||
# 绘图区最大化用的工具名(旧版插件没有此工具 → 静默降级)
|
||||
_TOOL_DRAWING_AREA = "set_drawing_area"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maximized_drawing_area(ctx):
|
||||
"""截屏前最大化绘图区(窗口铺满屏幕 + 干净屏幕),退出时还原。
|
||||
|
||||
实测绘图区像素 2110x462 → 3310x1084(3.68x);在同样的长边下采样预算下,
|
||||
有效像素更多、细节更清楚,自适应瓦片张数也更少。
|
||||
|
||||
- 受 `config.DRAWING_AREA_MAXIMIZE` 控制;
|
||||
- 工具不存在(旧版插件)/ 调用失败 → **静默降级**:照常取证,只是绘图区小一些;
|
||||
- 退出时一定尝试还原;**还原失败不掩盖业务异常**(不 raise)。
|
||||
"""
|
||||
if not config.DRAWING_AREA_MAXIMIZE:
|
||||
yield
|
||||
return
|
||||
|
||||
entered = False
|
||||
try:
|
||||
if ctx.tools.has(_TOOL_DRAWING_AREA):
|
||||
ctx.tools.call(_TOOL_DRAWING_AREA, {"mode": "max"})
|
||||
entered = True
|
||||
time.sleep(config.DRAWING_AREA_ENTER_WAIT)
|
||||
except Exception: # noqa: BLE001 - 界面优化失败不应中断审查
|
||||
entered = False
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if entered:
|
||||
try:
|
||||
ctx.tools.call(_TOOL_DRAWING_AREA, {"mode": "restore"})
|
||||
time.sleep(config.DRAWING_AREA_RESTORE_WAIT)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
def bbox_to_window(bbox):
|
||||
"""[minx,miny,maxx,maxy] -> window 对象。"""
|
||||
@@ -101,6 +140,32 @@ def _text_boxes(entities, texts, dimensions):
|
||||
return boxes
|
||||
|
||||
|
||||
def _content_predicate(boxes):
|
||||
"""构造「该窗口内是否有实体」谓词;无实体返回 None。
|
||||
|
||||
用于剔除空白瓦片:簇 / 象限的 bbox 只是**包围盒**,L 形或斜向分布会含大片空白。
|
||||
这里直接按**实体 bbox** 判断——比先量化成占用格更精确(格子的跨界膨胀会把
|
||||
相邻空白瓦片也判成有内容)。接触即算有内容(保守,宁可多拍不漏)。
|
||||
"""
|
||||
boxes = [b for b in (boxes or []) if b]
|
||||
if not boxes:
|
||||
return None
|
||||
|
||||
def _has_content(win):
|
||||
box = (win["min_x"], win["min_y"], win["max_x"], win["max_y"])
|
||||
return any(tiling.boxes_intersect(box, b) for b in boxes)
|
||||
|
||||
return _has_content
|
||||
|
||||
|
||||
def _apply_keep(regions, keep):
|
||||
"""按 keep 过滤窗口;过滤后为空则**退回原集合**(宁可多拍也不漏)。"""
|
||||
if keep is None or not regions:
|
||||
return regions
|
||||
kept = [w for w in regions if keep(w)]
|
||||
return kept or regions
|
||||
|
||||
|
||||
def _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px):
|
||||
"""尝试按实体分布规划瓦片窗口;任何条件不满足都返回 [](调用方退回固定策略)。
|
||||
|
||||
@@ -128,18 +193,11 @@ def _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px):
|
||||
flags = tiling.cluster_text_flags(clusters, text_boxes)
|
||||
text_idx = {i for i, is_text in enumerate(flags) if is_text}
|
||||
|
||||
# 内容过滤:簇 bbox 是包围盒,L 形/斜向分布会含大片空白 → 按**占用格**剔除空瓦片
|
||||
occ = heatmap.occupied_boxes(grid)
|
||||
|
||||
def _has_content(win):
|
||||
box = (win["min_x"], win["min_y"], win["max_x"], win["max_y"])
|
||||
return any(tiling.boxes_intersect(box, b) for b in occ)
|
||||
|
||||
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,
|
||||
keep=_has_content,
|
||||
keep=_content_predicate(boxes),
|
||||
)
|
||||
|
||||
|
||||
@@ -149,7 +207,8 @@ def capture_images(ctx, window, *, tiles=0, max_images=None, frame=None,
|
||||
|
||||
- 整体 label="overall",局部 label="tile-1"..;行优先。
|
||||
- 局部窗口优先由 `_plan_tiles` 按**实体分布**给出(空白区不出图);
|
||||
不可用时退回 `sub_windows(window, tiles)`(行为与旧版完全一致)。
|
||||
不可用时退回 `sub_windows(window, tiles)`,**同样按内容过滤**空白象限
|
||||
(过滤后为空则退回未过滤集合,保证不漏拍)。
|
||||
- 结束时**复位到整体视口**,保证后续流程从整幅视图起手。
|
||||
- 受 max_images(默认 config.EVIDENCE_MAX_IMAGES)截断。
|
||||
"""
|
||||
@@ -166,10 +225,14 @@ def capture_images(ctx, window, *, tiles=0, max_images=None, frame=None,
|
||||
view_px = image_size_b64(overall[0]) if overall else None
|
||||
_add(overall, _LABEL_OVERALL)
|
||||
|
||||
# 局部:优先自适应瓦片,失败退回等分象限
|
||||
# 内容过滤:空白区不出图。按**实体 bbox** 判定,规划与回退两条路径共用同一谓词;
|
||||
# 实体数据被截断时不可信,放弃过滤(宁可多拍也不漏)。
|
||||
keep = None if truncated else _content_predicate(boxes)
|
||||
|
||||
# 局部:优先自适应瓦片,失败退回等分象限(回退路径同样按内容过滤)
|
||||
regions = _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px)
|
||||
if not regions:
|
||||
regions = sub_windows(window, tiles)
|
||||
regions = _apply_keep(sub_windows(window, tiles), keep)
|
||||
|
||||
for i, sub in enumerate(regions, 1):
|
||||
if len(images) >= max_images:
|
||||
|
||||
Reference in new issue
Block a user