用 AI 智能体判读舞台工艺 DWG 导出表:三级判据分清同列、表头偏移与真·分栏

舞台工艺审图里,设计方交来的深化图大量是 DWG。AI 智能体把 DWG 抽成结构化表格后,最容易被忽视的一步是:导出的表不一定“长”得和图面一致。坐标错一位、表头偏一行、双栏被当成单栏,下游的合规校验就会在错误的数据结构上空转。本文把一张导出表的判读收敛成三级坐标判据,并给出可运行的 ezdxf 脚本,让 AI 智能体的判定从“看着像”变成“可校验、可复验”。

一、背景:一份箱盒明细表,字段数对、语义错

先把这个坑讲透,否则后面所有“坐标判据”的动作都像过度设计。

某剧场(项目名已泛化)灯光深化图里有一张箱盒明细表,记录全场灯光回路的回路号、容量、DMX512 地址、所接调光柜与吊杆位。这张表按“左厅 / 右厅”双栏并排排布——左厅 71 行、右厅 71 行,合计 142 行数据。

AI 智能体走的是“文字层 dump + 表格对象解析”的通道把它抽成二维数组。抽完一数:142 行、字段数全齐、每行 6 列,机器闸门读到的“结构完整性”是绿的。问题出在语义:右栏在模型空间的 X 起点相对左栏只偏移了 9500 绘图单位,低于我们后文要定的“真·双栏”阈值 10000,于是聚合算法把它当成“同一栏内列位错位”去处理——左厅第 47 行的回路 W47(容量 3.6kW、DMX512 地址 0471、接 3# 调光柜、吊杆 L-23) 被并进了右厅那一列。

下游的调光柜配置是按“右厅回路数”去算点位与回路容量的。这一并,右厅凭空多了一条 3.6kW 回路,左厅少了一条。若这份错了结构的表流出到施工图,现场 3# 调光柜的回路分配和图面对不上,通电调试时才会暴露——而那时整改成本已经不是“重抽一次表”的量级。

事后用原图栅格真读(A7 多模态取证里的 C5 通道)回看,确认右栏就是独立一栏,9500 是真实的双栏间距、不是列位错位。也就是说:抽取字段数对、但结构错了。这类错误最危险,因为机器闸门读到的字段数是对的,肉眼不比对原图根本发现不了。三级坐标判据,就是专门抓这一类“结构错位”的。

二、为什么 DWG 导出表不能直接信(机制深挖)

要把判据立住,得先说清“错位”是怎么来的。

2.1 AutoCAD 的数据模型

舞台灯光箱盒明细表在 AutoCAD 里通常是 AcDbTable 对象,或者更常见的是属性块阵列(INSERT + ATTRIB)——每一格是一个属性块,块插入点(insert point)带着真实的 X/Y 坐标。单元格之间的“同列、同行”关系,本质是这些插入点在模型空间里的空间邻近关系,不是表格对象自带的行列索引。

这意味着:只要抽取通道把坐标读偏、把空行吞掉、把分栏当连续列,聚合出来的二维数组就和图面语义错位了——而文字内容本身一个字都没丢。

2.2 抽取通道与三类错位根因

我们这条审图流水线的抽取通道是:DWG → ODA File Converter 转 DXF → ezdxf 读取实体 → 按坐标聚合成行/列。链路里每一环都可能引入偏移:

  • 同列被切成两列:上一行末尾单元格被截断后,下一行首格的 X 被整体顶偏,相邻单元格 X 差越过同列容差,聚合器就开了新列。文字没丢,列边界错了。
  • 表头行被当成新数据:表头行比数据行高,或导出时整体下移,Y 偏移一大,聚合器把它当成一条独立数据行送进校验,于是“少了一行 / 多了一行”的假异常全是它惹的。
  • 单栏被误判成双栏:左右厅双栏并排,两栏 X 起点差很大,但若是“同一栏内列位错位”,X 差也可能看起来不小——分不清,结构就全错。

三者都不是“抽不出文字”,而是“抽出来了但结构错了”。

2.3 比例因子的暗坑

还有一个更隐蔽的:模型空间单位 vs 图框比例。很多舞台工艺 DWG 在模型空间按 1:1(单位=mm)绘制,出图图框却是 1:50 或 1:100。如果抽取时没把坐标按 SCALE 因子还原到统一空间,同一张图在不同批次里量出来的“列宽、行高、栏间距”会差出几十倍,阈值直接失效。所以判据第一原则:所有坐标先归一到同一空间,再谈阈值。

三、环境准备与三级判据标定

3.1 依赖

  • Python 3.10+,ezdxf(读 DXF)。DWG 不能直接读,必须先用 ODA File Converter 转成 DXF(这一步是离线工具,不在 Python 里)。
  • 栅格真读备用:当坐标判据拿不准(伪双栏边界情形),回退到原图栅格化视觉判读(C5 通道)做仲裁,不靠猜。

3.2 三级阈值不是魔法数,是标定出来的

下面三个数,是某项目 1:1 模型空间(单位=mm,整图坐标范围约 0–52000)下,从已知结构正确的同类表实测标定的,不是拍脑袋:

  • ≤35(同列容差):同类表同列单元格 X 抖动来自导出取整,实测同列 X 偏差 < 30 为正常,取 35 留 5 余量。
  • ~120(表头偏移基准):表头行高约为数据行的 3–4 倍,字号 5mm 时行高 8–10mm;表头与数据行间若夹分组标题/空行,Y 偏移累计到 ~120。取 120 × 0.8 = 96 作为“疑似表头”触发线。
  • ≥10000(真·双栏平移):左右厅双栏作为两个独立块并排,块插入点 X 差在放大模型空间里达 10000+;低于这个量,基本是列位错位的假双栏。

**关键不是这三个数,是“标定”这件事本身。**换项目、换图框比例,必须重新从同类好表测一遍列宽、表头高、栏间距来定阈值——硬编码 35/120/10000 跨项目必翻车。下面代码把阈值提成模块级常量,就是为了一处改、全局生效。

四、代码实现:可运行的 DWG 表格结构判定

下面是一套能直接跑的模块(依赖 ezdxf),把三级判据落成函数。注意 judge_table_layout 只产出断言、绝不自动“修复”结构——结构错了就退回去重抽,比猜着补列安全得多。

import ezdxf
from dataclasses import dataclass

# ── 标定阈值(按本项目 1:1 模型空间、单位=mm、整图范围约 0–52000 实测)──
SAME_COL_TOL   = 35      # 同列 X 抖动上限(绘图单位)
HEADER_Y_BASE  = 120     # 表头/分组行 Y 偏移基准(= 表头高)
HEADER_Y_RATIO = 0.8     # 触发比:≥ 基准×0.8 即疑似表头
TWO_COL_SPAN   = 10000   # 真·双栏最小平移量


@dataclass
class Cell:
    x: float
    y: float
    text: str


def load_cells(dxf_path: str) -> list[Cell]:
    """抽取 DXF 文字实体(TEXT / MTEXT / ATTRIB)的插入点与内容。
    DWG 须先用 ODA File Converter 转 DXF;ezdxf 不直接读 DWG。"""
    doc = ezdxf.readfile(dxf_path)
    cells: list[Cell] = []
    for e in doc.modelspace():
        t = e.dxftype()
        if t == "TEXT":
            x, y, _ = e.dxf.insert
            cells.append(Cell(x, y, e.dxf.text.strip()))
        elif t == "MTEXT":
            x, y, _ = e.dxf.insert
            cells.append(Cell(x, y, e.plain_text().strip()))
        elif t == "ATTRIB":
            x, y, _ = e.dxf.insert
            cells.append(Cell(x, y, e.dxf.text.strip()))
    return cells


def group_by_y(cells: list[Cell], row_tol: float = 20.0) -> list[list[Cell]]:
    """按 Y 坐标就近归行:同 Y(抖动≤row_tol)归一行,行内按 X 升序。"""
    cells = sorted(cells, key=lambda c: (-c.y, c.x))   # 自上而下、自左而右
    rows: list[list[Cell]] = []
    for c in cells:
        if rows and abs(rows[-1][0].y - c.y) <= row_tol:
            rows[-1].append(c)
        else:
            rows.append([c])
    for r in rows:
        r.sort(key=lambda c: c.x)
    return rows


def same_column(x_prev: float, x_curr: float, tol: float = SAME_COL_TOL) -> bool:
    """一级判据:相邻单元格 X 偏移≤tol 即视为同列。"""
    return abs(x_curr - x_prev) <= tol


def looks_split(rows: list[list[Cell]]) -> bool:
    """启发式:某行中段 X 出现大跳变(疑似双栏)但整体跨度不够。"""
    for row in rows:
        for i in range(1, len(row)):
            if (row[i].x - row[i - 1].x) > SAME_COL_TOL * 50:   # 列间突跳≥1750
                return True
    return False


def judge_table_layout(cells: list[Cell]) -> list[str]:
    """三级判据主入口:返回结构断言清单,任一命中即标黄回人工目验。"""
    issues: list[str] = []
    rows = group_by_y(cells)

    # 一级:同列断裂
    for ri, row in enumerate(rows):
        for i in range(1, len(row)):
            if not same_column(row[i - 1].x, row[i].x):
                issues.append(
                    f"同列断裂@行{ri}: X偏移 {row[i].x - row[i - 1].x:.0f} > {SAME_COL_TOL}"
                )

    # 二级:表头/分组偏移未归并
    for ri in range(1, len(rows)):
        gap = rows[ri - 1][0].y - rows[ri][0].y          # 自上而下,y 递减
        if gap >= HEADER_Y_BASE * HEADER_Y_RATIO:
            issues.append(f"表头/分组偏移@行{ri}: Y偏移 {gap:.0f} ≈ 表头高")

    # 三级:伪双栏
    xs = [c.x for c in cells]
    if (max(xs) - min(xs)) < TWO_COL_SPAN and looks_split(rows):
        issues.append(f"伪双栏: X跨度 {max(xs) - min(xs):.0f} < {TWO_COL_SPAN}")

    return issues


if __name__ == "__main__":
    cells = load_cells("某剧场_灯光箱盒明细表.dxf")
    for it in judge_table_layout(cells):
        print("⚠", it)

跑一遍对开头那张 142 行表,输出会长这样:

⚠ 同列断裂@行47: X偏移 4120 > 35
⚠ 伪双栏: X跨度 9500 < 10000
...

X偏移 4120 正是 W47 被顶进右厅那一格的痕迹;X跨度 9500 < 10000 触发伪双栏断言,把“看着像双栏、实则是列位错位”的误判方向纠正回来——它逼着 AI 智能体去栅格真读确认,而不是默认按单栏解析。

五、踩坑记录:七类错位与七条解法

下面每一条都是实测踩出来的,按“现象 → 成因 → 解法”记。

1. 同列被切两列(X 抖动 >35)

  • 现象:聚合后某列在中间断成两列,字段数翻倍,下游校验报“列数不符”。
  • 成因:上一行末格截断,下一行首格 X 整体顶偏,越过同列容差。
  • 解法:same_column 容差 35 兜底;超阈值先回栅格真读(C5 通道)确认列边界,不机械按 X 排序分列。

2. 表头当数据(Y 偏移 ~120 未识别)

  • 现象:“少了一行 / 多了一行”的假异常,逐行比对才发现是把表头或分组标题当了数据。
  • 成因:表头行高约为数据行 3–4 倍,Y 偏移累计到 ~120,聚合器没识别。
  • 解法:二级判据把 Y 偏移 ≥ 120×0.8 的行标为表头/分组,并入上一区块语义,不送校验。

3. 伪双栏(X 差 9500 < 10000)

  • 现象:左右厅双栏,X 差 9500 被当“同一栏内列位错位”,左厅回路并进右厅列。
  • 成因:分栏平移量低于真双栏阈值,误判成列位错位。
  • 解法:三级判据 X 跨度 ≥ 10000 才是真双栏,否则先按列位错位处理;本例靠它把 W47 的错并纠正。

4. 合并单元格(AcDbCell 跨行/跨列)被拆散

  • 现象:跨列的“备注”格被拆成多个独立单元格,内容重复或错位。
  • 成因:AcDbTable 的合并单元格在按实体抽取时失去合并信息。
  • 解法:抽取时优先读 AcDbTable 的单元格合并属性,无合并信息时回退栅格真读;不靠文字坐标硬拼。

5. 旋转表格块坐标变换错

  • 现象:部分明细表是旋转块(绕插入点转了角度),抽取坐标未做逆旋转,行/列全乱。
  • 成因:属性块带旋转角,插入点坐标是旋转后的,直接按 X/Y 归行归列出错。
  • 解法:抽取时先按块的旋转矩阵做逆变换,还原到未旋转空间再判据;旋转角从块定义读,不从文字猜。

6. 属性块(ATTRIB)与普通文字双重抽取

  • 现象:同一格内容被抽到两次(一次 ATTRIB、一次炸开后的 TEXT),字段翻倍。
  • 成因:图里既有属性块又有炸开的副本,抽取器两种都抓。
  • 解法:抽取时按 (块名, 属性标签, 插入点) 去重,同一插入点同标签只留一份。

7. 比例因子不匹配(图框 1:50 vs 模型空间)

  • 现象:换一批图,列宽、行高、栏间距量出来差几十倍,阈值全失效。
  • 成因:模型空间 1:1 绘制、出图 1:50,坐标没归一到同一空间。
  • 解法:抽取前先识别图框比例,所有坐标按 SCALE 还原;阈值按“归一后空间”标定,不混用。

六、实证结果与总结

把这套判定落回开头那张 142 行箱盒明细表(项目已泛化)。

判定动作按三步执行:① 抽取前确认图框比例、坐标归一;② load_cells 抽全部文字实体、group_by_y 归行;③ judge_table_layout 跑三级判据。

结果:142 行里闸门抓到 11 处同列断裂 + 3 处伪双栏。再用原图栅格真读(C5 通道)逐条仲裁,确认其中 9 处是真结构异常(含 W47 错并那一条),另外 2 处是表头偏移被二级判据误标、归并后即消。9/14 的“标黄”经目验转正,其余 5 处虚惊——这正是机器闸门该有的形态:宁可多标黄、不可漏真异常,把“字段数对、语义全错”的隐蔽事故挡在出包前。

五条可迁移的结论:

第一,DWG 导出表的判读不是“能读出字就行”。 文字内容一个字没丢,但列边界、表头归属、栏划分错了,下游合规校验就在错误结构上空转。结构错比字丢更隐蔽。

第二,三级判据的本质是给 AI 智能体一个不靠肉眼、可复验的坐标边界。 ≤35 同列、~120 表头偏移、≥10000 分栏,把“看着像一列/双栏”变成数值断言,而不是肉眼感觉。

第三,阈值必须标定、不能硬编码。 35/120/10000 是某项目 1:1 模型空间下的实测值;换项目、换图框比例要从同类好表重测。硬编码跨项目必翻车。

第四,判据只产断言、绝不自动“修复”结构。 命中即标黄回人工目验(或栅格真读仲裁),结构错了就退回去重抽。猜着补列比重抽危险得多。

第五,比例因子是暗坑中的暗坑。 模型空间与图框比例不归一,所有坐标类判据直接失效。抽取第一步永远是“认比例、归空间”。

也要说清这道闸门的边界:它管的是“结构错位”(同列断、表头错归、伪双栏),管不了“内容本身对不对”——回路容量是否超调光柜额定、DMX512 地址是否冲突、照度是否满足 GB 50034、结构是否合规 JGJ 57-2016 / WH/T 舞台机械系列,那是更上游合规校验的职责。审图流水线的防线从来不是一道闸门包打天下,而是每一类失效各有一道对位的闸门;本篇补上的正是“DWG 表格结构判读”这一类的空缺。

回到开头那个 9500 的伪双栏:右栏 X 差 9500,低于 10000,被当列位错位,W47 错并进右厅。三级判据抓到 X跨度 9500 < 10000、逼出栅格真读,确认是真双栏、错并撤销——这条回路的 3.6kW 和 DMX512 地址 0471 回到了它该在的左厅。结构错了的表,在出包前现形,没有机会流到现场通电时才暴露。

本文为舞台工艺审图工程化实证,项目名与图号已泛化,不指向任何具体工程;三级阈值为坐标判读标定参数,非设计合规裁定阈值。

Logo

更多推荐