- 필터 = 업체 분류 · 점진 TIN · 둘의 다듬기 판만 · 분석 허용 목록 = Ground 등록부(available_filters) 한 벌 - 표현 = DTM · implicit 만 · 평활 = DTM 만 · config 기본 목록 · PRECOMPUTE · 설정 사전 맞춤(설정 지문 바뀜 — 768 에서 다시 지음) - 등고선 = 격자 · 굳힌 격자만(삼각망 굽기 · 외곽 맞춤 · implicit RBF 폴백 걷음) · 표본기 · 경로 찾기 tin · nurbs · meshfree 갈래 걷음 - 수치지형도 = 라플라스만(TIN 둘 · TPS · ANUDEM형 · 다중해상도 걷음) · B04 단추 하나 - 안 부르는 길 — B04 JSON 점군 GET · ground-stats GET + TS · B07 옛 계획평면도(라이다) 짓기 · lidar_source · LIDAR_ID · plan_lidar 종류 이름 · SOURCE_FILTER_LABELS · MODEL_METHODS(py) · MESH_* 설정 - 옛 manifest 의 지운 갈래 줄은 DB 로 되살리지 않음 · 옛 csf 행은 미리보기 · 등고선 그대로 열림 · 지운 이름 요청 = 400 - 낡은 글 — Ground · Pipeline 머리 · WF1 「없으면 csf」 · B07 음영 기본 「csf」 → 빈 값 · 등고선 폴백 「csf」 → 404 · B04 3D 보기 처음값 · B05 Schema · B06 Prebuild · 씨닝 설명 - 시험 지움 2 — test_b04_contour_raster · test_b04_contour_equivalence(지운 삼각망 굽기 · 없는 프로젝트를 봐 건너뛰던 것) - 시험 덜어냄 — test_b04_ground_filters CSF · 격자 최저 네 경우(지운 필터) - 시험 고침 — 허용 목록 · 형 검사에 걸린 csf · tin · grid_min_z · meshfree · tin_sheet 견본을 ptd · implicit · laplace 로(54_22 · 238_1 · 443 · 342_2 · 346_3 · 351_1 · 442_2 · 271 + helper · 344_1 · 133_1 · 343_2 · 637 · 754_1 · 763_1 깨진 tin → 깨진 implicit · 763_3 · b05 둘 · b06 · sheet_surface TIN(도엽선) 경우 지움) · 이름만 바꿈(273_2 · 343_1 · 343_3 · 624_1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
"""B07 지표면 음영기복 그림 — 계획평면도 배경 · D03 지형 층이 같이 씀.
|
|
|
|
이미 만들어 둔 **지표면 격자(DTM)** 로 음영기복 PNG 를 서버에서 만든다 — 점구름을 그대로
|
|
그리면 수천만 점이라 느려진다(용화_LAS 실측 4,900만 점). 옛 「계획평면도(라이다)」 짓기는 걷음(608).
|
|
"""
|
|
|
|
import base64
|
|
import io
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
# 음영기복 광원 — 도면 관행대로 북서(방위각 315°)에서 45° 높이로 비춘다.
|
|
_AZIMUTH_DEG = 315.0
|
|
_ALTITUDE_DEG = 45.0
|
|
# 그림이 지나치게 커지지 않도록 한 변 최대 픽셀 수 (A1 에 인쇄하면 1,200 px 이면 충분하다).
|
|
_MAX_PIXELS = 1600
|
|
|
|
|
|
def hillshade_png(z: np.ndarray, valid: np.ndarray, resolution_m: float) -> tuple[str, int, int]:
|
|
"""지표면 격자에서 음영기복 PNG(data URL)를 만든다. (data_url, 가로 px, 세로 px).
|
|
|
|
입력 `z`는 행이 남→북 순서(격자 y 오름차순)다. 그림은 위가 북이어야 하므로 뒤집는다.
|
|
빈 칸(`valid`가 False)은 흰색으로 두어 도면에서 배경과 구분되게 한다.
|
|
"""
|
|
from PIL import Image
|
|
|
|
grid = np.asarray(z, dtype=np.float64)
|
|
mask = np.asarray(valid, dtype=bool)
|
|
if grid.ndim != 2 or grid.size == 0:
|
|
raise ValueError("지표면 격자가 비어 있습니다.")
|
|
|
|
# 큰 격자는 미리 솎는다 — A1 한 장에 1,600 px 이상은 눈으로 구분되지 않는다.
|
|
rows, columns = grid.shape
|
|
stride = max(1, math.ceil(max(rows, columns) / _MAX_PIXELS))
|
|
if stride > 1:
|
|
grid = grid[::stride, ::stride]
|
|
mask = mask[::stride, ::stride]
|
|
resolution_m *= stride
|
|
|
|
filled = np.where(mask, grid, np.nan)
|
|
# 빈 칸이 기울기를 망치지 않도록 평균으로 메운 뒤 기울기를 잰다.
|
|
mean = float(np.nanmean(filled)) if np.isfinite(filled).any() else 0.0
|
|
filled = np.nan_to_num(filled, nan=mean)
|
|
dz_dy, dz_dx = np.gradient(filled, max(resolution_m, 1e-6))
|
|
|
|
slope = np.arctan(np.hypot(dz_dx, dz_dy))
|
|
aspect = np.arctan2(-dz_dx, dz_dy)
|
|
azimuth = math.radians(360.0 - _AZIMUTH_DEG + 90.0)
|
|
altitude = math.radians(_ALTITUDE_DEG)
|
|
shade = np.sin(altitude) * np.cos(slope) + np.cos(altitude) * np.sin(slope) * np.cos(
|
|
azimuth - aspect
|
|
)
|
|
shade = np.clip(shade, 0.0, 1.0)
|
|
# 배경이므로 완전히 검지 않게 누르되, 능선·계곡이 인쇄에서 보일 만큼은 대비를 준다
|
|
# (2026-09-04 실측: 120~255 는 너무 흐렸음).
|
|
pixels = (90 + 160 * shade).astype(np.uint8)
|
|
pixels[~mask] = 255
|
|
|
|
image = Image.fromarray(np.flipud(pixels), mode="L")
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format="PNG", optimize=True)
|
|
data_url = "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
return (data_url, image.width, image.height)
|