wing-ops/prediction/opendrift/createKmaImage.py
jeonghyo.k 88eb6b121a feat(prediction): OpenDrift 유류 확산 시뮬레이션 통합 + CCTV/관리자 고도화
[예측]
- OpenDrift Python API 서버 및 스크립트 추가 (prediction/opendrift/)
- 시뮬레이션 상태 폴링 훅(useSimulationStatus), 로딩 오버레이 추가
- HydrParticleOverlay: deck.gl 기반 입자 궤적 시각화 레이어
- OilSpillView/LeftPanel/RightPanel: 시뮬레이션 실행·결과 표시 UI 개편
- predictionService/predictionRouter: 시뮬레이션 CRUD 및 상태 관리 API
- simulation.ts: OpenDrift 연동 엔드포인트 확장
- docs/PREDICTION-GUIDE.md: 예측 기능 개발 가이드 추가

[CCTV/항공방제]
- CCTV 오일 감지 GPU 추론 연동 (OilDetectionOverlay, useOilDetection)
- CCTV 안전관리 감지 기능 추가 (선박 출입, 침입 감지)
- oil_inference_server.py: Python GPU 추론 서버

[관리자]
- 관리자 화면 고도화 (사용자/권한/게시판/선박신호 패널)
- AdminSidebar, BoardMgmtPanel, VesselSignalPanel 신규 컴포넌트

[기타]
- DB: 시뮬레이션 결과, 선박보험 시드(1391건), 역할 정리 마이그레이션
- 팀 워크플로우 v1.6.1 동기화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 14:55:46 +09:00

84 lines
3.1 KiB
Python

from PIL import Image
import base64
from io import BytesIO
def crop_and_encode_geographic_kma_image(
image_path: str,
type: str
) -> str:
"""
지정된 PNG 이미지에서 특정 위경도 중심을 기준으로
주변 crop_radius_km 영역을 잘라내고 Base64 문자열로 인코딩합니다.
:param image_path: 입력 PNG 파일 경로.
:param image_bounds: 이미지 전체가 나타내는 영역의 (min_lon, min_lat, max_lon, max_lat)
좌표. (경도 최소, 위도 최소, 경도 최대, 위도 최대)
:param center_point: 자르기 영역의 중심이 될 (lon, lat) 좌표.
:param crop_radius_km: 중심에서 상하좌우로 자를 거리 (km).
:return: 잘린 이미지의 PNG Base64 문자열.
"""
if type == 'wind':
full_image_bounds = (78.0993797274984161,12.1585396012363489, 173.8566763130032484,61.1726557651764793)
elif type == 'hydr':
full_image_bounds = (101.57732, 12.21703, 155.62642, 57.32893)
image_bounds = (121.8399658203125, 32.2400016784668, 131.679931640625, 42.79999923706055)
TARGET_WIDTH = 50
TARGET_HEIGHT = 90
# 1. 이미지 로드
try:
img = Image.open(image_path)
except FileNotFoundError:
return f"Error: File not found at {image_path}"
except Exception as e:
return f"Error opening image: {e}"
width, height = img.size
full_min_lon, full_min_lat, full_max_lon, full_max_lat = full_image_bounds
full_deg_lat_span = full_max_lat - full_min_lat
full_deg_lon_span = full_max_lon - full_min_lon
min_lon, min_lat, max_lon, max_lat = image_bounds
def lon_to_pixel_x(lon):
return int(width * (lon - full_min_lon) / full_deg_lon_span)
def lat_to_pixel_y(lat):
# Y축은 위도에 반비례 (큰 위도가 작은 Y 픽셀)
return int(height * (full_max_lat - lat) / full_deg_lat_span)
# 자를 영역의 픽셀 좌표 계산
pixel_x_min = max(0, lon_to_pixel_x(min_lon))
pixel_y_min = max(0, lat_to_pixel_y(max_lat)) # 위도 최대가 y_min (상단)
pixel_x_max = min(width, lon_to_pixel_x(max_lon))
pixel_y_max = min(height, lat_to_pixel_y(min_lat)) # 위도 최소가 y_max (하단)
# PIL의 crop 함수는 (left, top, right, bottom) 순서의 픽셀 좌표를 사용
crop_box = (pixel_x_min, pixel_y_min, pixel_x_max, pixel_y_max)
# 4. 이미지 자르기
if pixel_x_min >= pixel_x_max or pixel_y_min >= pixel_y_max:
return "Error: Crop area is outside the image bounds or zero size."
cropped_img = img.crop(crop_box)
if cropped_img.size != (TARGET_WIDTH, TARGET_HEIGHT):
cropped_img = cropped_img.resize(
(TARGET_WIDTH, TARGET_HEIGHT),
Image.LANCZOS
)
# 5. Base64 문자열로 인코딩
buffer = BytesIO()
cropped_img.save(buffer, format="PNG")
base64_encoded_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
# Base64 문자열 앞에 MIME 타입 정보 추가
# base64_string = f"data:image/png;base64,{base64_encoded_data}"
return base64_encoded_data