Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-11-01 23:03:44 +09:00
7 changed files with 318 additions and 11 deletions
+52
View File
@@ -0,0 +1,52 @@
"""642-2 점진 TIN(PTD) 지면 필터 지킴 — 합성 비탈 + 나무 점에서 땅 점 고름 · 급경사 놓침 상한."""
import numpy as np
import pytest
from B04_PreProcess.B04_PreProcess_Engine_Filter_PTD import filter_ptd
from B04_PreProcess.B04_PreProcess_Engine_Ground import available_filters, run_ground_filter
def _terrain(x, y):
# 서쪽 완경사(약 10 %) → 동쪽 급경사(끝 약 100 %) + 골 하나
return 0.1 * x + 0.0075 * np.maximum(x - 60, 0) ** 2 + 2.0 * np.sin(y / 15)
def _scene(seed=642):
rng = np.random.default_rng(seed)
n_ground = 60_000
gx, gy = rng.uniform(0, 120, n_ground), rng.uniform(0, 120, n_ground)
ground = np.column_stack([gx, gy, _terrain(gx, gy) + rng.normal(0, 0.03, n_ground)])
# 나무 = 줄기 둘레 수관(2~15 m 위) · 관목(0.9~2 m 위)
trunks = rng.uniform(0, 120, (120, 2))
tx = np.repeat(trunks[:, 0], 300) + rng.normal(0, 2.5, 36_000)
ty = np.repeat(trunks[:, 1], 300) + rng.normal(0, 2.5, 36_000)
tz = _terrain(tx, ty) + rng.uniform(2, 15, 36_000)
sx, sy = rng.uniform(0, 120, 8_000), rng.uniform(0, 120, 8_000)
sz = _terrain(sx, sy) + rng.uniform(0.9, 2.0, 8_000)
other = np.column_stack([np.r_[tx, sx], np.r_[ty, sy], np.r_[tz, sz]])
xyz = np.vstack([ground, other])
truth = np.r_[np.ones(n_ground, bool), np.zeros(len(other), bool)]
return xyz, truth
def test_ptd_picks_ground_on_slope_with_trees():
xyz, truth = _scene()
mask = filter_ptd({"xyz": xyz})
hit = (mask & truth).sum() / truth.sum()
wrong = (mask & ~truth).sum() / truth.sum()
assert hit >= 0.95, hit
assert wrong <= 0.02, wrong
# 급경사 칸(x > 90 · 기울기 55 % 넘음) 놓침 상한
steep = truth & (xyz[:, 0] > 90)
assert (~mask[steep]).mean() <= 0.08, (~mask[steep]).mean()
def test_ptd_registered_and_scenes():
assert "ptd" in available_filters()
xyz, _ = _scene(1)
flat = run_ground_filter("ptd", {"xyz": xyz})
assert flat.dtype == bool and len(flat) == len(xyz)
with pytest.raises(ValueError):
filter_ptd({"xyz": xyz}, scene="moon")
assert filter_ptd({"xyz": np.zeros((0, 3))}).shape == (0,)
+7 -7
View File
@@ -102,20 +102,20 @@ def test_classification_filter_reads_class_two() -> None:
assert mask.sum() == len(points[::4])
def test_auto_filters_prefer_classification_but_always_build_csf() -> None:
"""자동 전처리 목록 — 첫 항목이 기본 확정값. csf는 분류가 있어도 늘 함께 만든다."""
def test_auto_filters_prefer_classification_but_always_build_ptd() -> None:
"""자동 전처리 목록 — 첫 항목이 기본 확정값. ptd(점진 TIN)는 분류가 있어도 늘 함께 만든다."""
points = _sloped_ground(10.0)
unclassified = np.zeros(len(points), dtype=np.uint8)
classified = np.ones(len(points), dtype=np.uint8)
classified[::4] = 2
# 분류가 있으면 classification 이 기본이고, csf 는 검증용으로 같이 만든다.
# 분류가 있으면 classification 이 기본이고, ptd 는 검증용으로 같이 만든다.
assert resolve_auto_source_filters(_structured(points, classified)) == [
"classification",
"csf",
"ptd",
]
# 분류가 없으면 csf 하나뿐 — 빈 마스크로 모델을 만들지 않는다.
assert resolve_auto_source_filters(_structured(points, unclassified)) == ["csf"]
# 분류가 없으면 ptd 하나뿐 — 빈 마스크로 모델을 만들지 않는다.
assert resolve_auto_source_filters(_structured(points, unclassified)) == ["ptd"]
assert not has_classified_ground(_structured(points, unclassified))
# classification 이 없는 구조화 데이터(옛 캐시)에서도 죽지 않는다.
assert resolve_auto_source_filters(_structured(points)) == ["csf"]
assert resolve_auto_source_filters(_structured(points)) == ["ptd"]