Files
Aislo/resources/tester/test_640_lidar_default.py
T
eomsangdonandClaude Opus 5.5 b85add0a51 feat(640-3): 외곽 틈 닫기 3 m — 성긴 숲 띠까지 스캔 범위 살림
- SURFACE_FOOTPRINT_GAP_CLOSE_M 1.0 → 3.0 · 「가장 큰 덩이만」 그대로
- 설정 지문에 들어가 다음 짓기 때 모든 표현 · 등고선 · 유효 범위가 새 외곽으로 다시 지어짐(기존 확정 파일 · DB 는 그대로)
- 지킴 시험 test_640_lidar_default 틈 닫기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:27:24 +09:00

156 lines
6.8 KiB
Python

"""640 라이다 기본 고름 · implicit 점 수 · 격자 칸 뽑기 · 지문 · 틈 닫기 (요구 558).
서버 함수만 — 작은 합성 점군 · 임시 폴더.
"""
from pathlib import Path
import numpy as np
from B04_PreProcess.B04_PreProcess_Engine_Filter_Classification import (
classified_coverage,
has_classified_coverage,
)
from B04_PreProcess.B04_PreProcess_Engine_Ground import resolve_auto_source_filters
from B04_PreProcess.B04_PreProcess_Engine_ModelBuild import implicit_point_count
from B04_PreProcess.B04_PreProcess_Engine_ModelContext import TerrainContext
from B04_PreProcess.B04_PreProcess_Engine_Pipeline import build_all_terrain_models
from config import config_system as cfg
def _plane(size_m: float = 60.0, spacing_m: float = 0.5) -> np.ndarray:
axis = np.arange(0.0, size_m, spacing_m)
x, y = np.meshgrid(axis, axis)
z = 100.0 + 0.2 * x + 0.1 * np.sin(y / 5.0)
return np.column_stack([x.ravel(), y.ravel(), z.ravel()])
def _structured(points: np.ndarray, classification: np.ndarray | None = None) -> dict:
data = {"xyz": points, "bounds": np.array([points.min(axis=0), points.max(axis=0)]).T}
if classification is not None:
data["classification"] = classification
return data
def test_확정_기본값은_implicit_평활_끔() -> None:
assert cfg.SURFACE_CONFIRM_DEFAULT_METHOD == "implicit"
assert cfg.SURFACE_CONFIRM_DEFAULT_SMOOTH is False
assert cfg.SURFACE_AUTO_METHODS == ("dtm",) # D03 자체 라이다는 dtm 만 그대로
def test_일부만_분류된_LAS_는_자동_기본에서_csf() -> None:
points = _plane(100.0)
partial = np.ones(len(points), dtype=np.uint8)
partial[(points[:, 0] < 30) & (np.arange(len(points)) % 4 == 0)] = 2 # 비율 7 % · 덮임 30 %
assert classified_coverage(_structured(points, partial)) < 0.5
assert resolve_auto_source_filters(_structured(points, partial)) == ["csf"]
spread = np.ones(len(points), dtype=np.uint8)
spread[::37] = 2 # 비율 2.7 % · 덮임 100 %
assert classified_coverage(_structured(points, spread)) == 1.0
assert resolve_auto_source_filters(_structured(points, spread)) == ["classification", "csf"]
def test_덮임은_저장본을_다시_씀(tmp_path: Path) -> None:
points = _plane(40.0)
classes = np.full(len(points), 2, dtype=np.uint8)
(tmp_path / "structured.npz").write_bytes(b"x")
assert has_classified_coverage(_structured(points, classes), tmp_path)
assert (tmp_path / "classification_coverage.json").is_file()
class _NoXyz(dict):
def __getitem__(self, key): # type: ignore[no-untyped-def]
assert key != "xyz", "저장본이 맞으면 점군을 풀지 않아야 함"
return dict.__getitem__(self, key)
assert has_classified_coverage(_NoXyz(_structured(points, classes)), tmp_path)
def _config(**override: object) -> dict:
config = cfg.build_surface_model_config()
config.update({"source_filters": ["a", "b"], "precompute": ["dtm", "implicit"]})
config.update(override)
return config
def test_implicit_은_기본_필터에만(tmp_path: Path) -> None:
points = _plane()
data = _structured(points)
masks = {"a": np.ones(len(points), bool), "b": np.arange(len(points)) % 2 == 0}
manifest = build_all_terrain_models(data, masks, tmp_path, _config())
assert set(manifest["source_filters"]["a"]["methods"]) == {"dtm", "implicit"}
assert set(manifest["source_filters"]["b"]["methods"]) == {"dtm"}
assert not (tmp_path / "implicit_b.npz").exists()
def test_implicit_점_수_비율_하한_상한() -> None:
config = cfg.build_surface_model_config()
assert (config["implicit_point_ratio"], config["implicit_min_points"]) == (0.175, 50_000)
assert config["implicit_max_points"] == 2_000_000
assert implicit_point_count(1_140_716, config) == 199_625 # 용화 분류
assert implicit_point_count(768_273, config) == 134_448 # 용화 CSF
assert implicit_point_count(100_000, config) == 50_000 # 하한
assert implicit_point_count(30_000, config) == 30_000 # 하한보다 적으면 전부
assert implicit_point_count(50_000_000, config) == 2_000_000 # 상한
def test_격자_칸_뽑기는_고르고_씨앗_고정() -> None:
rng = np.random.default_rng(1)
dense = rng.uniform(0, 20, (200_000, 2)) # 왼쪽 아래 트인 곳 — 점 몰림
sparse = rng.uniform(0, 100, (30_000, 2)) # 전체 성긴 숲 밑
xy = np.vstack([dense, sparse])
points = np.column_stack([xy, np.zeros(len(xy))])
context = TerrainContext(
xyz=points,
mask=np.ones(len(points), bool),
bounds=np.array([points.min(axis=0), points.max(axis=0)]).T,
config=cfg.build_surface_model_config(),
)
picked = context.grid_sample(5_000)
assert len(picked) == 5_000 and len(np.unique(picked[:, :2], axis=0)) == 5_000
assert np.array_equal(picked, context.grid_sample(5_000))
in_dense = np.count_nonzero((picked[:, 0] < 20) & (picked[:, 1] < 20)) / len(picked)
file_order = context.sample(5_000)
in_dense_file = np.count_nonzero((file_order[:, 0] < 20) & (file_order[:, 1] < 20))
assert in_dense < 0.1 < 0.5 < in_dense_file / len(file_order) # 파일 차례는 몰림 · 격자는 고름
assert len(context.grid_sample(300_000)) == len(points) # 모자라면 전부
def test_설정_지문이_바뀌면_implicit_다시_지음(tmp_path: Path) -> None:
points = _plane()
data = _structured(points)
masks = {"a": np.ones(len(points), bool)}
config = _config(
source_filters=["a"],
precompute=["implicit"],
implicit_min_points=500,
implicit_max_points=1_000,
)
build_all_terrain_models(data, masks, tmp_path, config)
with np.load(tmp_path / "implicit_a.npz") as old:
assert len(old["center_z"]) == 1_000
changed = dict(config, implicit_max_points=2_000)
manifest = build_all_terrain_models(data, masks, tmp_path, changed)
with np.load(tmp_path / "implicit_a.npz") as new:
assert len(new["center_z"]) == 2_000
assert manifest["source_filters"]["a"]["methods"]["implicit"]["center_count"] == 2_000
def test_외곽_틈_닫기_3m_는_성긴_띠를_살림() -> None:
points = _plane(60.0)
gap = (points[:, 0] > 28) & (points[:, 0] < 33) # 5 m 빈 띠 — 1 m 닫기면 두 덩이
points = points[~gap]
def area(close_m: float) -> float:
config = dict(cfg.build_surface_model_config(), footprint_gap_close_meters=close_m)
context = TerrainContext(
xyz=points,
mask=np.ones(len(points), bool),
bounds=np.array([points.min(axis=0), points.max(axis=0)]).T,
config=config,
)
return context.footprint_metadata()["footprint_area_m2"]
assert cfg.SURFACE_FOOTPRINT_GAP_CLOSE_M == 3.0
assert area(3.0) > 1.8 * area(1.0) # 한 덩이(띠 포함) 대 큰 덩이 하나