wing-ops/prediction/opendrift/createImage.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

104 lines
4.3 KiB
Python

from PIL import Image
import base64
from io import BytesIO
def crop_and_encode_geographic_image(
image_path: str,
center_point: tuple[float, float] # (center_lon, center_lat)
) -> 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 문자열.
"""
image_bounds = (124.2083983267507250, 32.7916655404227129, 129.9583954964914767, 38.9583268301827559)
crop_radius_km = 25.0
# 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
min_lon, min_lat, max_lon, max_lat = image_bounds
center_lon, center_lat = center_point
# 2. 위경도 경계 계산 (25km 반경)
# 1도당 근사적인 거리 (대한민국 지역 기준)
# 위도 1도: 약 111 km (거의 일정)
# 경도 1도: 위도에 따라 달라지지만, 한국의 위도(약 33~38도)에서 약 88~93 km 정도.
# 안전을 위해 WGS84 타원체 기준 위도 35도에서 경도 1도당 약 91.2km 가정
# 더 정확한 계산을 위해선 `pyproj` 등의 라이브러리 사용이 권장되나, 여기선 근사치 사용
KM_PER_DEG_LAT = 111.0 # 위도 1도당 km (근사치)
KM_PER_DEG_LON_AT_35 = 91.2 # 위도 35도에서 경도 1도당 km (근사치)
# 위도/경도 1도에 해당하는 픽셀 수 계산
deg_lat_span = max_lat - min_lat
deg_lon_span = max_lon - min_lon
# KM_PER_DEG_LON을 중심 위도에 맞게 조정 (단순화를 위해 상수 사용을 유지)
# 25km에 해당하는 위도/경도 변화량 계산
delta_lat = crop_radius_km / KM_PER_DEG_LAT
delta_lon = crop_radius_km / KM_PER_DEG_LON_AT_35 # 근사치 사용
# 자를 영역의 위경도 바운딩 박스 (Bounding Box)
crop_min_lon = center_lon - delta_lon
crop_max_lon = center_lon + delta_lon
crop_min_lat = center_lat - delta_lat
crop_max_lat = center_lat + delta_lat
bounds = {
"min_lon": float(crop_min_lon),
"max_lon": float(crop_max_lon),
"min_lat": float(crop_min_lat),
"max_lat": float(crop_max_lat)
}
# 3. 위경도 좌표를 픽셀 좌표로 변환 (선형 매핑 가정)
# 픽셀 좌표 x: min_lon -> 0, max_lon -> width
# 픽셀 좌표 y: max_lat -> 0, min_lat -> height (GIS 이미지는 보통 북쪽(위도 최대)이 0에 해당)
def lon_to_pixel_x(lon):
return int(width * (lon - min_lon) / deg_lon_span)
def lat_to_pixel_y(lat):
# Y축은 위도에 반비례 (큰 위도가 작은 Y 픽셀)
return int(height * (max_lat - lat) / deg_lat_span)
# 자를 영역의 픽셀 좌표 계산
pixel_x_min = max(0, lon_to_pixel_x(crop_min_lon))
pixel_y_min = max(0, lat_to_pixel_y(crop_max_lat)) # 위도 최대가 y_min (상단)
pixel_x_max = min(width, lon_to_pixel_x(crop_max_lon))
pixel_y_max = min(height, lat_to_pixel_y(crop_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)
# 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, bounds