368 lines
12 KiB
Python
368 lines
12 KiB
Python
import io
|
|
import base64
|
|
import matplotlib
|
|
matplotlib.use('Agg')
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
from PIL import Image
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
bp = Blueprint('water_body_segmentation', __name__, url_prefix='/api/water_body_segmentation')
|
|
|
|
# DeepLabV3 모델 (나중에 실제 모델로 교체)
|
|
# from app.AI_modules.DeepLabV3 import create_model, load_model
|
|
deeplabv3_model = None
|
|
|
|
|
|
def generate_satellite_image(lat, lng):
|
|
"""위성 이미지 시뮬레이션 생성 (INPUT)"""
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
|
|
np.random.seed(int((lat + lng) * 1000) % 10000)
|
|
|
|
# 배경 (녹지/농경지)
|
|
terrain = np.random.rand(100, 100) * 0.3 + 0.3
|
|
|
|
# 도로/경계선 추가
|
|
for i in range(5):
|
|
start = np.random.randint(0, 100)
|
|
terrain[start:start+2, :] = 0.9
|
|
terrain[:, start:start+2] = 0.9
|
|
|
|
# 물 영역 생성 (불규칙한 형태)
|
|
y, x = np.ogrid[:100, :100]
|
|
|
|
# 주요 수역 1 (호수/저수지)
|
|
center1_x, center1_y = 35 + np.random.randint(-5, 5), 50 + np.random.randint(-5, 5)
|
|
dist1 = np.sqrt((x - center1_x)**2 + (y - center1_y)**2)
|
|
water_mask1 = dist1 < (15 + np.random.rand(100, 100) * 5)
|
|
|
|
# 주요 수역 2 (강/하천)
|
|
river_path = 60 + np.sin(np.linspace(0, 4*np.pi, 100)) * 10
|
|
for i in range(100):
|
|
width = 3 + int(np.random.rand() * 3)
|
|
river_col = int(river_path[i])
|
|
if 0 <= river_col < 100:
|
|
terrain[i, max(0, river_col-width):min(100, river_col+width)] = 0.2
|
|
|
|
terrain[water_mask1] = 0.15 + np.random.rand(np.sum(water_mask1)) * 0.1
|
|
|
|
# 녹색 채널 강조 (위성 이미지처럼)
|
|
rgb_image = np.zeros((100, 100, 3))
|
|
rgb_image[:, :, 0] = terrain * 0.4 + 0.1 # R
|
|
rgb_image[:, :, 1] = terrain * 0.8 + 0.1 # G (녹지 강조)
|
|
rgb_image[:, :, 2] = terrain * 0.3 + 0.1 # B
|
|
|
|
# 물 영역은 파란색으로
|
|
water_full_mask = (terrain < 0.3)
|
|
rgb_image[water_full_mask, 0] = 0.1
|
|
rgb_image[water_full_mask, 1] = 0.3
|
|
rgb_image[water_full_mask, 2] = 0.5
|
|
|
|
ax.imshow(rgb_image, extent=[lng-0.05, lng+0.05, lat-0.05, lat+0.05])
|
|
ax.set_xlabel(f'경도 (Longitude)', fontsize=10)
|
|
ax.set_ylabel(f'위도 (Latitude)', fontsize=10)
|
|
ax.set_title(f'위성 이미지 - ({lat:.4f}, {lng:.4f})', fontsize=12)
|
|
|
|
plt.tight_layout()
|
|
buf = io.BytesIO()
|
|
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
|
buf.seek(0)
|
|
image_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
|
plt.close(fig)
|
|
|
|
return image_base64, water_full_mask
|
|
|
|
|
|
def generate_segmentation_image(lat, lng, water_mask):
|
|
"""Water Body 세그멘테이션 결과 생성 (OUTPUT)"""
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
|
|
np.random.seed(int((lat + lng) * 1000) % 10000)
|
|
|
|
# 배경 (원본 이미지 흐리게)
|
|
terrain = np.random.rand(100, 100) * 0.3 + 0.3
|
|
|
|
# 도로/경계선
|
|
for i in range(5):
|
|
start = np.random.randint(0, 100)
|
|
terrain[start:start+2, :] = 0.9
|
|
terrain[:, start:start+2] = 0.9
|
|
|
|
# RGB 이미지 생성 (흐린 배경)
|
|
rgb_image = np.zeros((100, 100, 3))
|
|
rgb_image[:, :, 0] = terrain * 0.5 + 0.3
|
|
rgb_image[:, :, 1] = terrain * 0.6 + 0.3
|
|
rgb_image[:, :, 2] = terrain * 0.5 + 0.3
|
|
|
|
# Water Body를 밝은 파란색/보라색으로 하이라이트
|
|
rgb_image[water_mask, 0] = 0.4
|
|
rgb_image[water_mask, 1] = 0.5
|
|
rgb_image[water_mask, 2] = 0.95
|
|
|
|
# 경계선 추가
|
|
from scipy import ndimage
|
|
edges = ndimage.binary_dilation(water_mask) & ~water_mask
|
|
rgb_image[edges, 0] = 1.0
|
|
rgb_image[edges, 1] = 1.0
|
|
rgb_image[edges, 2] = 1.0
|
|
|
|
ax.imshow(rgb_image, extent=[lng-0.05, lng+0.05, lat-0.05, lat+0.05])
|
|
ax.set_xlabel(f'경도 (Longitude)', fontsize=10)
|
|
ax.set_ylabel(f'위도 (Latitude)', fontsize=10)
|
|
ax.set_title(f'Water Body 추출 결과', fontsize=12)
|
|
|
|
# 범례 추가
|
|
from matplotlib.patches import Patch
|
|
legend_elements = [Patch(facecolor='#6680f2', edgecolor='white', label='Water Body')]
|
|
ax.legend(handles=legend_elements, loc='upper right')
|
|
|
|
plt.tight_layout()
|
|
buf = io.BytesIO()
|
|
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
|
buf.seek(0)
|
|
image_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
|
plt.close(fig)
|
|
|
|
# 면적 계산 (시뮬레이션)
|
|
water_pixel_count = np.sum(water_mask)
|
|
total_pixels = water_mask.size
|
|
water_ratio = (water_pixel_count / total_pixels) * 100
|
|
|
|
return image_base64, water_pixel_count, water_ratio
|
|
|
|
|
|
@bp.route('/extract', methods=['GET'])
|
|
def extract():
|
|
"""Water Body 추출 결과 반환"""
|
|
try:
|
|
lat = float(request.args.get('lat', 34.6047))
|
|
lng = float(request.args.get('lng', 127.2855))
|
|
radius = float(request.args.get('radius', 5))
|
|
|
|
# 입력 이미지 생성
|
|
input_image, water_mask = generate_satellite_image(lat, lng)
|
|
|
|
# 출력 이미지 생성
|
|
output_image, water_pixels, water_ratio = generate_segmentation_image(lat, lng, water_mask)
|
|
|
|
# 면적 계산 (시뮬레이션: 1 pixel = 약 100m²)
|
|
area_m2 = water_pixels * 100
|
|
water_body_count = np.random.randint(2, 8)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Water Body 추출 완료',
|
|
'input_image': input_image,
|
|
'output_image': output_image,
|
|
'data': {
|
|
'area': f'{area_m2:,} ㎡',
|
|
'ratio': f'{water_ratio:.1f}%',
|
|
'count': water_body_count,
|
|
'lat': lat,
|
|
'lng': lng
|
|
}
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e),
|
|
'input_image': None,
|
|
'output_image': None
|
|
})
|
|
|
|
|
|
def simulate_water_segmentation(image_array):
|
|
"""
|
|
Water Body 세그멘테이션 시뮬레이션
|
|
(실제 DeepLabV3 모델로 교체 예정)
|
|
"""
|
|
height, width = image_array.shape[:2]
|
|
|
|
# 파란색 계열 검출 (간단한 색상 기반 검출)
|
|
if len(image_array.shape) == 3:
|
|
# RGB -> HSV 변환 대신 간단한 파란색 검출
|
|
r = image_array[:, :, 0].astype(float)
|
|
g = image_array[:, :, 1].astype(float)
|
|
b = image_array[:, :, 2].astype(float)
|
|
|
|
# 파란색/청록색 영역 검출
|
|
blue_ratio = b / (r + g + b + 1e-6)
|
|
green_blue_diff = np.abs(g - b) / 255.0
|
|
|
|
# 물로 추정되는 영역 (파란색 비율이 높고, 전체적으로 어두운 영역)
|
|
brightness = (r + g + b) / 3.0
|
|
water_mask = (blue_ratio > 0.35) | ((brightness < 100) & (b > r))
|
|
|
|
# 노이즈 제거를 위한 morphological operation 시뮬레이션
|
|
from scipy import ndimage
|
|
water_mask = ndimage.binary_opening(water_mask, iterations=2)
|
|
water_mask = ndimage.binary_closing(water_mask, iterations=2)
|
|
else:
|
|
# 그레이스케일 이미지
|
|
water_mask = image_array < 100
|
|
|
|
return water_mask
|
|
|
|
|
|
def generate_prediction_overlay(original_image, water_mask):
|
|
"""
|
|
원본 이미지에 Water Body 예측 결과 오버레이
|
|
"""
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
|
|
# 원본 이미지 표시
|
|
ax.imshow(original_image)
|
|
|
|
# Water Body 마스크를 반투명 파란색으로 오버레이
|
|
overlay = np.zeros((*water_mask.shape, 4))
|
|
overlay[water_mask, 0] = 0.2 # R
|
|
overlay[water_mask, 1] = 0.5 # G
|
|
overlay[water_mask, 2] = 1.0 # B
|
|
overlay[water_mask, 3] = 0.5 # Alpha
|
|
|
|
ax.imshow(overlay)
|
|
|
|
# 경계선 추가
|
|
from scipy import ndimage
|
|
edges = ndimage.binary_dilation(water_mask, iterations=2) & ~water_mask
|
|
edge_overlay = np.zeros((*water_mask.shape, 4))
|
|
edge_overlay[edges, 0] = 0.0
|
|
edge_overlay[edges, 1] = 1.0
|
|
edge_overlay[edges, 2] = 1.0
|
|
edge_overlay[edges, 3] = 1.0
|
|
ax.imshow(edge_overlay)
|
|
|
|
ax.axis('off')
|
|
ax.set_title('Water Body Detection Result', fontsize=14, fontweight='bold')
|
|
|
|
# 범례
|
|
from matplotlib.patches import Patch
|
|
legend_elements = [
|
|
Patch(facecolor='#3380ff', alpha=0.5, edgecolor='cyan', label='Water Body')
|
|
]
|
|
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
|
|
|
|
plt.tight_layout()
|
|
buf = io.BytesIO()
|
|
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
|
buf.seek(0)
|
|
image_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
|
plt.close(fig)
|
|
|
|
return image_base64
|
|
|
|
|
|
@bp.route('/predict', methods=['POST'])
|
|
def predict():
|
|
"""
|
|
업로드된 이미지에서 Water Body 추출
|
|
POST /api/water_body_segmentation/predict
|
|
Form Data: image (file)
|
|
"""
|
|
try:
|
|
# 이미지 파일 확인
|
|
if 'image' not in request.files:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': '이미지 파일이 필요합니다.'
|
|
}), 400
|
|
|
|
file = request.files['image']
|
|
if file.filename == '':
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': '파일이 선택되지 않았습니다.'
|
|
}), 400
|
|
|
|
# 이미지 로드
|
|
image = Image.open(file.stream)
|
|
if image.mode != 'RGB':
|
|
image = image.convert('RGB')
|
|
|
|
# numpy 배열로 변환
|
|
image_array = np.array(image)
|
|
|
|
# Water Body 세그멘테이션 (현재는 시뮬레이션)
|
|
# TODO: 실제 DeepLabV3 모델로 교체
|
|
# if deeplabv3_model is not None:
|
|
# water_mask = run_deeplabv3_prediction(deeplabv3_model, image_array)
|
|
# else:
|
|
water_mask = simulate_water_segmentation(image_array)
|
|
|
|
# 결과 이미지 생성
|
|
output_image = generate_prediction_overlay(image_array, water_mask)
|
|
|
|
# 통계 계산
|
|
water_pixel_count = np.sum(water_mask)
|
|
total_pixels = water_mask.size
|
|
water_ratio = (water_pixel_count / total_pixels) * 100
|
|
|
|
# 연결된 영역 수 계산 (Water Body 개수)
|
|
from scipy import ndimage
|
|
labeled_array, num_features = ndimage.label(water_mask)
|
|
|
|
# 면적 추정 (픽셀 기반, 실제 스케일은 이미지 메타데이터 필요)
|
|
# 가정: 1 pixel = 약 1 m² (해상도에 따라 조정 필요)
|
|
pixel_area = 1.0 # m²/pixel
|
|
estimated_area = water_pixel_count * pixel_area
|
|
|
|
# 신뢰도 (시뮬레이션)
|
|
confidence = min(95.0, 70.0 + water_ratio * 0.5)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Water Body 추출 완료',
|
|
'output_image': output_image,
|
|
'data': {
|
|
'area': f'{estimated_area:,.0f} ㎡',
|
|
'ratio': f'{water_ratio:.1f}%',
|
|
'count': num_features,
|
|
'confidence': f'{confidence:.1f}%',
|
|
'pixels': int(water_pixel_count)
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
|
|
@bp.route('/load-model', methods=['POST'])
|
|
def load_deeplabv3_model():
|
|
"""
|
|
DeepLabV3 모델 로드
|
|
POST /api/water_body_segmentation/load-model
|
|
JSON: { "model_path": "path/to/model.h5" }
|
|
"""
|
|
global deeplabv3_model
|
|
|
|
try:
|
|
data = request.get_json()
|
|
model_path = data.get('model_path')
|
|
|
|
if not model_path:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': '모델 경로가 필요합니다.'
|
|
}), 400
|
|
|
|
# TODO: 실제 모델 로드
|
|
# from app.AI_modules.DeepLabV3 import load_model
|
|
# deeplabv3_model = load_model(model_path)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': f'모델 로드 완료: {model_path}'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|