Files
fletimageanalysis/back_src/temp_backup/dxf_processor_backup.py
2025-07-16 17:33:20 +09:00

634 lines
27 KiB
Python

# -*- coding: utf-8 -*-
"""
DXF 파일 처리 모듈
ezdxf 라이브러리를 사용하여 DXF 파일에서 도곽 정보 및 Block Reference/Attribute Reference를 추출
"""
import os
import json
import logging
from typing import Dict, List, Optional, Tuple, Union, Any
from dataclasses import dataclass, asdict, field
try:
import ezdxf
from ezdxf.document import Drawing
from ezdxf.entities import Insert, Attrib, AttDef, Text, MText
from ezdxf.layouts import BlockLayout, Modelspace
EZDXF_AVAILABLE = True
except ImportError:
EZDXF_AVAILABLE = False
logging.warning("ezdxf 라이브러리가 설치되지 않았습니다. DXF 기능이 비활성화됩니다.")
from config import Config
@dataclass
class BoundingBox:
"""바운딩 박스 정보를 담는 데이터 클래스"""
min_x: float
min_y: float
max_x: float
max_y: float
@property
def width(self) -> float:
return self.max_x - self.min_x
@property
def height(self) -> float:
return self.max_y - self.min_y
@property
def center(self) -> Tuple[float, float]:
return ((self.min_x + self.max_x) / 2, (self.min_y + self.max_y) / 2)
@dataclass
class AttributeInfo:
"""속성 정보를 담는 데이터 클래스 - 모든 DXF 속성 포함"""
tag: str
text: str
position: Tuple[float, float, float] # insert point (x, y, z)
height: float
width: float
rotation: float
layer: str
bounding_box: Optional[BoundingBox] = None
# 추가 DXF 속성들
prompt: Optional[str] = None # 프롬프트 문자열 (ATTDEF에서 가져옴)
style: Optional[str] = None # 텍스트 스타일
invisible: bool = False # 보이지 않는 속성
const: bool = False # 상수 속성
verify: bool = False # 검증 필요
preset: bool = False # 프롬프트 없이 삽입
align_point: Optional[Tuple[float, float, float]] = None # 정렬점
halign: int = 0 # 수평 정렬 (0=LEFT, 2=RIGHT, etc.)
valign: int = 0 # 수직 정렬 (0=BASELINE, 1=BOTTOM, etc.)
text_generation_flag: int = 0 # 텍스트 생성 플래그
oblique_angle: float = 0.0 # 기울기 각도
width_factor: float = 1.0 # 폭 비율
color: Optional[int] = None # 색상 코드
linetype: Optional[str] = None # 선 타입
lineweight: Optional[int] = None # 선 굵기
# 좌표 정보
insert_x: float = 0.0 # X 좌표
insert_y: float = 0.0 # Y 좌표
insert_z: float = 0.0 # Z 좌표
# 계산된 정보
estimated_width: float = 0.0 # 추정 텍스트 폭
entity_handle: Optional[str] = None # DXF 엔티티 핸들
@dataclass
class BlockInfo:
"""블록 정보를 담는 데이터 클래스"""
name: str
position: Tuple[float, float, float]
scale: Tuple[float, float, float]
rotation: float
layer: str
attributes: List[AttributeInfo]
bounding_box: Optional[BoundingBox] = None
@dataclass
class TitleBlockInfo:
"""도곽 정보를 담는 데이터 클래스"""
drawing_name: Optional[str] = None # 도면명
drawing_number: Optional[str] = None # 도면번호
construction_field: Optional[str] = None # 건설분야
construction_stage: Optional[str] = None # 건설단계
scale: Optional[str] = None # 축척
project_name: Optional[str] = None # 프로젝트명
designer: Optional[str] = None # 설계자
date: Optional[str] = None # 날짜
revision: Optional[str] = None # 리비전
location: Optional[str] = None # 위치
bounding_box: Optional[BoundingBox] = None # 도곽 전체 바운딩 박스
block_name: Optional[str] = None # 도곽 블록 이름
# 모든 attributes 정보 저장
all_attributes: List[AttributeInfo] = field(default_factory=list) # 도곽의 모든 속성 정보 리스트
attributes_count: int = 0 # 속성 개수
# 추가 메타데이터
block_position: Optional[Tuple[float, float, float]] = None # 블록 위치
block_scale: Optional[Tuple[float, float, float]] = None # 블록 스케일
block_rotation: float = 0.0 # 블록 회전각
block_layer: Optional[str] = None # 블록 레이어
def __post_init__(self):
"""초기화 후 처리"""
self.attributes_count = len(self.all_attributes)
class DXFProcessor:
"""DXF 파일 처리 클래스"""
# 도곽 식별을 위한 키워드 정의
TITLE_BLOCK_KEYWORDS = {
'건설분야': ['construction_field', 'field', '분야', '공사', 'category'],
'건설단계': ['construction_stage', 'stage', '단계', 'phase'],
'도면명': ['drawing_name', 'title', '제목', 'name', ''],
'축척': ['scale', '축척', 'ratio', '비율'],
'도면번호': ['drawing_number', 'number', '번호', 'no', 'dwg'],
'설계자': ['designer', '설계', 'design', 'drawn'],
'프로젝트': ['project', '사업', '공사명'],
'날짜': ['date', '일자', '작성일'],
'리비전': ['revision', 'rev', '개정'],
'위치': ['location', '위치', '지역']
}
def __init__(self):
"""DXF 처리기 초기화"""
self.logger = logging.getLogger(__name__)
if not EZDXF_AVAILABLE:
raise ImportError("ezdxf 라이브러리가 필요합니다. 'pip install ezdxf'로 설치하세요.")
def validate_dxf_file(self, file_path: str) -> bool:
"""DXF 파일 유효성 검사"""
try:
if not os.path.exists(file_path):
self.logger.error(f"파일이 존재하지 않습니다: {file_path}")
return False
if not file_path.lower().endswith('.dxf'):
self.logger.error(f"DXF 파일이 아닙니다: {file_path}")
return False
# ezdxf로 파일 읽기 시도
doc = ezdxf.readfile(file_path)
if doc is None:
return False
self.logger.info(f"DXF 파일 유효성 검사 성공: {file_path}")
return True
except ezdxf.DXFStructureError as e:
self.logger.error(f"DXF 구조 오류: {e}")
return False
except Exception as e:
self.logger.error(f"DXF 파일 검증 중 오류: {e}")
return False
def load_dxf_document(self, file_path: str) -> Optional[Drawing]:
"""DXF 문서 로드"""
try:
doc = ezdxf.readfile(file_path)
self.logger.info(f"DXF 문서 로드 성공: {file_path}")
return doc
except Exception as e:
self.logger.error(f"DXF 문서 로드 실패: {e}")
return None
def calculate_text_bounding_box(self, entity: Union[Text, MText, Attrib]) -> Optional[BoundingBox]:
"""텍스트 엔티티의 바운딩 박스 계산"""
try:
if hasattr(entity, 'dxf'):
# 텍스트 위치 가져오기
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
height = getattr(entity.dxf, 'height', 1.0)
# 텍스트 내용 길이 추정 (폰트에 따라 다르지만 대략적으로)
text_content = ""
if hasattr(entity.dxf, 'text'):
text_content = entity.dxf.text
elif hasattr(entity, 'plain_text'):
text_content = entity.plain_text()
# 텍스트 너비 추정 (높이의 0.6배 * 글자 수)
estimated_width = len(text_content) * height * 0.6
# 회전 고려 (기본값)
rotation = getattr(entity.dxf, 'rotation', 0)
# 바운딩 박스 계산
x, y = insert_point[0], insert_point[1]
return BoundingBox(
min_x=x,
min_y=y,
max_x=x + estimated_width,
max_y=y + height
)
except Exception as e:
self.logger.warning(f"텍스트 바운딩 박스 계산 실패: {e}")
return None
def extract_block_references(self, doc: Drawing) -> List[BlockInfo]:
"""문서에서 모든 Block Reference 추출"""
block_refs = []
try:
# 모델스페이스에서 INSERT 엔티티 찾기
msp = doc.modelspace()
for insert in msp.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
# 페이퍼스페이스도 확인
for layout_name in doc.layout_names_in_taborder():
if layout_name.startswith('*'): # 모델스페이스 제외
continue
try:
layout = doc.paperspace(layout_name)
for insert in layout.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
except Exception as e:
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
self.logger.info(f"{len(block_refs)}개의 블록 참조를 찾았습니다.")
return block_refs
except Exception as e:
self.logger.error(f"블록 참조 추출 중 오류: {e}")
return []
def _process_block_reference(self, doc: Drawing, insert: Insert) -> Optional[BlockInfo]:
"""개별 Block Reference 처리 - ATTDEF 정보도 함께 수집"""
try:
# 블록 정보 추출
block_name = insert.dxf.name
position = (insert.dxf.insert.x, insert.dxf.insert.y, insert.dxf.insert.z)
scale = (
getattr(insert.dxf, 'xscale', 1.0),
getattr(insert.dxf, 'yscale', 1.0),
getattr(insert.dxf, 'zscale', 1.0)
)
rotation = getattr(insert.dxf, 'rotation', 0.0)
layer = getattr(insert.dxf, 'layer', '0')
# ATTDEF 정보 수집 (프롬프트 정보 포함)
attdef_info = {}
try:
block_layout = doc.blocks.get(block_name)
if block_layout:
for attdef in block_layout.query('ATTDEF'):
tag = getattr(attdef.dxf, 'tag', '')
prompt = getattr(attdef.dxf, 'prompt', '')
if tag:
attdef_info[tag] = {
'prompt': prompt,
'default_text': getattr(attdef.dxf, 'text', ''),
'position': (attdef.dxf.insert.x, attdef.dxf.insert.y, attdef.dxf.insert.z),
'height': getattr(attdef.dxf, 'height', 1.0),
'style': getattr(attdef.dxf, 'style', 'Standard'),
'invisible': getattr(attdef.dxf, 'invisible', False),
'const': getattr(attdef.dxf, 'const', False),
'verify': getattr(attdef.dxf, 'verify', False),
'preset': getattr(attdef.dxf, 'preset', False)
}
except Exception as e:
self.logger.debug(f"ATTDEF 정보 수집 실패: {e}")
# ATTRIB 속성 추출 및 ATTDEF 정보와 결합
attributes = []
for attrib in insert.attribs:
attr_info = self._extract_attribute_info(attrib)
if attr_info and attr_info.tag in attdef_info:
# ATTDEF에서 프롬프트 정보 추가
attr_info.prompt = attdef_info[attr_info.tag]['prompt']
if attr_info:
attributes.append(attr_info)
return BlockInfo(
name=block_name,
position=position,
scale=scale,
rotation=rotation,
layer=layer,
attributes=attributes
)
except Exception as e:
self.logger.warning(f"블록 참조 처리 중 오류: {e}")
return None
def _extract_attribute_info(self, attrib: Attrib) -> Optional[AttributeInfo]:
"""Attribute Reference에서 모든 정보 추출"""
try:
# 기본 속성
tag = getattr(attrib.dxf, 'tag', '')
text = getattr(attrib.dxf, 'text', '')
# 위치 정보
insert_point = getattr(attrib.dxf, 'insert', (0, 0, 0))
position = (insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
insert_point.z if hasattr(insert_point, 'z') else insert_point[2])
# 텍스트 속성
height = getattr(attrib.dxf, 'height', 1.0)
width = getattr(attrib.dxf, 'width', 1.0)
rotation = getattr(attrib.dxf, 'rotation', 0.0)
# 레이어 및 스타일
layer = getattr(attrib.dxf, 'layer', '0')
style = getattr(attrib.dxf, 'style', 'Standard')
# 속성 플래그
invisible = getattr(attrib.dxf, 'invisible', False)
const = getattr(attrib.dxf, 'const', False)
verify = getattr(attrib.dxf, 'verify', False)
preset = getattr(attrib.dxf, 'preset', False)
# 정렬 정보
align_point_data = getattr(attrib.dxf, 'align_point', None)
align_point = None
if align_point_data:
align_point = (align_point_data.x if hasattr(align_point_data, 'x') else align_point_data[0],
align_point_data.y if hasattr(align_point_data, 'y') else align_point_data[1],
align_point_data.z if hasattr(align_point_data, 'z') else align_point_data[2])
halign = getattr(attrib.dxf, 'halign', 0)
valign = getattr(attrib.dxf, 'valign', 0)
# 텍스트 형식
text_generation_flag = getattr(attrib.dxf, 'text_generation_flag', 0)
oblique_angle = getattr(attrib.dxf, 'oblique_angle', 0.0)
width_factor = getattr(attrib.dxf, 'width_factor', 1.0)
# 시각적 속성
color = getattr(attrib.dxf, 'color', None)
linetype = getattr(attrib.dxf, 'linetype', None)
lineweight = getattr(attrib.dxf, 'lineweight', None)
# 엔티티 핸들
entity_handle = getattr(attrib.dxf, 'handle', None)
# 텍스트 폭 추정 (높이의 0.6배 * 글자 수)
estimated_width = len(text) * height * 0.6 * width_factor
# 바운딩 박스 계산
bounding_box = self.calculate_text_bounding_box(attrib)
# 프롬프트 정보는 ATTDEF에서 가져와야 함 (필요시 별도 처리)
prompt = None
return AttributeInfo(
tag=tag,
text=text,
position=position,
height=height,
width=width,
rotation=rotation,
layer=layer,
bounding_box=bounding_box,
prompt=prompt,
style=style,
invisible=invisible,
const=const,
verify=verify,
preset=preset,
align_point=align_point,
halign=halign,
valign=valign,
text_generation_flag=text_generation_flag,
oblique_angle=oblique_angle,
width_factor=width_factor,
color=color,
linetype=linetype,
lineweight=lineweight,
insert_x=position[0],
insert_y=position[1],
insert_z=position[2],
estimated_width=estimated_width,
entity_handle=entity_handle
)
except Exception as e:
self.logger.warning(f"속성 정보 추출 중 오류: {e}")
return None
def identify_title_block(self, block_refs: List[BlockInfo]) -> Optional[TitleBlockInfo]:
"""블록 참조들 중에서 도곽을 식별하고 정보 추출"""
title_block_candidates = []
for block_ref in block_refs:
# 도곽 키워드를 포함한 속성이 있는지 확인
keyword_matches = 0
for attr in block_ref.attributes:
for keyword_group in self.TITLE_BLOCK_KEYWORDS.keys():
if self._contains_keyword(attr.tag, keyword_group) or \
self._contains_keyword(attr.text, keyword_group):
keyword_matches += 1
break
# 충분한 키워드가 매칭되면 도곽 후보로 추가
if keyword_matches >= 2: # 최소 2개 이상의 키워드 매칭
title_block_candidates.append((block_ref, keyword_matches))
if not title_block_candidates:
self.logger.warning("도곽 블록을 찾을 수 없습니다.")
return None
# 가장 많은 키워드를 포함한 블록을 도곽으로 선택
title_block_candidates.sort(key=lambda x: x[1], reverse=True)
best_candidate = title_block_candidates[0][0]
self.logger.info(f"도곽 블록 발견: {best_candidate.name} (키워드 매칭: {title_block_candidates[0][1]})")
return self._extract_title_block_info(best_candidate)
def _contains_keyword(self, text: str, keyword_group: str) -> bool:
"""텍스트에 특정 키워드 그룹의 단어가 포함되어 있는지 확인"""
if not text:
return False
text_lower = text.lower()
keywords = self.TITLE_BLOCK_KEYWORDS.get(keyword_group, [])
return any(keyword.lower() in text_lower for keyword in keywords)
def _extract_title_block_info(self, block_ref: BlockInfo) -> TitleBlockInfo:
"""도곽 블록에서 상세 정보 추출 - 모든 attributes 정보 포함"""
# TitleBlockInfo 객체 생성
title_block = TitleBlockInfo(
block_name=block_ref.name,
all_attributes=block_ref.attributes.copy(), # 모든 attributes 정보 저장
block_position=block_ref.position,
block_scale=block_ref.scale,
block_rotation=block_ref.rotation,
block_layer=block_ref.layer
)
# 속성들을 분석하여 도곽 정보 매핑
for attr in block_ref.attributes:
tag_lower = attr.tag.lower()
text_value = attr.text.strip()
if not text_value:
continue
# 각 키워드 그룹별로 매칭 시도
if self._contains_keyword(attr.tag, '도면명') or self._contains_keyword(attr.text, '도면명'):
title_block.drawing_name = text_value
elif self._contains_keyword(attr.tag, '도면번호') or self._contains_keyword(attr.text, '도면번호'):
title_block.drawing_number = text_value
elif self._contains_keyword(attr.tag, '건설분야') or self._contains_keyword(attr.text, '건설분야'):
title_block.construction_field = text_value
elif self._contains_keyword(attr.tag, '건설단계') or self._contains_keyword(attr.text, '건설단계'):
title_block.construction_stage = text_value
elif self._contains_keyword(attr.tag, '축척') or self._contains_keyword(attr.text, '축척'):
title_block.scale = text_value
elif self._contains_keyword(attr.tag, '설계자') or self._contains_keyword(attr.text, '설계자'):
title_block.designer = text_value
elif self._contains_keyword(attr.tag, '프로젝트') or self._contains_keyword(attr.text, '프로젝트'):
title_block.project_name = text_value
elif self._contains_keyword(attr.tag, '날짜') or self._contains_keyword(attr.text, '날짜'):
title_block.date = text_value
elif self._contains_keyword(attr.tag, '리비전') or self._contains_keyword(attr.text, '리비전'):
title_block.revision = text_value
elif self._contains_keyword(attr.tag, '위치') or self._contains_keyword(attr.text, '위치'):
title_block.location = text_value
# 도곽 전체 바운딩 박스 계산
title_block.bounding_box = self._calculate_title_block_bounding_box(block_ref)
# 속성 개수 업데이트
title_block.attributes_count = len(title_block.all_attributes)
# 디버깅 로그 - 모든 attributes 정보 출력
self.logger.info(f"도곽 '{block_ref.name}'에서 {title_block.attributes_count}개의 속성 추출:")
for i, attr in enumerate(title_block.all_attributes):
self.logger.debug(f" [{i+1}] Tag: '{attr.tag}', Text: '{attr.text}', "
f"Position: ({attr.insert_x:.2f}, {attr.insert_y:.2f}, {attr.insert_z:.2f}), "
f"Height: {attr.height:.2f}, Prompt: '{attr.prompt or 'N/A'}'")
return title_block
def _calculate_title_block_bounding_box(self, block_ref: BlockInfo) -> Optional[BoundingBox]:
"""도곽의 전체 바운딩 박스 계산"""
try:
valid_boxes = [attr.bounding_box for attr in block_ref.attributes
if attr.bounding_box is not None]
if not valid_boxes:
self.logger.warning("유효한 바운딩 박스가 없습니다.")
return None
# 모든 바운딩 박스를 포함하는 최외곽 박스 계산
min_x = min(box.min_x for box in valid_boxes)
min_y = min(box.min_y for box in valid_boxes)
max_x = max(box.max_x for box in valid_boxes)
max_y = max(box.max_y for box in valid_boxes)
return BoundingBox(min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y)
except Exception as e:
self.logger.warning(f"도곽 바운딩 박스 계산 실패: {e}")
return None
def process_dxf_file(self, file_path: str) -> Dict[str, Any]:
"""DXF 파일 전체 처리"""
result = {
'success': False,
'error': None,
'file_path': file_path,
'title_block': None,
'block_references': [],
'summary': {}
}
try:
# 파일 유효성 검사
if not self.validate_dxf_file(file_path):
result['error'] = "유효하지 않은 DXF 파일입니다."
return result
# DXF 문서 로드
doc = self.load_dxf_document(file_path)
if not doc:
result['error'] = "DXF 문서를 로드할 수 없습니다."
return result
# Block Reference 추출
block_refs = self.extract_block_references(doc)
result['block_references'] = [asdict(block_ref) for block_ref in block_refs]
# 도곽 정보 추출
title_block = self.identify_title_block(block_refs)
if title_block:
result['title_block'] = asdict(title_block)
# 요약 정보
result['summary'] = {
'total_blocks': len(block_refs),
'title_block_found': title_block is not None,
'title_block_name': title_block.block_name if title_block else None,
'attributes_count': sum(len(br.attributes) for br in block_refs)
}
result['success'] = True
self.logger.info(f"DXF 파일 처리 완료: {file_path}")
except Exception as e:
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
result['error'] = str(e)
return result
def save_analysis_result(self, result: Dict[str, Any], output_file: str) -> bool:
"""분석 결과를 JSON 파일로 저장"""
try:
os.makedirs(Config.RESULTS_FOLDER, exist_ok=True)
output_path = os.path.join(Config.RESULTS_FOLDER, output_file)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2, default=str)
self.logger.info(f"분석 결과 저장 완료: {output_path}")
return True
except Exception as e:
self.logger.error(f"분석 결과 저장 실패: {e}")
return False
def main():
"""테스트용 메인 함수"""
logging.basicConfig(level=logging.INFO)
if not EZDXF_AVAILABLE:
print("ezdxf 라이브러리가 설치되지 않았습니다.")
return
processor = DXFProcessor()
# 테스트 파일 경로 (실제 파일 경로로 변경 필요)
test_file = "test_drawing.dxf"
if os.path.exists(test_file):
result = processor.process_dxf_file(test_file)
if result['success']:
print("DXF 파일 처리 성공!")
print(f"블록 수: {result['summary']['total_blocks']}")
print(f"도곽 발견: {result['summary']['title_block_found']}")
if result['title_block']:
print("\n도곽 정보:")
title_block = result['title_block']
for key, value in title_block.items():
if value and key != 'bounding_box':
print(f" {key}: {value}")
else:
print(f"처리 실패: {result['error']}")
else:
print(f"테스트 파일을 찾을 수 없습니다: {test_file}")
if __name__ == "__main__":
main()