98 lines
3.7 KiB
Python
98 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
|
|
def numpy_min_max_filter(grid: np.ndarray, w_size: int, mode: str = 'min') -> np.ndarray:
|
|
"""SciPy ndimage 필터 대용: 순수 NumPy 기반의 2D 격자 이동 윈도우 최댓값/최솟값 필터.
|
|
|
|
경계면은 nearest 패딩 처리로 처리합니다.
|
|
"""
|
|
h, w = grid.shape
|
|
pad_val = w_size // 2
|
|
|
|
# 격자 경계 패딩 (Nearest 보간 모사)
|
|
padded = np.pad(grid, pad_val, mode='edge')
|
|
|
|
# NumPy sliding_window_view를 사용하여 윈도우 슬라이싱 뷰 생성
|
|
# shape: (h, w, w_size, w_size)
|
|
from numpy.lib.stride_tricks import sliding_window_view
|
|
windows = sliding_window_view(padded, (w_size, w_size))
|
|
|
|
# 마지막 두 축(윈도우 영역)에 대해 최솟값 또는 최댓값 산출
|
|
if mode == 'min':
|
|
return np.min(windows, axis=(2, 3))
|
|
else:
|
|
return np.max(windows, axis=(2, 3))
|
|
|
|
|
|
def filter_pmf(
|
|
structured_data: dict | np.lib.npyio.NpzFile,
|
|
project_instance_dir: Path | None = None, # 호환성 유지
|
|
max_window_size: int = 40,
|
|
slope: float = 1.0,
|
|
initial_window_size: int = 3,
|
|
max_distance: float = 2.5
|
|
) -> np.ndarray:
|
|
"""scipy 임포트 오류를 원천 차단하기 위해 순수 NumPy만으로 형태학적 PMF 필터를 완수합니다.
|
|
|
|
원리:
|
|
1. XY 평면을 격자(기본 1.0m)로 투영해 Z-min 지형 맵을 얻습니다.
|
|
2. 윈도우 폭을 단계적으로 키워가며 numpy를 활용해 Erosion(최저) 및 Dilation(최고) 윈도우 필터를 연산합니다.
|
|
3. 지형의 높이차가 경사도 임계치를 넘는 수목 등의 높이를 억제 보완합니다.
|
|
4. 최종 필터링된 지면 대비 높이 차가 max_distance 이내인 포인트를 지면으로 분류합니다.
|
|
"""
|
|
xyz = structured_data["xyz"]
|
|
bounds = structured_data["bounds"]
|
|
n_points = len(xyz)
|
|
if n_points == 0:
|
|
return np.zeros(0, dtype=bool)
|
|
|
|
xs, ys, zs = xyz[:, 0], xyz[:, 1], xyz[:, 2]
|
|
x_min, y_min, z_min = bounds[0][0], bounds[1][0], bounds[2][0]
|
|
x_max, y_max = bounds[0][1], bounds[1][1]
|
|
|
|
# 격자 크기 2.0m 설정 (대용량 연산 속도를 보완하면서도 지형 곡률 보존)
|
|
cell_size = 2.0
|
|
grid_w = int(np.ceil((x_max - x_min) / cell_size)) + 2
|
|
grid_h = int(np.ceil((y_max - y_min) / cell_size)) + 2
|
|
|
|
z_grid = np.full((grid_h, grid_w), np.inf, dtype=np.float32)
|
|
gx = np.clip(((xs - x_min) / cell_size).astype(np.int32), 0, grid_w - 1)
|
|
gy = np.clip(((ys - y_min) / cell_size).astype(np.int32), 0, grid_h - 1)
|
|
|
|
# 1. Z-min 지형 구성
|
|
np.minimum.at(z_grid, (gy, gx), zs.astype(np.float32))
|
|
z_grid[z_grid == np.inf] = z_min
|
|
|
|
# 2. 점진적 형태학 필터 연산 (Opening = Dilation of Erosion)
|
|
current_grid = z_grid.copy()
|
|
|
|
# 윈도우 단계별 리스트 생성
|
|
w_sizes = []
|
|
w = initial_window_size
|
|
while w <= max_window_size:
|
|
w_sizes.append(w)
|
|
w = w * 2 + 1
|
|
|
|
for w_size in w_sizes:
|
|
# Opening = Dilation of Erosion
|
|
# (1) Erosion (Minimum)
|
|
eroded = numpy_min_max_filter(current_grid, w_size, mode='min')
|
|
# (2) Dilation (Maximum)
|
|
opened = numpy_min_max_filter(eroded, w_size, mode='max')
|
|
|
|
# 경사도 기반 높이 변화 임계값
|
|
t_dist = slope * w_size * cell_size * 0.15 + 0.5
|
|
t_dist = min(t_dist, max_distance)
|
|
|
|
# 비지면(나무/구조물) 차단 및 지면 스무딩 보존
|
|
mask_elev = (current_grid - opened) > t_dist
|
|
current_grid[mask_elev] = opened[mask_elev]
|
|
|
|
# 3. 마스크 맵 매핑 및 원본 비교
|
|
simulated_z = current_grid[gy, gx]
|
|
mask = (zs >= simulated_z - 0.4) & (zs <= simulated_z + max_distance)
|
|
|
|
return mask
|