Files
ExperionCrawler/dxf-graph/Graph_Pipeline_Phase1.md
2026-05-08 17:22:10 +09:00

10 KiB

🛠️ Graph Pipeline Phase 1: 기하학적 데이터 추출 (Geometric Extraction)

이 문서는 P&ID Graph Pipeline의 첫 번째 단계인 기하학적 데이터 추출의 상세 구현 계획을 다룹니다. 목표는 단순한 텍스트 추출을 넘어, 도면 내 모든 객체의 **물리적 위치(좌표)**와 기하학적 속성을 보존하여 이후 위상 모델링(Topology Modeling)이 가능하도록 하는 것입니다.


📦 1. 필수 패키지 및 환경 설정

1.1 Python 패키지

패키지 용도 비고
ezdxf DXF 파일 파싱 및 엔티티 추출 핵심 라이브러리
shapely 기하학적 연산 (Intersection, Distance, Bounding Box) 좌표 기반 분석 필수
numpy 대량의 좌표 데이터 계산 및 행렬 연산 성능 최적화
pandas 추출된 객체 데이터의 구조화 및 CSV/JSON 저장 데이터 관리
pydantic 추출 데이터의 스키마 정의 및 유효성 검증 데이터 무결성 보장
pytesseract / pdf2image PDF 도면의 영역 기반 OCR 추출 PDF 처리 시 필요

1.2 설치 명령어

pip install ezdxf shapely numpy pandas pydantic pytesseract pdf2image

📐 2. 상세 설계 구조

2.1 데이터 모델 (Schema)

모든 추출 객체는 다음과 같은 공통 속성을 갖는 GeometricEntity 모델을 따릅니다.

from pydantic import BaseModel
from typing import List, Optional, Union, Tuple

class BoundingBox(BaseModel):
    min_x: float
    min_y: float
    max_x: float
    max_y: float
    center: Tuple[float, float]

class GeometricEntity(BaseModel):
    entity_id: str
    entity_type: str  # TEXT, LINE, CIRCLE, POLYLINE, ARC
    layer: str
    bbox: BoundingBox
    properties: dict  # 텍스트 값, 색상, 선 굵기 등
    coordinates: List[Tuple[float, float]]  # 시작점, 끝점 또는 정점 리스트

2.2 처리 파이프라인 흐름

  1. DXF Load: ezdxf.readfile()을 통해 도면 로드.
  2. Entity Iteration: 모든 레이어의 엔티티를 순회하며 타입별 분류.
  3. Coordinate Extraction:
    • TEXT: 삽입점(Insertion Point) 및 텍스트 길이를 이용한 BBox 계산.
    • LINE: 시작점(Start)과 끝점(End) 추출.
    • POLYLINE: 모든 정점(Vertices) 리스트 추출.
    • CIRCLE/ARC: 중심점(Center)과 반지름(Radius) 추출.
  4. Spatial Normalization: 도면 좌표계를 분석 시스템 좌표계로 정규화.
  5. Structured Export: JSON 또는 DB(PostgreSQL/PostGIS)에 저장.

💻 3. 실제 구현 코딩 가이드 (Example)

3.1 DXF 기하학적 추출 핵심 코드

import ezdxf
import re
import json
from shapely.geometry import box, LineString, Point
from typing import List, Optional, Tuple

