wing-ops/prediction/scat/image_extractor.py
leedano d9fb4506bc feat(scat): Pre-SCAT 관할서 필터링 + 해안조사 데이터 파이프라인 구축
- 백엔드: 관할서 목록 API, zone 필터링 쿼리 추가
- 프론트: ScatLeftPanel 관할서 드롭다운, ScatMap/ScatPopup 개선
- 기상탭: WeatherRightPanel 리팩토링
- prediction/scat: PDF 파싱 → 지오코딩 → ESI 매핑 파이프라인
- vite.config: proxy 설정 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:53:19 +09:00

125 lines
3.7 KiB
Python

"""PDF에서 해안조사 사진을 추출하여 scat-photos 폴더에 저장.
Type A (해안사전평가정보집): 1페이지 = 1구간 → 가장 큰 RGB 사진
Type B (방제정보집): 1페이지 = 2구간 → RGB 사진 2장, 순서대로 매칭
저장 네이밍: {sect_cd}-1.png (ScatPopup에서 참조하는 형식)
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import List, Tuple
import fitz # PyMuPDF
from pdf_parser import is_data_page, _CODE_RE
from pdf_parser_b import is_data_page_b
import config
def _get_page_photos(
doc: fitz.Document, page: fitz.Page,
) -> List[fitz.Pixmap]:
"""페이지에서 RGB 사진만 추출 (배경 제외). 크기 내림차순."""
photos: List[Tuple[fitz.Pixmap, int]] = []
for img_info in page.get_images(full=True):
xref = img_info[0]
pix = fitz.Pixmap(doc, xref)
# CMYK → RGB
if pix.n > 4:
pix = fitz.Pixmap(fitz.csRGB, pix)
elif pix.n == 4:
pix = fitz.Pixmap(fitz.csRGB, pix)
# 흑백(n=1) 또는 배경(2000px 이상) 스킵
if pix.n < 3 or pix.width >= 2000:
continue
photos.append((pix, pix.width * pix.height))
photos.sort(key=lambda x: x[1], reverse=True)
return photos
def _extract_codes_type_a(page: fitz.Page) -> List[str]:
"""Type A 페이지에서 sect_cd 추출."""
text = page.get_text('text')
return _CODE_RE.findall(text)
def _extract_codes_type_b(page: fitz.Page) -> List[str]:
"""Type B 페이지에서 sect_cd 추출 (순서 유지)."""
text = page.get_text('text')
codes = re.findall(r'([A-Z]{4,}-\d+(?:-[A-Z]){3,})', text)
seen = set()
unique = []
for c in codes:
if c not in seen:
seen.add(c)
unique.append(c)
return unique
def _save_pixmap(pix: fitz.Pixmap, output_path: Path):
"""Pixmap을 PNG로 저장."""
if pix.alpha:
pix = fitz.Pixmap(fitz.csRGB, pix)
pix.save(str(output_path))
def extract_images_from_pdf(
pdf_path: str | Path,
output_dir: str | Path | None = None,
pdf_type: str = 'A',
) -> int:
"""PDF에서 데이터 페이지별 대표 사진을 추출.
Args:
pdf_path: PDF 파일 경로
output_dir: 이미지 저장 경로 (기본: config.SCAT_PHOTOS_DIR)
pdf_type: 'A' 또는 'B'
Returns:
추출된 이미지 수
"""
pdf_path = Path(pdf_path)
out_dir = Path(output_dir) if output_dir else config.SCAT_PHOTOS_DIR
out_dir.mkdir(parents=True, exist_ok=True)
doc = fitz.open(str(pdf_path))
saved = 0
for i in range(doc.page_count):
page = doc[i]
if pdf_type == 'A':
if not is_data_page(page):
continue
codes = _extract_codes_type_a(page)
photos = _get_page_photos(doc, page)
if not codes or not photos:
continue
# 가장 작은 RGB 이미지 = 실제 현장 사진
pix = photos[-1][0]
out_path = out_dir / f'{codes[0]}-1.png'
_save_pixmap(pix, out_path)
saved += 1
elif pdf_type == 'B':
if not is_data_page_b(page):
continue
codes = _extract_codes_type_b(page)
photos = _get_page_photos(doc, page)
if not codes or not photos:
continue
# Type B: 사진 크기 동일, 순서대로 매칭
for idx, code in enumerate(codes):
if idx < len(photos):
pix = photos[idx][0]
out_path = out_dir / f'{code}-1.png'
_save_pixmap(pix, out_path)
saved += 1
doc.close()
return saved