Compare commits

...

2 Commits

Author SHA1 Message Date
zwt13703 4983048268 feat: 新增 CAD 元素识别模块,支持柱子/文字/墙体候选检测
- 新增 element_detector.py,包含 ColumnDetector、TextExtractor、WallCandidateDetector
- ColumnDetector:通过 INSERT 块名关键词识别柱子候选
- TextExtractor:提取 TEXT/MTEXT 内容、坐标、图层
- WallCandidateDetector:按长度阈值筛选 LINE/LWPOLYLINE,排除标注图层
- 采用 OOP 设计,所有规则可配置,保留原始 handle 追溯
- 输出 element_detection.json 和 element_report.txt
- 更新 main.py 集成元素识别步骤
2026-07-08 14:39:54 +08:00
zwt13703 9feeb14910 feat: 拆分 DXF 解析为模块化结构,新增图层分析和实体分析模块
- 新增 layer_analysis.py:图层实体统计 + 角色推断(22条启发式规则)
- 新增 entity_analysis.py:实体类型统计、INSERT 块聚合、文本提取
- 重构 main.py:调度分析模块,输出 {文件名}_cad_analysis.json
- 新增 README.md 使用说明
- 输出文件名使用模型名作为前缀,避免多次测试覆盖
2026-07-08 14:31:27 +08:00
6 changed files with 909 additions and 394 deletions
+24
View File
@@ -15,3 +15,27 @@
- 布局/图纸空间列表
4. 运行程序验证,成功解析目标 DXF 文件,输出 20 个实体(9 个 DIMENSION、6 个 MTEXT、5 个 LWPOLYLINE)、13 个块定义、3 个布局。
- **执行结果**: 程序正常运行,完整输出了 samples/decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf 的全部结构和内容信息。
## 会话 ID: 2
- [2026-07-08 13:58]
- **执行原因**: 将 main.py 单文件脚本拆分为模块化结构(layer_analysis.py + entity_analysis.py + main.py),面向第二阶段墙体识别、面积计算等功能扩展。
- **执行过程**:
1. 创建 layer_analysis.py:遍历图层统计实体数量和类型,基于常见中英文命名规则进行角色推断(墙体/门窗/标注/文字/柱/设备/填充等)。
2. 创建 entity_analysis.py:实体类型统计、INSERT 按块名聚合(含插入坐标/旋转/缩放)、TEXT/MTEXT 文本内容提取,保留 extract_entity_props() 逻辑。
3. 重构 main.py:读取 DXF 后调度两个分析模块,组装输出 output/cad_analysis.json,保留终端摘要打印。
4. 创建 README.md 使用说明文档。
5. 运行验证:成功解析洗浴中心C-48.dxf(719个实体、12个图层、11种块、71条文本),JSON 输出格式正确。
- **执行结果**: 模块化拆分完成,代码结构清晰,JSON 输出符合 Task1 规范,为后续墙体识别等功能扩展奠定基础。
## 会话 ID: 3
- [2026-07-08 14:39]
- **执行原因**: 实现 Task2 —— CAD 基础构件候选识别 Demo,从 DXF 数据中识别柱子、文字和墙体候选。
- **执行过程**:
1. 创建 element_detector.py,采用 OOP 设计,包含三个独立识别器:
- ColumnDetector:通过 INSERT 块名关键词(柱/column/COL)识别柱子候选
- TextExtractor:提取所有 TEXT/MTEXT 的内容、坐标、图层
- WallCandidateDetector:分析 LINE/LWPOLYLINE,按长度阈值(>=500)筛选,排除标注图层(DIM/TEXT/DEFPOINTS
2. ElementDetector 作为编排器统一调度,所有规则可配置。
3. 更新 main.py 集成元素识别步骤,输出 element_detection.json 和 element_report.txt。
4. 修复 ezdxf Vec3 切片兼容问题。
- **执行结果**: 成功识别 6 个柱子候选(柱子01)、71 条文字、231 条墙体候选(179 LINE + 52 LWPOLYLINE),输出文件格式符合 Task2 规范。
+83
View File
@@ -0,0 +1,83 @@
# DXF 解析工具
解析 DXF 格式的 CAD 图纸文件,输出结构化的图层分析、实体统计、块引用和文本信息。
## 目录结构
```
experiments/dxf-parser/
├── main.py # 入口脚本
├── layer_analysis.py # 图层分析模块
├── entity_analysis.py # 实体分析模块
├── requirements.txt # 依赖
├── output/ # 输出目录
│ └── {文件名}_cad_analysis.json
└── README.md
```
## 安装依赖
```bash
cd experiments/dxf-parser
pip install -r requirements.txt
```
## 使用方式
1. 将要解析的 DXF 文件放入 `samples/` 目录
2. 修改 `main.py` 中的 `DXF_FILE` 变量指向目标文件
3. 运行:
```bash
python main.py
```
## 输出说明
终端会打印解析摘要(图层、实体类型统计、块引用、文本内容),同时在 `output/{文件名}_cad_analysis.json` 中生成结构化 JSON
```json
{
"file_info": {
"filename": "洗浴中心C-48.dxf",
"dxf_version": "AC1032",
"encoding": "gbk",
"units": null,
"extents": { "min": [...], "max": [...] },
"total_entities": 719
},
"layers": [
{
"name": "DOOR",
"entity_count": 52,
"entity_types": ["INSERT", "LINE"],
"suggested_role": "门窗"
}
],
"entities": {
"LINE": 321,
"TEXT": 71
},
"blocks": [
{
"block_name": "A$C6B90F135",
"count": 8,
"inserts": [...]
}
],
"texts": [
{
"handle": "2A",
"layer": "TEXT",
"type": "TEXT",
"content": "办公室",
"insert": [150, 300]
}
]
}
```
## 模块说明
- **layer_analysis.py**:遍历所有图层,统计每层实体数量和类型,基于常见命名规则推断图层角色(仅作建议)
- **entity_analysis.py**:统计模型空间中各实体类型数量,对 INSERT 实体按块名聚合,提取所有 TEXT/MTEXT 文本内容
+445
View File
@@ -0,0 +1,445 @@
"""CAD 基础构件候选识别模块 —— 从 DXF 数据中识别柱子、文字和墙体候选。
设计原则:
- 面向对象,每个识别模块独立
- 所有规则可配置
- 保留原始实体 handle 方便追溯
- 不假设图层命名规范,仅做启发式匹配
"""
import math
import json
from pathlib import Path
from typing import Optional
import ezdxf
# ============================================================
# 配置常量(可修改)
# ============================================================
# 柱子识别:INSERT 块名中包含以下关键词之一
COLUMN_KEYWORDS = ["", "column", "COL"]
# 墙体候选:最小线段长度(图纸单位,通常为毫米)
WALL_MIN_LENGTH = 500
# 墙体候选:排除的图层关键词(标注、文字等非建筑图层)
WALL_EXCLUDE_LAYERS = ["DIM", "TEXT", "DEFPOINTS", "标注", "NOTE", "PUB_DIM", "AXIS"]
# 墙体候选:优先包含的图层关键词(空列表 = 不限制)
WALL_INCLUDE_LAYERS = []
# ============================================================
# 柱子识别器
# ============================================================
class ColumnDetector:
"""从 INSERT 实体中识别候选柱子。
规则:块名包含柱/column/COL 等关键词。
"""
def __init__(self, keywords: Optional[list[str]] = None):
"""
Args:
keywords: 块名匹配关键词列表,默认使用 COLUMN_KEYWORDS
"""
self.keywords = keywords or COLUMN_KEYWORDS
def detect(self, msp) -> list[dict]:
"""遍历模型空间,检测柱子候选。
Args:
msp: ezdxf 模型空间对象
Returns:
list[dict]: 柱子候选列表
"""
columns = []
for entity in msp:
if entity.dxftype() != "INSERT":
continue
block_name = entity.dxf.name
if self._match(block_name):
insert = entity.dxf.insert
columns.append({
"name": block_name,
"position": {
"x": round(insert[0], 4),
"y": round(insert[1], 4),
},
"source": "INSERT",
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
"rotation": round(entity.dxf.rotation, 2),
"scale": [
round(entity.dxf.xscale, 2),
round(entity.dxf.yscale, 2),
round(entity.dxf.zscale, 2),
],
})
return columns
def _match(self, block_name: str) -> bool:
"""检查块名是否匹配柱子关键词。"""
upper = block_name.upper()
for kw in self.keywords:
if kw.upper() in upper or kw in block_name:
return True
return False
# ============================================================
# 文字提取器
# ============================================================
class TextExtractor:
"""提取所有 TEXT / MTEXT 实体的文本内容及其坐标。"""
def extract(self, msp) -> list[dict]:
"""提取模型空间中所有文字实体。
Args:
msp: ezdxf 模型空间对象
Returns:
list[dict]: 文字信息列表
"""
texts = []
for entity in msp:
dxftype = entity.dxftype()
if dxftype not in ("TEXT", "MTEXT"):
continue
content = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
insert = entity.dxf.insert if hasattr(entity.dxf, "insert") else None
item = {
"content": content,
"type": dxftype,
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
}
if insert is not None:
item["x"] = round(insert[0], 4)
item["y"] = round(insert[1], 4)
if hasattr(entity.dxf, "height"):
item["height"] = round(entity.dxf.height, 2)
texts.append(item)
return texts
# ============================================================
# 墙体候选识别器
# ============================================================
class WallCandidateDetector:
"""从 LINE / LWPOLYLINE 中识别墙体候选线段。
规则:
1. 线段长度 >= min_length
2. 所在图层不是标注类图层
3. 可选:限制在主要建筑图层
"""
def __init__(
self,
min_length: float = WALL_MIN_LENGTH,
exclude_layers: Optional[list[str]] = None,
include_layers: Optional[list[str]] = None,
):
"""
Args:
min_length: 最小线段长度阈值
exclude_layers: 要排除的图层关键词列表
include_layers: 仅包含的图层关键词列表(空 = 不限制)
"""
self.min_length = min_length
self.exclude_layers = exclude_layers or WALL_EXCLUDE_LAYERS
self.include_layers = include_layers or WALL_INCLUDE_LAYERS
def detect(self, msp) -> list[dict]:
"""检测墙体候选线段。"""
candidates = []
for entity in msp:
dxftype = entity.dxftype()
layer = entity.dxf.layer
if self._is_excluded_layer(layer):
continue
if dxftype == "LINE":
start = entity.dxf.start
end = entity.dxf.end
length = math.dist([start[0], start[1]], [end[0], end[1]])
if length >= self.min_length:
candidates.append({
"entity_type": "LINE",
"handle": entity.dxf.handle,
"layer": layer,
"length": round(length, 2),
"start": [round(start[0], 4), round(start[1], 4)],
"end": [round(end[0], 4), round(end[1], 4)],
})
elif dxftype == "LWPOLYLINE":
try:
pts = entity.get_points()
except Exception:
continue
if len(pts) < 2:
continue
# 计算总长度和最长段
total_length = 0.0
max_seg_length = 0.0
segments = []
for i in range(len(pts) - 1):
seg_len = math.dist([pts[i][0], pts[i][1]], [pts[i + 1][0], pts[i + 1][1]])
total_length += seg_len
if seg_len > max_seg_length:
max_seg_length = seg_len
segments.append({
"start": [round(pts[i][0], 4), round(pts[i][1], 4)],
"end": [round(pts[i + 1][0], 4), round(pts[i + 1][1], 4)],
"length": round(seg_len, 2),
})
# 至少最长段满足阈值才算候选
if max_seg_length >= self.min_length:
candidates.append({
"entity_type": "LWPOLYLINE",
"handle": entity.dxf.handle,
"layer": layer,
"vertex_count": len(pts),
"closed": entity.closed,
"total_length": round(total_length, 2),
"max_segment_length": round(max_seg_length, 2),
"segments": [s for s in segments if s["length"] >= self.min_length],
"points": [[round(p[0], 4), round(p[1], 4)] for p in pts],
})
return candidates
def _is_excluded_layer(self, layer_name: str) -> bool:
"""检查图层是否属于排除范围(标注类图层)。"""
upper = layer_name.upper()
for kw in self.exclude_layers:
if kw.upper() in upper or kw in layer_name:
return True
# 如果配置了 include 列表,则仅包含匹配的图层
if self.include_layers:
for kw in self.include_layers:
if kw.upper() in upper or kw in layer_name:
return False
return True # 不在 include 列表中则排除
return False
# ============================================================
# 主检测器(编排)
# ============================================================
class ElementDetector:
"""编排各识别器,输出统一的检测结果。"""
def __init__(
self,
column_keywords: Optional[list[str]] = None,
wall_min_length: float = WALL_MIN_LENGTH,
wall_exclude_layers: Optional[list[str]] = None,
wall_include_layers: Optional[list[str]] = None,
):
self.column_detector = ColumnDetector(keywords=column_keywords)
self.text_extractor = TextExtractor()
self.wall_detector = WallCandidateDetector(
min_length=wall_min_length,
exclude_layers=wall_exclude_layers,
include_layers=wall_include_layers,
)
def detect_all(self, doc) -> dict:
"""执行全部识别,返回结构化结果。
Args:
doc: ezdxf Drawing 对象
Returns:
dict: {"columns": [...], "texts": [...], "wall_candidates": [...]}
"""
msp = doc.modelspace()
return {
"columns": self.column_detector.detect(msp),
"texts": self.text_extractor.extract(msp),
"wall_candidates": self.wall_detector.detect(msp),
}
# ============================================================
# 报告生成
# ============================================================
def generate_report(result: dict) -> str:
"""根据检测结果生成纯文本统计报告。
Args:
result: detect_all() 的返回值
Returns:
str: 格式化报告文本
"""
lines = []
lines.append("=" * 50)
lines.append(" CAD 元素分析报告")
lines.append("=" * 50)
lines.append("")
# 柱子
columns = result["columns"]
lines.append(f"【柱子候选】")
lines.append(f" 数量: {len(columns)}")
if columns:
# 按块名分组统计
block_counts = {}
for col in columns:
name = col["name"]
block_counts[name] = block_counts.get(name, 0) + 1
for name, count in sorted(block_counts.items()):
lines.append(f" - {name}: {count}")
lines.append("")
# 文字
texts = result["texts"]
lines.append(f"【文字】")
lines.append(f" 数量: {len(texts)}")
if texts:
# 按图层分组统计
layer_counts = {}
for t in texts:
ly = t["layer"]
layer_counts[ly] = layer_counts.get(ly, 0) + 1
for ly, count in sorted(layer_counts.items()):
lines.append(f" - 图层 {ly}: {count}")
lines.append("")
# 墙体候选
walls = result["wall_candidates"]
lines.append(f"【墙体候选】")
lines.append(f" 数量: {len(walls)}")
if walls:
# 按类型统计
type_counts = {}
layer_counts = {}
lengths = []
for w in walls:
etype = w["entity_type"]
type_counts[etype] = type_counts.get(etype, 0) + 1
ly = w["layer"]
layer_counts[ly] = layer_counts.get(ly, 0) + 1
# 获取长度
if etype == "LINE":
lengths.append(w["length"])
elif etype == "LWPOLYLINE":
lengths.append(w["max_segment_length"])
lines.append(" 按类型:")
for etype, count in sorted(type_counts.items()):
lines.append(f" - {etype}: {count}")
lines.append(" 按图层:")
for ly, count in sorted(layer_counts.items(), key=lambda x: -x[1]):
lines.append(f" - 图层 {ly}: {count}")
if lengths:
lines.append(f" 最长段: {max(lengths):.2f}")
lines.append(f" 最短段: {min(lengths):.2f}")
lines.append(f" 平均长: {sum(lengths) / len(lengths):.2f}")
lines.append("")
lines.append("=" * 50)
lines.append(" 报告结束")
lines.append("=" * 50)
return "\n".join(lines)
# ============================================================
# 入口
# ============================================================
def run_detection(
dxf_path: str,
output_dir: str,
column_keywords: Optional[list[str]] = None,
wall_min_length: float = WALL_MIN_LENGTH,
wall_exclude_layers: Optional[list[str]] = None,
wall_include_layers: Optional[list[str]] = None,
) -> dict:
"""执行元素检测并输出 JSON 和报告文件。
Args:
dxf_path: DXF 文件路径
output_dir: 输出目录路径
column_keywords: 柱子识别关键词
wall_min_length: 墙体最小长度阈值
wall_exclude_layers: 墙体排除图层关键词
wall_include_layers: 墙体限定图层关键词
Returns:
dict: 检测结果
"""
doc = ezdxf.readfile(dxf_path)
filename = Path(dxf_path).stem
detector = ElementDetector(
column_keywords=column_keywords,
wall_min_length=wall_min_length,
wall_exclude_layers=wall_exclude_layers,
wall_include_layers=wall_include_layers,
)
result = detector.detect_all(doc)
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
# 输出 JSON
json_path = out_dir / f"{filename}_element_detection.json"
json_path.write_text(
json.dumps(result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"检测结果已保存到: {json_path}")
# 输出报告
report_path = out_dir / f"{filename}_element_report.txt"
report = generate_report(result)
report_path.write_text(report, encoding="utf-8")
print(f"分析报告已保存到: {report_path}")
# 打印摘要
print(f"\n 柱子候选 : {len(result['columns'])}")
print(f" 文字 : {len(result['texts'])}")
print(f" 墙体候选 : {len(result['wall_candidates'])}")
print(report)
return result
# ============================================================
# 独立运行入口
# ============================================================
if __name__ == "__main__":
import sys
BASE_DIR = Path(__file__).parent.parent.parent
SAMPLE_DIR = BASE_DIR / "samples"
OUTPUT_DIR = Path(__file__).parent / "output"
dxf_file = SAMPLE_DIR / "洗浴中心C-48.dxf"
if not dxf_file.exists():
print(f"错误:文件不存在 -> {dxf_file}")
sys.exit(1)
run_detection(str(dxf_file), str(OUTPUT_DIR))
+148
View File
@@ -0,0 +1,148 @@
"""实体分析模块 —— 统计模型空间中各实体类型的数量、INSERT 块聚合、文本提取。"""
def _extract_entity_props(entity):
"""提取单个实体的关键属性为 JSON 友好格式。"""
dxftype = entity.dxftype()
props = {}
if dxftype == "LINE":
props["start"] = list(entity.dxf.start)
props["end"] = list(entity.dxf.end)
elif dxftype == "CIRCLE":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
elif dxftype == "ARC":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
props["start_angle"] = round(entity.dxf.start_angle, 1)
props["end_angle"] = round(entity.dxf.end_angle, 1)
elif dxftype == "LWPOLYLINE":
pts = entity.get_points()
props["vertex_count"] = len(entity)
props["closed"] = entity.closed
props["points"] = [[round(p[0], 4), round(p[1], 4)] for p in pts]
elif dxftype == "POLYLINE":
verts = list(entity.vertices())
props["vertex_count"] = len(verts)
props["points"] = [[round(v.dxf.location[0], 4), round(v.dxf.location[1], 4)] for v in verts]
elif dxftype == "INSERT":
props["block_name"] = entity.dxf.name
props["insert"] = list(entity.dxf.insert)
props["scale"] = [round(entity.dxf.xscale, 2), round(entity.dxf.yscale, 2), round(entity.dxf.zscale, 2)]
props["rotation"] = round(entity.dxf.rotation, 2)
elif dxftype in ("MTEXT", "TEXT"):
text = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
props["text"] = text
if hasattr(entity.dxf, "insert"):
props["insert"] = list(entity.dxf.insert)
if hasattr(entity.dxf, "height"):
props["height"] = round(entity.dxf.height, 2)
elif dxftype == "DIMENSION":
props["text"] = entity.dxf.text
if hasattr(entity.dxf, "dimtype"):
props["dimtype"] = entity.dxf.dimtype
elif dxftype == "HATCH":
props["pattern"] = entity.dxf.pattern_name
elif dxftype in ("SOLID", "TRACE"):
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
elif dxftype == "POINT":
props["location"] = list(entity.dxf.location)
elif dxftype == "ELLIPSE":
props["center"] = list(entity.dxf.center)
props["major_axis"] = list(entity.dxf.major_axis)
props["ratio"] = round(entity.dxf.ratio, 4)
elif dxftype == "SPLINE":
props["fit_points"] = [list(p) for p in entity.fit_points]
elif dxftype == "3DFACE":
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
else:
for attr in entity.dxf.all_existing_dxf_attribs():
if attr in ("handle", "owner", "layer", "linetype", "color", "lineweight"):
continue
val = getattr(entity.dxf, attr)
if val is not None:
props[attr] = list(val) if hasattr(val, "__iter__") and not isinstance(val, str) else val
return props
def analyze_entities(doc) -> dict:
"""分析模型空间中的所有实体。
Args:
doc: ezdxf Drawing 对象
Returns:
dict: {
"total_entities": int,
"type_counts": dict, # {dxftype: count}
"blocks": list[dict], # INSERT 按块名聚合
"texts": list[dict], # TEXT/MTEXT 文本列表
}
"""
msp = doc.modelspace()
type_counts = {}
# INSERT 按块名聚合
block_map = {} # block_name -> list of insert_info
texts = []
for entity in msp:
dxftype = entity.dxftype()
type_counts[dxftype] = type_counts.get(dxftype, 0) + 1
if dxftype == "INSERT":
block_name = entity.dxf.name
insert_info = {
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
"insert_point": list(entity.dxf.insert),
"rotation": round(entity.dxf.rotation, 2),
"scale": [
round(entity.dxf.xscale, 2),
round(entity.dxf.yscale, 2),
round(entity.dxf.zscale, 2),
],
}
if block_name not in block_map:
block_map[block_name] = []
block_map[block_name].append(insert_info)
elif dxftype in ("TEXT", "MTEXT"):
content = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
text_info = {
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
"type": dxftype,
"content": content,
}
if hasattr(entity.dxf, "insert"):
text_info["insert"] = list(entity.dxf.insert)
texts.append(text_info)
# 块聚合结果
blocks = []
for block_name, inserts in sorted(block_map.items()):
blocks.append({
"block_name": block_name,
"count": len(inserts),
"inserts": inserts,
})
return {
"total_entities": sum(type_counts.values()),
"type_counts": type_counts,
"blocks": blocks,
"texts": texts,
}
+94
View File
@@ -0,0 +1,94 @@
"""图层分析模块 —— 统计每层实体数量与类型,并基于图层名称进行角色推断。"""
# 角色推断规则(启发式,基于常见中文/英文命名)
_ROLE_RULES = [
(["WALL", "", "W", "A-WALL", "A-WALL-"], "墙体"),
(["DOOR", "", "", "WINDOW", "D", "M"], "门窗"),
(["DIM", "标注", "DEFPOINTS", "PUB_DIM", "PUB_DIM"], "标注"),
(["TEXT", "文字", "NOTE"], "文字"),
(["COLUMN", "", "Z"], ""),
(["E", "EQUIP", "设备"], "设备"),
(["HATCH", "填充", "BH"], "填充"),
(["AXIS", "", "AXIS"], "轴网"),
(["GRID", "网格"], "网格"),
(["FURN", "家具", "FURNITURE"], "家具"),
(["BEAM", "", "B"], ""),
(["SLAB", ""], ""),
(["STAIR", "楼梯", "S"], "楼梯"),
(["ROOF", "屋顶", "R"], "屋顶"),
(["PIPE", "", "P"], "管道"),
(["DUCT", "风管"], "风管"),
(["ELEC", "电气", "E-"], "电气"),
(["PLUMB", "给排水", "P-"], "给排水"),
(["FOUND", "基础", "F"], "基础"),
(["SITE", "场地", "总图"], "场地"),
(["CEIL", "天花", "吊顶", "C"], "天花"),
(["FLOOR", "地面", "FL"], "地面"),
(["SECTION", "剖面", "SECT"], "剖面"),
(["ELEV", "立面", "ELEVATION"], "立面"),
(["0", "A-"], "主体结构"),
]
def _infer_role(layer_name: str) -> str:
"""根据图层名称推断图层角色(仅作建议)。"""
upper_name = layer_name.upper()
for keywords, role in _ROLE_RULES:
for kw in keywords:
if kw in upper_name or kw in layer_name:
return role
return "其他"
def analyze_layers(doc) -> list:
"""分析所有图层,返回每层的实体统计和角色推断。
Args:
doc: ezdxf Drawing 对象
Returns:
list[dict]: 每层信息,格式为 {
"name": str,
"entity_count": int,
"entity_types": list[str],
"suggested_role": str
}
"""
msp = doc.modelspace()
# 按图层聚合实体
layer_entities = {} # layer_name -> list of dxftype
for entity in msp:
layer_name = entity.dxf.layer
if layer_name not in layer_entities:
layer_entities[layer_name] = []
layer_entities[layer_name].append(entity.dxftype())
# 也包含没有实体的图层(在 doc.layers 中定义但模型空间无实体)
result = []
seen_layers = set()
for layer_name, types in layer_entities.items():
seen_layers.add(layer_name)
unique_types = sorted(set(types))
result.append({
"name": layer_name,
"entity_count": len(types),
"entity_types": unique_types,
"suggested_role": _infer_role(layer_name),
})
# 补充空图层(在图层定义中存在但无实体)
for layer in doc.layers:
name = layer.dxf.name
if name not in seen_layers:
result.append({
"name": name,
"entity_count": 0,
"entity_types": [],
"suggested_role": _infer_role(name),
})
# 按实体数降序排列
result.sort(key=lambda x: -x["entity_count"])
return result
+110 -389
View File
@@ -1,387 +1,102 @@
"""DXF 解析测试程序 —— 使用 ezdxf 读取并输出 DXF 文件内容"""
"""DXF 解析工具 —— 读取 DXF 文件,输出图层分析和实体分析结果"""
import io
import json
import sys
from datetime import datetime
from pathlib import Path
import ezdxf
# 目录
from layer_analysis import analyze_layers
from entity_analysis import analyze_entities
from element_detector import ElementDetector
# 路径配置
BASE_DIR = Path(__file__).parent.parent.parent
SAMPLE_DIR = BASE_DIR / "samples"
OUTPUT_DIR = Path(__file__).parent / "output"
#DXF_FILE = SAMPLE_DIR / "decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf"
# 要解析的 DXF 文件
DXF_FILE = SAMPLE_DIR / "洗浴中心C-48.dxf"
class OutputRecorder:
"""同时输出到 stdout 和内存缓冲,最终写入文件。"""
def __init__(self):
self._buffer = io.StringIO()
self._terminal = sys.stdout
def write(self, message):
self._terminal.write(message)
self._buffer.write(message)
def flush(self):
self._terminal.flush()
self._buffer.flush()
def getvalue(self):
return self._buffer.getvalue()
def extract_entity_props(entity):
"""提取单个实体的关键属性为 JSON 友好格式。"""
dxftype = entity.dxftype()
props = {}
if dxftype == "LINE":
props["start"] = list(entity.dxf.start)
props["end"] = list(entity.dxf.end)
elif dxftype == "CIRCLE":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
elif dxftype == "ARC":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
props["start_angle"] = round(entity.dxf.start_angle, 1)
props["end_angle"] = round(entity.dxf.end_angle, 1)
elif dxftype == "LWPOLYLINE":
pts = entity.get_points()
props["vertex_count"] = len(entity)
props["closed"] = entity.closed
props["points"] = [[round(p[0], 4), round(p[1], 4)] for p in pts]
elif dxftype == "POLYLINE":
verts = list(entity.vertices())
props["vertex_count"] = len(verts)
props["points"] = [[round(v.dxf.location[0], 4), round(v.dxf.location[1], 4)] for v in verts]
elif dxftype == "INSERT":
props["block_name"] = entity.dxf.name
props["insert"] = list(entity.dxf.insert)
props["scale"] = [round(entity.dxf.xscale, 2), round(entity.dxf.yscale, 2), round(entity.dxf.zscale, 2)]
props["rotation"] = round(entity.dxf.rotation, 2)
elif dxftype in ("MTEXT", "TEXT"):
text = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
props["text"] = text
if hasattr(entity.dxf, "insert"):
props["insert"] = list(entity.dxf.insert)
if hasattr(entity.dxf, "height"):
props["height"] = round(entity.dxf.height, 2)
elif dxftype == "DIMENSION":
props["text"] = entity.dxf.text
if hasattr(entity.dxf, "dimtype"):
props["dimtype"] = entity.dxf.dimtype
elif dxftype == "HATCH":
props["pattern"] = entity.dxf.pattern_name
elif dxftype == "SOLID" or dxftype == "TRACE":
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
elif dxftype == "POINT":
props["location"] = list(entity.dxf.location)
elif dxftype == "ELLIPSE":
props["center"] = list(entity.dxf.center)
props["major_axis"] = list(entity.dxf.major_axis)
props["ratio"] = round(entity.dxf.ratio, 4)
elif dxftype == "SPLINE":
props["fit_points"] = [list(p) for p in entity.fit_points]
elif dxftype == "3DFACE":
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
else:
for attr in entity.dxf.all_existing_dxf_attribs():
if attr in ("handle", "owner", "layer", "linetype", "color", "lineweight"):
continue
val = getattr(entity.dxf, attr)
if val is not None:
props[attr] = list(val) if hasattr(val, "__iter__") and not isinstance(val, str) else val
return props
def extract_to_dict(doc: ezdxf.document.Drawing, filename: str, parse_time: str):
"""提取 DXF 文档全部内容为 JSON 可序列化的 dict。"""
result = {
"file": filename,
"parse_time": parse_time,
"header": {
def build_file_info(doc, filename: str) -> dict:
"""提取 DXF 文件基本信息。"""
info = {
"filename": filename,
"dxf_version": doc.dxfversion,
"encoding": doc.encoding,
"acad_ver": doc.header.get("$ACADVER", None),
},
}
# 单位
units = doc.header.get("$INSUNITS", None)
unit_names = {1: "inches", 2: "feet", 4: "millimeters", 5: "centimeters", 6: "meters"}
result["header"]["units"] = unit_names.get(units, str(units)) if units else None
info["units"] = units
# 图形范围
extmin = doc.header.get("$EXTMIN", None)
extmax = doc.header.get("$EXTMAX", None)
if extmin and extmax:
result["header"]["extents"] = {"min": list(extmin), "max": list(extmax)}
# 图层
layers = []
for layer in doc.layers:
layers.append({
"name": layer.dxf.name,
"color": layer.dxf.color,
"linetype": layer.dxf.linetype,
"lineweight": layer.dxf.lineweight,
"on": layer.is_on(),
"frozen": layer.is_frozen(),
"locked": layer.is_locked(),
})
result["layers"] = layers
# 实体
entities = []
entity_type_counts = {}
for entity in doc.modelspace():
dxftype = entity.dxftype()
entity_type_counts[dxftype] = entity_type_counts.get(dxftype, 0) + 1
entities.append({
"index": len(entities) + 1,
"type": dxftype,
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
"properties": extract_entity_props(entity),
})
result["entity_summary"] = entity_type_counts
result["entities"] = entities
# 块定义
blocks = []
for block in doc.blocks:
blocks.append({
"name": block.name,
"entity_count": len(list(block)),
})
result["blocks"] = blocks
# 布局
layouts = []
for layout in doc.layouts:
try:
pw = layout.dxf.paper_width if hasattr(layout.dxf, "paper_width") else None
ph = layout.dxf.paper_height if hasattr(layout.dxf, "paper_height") else None
layouts.append({"name": layout.name, "paper_width": pw, "paper_height": ph})
except Exception:
layouts.append({"name": layout.name, "paper_width": None, "paper_height": None})
result["layouts"] = layouts
return result
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f" {title}")
print(f"{'=' * 70}")
def print_dxf_info(doc: ezdxf.document.Drawing):
"""打印 DXF 文档基本信息。"""
print(f" DXF 版本 : {doc.dxfversion}")
print(f" 编码 : {doc.encoding}")
print(f" 创建者 : {doc.header.get('$ACADVER', 'N/A')}")
# 绘图单位
units = doc.header.get("$INSUNITS", None)
unit_map = {
1: "英寸 (Inches)",
2: "英尺 (Feet)",
4: "毫米 (Millimeters)",
5: "厘米 (Centimeters)",
6: "米 (Meters)",
}
unit_name = unit_map.get(units, f"未知 ({units})") if units else "未指定"
print(f" 绘图单位 : {unit_name}")
# 图形范围
extmin = doc.header.get("$EXTMIN", None)
extmax = doc.header.get("$EXTMAX", None)
if extmin and extmax:
print(f" 图形范围 : {extmin} -> {extmax}")
def print_layers(doc: ezdxf.document.Drawing):
"""打印所有图层信息。"""
layers = list(doc.layers)
if not layers:
print(" (无图层)")
return
print(f"{len(layers)} 个图层:")
print(f" {'名称':<30} {'颜色':<8} {'线型':<20} {'线宽':<8} {'状态'}")
print(f" {'-' * 80}")
for layer in layers:
name = layer.dxf.name
color = layer.dxf.color
linetype = layer.dxf.linetype
# 线宽:-1 表示 DEFAULT
lineweight = layer.dxf.lineweight
lw_display = "默认" if lineweight == -1 else str(lineweight)
flags = []
if layer.is_on():
flags.append("ON")
info["extents"] = {"min": list(extmin), "max": list(extmax)}
else:
flags.append("OFF")
if layer.is_frozen():
flags.append("FROZEN")
if layer.is_locked():
flags.append("LOCKED")
print(f" {name:<30} {str(color):<8} {linetype:<20} {lw_display:<8} {', '.join(flags)}")
info["extents"] = None
return info
def print_entity_summary(doc: ezdxf.document.Drawing):
"""统计模型空间中各类型实体数量"""
msp = doc.modelspace()
entity_types = {}
for entity in msp:
dtype = entity.dxftype()
entity_types[dtype] = entity_types.get(dtype, 0) + 1
def print_summary(file_info: dict, layers: list, entities: dict):
"""终端打印解析摘要"""
print(f"\n{'=' * 60}")
print(f" CAD 文件解析报告")
print(f"{'=' * 60}")
print(f" 文件 : {file_info['filename']}")
print(f" 版本 : {file_info['dxf_version']}")
print(f" 编码 : {file_info['encoding']}")
print(f" 实体总数 : {entities['total_entities']}")
if file_info.get("extents"):
print(f" 图形范围 : {file_info['extents']}")
if not entity_types:
print(" (模型空间无实体)")
return
# 图层摘要
print(f"\n {'' * 56}")
print(f" 图层分析(共 {len(layers)} 个图层):")
print(f" {'名称':<25} {'实体数':<8} {'角色推断'}")
print(f" {'-' * 50}")
for layer in layers[:20]: # 最多显示前 20 个
print(f" {layer['name']:<25} {layer['entity_count']:<8} {layer['suggested_role']}")
if len(layers) > 20:
print(f" ... 还有 {len(layers) - 20} 个图层")
print(f"{sum(entity_types.values())} 个实体:")
# 实体类型统计
type_counts = entities["type_counts"]
print(f"\n {'' * 56}")
print(f" 实体类型统计:")
print(f" {'类型':<25} {'数量':<8}")
print(f" {'-' * 35}")
for dtype, count in sorted(entity_types.items(), key=lambda x: -x[1]):
for dtype, count in sorted(type_counts.items(), key=lambda x: -x[1]):
print(f" {dtype:<25} {count:<8}")
# 块统计
blocks = entities["blocks"]
if blocks:
print(f"\n {'' * 56}")
print(f" 块引用统计(共 {len(blocks)} 种块):")
print(f" {'块名':<35} {'引用次数':<10}")
print(f" {'-' * 47}")
for blk in blocks[:15]:
print(f" {blk['block_name']:<35} {blk['count']:<10}")
if len(blocks) > 15:
print(f" ... 还有 {len(blocks) - 15} 种块")
def print_entity_details(doc: ezdxf.document.Drawing):
"""打印模型空间中每个实体的详细信息。"""
msp = doc.modelspace()
entities = list(msp)
if not entities:
print(" (模型空间无实体)")
return
# 文本
texts = entities["texts"]
if texts:
print(f"\n {'' * 56}")
print(f" 文本内容(共 {len(texts)} 条):")
for t in texts[:10]:
print(f" [{t['layer']}] {t['content'][:60]}")
if len(texts) > 10:
print(f" ... 还有 {len(texts) - 10} 条文本")
print(f"{len(entities)} 个实体:")
print(f" {'序号':<6} {'类型':<25} {'句柄':<12} {'图层':<20} {'关键属性'}")
print(f" {'-' * 100}")
for i, entity in enumerate(entities):
dxftype = entity.dxftype()
handle = entity.dxf.handle
layer = entity.dxf.layer
# 提取各类型的关键属性
key_attrs = []
if dxftype == "LINE":
key_attrs.append(f"起点={entity.dxf.start}, 终点={entity.dxf.end}")
elif dxftype == "CIRCLE":
key_attrs.append(f"圆心={entity.dxf.center}, 半径={entity.dxf.radius:.4f}")
elif dxftype == "ARC":
key_attrs.append(f"圆心={entity.dxf.center}, 半径={entity.dxf.radius:.4f}, 角度={entity.dxf.start_angle:.1f}~{entity.dxf.end_angle:.1f}")
elif dxftype == "LWPOLYLINE":
count = len(entity)
closed = "闭合" if entity.closed else "开放"
pts = entity.get_points()
first_pt = pts[0] if pts else "N/A"
key_attrs.append(f"顶点数={count}, {closed}, 起点={first_pt[:2]}")
elif dxftype == "POLYLINE":
count = len(list(entity.vertices()))
key_attrs.append(f"顶点数={count}")
elif dxftype == "INSERT":
key_attrs.append(f"块名={entity.dxf.name}, 插入点={entity.dxf.insert}, 缩放={entity.dxf.xscale:.2f},{entity.dxf.yscale:.2f}")
elif dxftype == "MTEXT":
text = entity.plain_text()[:50]
key_attrs.append(f'内容="{text}..."' if len(entity.plain_text()) > 50 else f'内容="{text}"')
elif dxftype == "TEXT":
text = entity.dxf.text[:50]
key_attrs.append(f'内容="{text}..."' if len(entity.dxf.text) > 50 else f'内容="{entity.dxf.text}"')
elif dxftype == "DIMENSION":
key_attrs.append(f"测量值={entity.dxf.text}")
elif dxftype == "HATCH":
key_attrs.append(f"图案={entity.dxf.pattern_name}")
elif dxftype == "SOLID" or dxftype == "TRACE":
key_attrs.append(f"顶点={entity.dxf.vtx0} {entity.dxf.vtx1} {entity.dxf.vtx2} {entity.dxf.vtx3}")
elif dxftype == "POINT":
key_attrs.append(f"位置={entity.dxf.location}")
elif dxftype == "ELLIPSE":
key_attrs.append(f"圆心={entity.dxf.center}, 长轴={entity.dxf.major_axis}")
elif dxftype == "SPLINE":
key_attrs.append(f"拟合点数={len(entity.fit_points)}")
elif dxftype == "3DFACE":
key_attrs.append(f"顶点={entity.dxf.vtx0} {entity.dxf.vtx1} {entity.dxf.vtx2} {entity.dxf.vtx3}")
else:
# 通用:列出非默认的 dxf 属性
for attr in entity.dxf.all_existing_dxf_attribs():
if attr in ("handle", "owner", "layer", "linetype", "color", "lineweight"):
continue
val = getattr(entity.dxf, attr)
if val is not None:
key_attrs.append(f"{attr}={val}")
attrs_str = "; ".join(key_attrs[:3]) if key_attrs else "(无额外属性)"
print(f" {i + 1:<6} {dxftype:<25} {handle:<12} {layer:<20} {attrs_str}")
def print_blocks(doc: ezdxf.document.Drawing):
"""打印所有块定义。"""
blocks = list(doc.blocks)
if not blocks:
print(" (无自定义块)")
return
print(f"{len(blocks)} 个块定义:")
print(f" {'名称':<35} {'实体数':<10} {'说明'}")
print(f" {'-' * 70}")
for block in blocks:
name = block.name
count = len(list(block))
desc = ""
if name.startswith("*"):
if name.startswith("*Model_Space"):
desc = "(模型空间)"
elif name.startswith("*Paper_Space"):
desc = "(图纸空间)"
elif name.startswith("*D"):
desc = "(标注块)"
elif name.startswith("*X"):
desc = "(外部参照)"
elif name.startswith("*U"):
desc = "(匿名块)"
else:
desc = "(特殊块)"
print(f" {name:<35} {count:<10} {desc}")
def print_layouts(doc: ezdxf.document.Drawing):
"""打印所有布局(图纸空间)。"""
layouts = list(doc.layouts)
if not layouts:
print(" (无布局)")
return
print(f"{len(layouts)} 个布局:")
print(f" {'名称':<30} {'尺寸 (宽 x 高)'}")
print(f" {'-' * 60}")
for layout in layouts:
name = layout.name
try:
page_w = layout.dxf.paper_width if hasattr(layout.dxf, 'paper_width') else "?"
page_h = layout.dxf.paper_height if hasattr(layout.dxf, 'paper_height') else "?"
print(f" {name:<30} {page_w} x {page_h}")
except Exception:
print(f" {name:<30} N/A")
print(f"\n{'=' * 60}")
print(f" 解析完成")
print(f"{'=' * 60}\n")
def main():
@@ -390,61 +105,67 @@ def main():
return
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
parse_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
output_txt = OUTPUT_DIR / f"{DXF_FILE.stem}_{timestamp}.txt"
output_json = OUTPUT_DIR / f"{DXF_FILE.stem}_{timestamp}.json"
recorder = OutputRecorder()
sys.stdout = recorder
try:
print(f"解析文件: {DXF_FILE.name}")
print(f"路径: {DXF_FILE}")
print(f"输出文件: {output_txt.relative_to(BASE_DIR)}")
print(f"JSON 文件: {output_json.relative_to(BASE_DIR)}")
print(f"解析时间: {parse_time}")
# 读取 DXF
doc = ezdxf.readfile(str(DXF_FILE))
# 1. 基本信息
print_header("一、文档基本信息")
print_dxf_info(doc)
# 文件基本信息
file_info = build_file_info(doc, DXF_FILE.name)
# 2. 图层
print_header("二、图层信息")
print_layers(doc)
# 图层分析
layers = analyze_layers(doc)
# 3. 实体统计
print_header("三、模型空间实体统计")
print_entity_summary(doc)
# 实体分析
entities = analyze_entities(doc)
# 4. 实体详情
print_header("四、模型空间实体详情")
print_entity_details(doc)
# 添加实体总数到文件信息
file_info["total_entities"] = entities["total_entities"]
# 5. 块定义
print_header("五、块定义")
print_blocks(doc)
# 终端打印摘要
print_summary(file_info, layers, entities)
# 6. 布局
print_header("六、布局(图纸空间)")
print_layouts(doc)
# 组装并输出 JSON
output = {
"file_info": file_info,
"layers": layers,
"entities": entities["type_counts"],
"blocks": entities["blocks"],
"texts": entities["texts"],
}
print_header("解析完成")
finally:
sys.stdout = recorder._terminal
# 写入文本文件
output_txt.write_text(recorder.getvalue(), encoding="utf-8")
print(f"\n结果已保存到: {output_txt}")
# 写入 JSON 文件
json_data = extract_to_dict(doc, DXF_FILE.name, parse_time)
output_json.write_text(json.dumps(json_data, ensure_ascii=False, indent=2), encoding="utf-8")
output_json = OUTPUT_DIR / f"{DXF_FILE.stem}_cad_analysis.json"
output_json.write_text(
json.dumps(output, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"JSON 已保存到: {output_json}")
# ========== 第二阶段:元素识别 ==========
print(f"\n{'' * 56}")
print(f" 开始元素识别...")
detector = ElementDetector()
detection_result = detector.detect_all(doc)
# 输出元素检测 JSON
detection_json = OUTPUT_DIR / f"{DXF_FILE.stem}_element_detection.json"
detection_json.write_text(
json.dumps(detection_result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"元素检测结果已保存到: {detection_json}")
# 输出统计报告
from element_detector import generate_report
report = generate_report(detection_result)
report_path = OUTPUT_DIR / f"{DXF_FILE.stem}_element_report.txt"
report_path.write_text(report, encoding="utf-8")
print(f"分析报告已保存到: {report_path}")
print(report)
if __name__ == "__main__":
main()