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