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 个布局。 4. 运行程序验证,成功解析目标 DXF 文件,输出 20 个实体(9 个 DIMENSION、6 个 MTEXT、5 个 LWPOLYLINE)、13 个块定义、3 个布局。
- **执行结果**: 程序正常运行,完整输出了 samples/decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf 的全部结构和内容信息。 - **执行结果**: 程序正常运行,完整输出了 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
+115 -394
View File
@@ -1,387 +1,102 @@
"""DXF 解析测试程序 —— 使用 ezdxf 读取并输出 DXF 文件内容""" """DXF 解析工具 —— 读取 DXF 文件,输出图层分析和实体分析结果"""
import io
import json import json
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import ezdxf 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 BASE_DIR = Path(__file__).parent.parent.parent
SAMPLE_DIR = BASE_DIR / "samples" SAMPLE_DIR = BASE_DIR / "samples"
OUTPUT_DIR = Path(__file__).parent / "output" 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" DXF_FILE = SAMPLE_DIR / "洗浴中心C-48.dxf"
class OutputRecorder:
"""同时输出到 stdout 和内存缓冲,最终写入文件。"""
def __init__(self): def build_file_info(doc, filename: str) -> dict:
self._buffer = io.StringIO() """提取 DXF 文件基本信息。"""
self._terminal = sys.stdout info = {
"filename": filename,
def write(self, message): "dxf_version": doc.dxfversion,
self._terminal.write(message) "encoding": doc.encoding,
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": {
"dxf_version": doc.dxfversion,
"encoding": doc.encoding,
"acad_ver": doc.header.get("$ACADVER", None),
},
} }
# 单位 # 单位
units = doc.header.get("$INSUNITS", None) units = doc.header.get("$INSUNITS", None)
unit_names = {1: "inches", 2: "feet", 4: "millimeters", 5: "centimeters", 6: "meters"} info["units"] = units
result["header"]["units"] = unit_names.get(units, str(units)) if units else None
# 图形范围 # 图形范围
extmin = doc.header.get("$EXTMIN", None) extmin = doc.header.get("$EXTMIN", None)
extmax = doc.header.get("$EXTMAX", None) extmax = doc.header.get("$EXTMAX", None)
if extmin and extmax: if extmin and extmax:
result["header"]["extents"] = {"min": list(extmin), "max": list(extmax)} info["extents"] = {"min": list(extmin), "max": list(extmax)}
else:
info["extents"] = None
# 图层 return info
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): def print_summary(file_info: dict, layers: list, entities: dict):
print(f"\n{'=' * 70}") """终端打印解析摘要。"""
print(f" {title}") print(f"\n{'=' * 60}")
print(f"{'=' * 70}") 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']}")
# 图层摘要
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} 个图层")
def print_dxf_info(doc: ezdxf.document.Drawing): # 实体类型统计
"""打印 DXF 文档基本信息。""" type_counts = entities["type_counts"]
print(f" DXF 版本 : {doc.dxfversion}") print(f"\n {'' * 56}")
print(f" 编码 : {doc.encoding}") print(f" 实体类型统计:")
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")
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)}")
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
if not entity_types:
print(" (模型空间无实体)")
return
print(f"{sum(entity_types.values())} 个实体:")
print(f" {'类型':<25} {'数量':<8}") print(f" {'类型':<25} {'数量':<8}")
print(f" {'-' * 35}") 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}") 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): # 文本
"""打印模型空间中每个实体的详细信息。""" texts = entities["texts"]
msp = doc.modelspace() if texts:
entities = list(msp) print(f"\n {'' * 56}")
if not entities: print(f" 文本内容(共 {len(texts)} 条):")
print(" (模型空间无实体)") for t in texts[:10]:
return print(f" [{t['layer']}] {t['content'][:60]}")
if len(texts) > 10:
print(f" ... 还有 {len(texts) - 10} 条文本")
print(f"{len(entities)} 个实体:") print(f"\n{'=' * 60}")
print(f" {'序号':<6} {'类型':<25} {'句柄':<12} {'图层':<20} {'关键属性'}") print(f" 解析完成")
print(f" {'-' * 100}") print(f"{'=' * 60}\n")
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")
def main(): def main():
@@ -390,61 +105,67 @@ def main():
return return
OUTPUT_DIR.mkdir(parents=True, exist_ok=True) 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") 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() print(f"解析文件: {DXF_FILE.name}")
sys.stdout = recorder print(f"解析时间: {parse_time}")
try: # 读取 DXF
print(f"解析文件: {DXF_FILE.name}") doc = ezdxf.readfile(str(DXF_FILE))
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}")
doc = ezdxf.readfile(str(DXF_FILE)) # 文件基本信息
file_info = build_file_info(doc, DXF_FILE.name)
# 1. 基本信息 # 图层分析
print_header("一、文档基本信息") layers = analyze_layers(doc)
print_dxf_info(doc)
# 2. 图层 # 实体分析
print_header("二、图层信息") entities = analyze_entities(doc)
print_layers(doc)
# 3. 实体统计 # 添加实体总数到文件信息
print_header("三、模型空间实体统计") file_info["total_entities"] = entities["total_entities"]
print_entity_summary(doc)
# 4. 实体详情 # 终端打印摘要
print_header("四、模型空间实体详情") print_summary(file_info, layers, entities)
print_entity_details(doc)
# 5. 块定义 # 组装并输出 JSON
print_header("五、块定义") output = {
print_blocks(doc) "file_info": file_info,
"layers": layers,
"entities": entities["type_counts"],
"blocks": entities["blocks"],
"texts": entities["texts"],
}
# 6. 布局 output_json = OUTPUT_DIR / f"{DXF_FILE.stem}_cad_analysis.json"
print_header("六、布局(图纸空间)") output_json.write_text(
print_layouts(doc) json.dumps(output, ensure_ascii=False, indent=2),
encoding="utf-8",
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")
print(f"JSON 已保存到: {output_json}") 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__": if __name__ == "__main__":
main() main()