- SURFACE_FOOTPRINT_GAP_CLOSE_M 1.0 → 3.0 · 「가장 큰 덩이만」 그대로 - 설정 지문에 들어가 다음 짓기 때 모든 표현 · 등고선 · 유효 범위가 새 외곽으로 다시 지어짐(기존 확정 파일 · DB 는 그대로) - 지킴 시험 test_640_lidar_default 틈 닫기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.8 KiB
Python
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) # 한 덩이(띠 포함) 대 큰 덩이 하나
|