Geo-Lab / engine /analysis.py
HANSOL
Add Plotly config for WebGL iframe compatibility
2163e58
"""
์ง€ํ˜• ๋ถ„์„ ์—”์ง„ (Terrain Analysis Engine)
๋Œ€ํ•™ ์—ฐ๊ตฌ์šฉ ์ง€ํ˜• ๋ถ„์„ ๋„๊ตฌ ๋ชจ์Œ
- ์ข…/ํšก๋‹จ๋ฉด ํ”„๋กœํŒŒ์ผ
- Hypsometric Curve
- ์‚ฌ๋ฉด ๊ฒฝ์‚ฌ ๋ถ„์„
- ๋ฐฐ์ˆ˜ ๋„คํŠธ์›Œํฌ ๋ถ„์„
"""
import numpy as np
from typing import Tuple, Dict, List, Optional
from dataclasses import dataclass
@dataclass
class ProfileResult:
"""ํ”„๋กœํŒŒ์ผ ๋ถ„์„ ๊ฒฐ๊ณผ"""
distance: np.ndarray # ๊ฑฐ๋ฆฌ (m)
elevation: np.ndarray # ๊ณ ๋„ (m)
slope: np.ndarray # ๊ฒฝ์‚ฌ๋„ (degrees)
points: List[Tuple[int, int]] # ์ƒ˜ํ”Œ ํฌ์ธํŠธ ์ขŒํ‘œ
@dataclass
class HypsometricResult:
"""Hypsometric ๋ถ„์„ ๊ฒฐ๊ณผ"""
relative_area: np.ndarray # a/A (0~1)
relative_elevation: np.ndarray # h/H (0~1)
hypsometric_integral: float # HI ๊ฐ’ (0~1)
stage: str # "Young", "Mature", "Old"
def extract_profile(
elevation: np.ndarray,
start: Tuple[int, int],
end: Tuple[int, int],
num_samples: int = 100,
cell_size: float = 1.0
) -> ProfileResult:
"""
๋‘ ์  ์‚ฌ์ด์˜ ๊ณ ๋„ ํ”„๋กœํŒŒ์ผ ์ถ”์ถœ
Args:
elevation: ๊ณ ๋„ ๋ฐฐ์—ด
start: ์‹œ์ž‘์  (row, col)
end: ๋์  (row, col)
num_samples: ์ƒ˜ํ”Œ ๊ฐœ์ˆ˜
cell_size: ์…€ ํฌ๊ธฐ (m)
Returns:
ProfileResult: ํ”„๋กœํŒŒ์ผ ๋ถ„์„ ๊ฒฐ๊ณผ
"""
# ์ƒ˜ํ”Œ ํฌ์ธํŠธ ์ƒ์„ฑ (์„ ํ˜• ๋ณด๊ฐ„)
rows = np.linspace(start[0], end[0], num_samples).astype(int)
cols = np.linspace(start[1], end[1], num_samples).astype(int)
# ๊ฒฝ๊ณ„ ์ฒดํฌ
h, w = elevation.shape
rows = np.clip(rows, 0, h - 1)
cols = np.clip(cols, 0, w - 1)
# ๊ณ ๋„ ์ถ”์ถœ
elevations = elevation[rows, cols]
# ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
total_dist = np.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2) * cell_size
distances = np.linspace(0, total_dist, num_samples)
# ๊ฒฝ์‚ฌ๋„ ๊ณ„์‚ฐ (์ค‘์•™ ์ฐจ๋ถ„)
slopes = np.zeros(num_samples)
for i in range(1, num_samples - 1):
dz = elevations[i + 1] - elevations[i - 1]
dx = distances[i + 1] - distances[i - 1]
if dx > 0:
slopes[i] = np.degrees(np.arctan(dz / dx))
slopes[0] = slopes[1]
slopes[-1] = slopes[-2]
points = [(int(r), int(c)) for r, c in zip(rows, cols)]
return ProfileResult(
distance=distances,
elevation=elevations,
slope=slopes,
points=points
)
def extract_cross_section(
elevation: np.ndarray,
row: int,
cell_size: float = 1.0
) -> ProfileResult:
"""ํšก๋‹จ๋ฉด ์ถ”์ถœ (ํŠน์ • row์˜ ์ „์ฒด ๋„ˆ๋น„)"""
h, w = elevation.shape
row = np.clip(row, 0, h - 1)
return extract_profile(
elevation,
start=(row, 0),
end=(row, w - 1),
num_samples=w,
cell_size=cell_size
)
def extract_longitudinal(
elevation: np.ndarray,
col: int,
cell_size: float = 1.0
) -> ProfileResult:
"""์ข…๋‹จ๋ฉด ์ถ”์ถœ (ํŠน์ • col์˜ ์ „์ฒด ๊ธธ์ด)"""
h, w = elevation.shape
col = np.clip(col, 0, w - 1)
return extract_profile(
elevation,
start=(0, col),
end=(h - 1, col),
num_samples=h,
cell_size=cell_size
)
def calculate_hypsometric_curve(
elevation: np.ndarray,
num_bins: int = 50
) -> HypsometricResult:
"""
Hypsometric Curve ๊ณ„์‚ฐ
์นจ์‹ ๋‹จ๊ณ„ ํŒ๋‹จ:
- HI > 0.6: Young (์œ ๋…„๊ธฐ) - ์นจ์‹ ์ดˆ๊ธฐ
- 0.35 < HI < 0.6: Mature (์žฅ๋…„๊ธฐ) - ํ‰ํ˜• ์ƒํƒœ
- HI < 0.35: Old (๋…ธ๋…„๊ธฐ) - ์นจ์‹ ํ›„๊ธฐ
Args:
elevation: ๊ณ ๋„ ๋ฐฐ์—ด
num_bins: ๊ณ ๋„ ๊ตฌ๊ฐ„ ๊ฐœ์ˆ˜
Returns:
HypsometricResult: Hypsometric ๋ถ„์„ ๊ฒฐ๊ณผ
"""
# ์œ ํšจ ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉ
valid_elev = elevation[~np.isnan(elevation)]
if len(valid_elev) == 0:
return HypsometricResult(
relative_area=np.array([0, 1]),
relative_elevation=np.array([1, 0]),
hypsometric_integral=0.5,
stage="Unknown"
)
z_min = np.min(valid_elev)
z_max = np.max(valid_elev)
z_range = z_max - z_min
if z_range == 0:
return HypsometricResult(
relative_area=np.array([0, 1]),
relative_elevation=np.array([0.5, 0.5]),
hypsometric_integral=0.5,
stage="Flat"
)
# ๊ณ ๋„ ๊ตฌ๊ฐ„๋ณ„ ๋ˆ„์  ๋ฉด์  ๊ณ„์‚ฐ
thresholds = np.linspace(z_min, z_max, num_bins + 1)
total_area = len(valid_elev)
relative_areas = []
relative_elevations = []
for threshold in thresholds:
# ์ด ๊ณ ๋„ ์ด์ƒ์ธ ๋ฉด์  ๋น„์œจ
area_above = np.sum(valid_elev >= threshold) / total_area
relative_areas.append(area_above)
# ์ƒ๋Œ€ ๊ณ ๋„
rel_elev = (threshold - z_min) / z_range
relative_elevations.append(rel_elev)
relative_areas = np.array(relative_areas)
relative_elevations = np.array(relative_elevations)
# Hypsometric Integral (๊ณก์„  ์•„๋ž˜ ๋ฉด์ )
hi = np.trapz(relative_elevations, relative_areas)
hi = abs(hi) # ์ ๋ถ„ ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ ๋ถ€ํ˜ธ ์ •๊ทœํ™”
# ์นจ์‹ ๋‹จ๊ณ„ ํŒ๋‹จ
if hi > 0.6:
stage = "Young (์œ ๋…„๊ธฐ)"
elif hi > 0.35:
stage = "Mature (์žฅ๋…„๊ธฐ)"
else:
stage = "Old (๋…ธ๋…„๊ธฐ)"
return HypsometricResult(
relative_area=relative_areas,
relative_elevation=relative_elevations,
hypsometric_integral=hi,
stage=stage
)
def calculate_slope_distribution(
elevation: np.ndarray,
cell_size: float = 1.0,
num_bins: int = 36
) -> Dict:
"""
์‚ฌ๋ฉด ๊ฒฝ์‚ฌ ๋ถ„ํฌ ๊ณ„์‚ฐ
Args:
elevation: ๊ณ ๋„ ๋ฐฐ์—ด
cell_size: ์…€ ํฌ๊ธฐ (m)
num_bins: ํžˆ์Šคํ† ๊ทธ๋žจ ๊ตฌ๊ฐ„ ์ˆ˜
Returns:
dict: ๊ฒฝ์‚ฌ๋„ ํ†ต๊ณ„ ๋ฐ ํžˆ์Šคํ† ๊ทธ๋žจ ๋ฐ์ดํ„ฐ
"""
# ๊ฒฝ์‚ฌ๋„ ๊ณ„์‚ฐ (Sobel ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ)
dy, dx = np.gradient(elevation, cell_size)
slope_rad = np.arctan(np.sqrt(dx**2 + dy**2))
slope_deg = np.degrees(slope_rad)
# ์œ ํšจ ๋ฐ์ดํ„ฐ
valid_slope = slope_deg[~np.isnan(slope_deg)]
# ํžˆ์Šคํ† ๊ทธ๋žจ
hist, bin_edges = np.histogram(valid_slope, bins=num_bins, range=(0, 90))
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
return {
'slope_grid': slope_deg,
'histogram': {
'counts': hist,
'bin_centers': bin_centers,
'bin_edges': bin_edges
},
'statistics': {
'mean': float(np.mean(valid_slope)),
'std': float(np.std(valid_slope)),
'min': float(np.min(valid_slope)),
'max': float(np.max(valid_slope)),
'median': float(np.median(valid_slope))
}
}
def calculate_relief_ratio(elevation: np.ndarray) -> float:
"""
๊ธฐ๋ณต๋น„ (Relief Ratio) ๊ณ„์‚ฐ
RR = H / L
H: ์ตœ๋Œ€ ๊ธฐ๋ณต๋Ÿ‰ (max - min)
L: ์œ ์—ญ ์ตœ๋Œ€ ๊ธธ์ด
"""
h, w = elevation.shape
max_length = np.sqrt(h**2 + w**2)
relief = np.nanmax(elevation) - np.nanmin(elevation)
return relief / max_length if max_length > 0 else 0
def calculate_curvature(
elevation: np.ndarray,
cell_size: float = 1.0
) -> Dict[str, np.ndarray]:
"""
๊ณก๋ฅ  (Curvature) ๊ณ„์‚ฐ
- Profile Curvature: ๊ฒฝ์‚ฌ ๋ฐฉํ–ฅ ๊ณก๋ฅ  (ํ๋ฆ„ ๊ฐ€์†/๊ฐ์†)
- Plan Curvature: ๋“ฑ๊ณ ์„  ๋ฐฉํ–ฅ ๊ณก๋ฅ  (ํ๋ฆ„ ์ˆ˜๋ ด/๋ฐœ์‚ฐ)
์–‘์ˆ˜: ๋ณผ๋ก (Convex)
์Œ์ˆ˜: ์˜ค๋ชฉ (Concave)
"""
# 1์ฐจ ๋„ํ•จ์ˆ˜
fy, fx = np.gradient(elevation, cell_size)
# 2์ฐจ ๋„ํ•จ์ˆ˜
fyy, fyx = np.gradient(fy, cell_size)
fxy, fxx = np.gradient(fx, cell_size)
# ๊ฒฝ์‚ฌ ํฌ๊ธฐ
p = fx
q = fy
p2 = p * p
q2 = q * q
pq = p * q
denom = p2 + q2
# Profile Curvature (๊ฒฝ์‚ฌ ๋ฐฉํ–ฅ)
with np.errstate(divide='ignore', invalid='ignore'):
profile_curv = -(fxx * p2 + 2 * fxy * pq + fyy * q2) / (denom * np.sqrt(denom + 1))
profile_curv = np.nan_to_num(profile_curv, nan=0.0, posinf=0.0, neginf=0.0)
# Plan Curvature (๋“ฑ๊ณ ์„  ๋ฐฉํ–ฅ)
with np.errstate(divide='ignore', invalid='ignore'):
plan_curv = -(fxx * q2 - 2 * fxy * pq + fyy * p2) / (denom ** 1.5)
plan_curv = np.nan_to_num(plan_curv, nan=0.0, posinf=0.0, neginf=0.0)
return {
'profile': profile_curv,
'plan': plan_curv,
'total': profile_curv + plan_curv
}
def compare_elevations(
elev1: np.ndarray,
elev2: np.ndarray,
label1: str = "DEM 1",
label2: str = "DEM 2"
) -> Dict:
"""
๋‘ ๊ณ ๋„ ๋ฐฐ์—ด ๋น„๊ต
Args:
elev1: ์ฒซ ๋ฒˆ์งธ ๊ณ ๋„ ๋ฐฐ์—ด (์˜ˆ: ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
elev2: ๋‘ ๋ฒˆ์งธ ๊ณ ๋„ ๋ฐฐ์—ด (์˜ˆ: ์‹ค์ธก DEM)
Returns:
dict: ์ฐจ์ด ๋ถ„์„ ๊ฒฐ๊ณผ
"""
# ํฌ๊ธฐ๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋ฆฌ์ƒ˜ํ”Œ๋ง
if elev1.shape != elev2.shape:
from scipy.ndimage import zoom
zoom_factors = (elev1.shape[0] / elev2.shape[0],
elev1.shape[1] / elev2.shape[1])
elev2 = zoom(elev2, zoom_factors, order=1)
diff = elev1 - elev2
return {
'difference_grid': diff,
'statistics': {
'mean_diff': float(np.nanmean(diff)),
'std_diff': float(np.nanstd(diff)),
'rmse': float(np.sqrt(np.nanmean(diff**2))),
'mae': float(np.nanmean(np.abs(diff))),
'max_diff': float(np.nanmax(diff)),
'min_diff': float(np.nanmin(diff)),
'correlation': float(np.corrcoef(elev1.flatten(), elev2.flatten())[0, 1])
},
'labels': (label1, label2)
}