feat(640-2): implicit 점 수 = 지면점 비율 · 격자 칸 뽑기 · 설정 지문 바뀌면 다시 지음

- SURFACE_IMPLICIT_POINT_RATIO 0.175 · _MIN_POINTS 5 만 · _MAX_POINTS 200 만(옛 _MAX_POINTS_PER_TILE 걷음)
- TerrainContext.grid_sample — 칸 = √(외곽 넓이 / 점 수) · 씨앗 558 · 칸마다 하나 · 모자라면 무작위 채움(sample() 그대로)
- 짓기 캐시 흠 — manifest 항목 완료일 때만 재사용(지문 바뀐 뒤 implicit · nurbs · meshfree 옛 파일 재사용 막음)
- 지킴 시험 test_640_lidar_default 점 수 · 격자 뽑기 · 지문 다시 지음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 21:25:08 +09:00
co-authored by Claude Opus 5.5
parent 366fd7a700
commit 437333c7af
5 changed files with 105 additions and 23 deletions
@@ -29,6 +29,8 @@ ProgressCallback = Callable[[int], None]
# 대용량 포인트 배치 처리 크기
_BATCH_SIZE = 500_000
# implicit 격자 칸 뽑기 씨앗 — 고정이라 다시 지어도 같은 중심점
IMPLICIT_SAMPLE_SEED = 558
# NURBS 평활 계수 — 제어점당 허용 잔차제곱(m²).
# 0.01(RMS 0.1m)은 요구가 너무 빡빡해 FITPACK이 제어점 수(122x113)보다 많은
@@ -262,6 +264,30 @@ class TerrainContext:
self._samples[maximum] = points
return points
def grid_sample(self, count: int, seed: int = IMPLICIT_SAMPLE_SEED) -> np.ndarray:
"""implicit 전용 — 격자 칸(√(외곽 넓이 / 점 수))마다 하나 · 모자라면 무작위로 채움.
파일 차례 뽑기는 점 많은 트인 곳에 몰려 숲 밑 성긴 곳이 빔(요구 558). 씨앗 고정.
"""
indices = self.indices()
if len(indices) <= count:
return np.asarray(self.xyz[indices], dtype=np.float32)
_, _, resolution, footprint = self.footprint()
cell = max(math.sqrt(float(footprint.sum()) * resolution * resolution / count), 0.01)
xy = np.asarray(self.xyz[indices][:, :2], dtype=np.float64)
rng = np.random.default_rng(seed)
order = rng.permutation(len(indices))
gx = ((xy[order, 0] - self.bounds[0, 0]) // cell).astype(np.int64)
gy = ((xy[order, 1] - self.bounds[1, 0]) // cell).astype(np.int64)
_, first = np.unique(gy * (int(gx.max()) + 1) + gx, return_index=True)
chosen = order[first]
if len(chosen) > count:
chosen = rng.choice(chosen, count, replace=False)
elif len(chosen) < count:
rest = np.setdiff1d(np.arange(len(indices)), chosen, assume_unique=True)
chosen = np.concatenate([chosen, rng.choice(rest, count - len(chosen), replace=False)])
return np.asarray(self.xyz[indices[np.sort(chosen)]], dtype=np.float32)
def footprint(self) -> tuple[float, float, float, np.ndarray]:
if self._footprint is not None:
return self._footprint