Files
s-canvas/filename_classifier.py
HYUNJUNGLEE b9342f6726 Import S-CANVAS source + iter=1~7 lint cleanup
S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:29:08 +09:00

235 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DXF 파일명에서 구조물 종류를 자동 추정.
토목 설계 도면 파일명에는 보통 구조물 종류가 포함됨:
"여수로 수문 설치도.dxf" → spillway_gate
"좌안옹벽 일반도.dxf" → retaining_wall
"신설 취수탑 설비 설치도.dxf" → building
"OO 교량 상세도.dxf" → bridge
"터널 갱구 설치도.dxf" → tunnel_portal
사용법:
from filename_classifier import classify_by_filename
tid = classify_by_filename("좌안옹벽 일반도.dxf")
# → "retaining_wall"
"""
from __future__ import annotations
import re
from pathlib import Path
# ---------------------------------------------------------------------------
# 구조물 유형별 키워드 패턴
# ---------------------------------------------------------------------------
# 우선순위가 높은 것을 먼저 (더 구체적인 패턴 → 일반적인 패턴)
FILENAME_PATTERNS = {
"spillway_gate": [
# 여수로 수문 관련
r"여수로.*수문",
r"수문.*여수로",
r"여수로.*게이트",
r"래디얼.*게이트",
r"테인터.*게이트",
r"tainter.*gate",
r"radial.*gate",
r"spillway.*gate",
r"spillway",
# 수문 단독
r"(?<![\uAC00-\uD7A3])수문(?![\uAC00-\uD7A3])", # 한글 경계
r"gate\b",
],
"retaining_wall": [
r"옹벽",
r"방벽",
r"(?:좌안|우안).*옹벽",
r"retaining.*wall",
r"\bwall\b(?!.*gate)", # wall but not "gate wall" etc.
],
"bridge": [
r"교량",
r"공도교",
r"연륙교",
r"인도교",
r"bridge",
r"(?<![가-힣a-z])교(?:\s|\.|_|$)", # 단독 "교"
r"viaduct",
r"overpass",
],
"tunnel_portal": [
r"터널",
r"갱구",
r"tunnel",
r"portal",
r"굴착.*입구",
],
"intake_tower": [
r"취수탑",
r"intake.*tower",
r"intake.*structure",
],
"valve_chamber": [
r"제수변실",
r"밸브실",
r"변실",
r"valve.*(?:room|chamber)",
r"도수관로",
r"도수.*관",
],
"building": [
# 일반 건축물
r"관리사무",
r"사무소",
r"관리동",
r"수문조작실",
r"변전소",
r"기계실",
r"전기실",
r"건축물",
r"건물",
r"building",
r"office",
r"powerhouse",
r"발전소",
r"펌프장",
r"pump(?:ing)?.*station",
r"조정지",
],
# Generic은 마지막 폴백이므로 패턴 없음
}
# 구조물 종류 힌트가 될 수 있는 추가 키워드 (신뢰도 낮음)
SECONDARY_KEYWORDS = {
"spillway_gate": ["스필웨이", "월류", "유수전환", "가물막이", "cofferdam"],
"retaining_wall": ["절토", "성토사면", "법면"],
"bridge": ["교대", "교각", "상판", "pier", "abutment", "deck"],
"tunnel_portal": ["터파기", "갱내"],
"building": ["사택", "청사", "센터"],
}
# ---------------------------------------------------------------------------
# 분류 함수
# ---------------------------------------------------------------------------
def classify_by_filename(filename: str) -> str | None:
"""파일명에서 구조물 유형을 추정.
Args:
filename: 파일 경로 또는 파일명
Returns:
template_id 문자열 ("spillway_gate", "building" 등) 또는
확실한 매칭이 없으면 None
"""
# 파일명만 추출 (경로/확장자 제거)
name = Path(filename).stem.lower()
# 구두점/숫자 코드 노이즈 제거 (예: "12995740-M40-001")
# 한글/영문 단어만 남김
cleaned = re.sub(r"[\d\-_/\\]+", " ", name)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
# 주 키워드 우선 매칭
for template_id, patterns in FILENAME_PATTERNS.items():
for pat in patterns:
if re.search(pat, cleaned, re.IGNORECASE):
return template_id
# 보조 키워드로 재시도
best_id = None
best_count = 0
for template_id, keywords in SECONDARY_KEYWORDS.items():
count = sum(1 for kw in keywords if kw.lower() in cleaned)
if count > best_count:
best_count = count
best_id = template_id
if best_count >= 1:
return best_id
return None
def classify_by_filenames(filenames: list[str]) -> str | None:
"""여러 파일의 이름을 종합해서 가장 가능성 높은 유형 추정."""
votes = {}
for f in filenames:
tid = classify_by_filename(f)
if tid:
votes[tid] = votes.get(tid, 0) + 1
if not votes:
return None
# 최다 득표
return max(votes.items(), key=lambda x: x[1])[0]
def suggest_with_confidence(filename: str) -> tuple[str | None, float]:
"""추정 결과 + 신뢰도 반환.
Returns:
(template_id, confidence): confidence ∈ [0.0, 1.0]
"""
name = Path(filename).stem.lower()
cleaned = re.sub(r"[\d\-_/\\]+", " ", name)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
# 주 키워드 매칭 개수 및 매칭된 패턴 확인
for template_id, patterns in FILENAME_PATTERNS.items():
matched_patterns = [pat for pat in patterns
if re.search(pat, cleaned, re.IGNORECASE)]
if matched_patterns:
# 매칭 개수에 따라 신뢰도 계산
# 1개 매칭 → 0.75, 2개 → 0.85, 3개+ → 0.95
conf = min(0.95, 0.65 + 0.1 * len(matched_patterns))
return template_id, conf
# 보조 키워드 시도
for template_id, keywords in SECONDARY_KEYWORDS.items():
matched = [kw for kw in keywords if kw.lower() in cleaned]
if matched:
conf = min(0.6, 0.3 + 0.1 * len(matched))
return template_id, conf
return None, 0.0
# ---------------------------------------------------------------------------
# 테스트
# ---------------------------------------------------------------------------
if __name__ == "__main__":
test_cases = [
"12995740-M40-001 여수로 수문 설치도(12).dxf",
"12995740-M40-002 여수로 수문 설치도(22).dxf",
"12996710-M40-001 신설 취수탑 설비 설치도(12).dxf",
"12996710-M40-002 신설 취수탑 설비 설치도(22).dxf",
"12996710-M43-002 신설 제수변실 설비 배치도.dxf",
"1. 좌안옹벽 일반도 작성(2026.0109).dxf",
"사연댐 전체계획 평면도.dxf",
"A-Line 교량 상세도.dxf",
"B-Road 터널 갱구 일반도.dxf",
"관리사무소 평면도.dxf",
"P-Station 펌프장 설치도.dxf",
"random_file.dxf",
]
print("파일명 기반 구조물 유형 추정:")
print("=" * 70)
for f in test_cases:
tid, conf = suggest_with_confidence(f)
if tid:
print(f" [{tid:16}] ({conf:.0%}) {f}")
else:
print(f" [{'' * 16}] (미매칭) {f}")