Files
Aislo/utils/filter_pmf.py
T
2026-07-05 14:05:22 +09:00

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