| """ |
| ์งํ ๋ถ์ ์์ง (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 |
| elevation: np.ndarray |
| slope: np.ndarray |
| points: List[Tuple[int, int]] |
|
|
|
|
| @dataclass |
| class HypsometricResult: |
| """Hypsometric ๋ถ์ ๊ฒฐ๊ณผ""" |
| relative_area: np.ndarray |
| relative_elevation: np.ndarray |
| hypsometric_integral: float |
| stage: str |
|
|
|
|
| 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) |
| |
| |
| 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: ๊ฒฝ์ฌ๋ ํต๊ณ ๋ฐ ํ์คํ ๊ทธ๋จ ๋ฐ์ดํฐ |
| """ |
| |
| 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) |
| """ |
| |
| fy, fx = np.gradient(elevation, cell_size) |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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) |
| } |
|
|