import os import requests import numpy as np from py3dtiles.tileset.content import B3dm from pyproj import Proj, Transformer from shapely.geometry import Polygon, Point from shapely.ops import unary_union import math from urllib.parse import urljoin from pygltflib import GLTF2 # 目标区域经纬度坐标(转换为多边形) region_coords = [ [102.22321717600258, 29.384100779345513], [102.22612442019208, 29.384506810595088], [102.22638603372953, 29.382061071072794], [102.22311237980807, 29.38186133280733], [102.22321717600258, 29.384100779345513] # 闭合多边形 ] # 创建多边形对象 region_polygon = Polygon(region_coords) # 两个3D Tiles模型的URL tileset_urls = [ "http://8.137.54.85:9000/300bdf2b-a150-406e-be63-d28bd29b409f/dszh/1748398014403562192_OUT/B3DM/tileset.json", "http://8.137.54.85:9000/300bdf2b-a150-406e-be63-d28bd29b409f/dszh/1748325943733189898_OUT/B3DM/tileset.json" ] # 坐标系转换 wgs84 = Proj(init='epsg:4326') # WGS84经纬度 web_mercator = Proj(init='epsg:3857') # Web墨卡托投影 def adjust_z_in_transform(tileset_path, output_path=None, delta_z=0): import json import numpy as np if not os.path.exists(tileset_path): print(f"❌ tileset.json 文件不存在: {tileset_path}") return with open(tileset_path, 'r', encoding='utf-8') as f: data = json.load(f) root = data.get('root', {}) # 插入默认 transform if 'transform' not in root: print("⚠️ 未找到 transform 字段,使用单位矩阵") root['transform'] = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] transform = np.array(root['transform']).reshape(4, 4) print(f"原始 Z 平移: {transform[3, 2]}") transform[3, 2] += delta_z print(f"修正后 Z 平移: {transform[3, 2]}") root['transform'] = transform.flatten().tolist() data['root'] = root if output_path is None: output_path = tileset_path.replace(".json", f"_adjusted_z{int(delta_z)}.json") with open(output_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) print(f"✅ 高度调整完成,输出文件: {output_path}") def download_tileset(tileset_url): """下载tileset.json数据""" try: response = requests.get(tileset_url) response.raise_for_status() return response.json() except Exception as e: print(f"下载tileset失败: {e}") return None def extract_vertices(gltf_bytes): try: with open("temp.glb", "wb") as f: f.write(gltf_bytes) gltf = GLTF2().load("temp.glb") for mesh in gltf.meshes: for primitive in mesh.primitives: if not hasattr(primitive.attributes, "POSITION"): continue accessor_idx = primitive.attributes.POSITION accessor = gltf.accessors[accessor_idx] buffer_view = gltf.bufferViews[accessor.bufferView] buffer = gltf.buffers[buffer_view.buffer] byte_offset = (buffer_view.byteOffset or 0) + (accessor.byteOffset or 0) byte_length = accessor.count * 3 * 4 # 3 floats per vertex data_bytes = gltf.binary_blob()[byte_offset: byte_offset + byte_length] vertices = np.frombuffer(data_bytes, dtype=np.float32).reshape((accessor.count, 3)) return vertices except Exception as e: print(f"提取顶点数据失败: {e}") return np.array([]) def find_closest_vertex(vertices, lon, lat): """找到离目标点最近的顶点""" if not vertices: return None # 计算距离并找到最近的顶点 min_distance = float('inf') closest_vertex = None for vertex in vertices: v_lon, v_lat, v_z = vertex # 计算经纬度距离(简化为平面距离) distance = math.hypot(v_lon - lon, v_lat - lat) if distance < min_distance: min_distance = distance closest_vertex = vertex return closest_vertex def compare_heights(heights1, heights2, tolerance=0.5): """比较两个高度数据集,找出差异""" # 找到所有点的并集 all_points = set(heights1.keys()).union(set(heights2.keys())) differences = [] for point in all_points: h1 = heights1.get(point, None) h2 = heights2.get(point, None) # 检查是否有一个模型在该点没有数据 if h1 is None or h2 is None: differences.append({ 'point': point, 'height1': h1, 'height2': h2, 'difference': None, 'type': 'missing_data' }) else: # 检查高度差异是否超过容忍度 diff = abs(h1 - h2) if diff > tolerance: differences.append({ 'point': point, 'height1': h1, 'height2': h2, 'difference': diff, 'type': 'height_difference' }) return differences def get_b3dm_from_tile_json(json_url): try: response = requests.get(json_url) response.raise_for_status() data = response.json() # 递归查找 b3dm uri def find_b3dm_uri(node): if 'content' in node and 'uri' in node['content']: uri = node['content']['uri'] if uri.endswith('.b3dm'): return uri if 'children' in node: for child in node['children']: result = find_b3dm_uri(child) if result: return result return None root = data.get('root', {}) b3dm_uri = find_b3dm_uri(root) if not b3dm_uri: print(f"{json_url} 中找不到 content.uri") return None base_url = os.path.dirname(json_url) full_b3dm_url = urljoin(base_url + '/', b3dm_uri) return full_b3dm_url except Exception as e: print(f"解析 JSON {json_url} 时出错: {e}") return None def get_heights_in_region(tileset_url, sample_density=10): """获取区域内的高度数据""" tileset_json = download_tileset(tileset_url) if not tileset_json: return {} tiles_in_region = get_tiles_in_region(tileset_json, tileset_url) if not tiles_in_region: print(f"在{tileset_url}中未找到区域内的瓦片") return {} min_lon, min_lat = min(p[0] for p in region_coords), min(p[1] for p in region_coords) max_lon, max_lat = max(p[0] for p in region_coords), max(p[1] for p in region_coords) lon_steps = np.linspace(min_lon, max_lon, sample_density) lat_steps = np.linspace(min_lat, max_lat, sample_density) heights = {} for tile_info in tiles_in_region: try: response = requests.get(tile_info['url']) response.raise_for_status() b3dm_data = response.content # ✅ 尝试解析为 b3dm try: gltf_bytes = parse_b3dm(b3dm_data) except Exception: # 可能 tile_info['url'] 是 JSON,不是真 b3dm print(f"尝试从 {tile_info['url']} 获取真实 b3dm 地址...") actual_b3dm_url = get_b3dm_from_tile_json(tile_info['url']) if not actual_b3dm_url: print(f"跳过:无法从 {tile_info['url']} 获取有效 b3dm") continue response = requests.get(actual_b3dm_url) response.raise_for_status() b3dm_data = response.content gltf_bytes = parse_b3dm(b3dm_data) # ✅ 模拟解析 glb vertices = extract_vertices(gltf_bytes) if not vertices.size: continue # ✅ 应用变换 transformed_vertices = [transform_point(v, tile_info['transform']) for v in vertices] transformer = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True) wgs84_vertices = [] for x, y, z in transformed_vertices: lon, lat = transformer.transform(x, y) wgs84_vertices.append((lon, lat, z)) for lon in lon_steps: for lat in lat_steps: if point_in_region(lon, lat): closest_vertex = find_closest_vertex(wgs84_vertices, lon, lat) if closest_vertex: key = (round(lon, 6), round(lat, 6)) heights[key] = closest_vertex[2] except Exception as e: print(f"处理瓦片 {tile_info['url']} 时出错: {e}") continue return heights def get_tiles_in_region(tileset_json, tileset_base_url): """获取区域内的所有瓦片""" tiles_in_region = [] # 去除 tileset.json 得到根路径 tileset_root_url = tileset_base_url.rsplit('/', 1)[0] def recursive_search(tile, parent_transform=None): tile_transform = tile.get('transform', [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]) combined_transform = multiply_matrices(parent_transform, tile_transform) if parent_transform else tile_transform if 'boundingVolume' in tile and is_bounding_volume_intersects_region(tile['boundingVolume']): if 'content' in tile and 'uri' in tile['content']: # 修复URL拼接 tile_url = urljoin(tileset_root_url + '/', tile['content']['uri']) tiles_in_region.append({ 'url': tile_url, 'transform': combined_transform }) if 'children' in tile: for child in tile['children']: recursive_search(child, combined_transform) if 'root' in tileset_json: recursive_search(tileset_json['root']) return tiles_in_region def is_bounding_volume_intersects_region(bounding_volume): """检查边界体是否与区域相交""" # 简化实现,实际需要根据不同边界体类型实现 if 'region' in bounding_volume: # region格式: [west, south, east, north, minHeight, maxHeight] region = bounding_volume['region'] bv_polygon = Polygon([ [region[0], region[1]], [region[2], region[1]], [region[2], region[3]], [region[0], region[3]], [region[0], region[1]] ]) return region_polygon.intersects(bv_polygon) elif 'box' in bounding_volume: # 对于box类型,需要转换到经纬度后再判断 # 这里简化处理,返回True让更细致的检查在后续进行 return True elif 'sphere' in bounding_volume: # 对于sphere类型,简化处理 return True return False def multiply_matrices(a, b): """计算两个4x4矩阵的乘积""" result = [0.0] * 16 for i in range(4): for j in range(4): result[i*4 + j] = a[i*4 + 0] * b[0*4 + j] + \ a[i*4 + 1] * b[1*4 + j] + \ a[i*4 + 2] * b[2*4 + j] + \ a[i*4 + 3] * b[3*4 + j] return result def parse_b3dm(b3dm_data: bytes): """ 解析 b3dm 文件,返回 glb 二进制数据 """ import struct if b3dm_data[:4] != b'b3dm': raise ValueError("不是有效的 b3dm 文件") # 读取 header(28 字节) header = struct.unpack('<4sIIIIII', b3dm_data[:28]) _, version, byte_length, ft_json_len, ft_bin_len, bt_json_len, bt_bin_len = header glb_start = 28 + ft_json_len + ft_bin_len + bt_json_len + bt_bin_len glb_bytes = b3dm_data[glb_start:] return glb_bytes def point_in_region(lon, lat): """判断点是否在目标区域内""" return region_polygon.contains(Point(lon, lat)) def transform_point(point, matrix): """应用变换矩阵到点""" x, y, z = point x_out = x * matrix[0] + y * matrix[4] + z * matrix[8] + matrix[12] y_out = x * matrix[1] + y * matrix[5] + z * matrix[9] + matrix[13] z_out = x * matrix[2] + y * matrix[6] + z * matrix[10] + matrix[14] return (x_out, y_out, z_out) def main(): sample_density = 20 print("正在从第一个3D Tiles模型提取区域高度数据...") heights1 = get_heights_in_region(tileset_urls[0], sample_density) print("正在从第二个3D Tiles模型提取区域高度数据...") heights2 = get_heights_in_region(tileset_urls[1], sample_density) if not heights1 or not heights2: print("无法获取足够的高度数据进行比较") return # 计算平均高度 avg1 = np.mean(list(heights1.values())) avg2 = np.mean(list(heights2.values())) print(f"\n模型1 平均高度: {avg1:.2f} 米") print(f"模型2 平均高度: {avg2:.2f} 米") delta = avg1 - avg2 print(f"高度差: {delta:.2f} 米") # 🔧 自动统一高度 if abs(delta) > 0.5: print("\n⚙️ 正在统一高度基准(修改模型2的 transform)...") # tileset_urls[1] 是远程 URL,下载后调整 try: ts2_url = tileset_urls[1] response = requests.get(ts2_url) response.raise_for_status() with open("tileset_model2.json", "w", encoding="utf-8") as f: f.write(response.text) adjust_z_in_transform("tileset_model2.json", "tileset_model2_adjusted.json", delta_z=delta) except Exception as e: print(f"❌ 调整高度失败: {e}") else: print("\n✅ 高度差异在容忍范围内,无需调整") # 🔍 差异分析 print("\n正在分析详细差异点...") differences = compare_heights(heights1, heights2, 0.5) if differences: print(f"共发现 {len(differences)} 处显著高度差异:") for i, diff in enumerate(differences[:10], 1): # 仅显示前10条 lon, lat = diff['point'] print(f"\n位置 {i}: 经度 {lon}, 纬度 {lat}") print(f"模型1高度: {diff['height1']:.2f}米") print(f"模型2高度: {diff['height2']:.2f}米") if diff['difference'] is not None: print(f"差异: {diff['difference']:.2f}米") else: print("差异: 一个模型在该位置没有数据") else: print("两个模型在指定区域高度基本一致 ✅") if __name__ == "__main__": main()