class PidGeometricExtractor:
    def __init__(self, file_path: str):
        self.doc = ezdxf.readfile(file_path)
        self.msp = self.doc.modelspace()

    def clean_text(self, text: str) -> str:
        """DXF 특수 제어 문자 및 MTEXT 포맷팅을 최대한 제거하여 LLM 토큰 부하 감소"""
        if not text:
            return ""
        
        # 1. MTEXT 포맷팅 및 제어 문자 제거
        # \P(줄바꿈), \W(너비), \L(밑줄), \A(정렬), \C(색상), \H(높이), \S(스택), \T(탭) 및 관련 인자 제거
        text = re.sub(r'\\([P|W|L|A|C|H|S|T])\d*;?', ' ', text)
        
        # 2. 중괄호 { } 제거 (MTEXT에서 서식 지정 시 사용됨)
        text = re.sub(r'[\{\}]', ' ', text)
        
        # 3. DXF 특수 제어 문자 제거 (%%U: Underline, %%O: Overline, %%S: Strikethrough, %%R: Registered)
        text = re.sub(r'%%[U|O|S|R]', ' ', text)
        
        # 4. 불필요한 특수 기호 및 반복되는 공백 정제
        # - 연속된 공백을 하나로 통합
        # - 텍스트 양 끝의 공백 제거
        text = re.sub(r'\s+', ' ', text).strip()
        
        return text

    def get_bbox(self, entity) -> Optional[box]:
        """엔티티의 Bounding Box를 계산하여 shapely box 객체로 반환"""
        try:
            if entity.dxftype() == 'TEXT':
                p = entity.dxf.insert
                h = entity.dxf.height
                # 텍스트 길이에 따른 대략적인 너비 계산 (글자수 * 높이 * 0.6)
                width = len(entity.dxf.text) * h * 0.6
                return box(p.x, p.y, p.x + width, p.y + h)
            
            elif entity.dxftype() == 'MTEXT':
                p = entity.dxf.insert
                h = entity.dxf.char_height if hasattr(entity.dxf, 'char_height') else 2.5
                # MTEXT는 보통 width 속성이 정의되어 있음
                w = entity.dxf.width if entity.dxf.width > 0 else len(entity.text) * h * 0.6
                return box(p.x, p.y, p.x + w, p.y + h)

            elif entity.dxftype() == 'LINE':
                start = entity.dxf.start
                end = entity.dxf.end
                return box(min(start.x, end.x), min(start.y, end.y),
                          max(start.x, end.x), max(start.y, end.y))
            
            elif entity.dxftype() == 'LWPOLYLINE':
                points = entity.get_points()
                xs = [p[0] for p in points]
                ys = [p[1] for p in points]
                return box(min(xs), min(ys), max(xs), max(ys))
                
        except Exception as e:
            print(f"Error calculating bbox for {entity.dxftype()}: {e}")
        return None

    def extract_and_save(self, output_path: str):
        """
        추출된 기하학적 데이터를 파일로 저장하여 Phase 3 Worker들이
        공유 메모리/파일 시스템을 통해 참조할 수 있도록 함 (Phase 5 병렬 아키텍처 반영)
        """
        results = []
        for entity in self.msp:
            bbox_obj = self.get_bbox(entity)
            if bbox_obj:
                # 텍스트 값 추출 및 정제
                raw_text = ""
                if entity.dxftype() == 'TEXT':
                    raw_text = entity.dxf.text
                elif entity.dxftype() == 'MTEXT':
                    raw_text = entity.text
                
                results.append({
                    "id": entity.dxf.handle,
                    "type": entity.dxftype(),
                    "layer": entity.dxf.layer,
                    "bbox": {
                        "min_x": bbox_obj.bounds[0],
                        "min_y": bbox_obj.bounds[1],
                        "max_x": bbox_obj.bounds[2],
                        "max_y": bbox_obj.bounds[3]
                    },
                    "raw_value": raw_text,
                    "clean_value": self.clean_text(raw_text) if raw_text else None,
                    "coordinates": entity.get_points() if hasattr(entity, 'get_points') else []
                })
        
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(results, f, ensure_ascii=False, indent=4)
        
        return output_path

# 사용 예시 (Phase 5 Orchestrator 관점)
extractor = PidGeometricExtractor("plant_drawing.dxf")
# 데이터를 직접 반환받지 않고 공유 저장소(파일)에 적재
geo_data_path = extractor.extract_and_save("shared_geo_data.json")

