[예측] - 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>
84 lines
3.1 KiB
Python
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
|
|
|