757 lines
29 KiB
Python
757 lines
29 KiB
Python
|
|
# data_3dtiles_to_dem52
|
|||
|
|
import os
|
|||
|
|
import json
|
|||
|
|
import numpy as np
|
|||
|
|
import pandas as pd
|
|||
|
|
import pyproj
|
|||
|
|
import struct
|
|||
|
|
from osgeo import gdal, osr
|
|||
|
|
import uuid
|
|||
|
|
from glb_with_draco import DracoGLBParser
|
|||
|
|
|
|||
|
|
# 解决GDAL中文路径/警告问题(生产必加)
|
|||
|
|
gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8", "YES")
|
|||
|
|
gdal.SetConfigOption("CPL_ZIP_ENCODING", "UTF-8")
|
|||
|
|
gdal.PushErrorHandler('CPLQuietErrorHandler')
|
|||
|
|
|
|||
|
|
class RegionFilter:
|
|||
|
|
"""完整的区域过滤器,支持所有格式"""
|
|||
|
|
|
|||
|
|
def __init__(self, region_coords=None, enable_tile_filter=True, debug=False):
|
|||
|
|
"""
|
|||
|
|
初始化区域过滤器
|
|||
|
|
:param debug: 是否输出调试信息
|
|||
|
|
"""
|
|||
|
|
self.region_coords = region_coords
|
|||
|
|
self.enable_tile_filter = enable_tile_filter
|
|||
|
|
self.debug = debug
|
|||
|
|
|
|||
|
|
if region_coords:
|
|||
|
|
# 提取区域边界
|
|||
|
|
lons = [coord[0] for coord in region_coords]
|
|||
|
|
lats = [coord[1] for coord in region_coords]
|
|||
|
|
self.min_lon = min(lons)
|
|||
|
|
self.max_lon = max(lons)
|
|||
|
|
self.min_lat = min(lats)
|
|||
|
|
self.max_lat = max(lats)
|
|||
|
|
|
|||
|
|
# 扩展边界(避免边缘误差)
|
|||
|
|
self.expand_factor = 0.1
|
|||
|
|
lon_expand = (self.max_lon - self.min_lon) * self.expand_factor
|
|||
|
|
lat_expand = (self.max_lat - self.min_lat) * self.expand_factor
|
|||
|
|
|
|||
|
|
self.filter_min_lon = self.min_lon - lon_expand
|
|||
|
|
self.filter_max_lon = self.max_lon + lon_expand
|
|||
|
|
self.filter_min_lat = self.min_lat - lat_expand
|
|||
|
|
self.filter_max_lat = self.max_lat + lat_expand
|
|||
|
|
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 区域过滤器初始化:")
|
|||
|
|
print(f" 目标区域: Lon[{self.min_lon:.6f}, {self.max_lon:.6f}], "
|
|||
|
|
f"Lat[{self.min_lat:.6f}, {self.max_lat:.6f}]")
|
|||
|
|
print(f" 过滤区域: Lon[{self.filter_min_lon:.6f}, {self.filter_max_lon:.6f}], "
|
|||
|
|
f"Lat[{self.filter_min_lat:.6f}, {self.filter_max_lat:.6f}]")
|
|||
|
|
else:
|
|||
|
|
self.min_lon = self.min_lat = -180
|
|||
|
|
self.max_lon = self.max_lat = 180
|
|||
|
|
self.filter_min_lon = self.filter_min_lat = -180
|
|||
|
|
self.filter_max_lon = self.filter_max_lat = 180
|
|||
|
|
if self.debug:
|
|||
|
|
print("[DEBUG] 区域过滤器: 未指定区域,将处理所有数据")
|
|||
|
|
|
|||
|
|
def check_tile_bounding_volume(self, bounding_volume):
|
|||
|
|
"""检查瓦片的包围体是否与指定区域相交"""
|
|||
|
|
if not self.region_coords or not self.enable_tile_filter:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if 'region' in bounding_volume:
|
|||
|
|
return self._check_region(bounding_volume['region'])
|
|||
|
|
|
|||
|
|
elif 'box' in bounding_volume:
|
|||
|
|
box = bounding_volume['box']
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 检查box包围体,长度={len(box)}")
|
|||
|
|
|
|||
|
|
if len(box) == 12:
|
|||
|
|
result = self._check_box_12(box)
|
|||
|
|
elif len(box) == 15:
|
|||
|
|
result = self._check_box_15(box)
|
|||
|
|
else:
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 异常box长度 {len(box)},默认通过")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
if self.debug and not result:
|
|||
|
|
print(f"[DEBUG] Box被过滤")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
elif 'sphere' in bounding_volume:
|
|||
|
|
return self._check_sphere(bounding_volume['sphere'])
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 包围体检查出错: {e}")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _check_region(self, region):
|
|||
|
|
"""检查region格式 [west, south, east, north, minHeight, maxHeight]"""
|
|||
|
|
if len(region) != 6:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
west, south, east, north, min_h, max_h = region
|
|||
|
|
|
|||
|
|
# 检查是否完全在过滤区域外
|
|||
|
|
if (east < self.filter_min_lon or west > self.filter_max_lon or
|
|||
|
|
north < self.filter_min_lat or south > self.filter_max_lat):
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] Region过滤: [{west:.3f},{south:.3f},{east:.3f},{north:.3f}]")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _check_box_12(self, box):
|
|||
|
|
"""检查12值box格式"""
|
|||
|
|
# 提取参数
|
|||
|
|
cx, cy, cz = box[0], box[1], box[2]
|
|||
|
|
halfX = box[3]
|
|||
|
|
halfY = box[7]
|
|||
|
|
halfZ = box[11]
|
|||
|
|
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 12值box: center=({cx:.1f},{cy:.1f},{cz:.1f}), "
|
|||
|
|
f"halfs=({halfX},{halfY},{halfZ})")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 获取转换器
|
|||
|
|
transformer = self._get_transformer()
|
|||
|
|
|
|||
|
|
# 转换中心点
|
|||
|
|
center_lon, center_lat, _ = transformer.transform(
|
|||
|
|
cx, cy, cz, radians=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 计算最大偏移(简化方法,避免计算所有角点)
|
|||
|
|
max_half = max(halfX, halfY, halfZ)
|
|||
|
|
|
|||
|
|
# 转换为经纬度偏移
|
|||
|
|
earth_radius = 6378137.0
|
|||
|
|
lon_offset = np.degrees(max_half / (earth_radius * np.cos(np.radians(center_lat))))
|
|||
|
|
lat_offset = np.degrees(max_half / earth_radius)
|
|||
|
|
|
|||
|
|
# 计算box范围
|
|||
|
|
box_min_lon = center_lon - lon_offset
|
|||
|
|
box_max_lon = center_lon + lon_offset
|
|||
|
|
box_min_lat = center_lat - lat_offset
|
|||
|
|
box_max_lat = center_lat + lat_offset
|
|||
|
|
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] 中心: ({center_lon:.6f}, {center_lat:.6f})")
|
|||
|
|
print(f"[DEBUG] 范围: Lon[{box_min_lon:.6f}, {box_max_lon:.6f}], "
|
|||
|
|
f"Lat[{box_min_lat:.6f}, {box_max_lat:.6f}]")
|
|||
|
|
|
|||
|
|
# 检查相交
|
|||
|
|
if (box_max_lon < self.filter_min_lon or box_min_lon > self.filter_max_lon or
|
|||
|
|
box_max_lat < self.filter_min_lat or box_min_lat > self.filter_max_lat):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
if self.debug:
|
|||
|
|
print(f"[DEBUG] Box12检查失败: {e}")
|
|||
|
|
return True # 失败时默认通过
|
|||
|
|
|
|||
|
|
def _check_box_15(self, box):
|
|||
|
|
"""检查标准15值box格式"""
|
|||
|
|
if len(box) < 15:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
cx, cy, cz = box[0], box[1], box[2]
|
|||
|
|
halfX, halfY, halfZ = box[12], box[13], box[14]
|
|||
|
|
|
|||
|
|
# 简化处理:只检查中心点
|
|||
|
|
transformer = self._get_transformer()
|
|||
|
|
center_lon, center_lat, _ = transformer.transform(cx, cy, cz, radians=False)
|
|||
|
|
|
|||
|
|
# 计算最大偏移
|
|||
|
|
max_half = max(halfX, halfY, halfZ)
|
|||
|
|
earth_radius = 6378137.0
|
|||
|
|
lon_offset = np.degrees(max_half / (earth_radius * np.cos(np.radians(center_lat))))
|
|||
|
|
lat_offset = np.degrees(max_half / earth_radius)
|
|||
|
|
|
|||
|
|
box_min_lon = center_lon - lon_offset
|
|||
|
|
box_max_lon = center_lon + lon_offset
|
|||
|
|
box_min_lat = center_lat - lat_offset
|
|||
|
|
box_max_lat = center_lat + lat_offset
|
|||
|
|
|
|||
|
|
if (box_max_lon < self.filter_min_lon or box_min_lon > self.filter_max_lon or
|
|||
|
|
box_max_lat < self.filter_min_lat or box_min_lat > self.filter_max_lat):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _check_sphere(self, sphere):
|
|||
|
|
"""检查sphere格式 [centerX, centerY, centerZ, radius]"""
|
|||
|
|
if len(sphere) < 4:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
cx, cy, cz, radius = sphere[0], sphere[1], sphere[2], sphere[3]
|
|||
|
|
|
|||
|
|
transformer = self._get_transformer()
|
|||
|
|
center_lon, center_lat, _ = transformer.transform(cx, cy, cz, radians=False)
|
|||
|
|
|
|||
|
|
# 计算半径对应的经纬度偏移
|
|||
|
|
earth_radius = 6378137.0
|
|||
|
|
lon_offset = np.degrees(radius / (earth_radius * np.cos(np.radians(center_lat))))
|
|||
|
|
lat_offset = np.degrees(radius / earth_radius)
|
|||
|
|
|
|||
|
|
sphere_min_lon = center_lon - lon_offset
|
|||
|
|
sphere_max_lon = center_lon + lon_offset
|
|||
|
|
sphere_min_lat = center_lat - lat_offset
|
|||
|
|
sphere_max_lat = center_lat + lat_offset
|
|||
|
|
|
|||
|
|
if (sphere_max_lon < self.filter_min_lon or sphere_min_lon > self.filter_max_lon or
|
|||
|
|
sphere_max_lat < self.filter_min_lat or sphere_min_lat > self.filter_max_lat):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _get_transformer(self):
|
|||
|
|
"""获取或创建坐标转换器"""
|
|||
|
|
if not hasattr(self, '_transformer'):
|
|||
|
|
ecef = pyproj.Proj(proj='geocent', ellps='WGS84', datum='WGS84')
|
|||
|
|
lla = pyproj.Proj(proj='latlong', ellps='WGS84', datum='WGS84')
|
|||
|
|
self._transformer = pyproj.Transformer.from_proj(ecef, lla)
|
|||
|
|
return self._transformer
|
|||
|
|
|
|||
|
|
def filter_points(self, points):
|
|||
|
|
"""
|
|||
|
|
过滤点集,只保留区域内的点
|
|||
|
|
:param points: 点列表或numpy数组,每行 [lon, lat, height]
|
|||
|
|
:return: 过滤后的点列表
|
|||
|
|
"""
|
|||
|
|
if not self.region_coords or len(points) == 0:
|
|||
|
|
return points
|
|||
|
|
|
|||
|
|
# 转换为numpy数组处理
|
|||
|
|
if isinstance(points, list):
|
|||
|
|
points_array = np.array(points)
|
|||
|
|
else:
|
|||
|
|
points_array = points
|
|||
|
|
|
|||
|
|
if len(points_array) == 0 or points_array.shape[1] < 2:
|
|||
|
|
return points_array.tolist() if isinstance(points, list) else points_array
|
|||
|
|
|
|||
|
|
# 检查每个点是否在区域内
|
|||
|
|
in_region_mask = (
|
|||
|
|
(points_array[:, 0] >= self.min_lon) &
|
|||
|
|
(points_array[:, 0] <= self.max_lon) &
|
|||
|
|
(points_array[:, 1] >= self.min_lat) &
|
|||
|
|
(points_array[:, 1] <= self.max_lat)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
filtered_points = points_array[in_region_mask]
|
|||
|
|
|
|||
|
|
print(f"区域过滤: {len(points_array)} 个点 -> {len(filtered_points)} 个点 "
|
|||
|
|
f"(过滤掉 {len(points_array) - len(filtered_points)} 个点)")
|
|||
|
|
|
|||
|
|
return filtered_points.tolist() if isinstance(points, list) else filtered_points
|
|||
|
|
|
|||
|
|
# ========== 核心工具函数:矩阵变换 ==========
|
|||
|
|
def apply_transform_matrix(vertices, transform_matrix):
|
|||
|
|
"""
|
|||
|
|
将模型的局部相对顶点坐标,通过transform矩阵转换为绝对ECEF坐标
|
|||
|
|
:param vertices: 原始局部顶点 (n,3) numpy数组
|
|||
|
|
:param transform_matrix: 瓦片的transform矩阵 一维列表/数组,长度16(4x4矩阵)
|
|||
|
|
:return: 绝对ECEF坐标 (n,3) numpy数组
|
|||
|
|
"""
|
|||
|
|
if transform_matrix is None or len(transform_matrix) != 16:
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
# reshape为4x4,然后转置(因为glTF是列主序)
|
|||
|
|
mat = np.array(transform_matrix).reshape(4, 4).astype(np.float64).T
|
|||
|
|
|
|||
|
|
# 顶点齐次坐标化 (n,3) -> (n,4) 最后一列补1
|
|||
|
|
ones = np.ones((vertices.shape[0], 1), dtype=np.float64)
|
|||
|
|
vertices_hom = np.hstack([vertices, ones])
|
|||
|
|
|
|||
|
|
# 矩阵乘法:顶点坐标 * 变换矩阵 = 绝对坐标
|
|||
|
|
vertices_ecef_hom = np.dot(vertices_hom, mat.T)
|
|||
|
|
|
|||
|
|
# 还原为三维坐标 (n,4) -> (n,3)
|
|||
|
|
vertices_ecef = vertices_ecef_hom[:, :3]
|
|||
|
|
|
|||
|
|
return vertices_ecef
|
|||
|
|
|
|||
|
|
def parse_b3dm_to_points(b3dm_path, region_filter=None, transform_matrix=None):
|
|||
|
|
"""
|
|||
|
|
解析B3DM文件,提取顶点的经纬度+高程
|
|||
|
|
【关键修改】区域过滤移到读取顶点后进行
|
|||
|
|
"""
|
|||
|
|
# 获取脚本所在目录
|
|||
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
temp_dir = os.path.join(script_dir, "temp_glb")
|
|||
|
|
|
|||
|
|
# 创建临时目录(如果不存在)
|
|||
|
|
os.makedirs(temp_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 1. 读取B3DM二进制文件
|
|||
|
|
with open(b3dm_path, "rb") as f:
|
|||
|
|
b3dm_data = f.read()
|
|||
|
|
|
|||
|
|
# 跳过头部
|
|||
|
|
header = struct.unpack('<4sIIIIII', b3dm_data[:28])
|
|||
|
|
ft_json_len, ft_bin_len, bt_json_len, bt_bin_len = header[3:7]
|
|||
|
|
|
|||
|
|
offset = 28
|
|||
|
|
offset += ft_json_len # 跳过Feature Table JSON
|
|||
|
|
offset += ft_bin_len # 跳过Feature Table Binary
|
|||
|
|
offset += bt_json_len # 跳过Batch Table JSON
|
|||
|
|
offset += bt_bin_len # 跳过Batch Table Binary
|
|||
|
|
|
|||
|
|
# 提取glb数据
|
|||
|
|
glb_data = b3dm_data[offset:]
|
|||
|
|
if len(glb_data) < 12:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# 2. 使用脚本目录下的临时文件
|
|||
|
|
temp_file_path = None
|
|||
|
|
try:
|
|||
|
|
# 生成唯一临时文件名
|
|||
|
|
temp_filename = f"temp_{uuid.uuid4().hex[:8]}.glb"
|
|||
|
|
temp_file_path = os.path.join(temp_dir, temp_filename)
|
|||
|
|
|
|||
|
|
# 将GLB数据写入临时文件
|
|||
|
|
with open(temp_file_path, "wb") as tmp_glb:
|
|||
|
|
tmp_glb.write(glb_data)
|
|||
|
|
|
|||
|
|
# 使用DracoGLBParser解析
|
|||
|
|
parser = DracoGLBParser(temp_file_path)
|
|||
|
|
# 解析 GLB 结构
|
|||
|
|
parser.parse_glb_structure()
|
|||
|
|
# 分析结构
|
|||
|
|
parser.analyze_structure()
|
|||
|
|
# 解码 Draco 网格
|
|||
|
|
mesh = parser.decode_draco_meshes()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"读取GLB数据失败 {b3dm_path}: {e}")
|
|||
|
|
return []
|
|||
|
|
finally:
|
|||
|
|
# 清理临时文件
|
|||
|
|
if temp_file_path and os.path.exists(temp_file_path):
|
|||
|
|
try:
|
|||
|
|
os.unlink(temp_file_path)
|
|||
|
|
except Exception as e:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if mesh is None:
|
|||
|
|
print(f"无法加载模型: {b3dm_path}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# 获取顶点数据
|
|||
|
|
vertices = parser.get_all_vertices()
|
|||
|
|
|
|||
|
|
if vertices.size == 0 or len(vertices.shape) < 2 or vertices.shape[1] != 3:
|
|||
|
|
print(f"顶点数据格式无效: {b3dm_path}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# 3. 应用transform矩阵,局部坐标 → 绝对ECEF坐标
|
|||
|
|
vertices = apply_transform_matrix(vertices, transform_matrix)
|
|||
|
|
|
|||
|
|
# 4. ECEF坐标转WGS84经纬度+高程
|
|||
|
|
try:
|
|||
|
|
ecef = pyproj.Proj(proj='geocent', ellps='WGS84', datum='WGS84')
|
|||
|
|
lla = pyproj.Proj(proj='latlong', ellps='WGS84', datum='WGS84')
|
|||
|
|
transformer = pyproj.Transformer.from_proj(ecef, lla, always_xy=True)
|
|||
|
|
|
|||
|
|
lons, lats, heights = transformer.transform(
|
|||
|
|
vertices[:, 0], vertices[:, 1], vertices[:, 2], radians=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 组合成点集
|
|||
|
|
points = np.column_stack([lons, lats, heights])
|
|||
|
|
|
|||
|
|
# 5. 基本数据清洗(过滤异常值)
|
|||
|
|
# 过滤nan/inf
|
|||
|
|
valid_mask = np.isfinite(points).all(axis=1)
|
|||
|
|
points = points[valid_mask]
|
|||
|
|
|
|||
|
|
if len(points) == 0:
|
|||
|
|
print(f"B3DM文件 {os.path.basename(b3dm_path)} 转换后无有效点")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# 过滤经纬度超限值
|
|||
|
|
geo_mask = (
|
|||
|
|
(points[:, 0] >= -180) & (points[:, 0] <= 180) &
|
|||
|
|
(points[:, 1] >= -90) & (points[:, 1] <= 90)
|
|||
|
|
)
|
|||
|
|
points = points[geo_mask]
|
|||
|
|
|
|||
|
|
if len(points) == 0:
|
|||
|
|
print(f"B3DM文件 {os.path.basename(b3dm_path)} 经纬度超限")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
print(f"从 {os.path.basename(b3dm_path)} 提取到 {len(points)} 个原始顶点")
|
|||
|
|
|
|||
|
|
# 6. 【关键修改】应用区域过滤器(如果提供)
|
|||
|
|
# 在读取顶点后进行区域过滤,而不是在瓦片级别过滤
|
|||
|
|
if region_filter:
|
|||
|
|
points = region_filter.filter_points(points)
|
|||
|
|
if len(points) == 0:
|
|||
|
|
print(f"B3DM文件 {os.path.basename(b3dm_path)} 的所有点都在区域外,跳过")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
print(f"最终保留 {len(points)} 个在区域内的顶点")
|
|||
|
|
return points.tolist()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"坐标转换失败 {b3dm_path}: {e}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def traverse_nested_tiles(tile_obj, base_dir, b3dm_paths, tile_transforms, region_filter=None, parent_transform=None):
|
|||
|
|
"""
|
|||
|
|
深度递归遍历瓦片,自动识别「子JSON」和「B3DM」
|
|||
|
|
【修改】移除瓦片级别的区域过滤,完全依赖顶点级别的过滤
|
|||
|
|
"""
|
|||
|
|
# 1. 计算当前瓦片的最终变换矩阵
|
|||
|
|
current_transform = parent_transform
|
|||
|
|
if "transform" in tile_obj:
|
|||
|
|
tile_mat = tile_obj["transform"]
|
|||
|
|
if current_transform is None:
|
|||
|
|
current_transform = tile_mat
|
|||
|
|
else:
|
|||
|
|
# 合并变换矩阵
|
|||
|
|
mat1 = np.array(current_transform).reshape(4, 4)
|
|||
|
|
mat2 = np.array(tile_mat).reshape(4, 4)
|
|||
|
|
combined_mat = np.dot(mat1, mat2).flatten().tolist()
|
|||
|
|
current_transform = combined_mat
|
|||
|
|
|
|||
|
|
# 2. 【修改】不再检查瓦片包围体,直接处理内容
|
|||
|
|
# 这样可以避免因粗略的包围体判断而漏掉部分在区域内的顶点
|
|||
|
|
|
|||
|
|
# 3. 处理当前瓦片的内容
|
|||
|
|
if "content" in tile_obj and "uri" in tile_obj["content"]:
|
|||
|
|
tile_uri = tile_obj["content"]["uri"]
|
|||
|
|
tile_abs_path = os.path.join(base_dir, tile_uri)
|
|||
|
|
|
|||
|
|
if tile_uri.lower().endswith(".json"):
|
|||
|
|
# 情况1:uri是子JSON文件 → 递归解析这个子JSON
|
|||
|
|
if os.path.exists(tile_abs_path):
|
|||
|
|
print(f"解析嵌套子JSON文件: {tile_abs_path}")
|
|||
|
|
with open(tile_abs_path, "r", encoding="utf-8") as f:
|
|||
|
|
sub_tileset = json.load(f)
|
|||
|
|
sub_base_dir = os.path.dirname(tile_abs_path)
|
|||
|
|
traverse_nested_tiles(sub_tileset["root"], sub_base_dir, b3dm_paths, tile_transforms, region_filter, current_transform)
|
|||
|
|
else:
|
|||
|
|
print(f"嵌套子JSON文件不存在,跳过: {tile_abs_path}")
|
|||
|
|
|
|||
|
|
elif tile_uri.lower().endswith(".b3dm"):
|
|||
|
|
# 情况2:uri是B3DM文件 → 收集路径+对应transform矩阵
|
|||
|
|
if os.path.exists(tile_abs_path):
|
|||
|
|
b3dm_paths.append(tile_abs_path)
|
|||
|
|
tile_transforms.append(current_transform)
|
|||
|
|
print(f"收集到B3DM文件: {tile_abs_path}")
|
|||
|
|
else:
|
|||
|
|
print(f"B3DM文件不存在,跳过: {tile_abs_path}")
|
|||
|
|
|
|||
|
|
# 4. 递归遍历当前tile的子节点children
|
|||
|
|
if "children" in tile_obj and len(tile_obj["children"]) > 0:
|
|||
|
|
for child_tile in tile_obj["children"]:
|
|||
|
|
traverse_nested_tiles(child_tile, base_dir, b3dm_paths, tile_transforms, region_filter, current_transform)
|
|||
|
|
|
|||
|
|
def parse_tileset(tileset_path, region_coords=None):
|
|||
|
|
"""重构主解析函数,支持区域过滤+矩阵变换"""
|
|||
|
|
if not os.path.exists(tileset_path):
|
|||
|
|
raise FileNotFoundError(f"根tileset.json文件不存在: {tileset_path}")
|
|||
|
|
|
|||
|
|
# 初始化区域过滤器(将在顶点级别使用)
|
|||
|
|
region_filter = RegionFilter(region_coords)
|
|||
|
|
|
|||
|
|
# 读取根tileset.json
|
|||
|
|
with open(tileset_path, "r", encoding="utf-8") as f:
|
|||
|
|
tileset_json = json.load(f)
|
|||
|
|
|
|||
|
|
root_dir = os.path.dirname(tileset_path)
|
|||
|
|
b3dm_paths = []
|
|||
|
|
tile_transforms = []
|
|||
|
|
|
|||
|
|
print(f"开始遍历tileset结构...")
|
|||
|
|
# 调用深度递归函数
|
|||
|
|
traverse_nested_tiles(tileset_json["root"], root_dir, b3dm_paths, tile_transforms, region_filter, None)
|
|||
|
|
|
|||
|
|
print(f"\n遍历完成,共发现 {len(b3dm_paths)} 个B3DM文件:")
|
|||
|
|
for i, b3dm_path in enumerate(b3dm_paths):
|
|||
|
|
print(f" {i+1}. {os.path.basename(b3dm_path)}")
|
|||
|
|
|
|||
|
|
# 批量解析所有B3DM文件,合并点云
|
|||
|
|
all_points = []
|
|||
|
|
if len(b3dm_paths) == 0:
|
|||
|
|
print("未提取到任何有效的B3DM文件!")
|
|||
|
|
return all_points
|
|||
|
|
|
|||
|
|
print(f"\n===== 开始解析B3DM文件 =====")
|
|||
|
|
for i, (b3dm_path, transform_mat) in enumerate(zip(b3dm_paths, tile_transforms), 1):
|
|||
|
|
print(f"解析文件 {i}/{len(b3dm_paths)}: {os.path.basename(b3dm_path)}")
|
|||
|
|
points = parse_b3dm_to_points(b3dm_path, region_filter, transform_mat)
|
|||
|
|
if points:
|
|||
|
|
all_points.extend(points)
|
|||
|
|
print(f" 提取到 {len(points)} 个点")
|
|||
|
|
else:
|
|||
|
|
print(f" 未提取到有效点")
|
|||
|
|
|
|||
|
|
# 点云去重+优化
|
|||
|
|
if all_points:
|
|||
|
|
all_points = np.array(all_points)
|
|||
|
|
original_count = len(all_points)
|
|||
|
|
all_points = np.unique(all_points.round(decimals=6), axis=0)
|
|||
|
|
print(f"\n最终提取点云数量: {len(all_points)} 个 (已去重, 去除了 {original_count - len(all_points)} 个重复点)")
|
|||
|
|
|
|||
|
|
# ========== 新增:输出整个地图文件的经纬度高程范围 ==========
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("地图文件总体范围统计:")
|
|||
|
|
print("-" * 60)
|
|||
|
|
|
|||
|
|
# 计算经纬度范围
|
|||
|
|
min_lon = np.min(all_points[:, 0])
|
|||
|
|
max_lon = np.max(all_points[:, 0])
|
|||
|
|
min_lat = np.min(all_points[:, 1])
|
|||
|
|
max_lat = np.max(all_points[:, 1])
|
|||
|
|
min_height = np.min(all_points[:, 2])
|
|||
|
|
max_height = np.max(all_points[:, 2])
|
|||
|
|
avg_height = np.mean(all_points[:, 2])
|
|||
|
|
std_height = np.std(all_points[:, 2])
|
|||
|
|
|
|||
|
|
# 计算中心点
|
|||
|
|
center_lon = (min_lon + max_lon) / 2
|
|||
|
|
center_lat = (min_lat + max_lat) / 2
|
|||
|
|
center_height = (min_height + max_height) / 2
|
|||
|
|
|
|||
|
|
print(f"经度范围: {min_lon:.6f}° ~ {max_lon:.6f}° (跨度: {max_lon - min_lon:.6f}°)")
|
|||
|
|
print(f"纬度范围: {min_lat:.6f}° ~ {max_lat:.6f}° (跨度: {max_lat - min_lat:.6f}°)")
|
|||
|
|
print(f"高程范围: {min_height:.2f}m ~ {max_height:.2f}m (总高差: {max_height - min_height:.2f}m)")
|
|||
|
|
print(f"平均高程: {avg_height:.2f}m (±{std_height:.2f}m)")
|
|||
|
|
print(f"\n中心点坐标:")
|
|||
|
|
print(f" 位置: ({center_lon:.6f}°, {center_lat:.6f}°)")
|
|||
|
|
print(f" 高程: {center_height:.2f}m")
|
|||
|
|
|
|||
|
|
# 输出边界坐标(用于复制使用)
|
|||
|
|
print(f"\n边界坐标:")
|
|||
|
|
print(f" 西北角: ({min_lon:.6f}, {max_lat:.6f})")
|
|||
|
|
print(f" 东北角: ({max_lon:.6f}, {max_lat:.6f})")
|
|||
|
|
print(f" 西南角: ({min_lon:.6f}, {min_lat:.6f})")
|
|||
|
|
print(f" 东南角: ({max_lon:.6f}, {min_lat:.6f})")
|
|||
|
|
|
|||
|
|
# 如果有区域过滤,显示过滤效果
|
|||
|
|
if region_coords:
|
|||
|
|
region_min_lon = min(coord[0] for coord in region_coords)
|
|||
|
|
region_max_lon = max(coord[0] for coord in region_coords)
|
|||
|
|
region_min_lat = min(coord[1] for coord in region_coords)
|
|||
|
|
region_max_lat = max(coord[1] for coord in region_coords)
|
|||
|
|
|
|||
|
|
print(f"\n区域过滤效果:")
|
|||
|
|
print(f" 原始地图范围: Lon[{region_min_lon:.6f}, {region_max_lon:.6f}], Lat[{region_min_lat:.6f}, {region_max_lat:.6f}]")
|
|||
|
|
print(f" 提取数据范围: Lon[{min_lon:.6f}, {max_lon:.6f}], Lat[{min_lat:.6f}, {max_lat:.6f}]")
|
|||
|
|
|
|||
|
|
# 计算覆盖率
|
|||
|
|
region_width = region_max_lon - region_min_lon
|
|||
|
|
region_height = region_max_lat - region_min_lat
|
|||
|
|
extracted_width = max_lon - min_lon
|
|||
|
|
extracted_height = max_lat - min_lat
|
|||
|
|
|
|||
|
|
width_coverage = extracted_width / region_width * 100 if region_width > 0 else 0
|
|||
|
|
height_coverage = extracted_height / region_height * 100 if region_height > 0 else 0
|
|||
|
|
|
|||
|
|
print(f" 经度方向覆盖率: {width_coverage:.1f}%")
|
|||
|
|
print(f" 纬度方向覆盖率: {height_coverage:.1f}%")
|
|||
|
|
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
return all_points
|
|||
|
|
|
|||
|
|
def points_to_dem(points, output_dem_path, pixel_size=0.0001):
|
|||
|
|
"""将离散点云插值为DEM(GeoTIFF格式)- 使用Scipy插值优化版本"""
|
|||
|
|
if len(points) == 0:
|
|||
|
|
raise ValueError("无有效点云数据,无法生成DEM")
|
|||
|
|
|
|||
|
|
# 转换为numpy数组
|
|||
|
|
points_array = np.array(points)
|
|||
|
|
lons = points_array[:, 0]
|
|||
|
|
lats = points_array[:, 1]
|
|||
|
|
heights = points_array[:, 2]
|
|||
|
|
|
|||
|
|
min_lon, max_lon = lons.min(), lons.max()
|
|||
|
|
min_lat, max_lat = lats.min(), lats.max()
|
|||
|
|
|
|||
|
|
print(f"DEM范围: Lon[{min_lon:.6f}, {max_lon:.6f}], Lat[{min_lat:.6f}, {max_lat:.6f}]")
|
|||
|
|
print(f"点云数量: {len(points)} 个")
|
|||
|
|
print(f"高程范围: {heights.min():.2f} ~ {heights.max():.2f} 米")
|
|||
|
|
|
|||
|
|
# 计算网格尺寸
|
|||
|
|
width = int((max_lon - min_lon) / pixel_size) + 1
|
|||
|
|
height = int((max_lat - min_lat) / pixel_size) + 1
|
|||
|
|
|
|||
|
|
# 限制网格大小,避免过大
|
|||
|
|
max_grid_size = 5000 # 最大网格尺寸
|
|||
|
|
if width > max_grid_size or height > max_grid_size:
|
|||
|
|
print(f"警告: 网格尺寸过大 ({width}x{height}),自动调整像素大小...")
|
|||
|
|
# 重新计算像素大小
|
|||
|
|
larger_dim = max(width, height)
|
|||
|
|
pixel_size = pixel_size * (larger_dim / max_grid_size)
|
|||
|
|
width = int((max_lon - min_lon) / pixel_size) + 1
|
|||
|
|
height = int((max_lat - min_lat) / pixel_size) + 1
|
|||
|
|
print(f"调整后像素大小: {pixel_size:.6f}°")
|
|||
|
|
|
|||
|
|
print(f"DEM网格: {width}x{height} (像素大小: {pixel_size:.6f}°)")
|
|||
|
|
|
|||
|
|
# 创建网格坐标
|
|||
|
|
x_grid = np.linspace(min_lon, max_lon, width)
|
|||
|
|
y_grid = np.linspace(max_lat, min_lat, height) # 纬度从上到下递减
|
|||
|
|
xi, yi = np.meshgrid(x_grid, y_grid)
|
|||
|
|
|
|||
|
|
# 使用scipy进行插值
|
|||
|
|
from scipy.interpolate import griddata
|
|||
|
|
|
|||
|
|
print("开始插值计算...")
|
|||
|
|
|
|||
|
|
# 方法1: 先尝试线性插值
|
|||
|
|
try:
|
|||
|
|
zi = griddata((lons, lats), heights, (xi, yi), method='linear')
|
|||
|
|
nan_count = np.isnan(zi).sum()
|
|||
|
|
nan_percent = nan_count / (width * height) * 100
|
|||
|
|
|
|||
|
|
print(f"线性插值完成,空白区域: {nan_count} 像素 ({nan_percent:.1f}%)")
|
|||
|
|
|
|||
|
|
# 如果有空白区域,使用最近邻方法填充
|
|||
|
|
if nan_count > 0:
|
|||
|
|
print("使用最近邻方法填充空白区域...")
|
|||
|
|
zi_nn = griddata((lons, lats), heights, (xi, yi), method='nearest')
|
|||
|
|
mask = np.isnan(zi)
|
|||
|
|
zi[mask] = zi_nn[mask]
|
|||
|
|
|
|||
|
|
# 再次检查
|
|||
|
|
nan_count = np.isnan(zi).sum()
|
|||
|
|
if nan_count > 0:
|
|||
|
|
print(f"仍有 {nan_count} 个空白像素,填充为最低高程值")
|
|||
|
|
min_height = heights.min()
|
|||
|
|
zi[np.isnan(zi)] = min_height
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"线性插值失败: {e}")
|
|||
|
|
print("尝试使用最近邻插值...")
|
|||
|
|
zi = griddata((lons, lats), heights, (xi, yi), method='nearest')
|
|||
|
|
|
|||
|
|
# 创建GeoTIFF
|
|||
|
|
print("创建GeoTIFF文件...")
|
|||
|
|
driver = gdal.GetDriverByName("GTiff")
|
|||
|
|
dem_ds = driver.Create(
|
|||
|
|
output_dem_path, width, height, 1, gdal.GDT_Float32,
|
|||
|
|
options=["COMPRESS=LZW", "TILED=YES", "PREDICTOR=2", "ZLEVEL=9"]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if dem_ds is None:
|
|||
|
|
raise RuntimeError(f"无法创建DEM文件: {output_dem_path}")
|
|||
|
|
|
|||
|
|
# 设置投影和地理变换
|
|||
|
|
srs = osr.SpatialReference()
|
|||
|
|
srs.ImportFromEPSG(4326) # WGS84
|
|||
|
|
dem_ds.SetProjection(srs.ExportToWkt())
|
|||
|
|
|
|||
|
|
geotransform = [
|
|||
|
|
min_lon, pixel_size, 0,
|
|||
|
|
max_lat, 0, -pixel_size
|
|||
|
|
]
|
|||
|
|
dem_ds.SetGeoTransform(geotransform)
|
|||
|
|
|
|||
|
|
# 写入数据
|
|||
|
|
band = dem_ds.GetRasterBand(1)
|
|||
|
|
band.WriteArray(zi)
|
|||
|
|
band.SetNoDataValue(-9999.0)
|
|||
|
|
band.SetDescription("Elevation")
|
|||
|
|
band.SetUnitType("meters")
|
|||
|
|
|
|||
|
|
# 计算统计信息
|
|||
|
|
print("计算统计信息...")
|
|||
|
|
band.FlushCache()
|
|||
|
|
band.ComputeStatistics(False)
|
|||
|
|
|
|||
|
|
# 设置颜色表(可选)
|
|||
|
|
try:
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
cmap = plt.cm.terrain
|
|||
|
|
colors = cmap(np.linspace(0, 1, 256))
|
|||
|
|
colors = (colors[:, :3] * 255).astype(np.uint8)
|
|||
|
|
|
|||
|
|
color_table = gdal.ColorTable()
|
|||
|
|
for i in range(256):
|
|||
|
|
color_table.SetColorEntry(i, (colors[i, 0], colors[i, 1], colors[i, 2], 255))
|
|||
|
|
|
|||
|
|
band.SetColorTable(color_table)
|
|||
|
|
band.SetColorInterpretation(gdal.GCI_PaletteIndex)
|
|||
|
|
except:
|
|||
|
|
pass # 如果设置颜色表失败,继续
|
|||
|
|
|
|||
|
|
dem_ds = None # 关闭文件
|
|||
|
|
|
|||
|
|
print(f"DEM生成成功: {output_dem_path}")
|
|||
|
|
print(f" 文件大小: {os.path.getsize(output_dem_path) / (1024*1024):.2f} MB")
|
|||
|
|
|
|||
|
|
# 验证文件
|
|||
|
|
if os.path.exists(output_dem_path):
|
|||
|
|
ds = gdal.Open(output_dem_path, gdal.GA_ReadOnly)
|
|||
|
|
if ds:
|
|||
|
|
band = ds.GetRasterBand(1)
|
|||
|
|
stats = band.GetStatistics(True, True)
|
|||
|
|
print(f" DEM统计: 最小值={stats[0]:.2f}m, 最大值={stats[1]:.2f}m")
|
|||
|
|
print(f" 平均值={stats[2]:.2f}m, 标准差={stats[3]:.2f}m")
|
|||
|
|
ds = None
|
|||
|
|
else:
|
|||
|
|
print("警告: DEM文件可能未成功生成")
|
|||
|
|
|
|||
|
|
def generate_dem(REGION_COORDS=None, tileset_path=None, dem_path=None):
|
|||
|
|
# 配置参数
|
|||
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
if tileset_path:
|
|||
|
|
TILESET_PATH = tileset_path
|
|||
|
|
else:
|
|||
|
|
TILESET_PATH = os.path.dirname(script_dir) + "/data/3dtiles/tileset.json"
|
|||
|
|
if dem_path :
|
|||
|
|
OUTPUT_DEM_PATH = dem_path
|
|||
|
|
else :
|
|||
|
|
OUTPUT_DEM_PATH = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
|
|||
|
|
PIXEL_SIZE = 0.0001
|
|||
|
|
|
|||
|
|
# 执行流程
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("开始解析3D Tiles...")
|
|||
|
|
if REGION_COORDS:
|
|||
|
|
print(f"启用区域过滤: {REGION_COORDS}")
|
|||
|
|
else:
|
|||
|
|
print("未启用区域过滤,将处理所有数据")
|
|||
|
|
|
|||
|
|
points = parse_tileset(TILESET_PATH, REGION_COORDS)
|
|||
|
|
print(f"解析完成,共提取点云: {len(points)}个")
|
|||
|
|
|
|||
|
|
if len(points) > 0:
|
|||
|
|
points_to_dem(points, OUTPUT_DEM_PATH, pixel_size=PIXEL_SIZE)
|
|||
|
|
return OUTPUT_DEM_PATH
|
|||
|
|
else:
|
|||
|
|
print("无点云数据,无法生成DEM")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
# 测试示例
|
|||
|
|
# 石棉县核心区域
|
|||
|
|
# SHIMIAN_CORE = [(100.22476304, 29.38340151), (110.32476304, 31.28340151)]
|
|||
|
|
SHIMIAN_CORE = [(100.22476304, 29.18340151), (110.32476304, 31.28340151)]
|
|||
|
|
|
|||
|
|
# 可以根据需要启用或禁用区域过滤
|
|||
|
|
REGION_COORDS = SHIMIAN_CORE # 启用区域过滤
|
|||
|
|
# REGION_COORDS = None # 禁用区域过滤
|
|||
|
|
|
|||
|
|
dem_path = generate_dem(REGION_COORDS)
|
|||
|
|
if dem_path:
|
|||
|
|
print(f"\nDEM文件已生成: {dem_path}")
|