- resolve_auto_source_filters 가 늘 `_refine` 판을 돌려줌 — 바탕 마스크(업체 분류 · 점진 TIN)는 다듬기 입력으로 그대로 저장(엔진 길 그대로) - SURFACE_CONFIRM_DEFAULT_FILTER 기본 = ptd_refine · WF1 자동 확정은 엔진이 고른 첫 필터 그대로 - B04 드롭다운 갈래 넷 그대로(관리자 비교용) - 지킴 시험 고침: test_640_lidar_default · test_b04_ground_filters 의 자동 목록 기대값을 다듬기 판으로 · test_762_1_refine 의 스위치 시험은 스위치가 없어져 「기본 흐름」 시험으로 바꿈 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""762-1 지면점 다듬기(SMRF 꼴) · 766-1 다듬기 기본 흐름 · 762-2 품질 지표 지킴 시험.
|
||
|
||
합성 비탈(17°) + 능선(높이 6 m · 폭 약 16 m) + 덤불 혹(3 × 3 m · 1.5 m 뜸) — 필터가
|
||
덤불까지 지면으로 고른 판 → 다듬기가 혹은 지우고 능선 · 비탈은 남김.
|
||
"""
|
||
|
||
import numpy as np
|
||
|
||
import B04_PreProcess.B04_PreProcess_Engine_Ground as ground
|
||
from B04_PreProcess.B04_PreProcess_Engine_Filter_Refine import refine_ground_mask
|
||
from B04_PreProcess.B04_PreProcess_Engine_Quality import spike_stats
|
||
|
||
RNG = np.random.default_rng(7621)
|
||
|
||
|
||
def _terrain(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
||
return 0.3 * x + 6.0 * np.exp(-(((x - 60.0) / 8.0) ** 2)) + 0.02 * y
|
||
|
||
|
||
def _scene() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
count = 120 * 120 * 4
|
||
x = RNG.uniform(0, 120, count)
|
||
y = RNG.uniform(0, 120, count)
|
||
z = _terrain(x, y) + RNG.normal(0, 0.03, count)
|
||
bush = np.zeros(count, dtype=bool)
|
||
for cx, cy in RNG.uniform(10, 110, (15, 2)):
|
||
if abs(cx - 60) < 12: # 능선 위 덤불은 따로 가름 — 여기선 비탈 · 골 덤불
|
||
cx += 25
|
||
inside = (np.abs(x - cx) < 1.5) & (np.abs(y - cy) < 1.5)
|
||
z[inside] += 1.5
|
||
bush |= inside
|
||
ridge = (np.abs(x - 60) < 4) & ~bush
|
||
return np.column_stack((x, y, z)), bush, ridge
|
||
|
||
|
||
def test_refine_drops_bushes_keeps_ridge() -> None:
|
||
xyz, bush, ridge = _scene()
|
||
kept = refine_ground_mask(xyz, np.ones(len(xyz), dtype=bool))
|
||
print(kept[bush].mean(), kept[ridge].mean(), kept[~bush].mean())
|
||
assert kept[bush].mean() < 0.1 # 덤불 혹 90 % 넘게 지움
|
||
assert kept[ridge].mean() > 0.95 # 능선 안 깎음
|
||
assert kept[~bush].mean() > 0.97 # 땅 점 거의 그대로
|
||
|
||
|
||
def test_refine_never_adds_points() -> None:
|
||
xyz, _, _ = _scene()
|
||
mask = RNG.random(len(xyz)) < 0.5
|
||
kept = refine_ground_mask(xyz, mask)
|
||
assert not (kept & ~mask).any()
|
||
assert refine_ground_mask(xyz, np.zeros(len(xyz), dtype=bool)).sum() == 0
|
||
|
||
|
||
def test_refine_flow_is_default() -> None:
|
||
xyz, _, _ = _scene()
|
||
bounds = np.array([[0, 120], [0, 120], [0, 50]], dtype=np.float64)
|
||
data = {"xyz": xyz, "bounds": bounds, "classification": np.full(len(xyz), 2, np.uint8)}
|
||
assert ground.resolve_auto_source_filters(data) == ["classification_refine", "ptd_refine"]
|
||
data["classification"][:] = 0
|
||
assert ground.resolve_auto_source_filters(data) == ["ptd_refine"]
|
||
assert ground.base_filter("ptd_refine") == "ptd"
|
||
assert ground.base_filter("ptd") is None
|
||
assert {"classification_refine", "ptd_refine"} <= set(ground.available_filters())
|
||
# 바탕 마스크를 넘기면 바탕 필터를 다시 돌리지 않고 다듬기만
|
||
base = np.ones(len(xyz), dtype=bool)
|
||
refined = ground.run_ground_filter("ptd_refine", data, base)
|
||
assert refined.sum() < base.sum()
|
||
|
||
|
||
def test_spike_stat_sees_bumps() -> None:
|
||
yy, xx = np.mgrid[0:100, 0:100].astype(float)
|
||
z = 0.3 * xx
|
||
valid = np.ones(z.shape, dtype=bool)
|
||
assert spike_stats(z, valid)["spike_pct"] == 0.0
|
||
z[40:42, 40:42] += 1.5
|
||
stats = spike_stats(z, valid)
|
||
assert stats["spike_count"] == 1 and stats["spike_pct"] > 0
|