3.2 유틸리티 함수: 인접성 체크 (Proximity Utility)

추후 2단계(위상 모델링)에서 사용할 핵심 유틸리티입니다.

from shapely.geometry import Point

def is_near(entity_a_bbox, entity_b_bbox, threshold=5.0):
    """두 객체의 Bounding Box 간의 최단 거리가 임계값 이내인지 확인"""
    return entity_a_bbox.distance(entity_b_bbox) <= threshold

def is_inside(point, bbox):
    """특정 점이 Bounding Box 내부에 있는지 확인"""
    return bbox.contains(Point(point))

🚀 4. Phase 1 완료 기준 (Definition of Done)

  • DXF 파일 내 모든 TEXT, LINE, POLYLINE의 좌표 데이터가 누락 없이 추출되는가?
  • 각 객체별로 정확한 Bounding Box가 계산되어 저장되는가?
  • 추출된 데이터가 GeometricEntity 스키마에 맞게 JSON 파일로 저장되어 Worker들이 공유 참조 가능한가? (Phase 5 반영)
  • (선택 사항) PDF 도면의 경우 OCR을 통해 텍스트의 좌표값이 추출되는가?

🧐 감독자 진단 결과 (2026-05-02)

1. 프로그램 설계 점검

  • 강점: ezdxfshapely를 조합하여 기하학적 데이터(BBox, 좌표)를 보존하려는 접근 방식이 매우 적절함. 특히 Phase 5의 병렬 아키텍처를 고려하여 데이터를 파일/공유 저장소에 적재하는 구조는 확장성 면에서 우수함.
  • 보완 필요 사항:
    • MTEXT 처리: 현재 예시 코드(3.1)는 TEXT 엔티티만 처리하고 있으나, 실제 DXF 파일 분석 결과 MTEXT 엔티티가 다수 존재함. MTEXT는 내부 포맷팅 코드(예: \P, \W)가 포함되어 있어 단순 텍스트 추출 시 정제가 필요함.
    • BBox 계산 정밀도: TEXT 엔티티의 BBox를 p.x + 10, p.y + 5와 같이 상수로 처리하고 있음. 실제 도면의 폰트 크기(height)와 정렬 방식(align)을 반영한 동적 계산 로직이 반드시 추가되어야 함.

2. 실제 도면(No-10_Plant_PID.dxf) 분석 기반 차이점

  • 엔티티 규모: 총 28,819개의 엔티티가 존재하여 데이터 양이 상당함. 단순 리스트 저장보다는 인덱싱 전략이 필요할 수 있음.
  • 텍스트 복잡도:
    • MTEXT 내에 \P (줄바꿈), \L (밑줄) 등 제어 문자가 포함된 수정 사항(Revision) 텍스트가 많음. 이를 그대로 추출하면 위상 분석 시 노이즈가 될 가능성이 높음.
    • %%U (Underline)와 같은 DXF 특수 제어 문자가 텍스트 값에 포함되어 있어, 이를 제거하는 전처리 과정이 필수적임.
  • 데이터 특성: IA-10922-25A-F1A-n와 같은 복합 파이프라인 번호(Pipe Line Number) 형식이 확인됨. 이를 일반 태그(Tag Name)와 명확히 구분하여 추출하고 관리하는 로직이 Phase 2/3에서 중요하게 작용할 것으로 보임.

3. 최종 권고 사항

  1. MTEXT 지원 추가: PidGeometricExtractorMTEXT 처리 로직을 추가하고, 제어 문자를 제거하는 clean_text() 유틸리티 함수를 구현할 것.
  2. 동적 BBox 구현: entity.dxf.height를 활용하여 텍스트 크기에 맞는 정확한 Bounding Box를 계산하도록 수정할 것.
  3. 전처리 파이프라인 강화: 추출 단계에서 %%U 등의 특수 문자를 제거하는 정제 단계를 추가하여 데이터 품질을 높일 것.