[예측] - 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>
104 lines
4.3 KiB
Python
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
|