feat(B04): 마지막 등고선 안쪽 평탄 마루를 골격 캡으로 돋운다
다음 등고선이 없는 봉우리·능선 마루는 TIN이 마지막 등고 표고로 평탄하게 채워 'ㅜ'가 갑자기 'ㅡ'로 변하는 단이 생긴다(2026-08-30 사용자 지적). 등표고 평탄 컴포넌트 중 바깥 유효 이웃이 전부 낮은 것(=마루)만 골라, 경계 거리 비례로 중심 골격이 +간격/2(주곡선 5m 기준 +2.5m)가 되게 올린다. 실제 마루는 마지막 등고와 +간격 사이가 보장되므로 오차 상한이 데이터 불확실성 이내다. 골짜기 바닥은 배수 방향을 모르므로 불변. 값별 사전 필터와 컴포넌트 bbox 창 연산으로 전체 생성 11.8s 유지. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,73 @@ def _densify_between_contours(spec: Any, features: list[dict[str, Any]], cloud:
|
||||
)
|
||||
|
||||
|
||||
def _cap_flat_summits(surface: np.ndarray, cell_m: float, interval_m: float) -> int:
|
||||
"""마지막 등고선 안쪽 평탄 마루를 완만한 능선 돔으로 올린다 (in-place).
|
||||
|
||||
다음 등고선이 없는 봉우리·능선 마루는 TIN이 마지막 등고 표고로 평탄하게 채운다
|
||||
('ㅜ'가 갑자기 'ㅡ'로 변하는 단 — 2026-08-30 사용자 지적). 실제 마루는 그 표고와
|
||||
+간격 사이이므로, 경계에서 가장 먼 골격선이 +간격/2가 되도록 거리 비례로 올린다
|
||||
(기대값 캡, 오차 상한 ±간격/2 = 원천 데이터 불확실성 이내).
|
||||
적용 조건: 등표고 평탄 컴포넌트이고 바깥 유효 이웃이 전부 더 낮을 때(=마루).
|
||||
골짜기 바닥은 배수 방향을 모르므로 건드리지 않는다. 돋운 컴포넌트 수를 반환.
|
||||
"""
|
||||
from scipy.ndimage import binary_dilation, distance_transform_edt, label
|
||||
|
||||
finite = np.isfinite(surface)
|
||||
quantized = np.round(surface * 100.0)
|
||||
capped = 0
|
||||
# 평탄 후보: 상하좌우 이웃 중 같은 표고가 있는 셀만 모아 컴포넌트를 짠다.
|
||||
same_right = np.zeros_like(finite)
|
||||
same_right[:, :-1] = finite[:, :-1] & finite[:, 1:] & (quantized[:, :-1] == quantized[:, 1:])
|
||||
same_down = np.zeros_like(finite)
|
||||
same_down[:-1, :] = finite[:-1, :] & finite[1:, :] & (quantized[:-1, :] == quantized[1:, :])
|
||||
flat = same_right | same_down
|
||||
flat[:, 1:] |= same_right[:, :-1]
|
||||
flat[1:, :] |= same_down[:-1, :]
|
||||
|
||||
# 서로 다른 표고의 평탄면이 맞닿아 있을 수 있으므로 표고값별로 컴포넌트를 짠다.
|
||||
# 값별 셀 수 25 미만은 컴포넌트도 25 미만이므로 라벨링 전에 거른다(속도).
|
||||
flat_values, flat_counts = np.unique(quantized[flat], return_counts=True)
|
||||
for value in flat_values[flat_counts >= 25]:
|
||||
value_labels, value_count = label(flat & (quantized == value))
|
||||
sizes = np.bincount(value_labels.reshape(-1))
|
||||
for component_id in range(1, value_count + 1):
|
||||
if sizes[component_id] < 25: # 25㎡ 미만은 시각적으로 단이 아니다
|
||||
continue
|
||||
rows, cols = np.nonzero(value_labels == component_id)
|
||||
if (
|
||||
rows.min() == 0
|
||||
or cols.min() == 0
|
||||
or rows.max() == surface.shape[0] - 1
|
||||
or cols.max() == surface.shape[1] - 1
|
||||
):
|
||||
continue # 격자 가장자리에 닿으면 바깥을 모른다
|
||||
# 이후 연산은 컴포넌트 bbox(+1) 창에서만 — 전체 격자 반복을 피한다.
|
||||
window = (
|
||||
slice(rows.min() - 1, rows.max() + 2),
|
||||
slice(cols.min() - 1, cols.max() + 2),
|
||||
)
|
||||
component = value_labels[window] == component_id
|
||||
ring = binary_dilation(component) & ~component & finite[window]
|
||||
if not ring.any():
|
||||
continue
|
||||
level = float(surface[rows[0], cols[0]])
|
||||
if float(surface[window][ring].max()) >= level - 1e-3:
|
||||
continue # 더 높은 이웃이 있으면 마루가 아니다(사면 벤치·골짜기)
|
||||
inner = distance_transform_edt(component, sampling=cell_m)
|
||||
peak = float(inner.max())
|
||||
if peak <= 0.0:
|
||||
continue
|
||||
patch = surface[window]
|
||||
patch[component] += (interval_m / 2.0) * (inner[component] / peak).astype(surface.dtype)
|
||||
capped += 1
|
||||
if capped:
|
||||
logger.info(
|
||||
"도엽 서피스: 평탄 마루 %d곳을 +%.1fm 골격 캡으로 돋움", capped, interval_m / 2.0
|
||||
)
|
||||
return capped
|
||||
|
||||
|
||||
def build_sheet_surface_model(
|
||||
project_root: Path,
|
||||
processed_dir: Path,
|
||||
@@ -193,9 +260,14 @@ def build_sheet_surface_model(
|
||||
return None
|
||||
|
||||
spec = grid_spec_from_bounds(x_min, y_min, x_max, y_max, SHEET_SURFACE_GRID_M)
|
||||
# 등고 간격(m) — 마루 캡 크기의 근거. 레벨이 하나뿐이면 5m(1:5,000 주곡선) 폴백.
|
||||
contour_levels = np.unique(cloud.z)
|
||||
interval_m = float(np.diff(contour_levels).min()) if len(contour_levels) > 1 else 5.0
|
||||
# 계단(평탄 삼각형) 해소 — 인접 등고선 사이 중간 보간선을 정점으로 추가.
|
||||
cloud = _densify_between_contours(spec, features, cloud)
|
||||
surface = interpolate_elevation(spec, cloud) # (R, C), 북→남 행 순서, 외부 NaN
|
||||
# 마지막 등고선 안쪽 평탄 마루를 완만한 돔으로 (2026-08-30 사용자 확정).
|
||||
_cap_flat_summits(surface, spec.cell_m, interval_m)
|
||||
|
||||
# DtmGridSampler 규약에 맞춰 y 오름차순으로 뒤집어 저장한다.
|
||||
x_coords = spec.cell_centers_x()
|
||||
|
||||
Reference in New Issue
Block a user