684 lines
27 KiB
Python
684 lines
27 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
三维模型体积计算器
|
|||
|
|
基于指定地理范围内的三维模型数据,使用三角构网方法计算体积
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import struct
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
import numpy as np
|
|||
|
|
from scipy.spatial import Delaunay
|
|||
|
|
import math
|
|||
|
|
from tqdm import tqdm
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import DracoPy
|
|||
|
|
except ImportError:
|
|||
|
|
print("警告: DracoPy库未安装,无法处理Draco压缩的数据")
|
|||
|
|
print("请运行: pip install DracoPy")
|
|||
|
|
DracoPy = None
|
|||
|
|
|
|||
|
|
class VolumeCalculator:
|
|||
|
|
def __init__(self, location_file):
|
|||
|
|
self.location_bounds = self.load_location_bounds(location_file)
|
|||
|
|
self.all_vertices = []
|
|||
|
|
self.filtered_vertices = []
|
|||
|
|
|
|||
|
|
def load_location_bounds(self, location_file):
|
|||
|
|
"""加载地理范围边界"""
|
|||
|
|
try:
|
|||
|
|
with open(location_file, 'r', encoding='utf-8') as f:
|
|||
|
|
coords = json.load(f)
|
|||
|
|
|
|||
|
|
# 提取经纬度范围
|
|||
|
|
lons = [coord[0] for coord in coords]
|
|||
|
|
lats = [coord[1] for coord in coords]
|
|||
|
|
elevs = [coord[2] for coord in coords]
|
|||
|
|
|
|||
|
|
bounds = {
|
|||
|
|
'min_lon': min(lons),
|
|||
|
|
'max_lon': max(lons),
|
|||
|
|
'min_lat': min(lats),
|
|||
|
|
'max_lat': max(lats),
|
|||
|
|
'min_elev': min(elevs),
|
|||
|
|
'max_elev': max(elevs),
|
|||
|
|
'coords': coords
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print(f"地理范围边界:")
|
|||
|
|
print(f" 经度: {bounds['min_lon']:.8f} ~ {bounds['max_lon']:.8f}")
|
|||
|
|
print(f" 纬度: {bounds['min_lat']:.8f} ~ {bounds['max_lat']:.8f}")
|
|||
|
|
print(f" 高程: {bounds['min_elev']:.2f} ~ {bounds['max_elev']:.2f}")
|
|||
|
|
|
|||
|
|
return bounds
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"加载地理范围文件失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def wgs84_to_cartesian(self, lon, lat, elev):
|
|||
|
|
"""WGS84坐标转换为笛卡尔坐标(高精度算法)"""
|
|||
|
|
# WGS84椭球参数(EPSG:4326)
|
|||
|
|
a = 6378137.0 # 长半轴 (米)
|
|||
|
|
f = 1/298.257223563 # 扁率
|
|||
|
|
e2 = 2*f - f*f # 第一偏心率平方
|
|||
|
|
|
|||
|
|
# 角度转弧度
|
|||
|
|
lon_rad = math.radians(lon)
|
|||
|
|
lat_rad = math.radians(lat)
|
|||
|
|
|
|||
|
|
# 计算卯酉圈曲率半径
|
|||
|
|
sin_lat = math.sin(lat_rad)
|
|||
|
|
cos_lat = math.cos(lat_rad)
|
|||
|
|
sin_lon = math.sin(lon_rad)
|
|||
|
|
cos_lon = math.cos(lon_rad)
|
|||
|
|
|
|||
|
|
N = a / math.sqrt(1 - e2 * sin_lat * sin_lat)
|
|||
|
|
|
|||
|
|
# 高精度笛卡尔坐标计算
|
|||
|
|
x = (N + elev) * cos_lat * cos_lon
|
|||
|
|
y = (N + elev) * cos_lat * sin_lon
|
|||
|
|
z = (N * (1 - e2) + elev) * sin_lat
|
|||
|
|
|
|||
|
|
return [x, y, z]
|
|||
|
|
|
|||
|
|
def cartesian_to_wgs84(self, x, y, z):
|
|||
|
|
"""笛卡尔坐标转换为WGS84坐标(高精度迭代法)"""
|
|||
|
|
# WGS84椭球参数
|
|||
|
|
a = 6378137.0 # 长半轴
|
|||
|
|
f = 1/298.257223563 # 扁率
|
|||
|
|
e2 = 2*f - f*f # 第一偏心率平方
|
|||
|
|
ep2 = e2 / (1 - e2) # 第二偏心率平方
|
|||
|
|
|
|||
|
|
# 计算经度(精确值)
|
|||
|
|
lon = math.atan2(y, x)
|
|||
|
|
|
|||
|
|
# 计算纬度和高程(使用Bowring迭代法)
|
|||
|
|
p = math.sqrt(x*x + y*y)
|
|||
|
|
|
|||
|
|
if p == 0:
|
|||
|
|
# 极点情况
|
|||
|
|
lat = math.pi/2 if z > 0 else -math.pi/2
|
|||
|
|
elev = abs(z) - a * math.sqrt(1 - e2)
|
|||
|
|
return [math.degrees(lon), math.degrees(lat), elev]
|
|||
|
|
|
|||
|
|
# 初始估计
|
|||
|
|
theta = math.atan2(z, p * (1 - f))
|
|||
|
|
lat_prev = math.atan2(z + ep2 * a * (1 - f) * math.sin(theta)**3,
|
|||
|
|
p - e2 * a * math.cos(theta)**3)
|
|||
|
|
|
|||
|
|
# 迭代求解纬度
|
|||
|
|
max_iterations = 10
|
|||
|
|
tolerance = 1e-12
|
|||
|
|
|
|||
|
|
for i in range(max_iterations):
|
|||
|
|
N = a / math.sqrt(1 - e2 * math.sin(lat_prev)**2)
|
|||
|
|
elev = p / math.cos(lat_prev) - N
|
|||
|
|
|
|||
|
|
# 更新纬度估计
|
|||
|
|
lat_new = math.atan2(z + e2 * N * math.sin(lat_prev), p)
|
|||
|
|
|
|||
|
|
# 检查收敛性
|
|||
|
|
if abs(lat_new - lat_prev) < tolerance:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
lat_prev = lat_new
|
|||
|
|
|
|||
|
|
# 最终计算高程
|
|||
|
|
N = a / math.sqrt(1 - e2 * math.sin(lat_prev)**2)
|
|||
|
|
elev = p / math.cos(lat_prev) - N
|
|||
|
|
|
|||
|
|
return [math.degrees(lon), math.degrees(lat_prev), elev]
|
|||
|
|
|
|||
|
|
def is_point_in_bounds(self, vertex):
|
|||
|
|
"""检查点是否在指定的地理范围内"""
|
|||
|
|
if not self.location_bounds:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 将笛卡尔坐标转换为WGS84
|
|||
|
|
try:
|
|||
|
|
lon, lat, elev = self.cartesian_to_wgs84(vertex[0], vertex[1], vertex[2])
|
|||
|
|
|
|||
|
|
# 检查是否在边界范围内
|
|||
|
|
return (self.location_bounds['min_lon'] <= lon <= self.location_bounds['max_lon'] and
|
|||
|
|
self.location_bounds['min_lat'] <= lat <= self.location_bounds['max_lat'])
|
|||
|
|
except:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def multiply_matrix_vector(self, matrix, vector):
|
|||
|
|
"""4x4矩阵与4D向量相乘"""
|
|||
|
|
m = [
|
|||
|
|
[matrix[0], matrix[4], matrix[8], matrix[12]],
|
|||
|
|
[matrix[1], matrix[5], matrix[9], matrix[13]],
|
|||
|
|
[matrix[2], matrix[6], matrix[10], matrix[14]],
|
|||
|
|
[matrix[3], matrix[7], matrix[11], matrix[15]]
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
v = [vector[0], vector[1], vector[2], 1.0]
|
|||
|
|
|
|||
|
|
result = [
|
|||
|
|
m[0][0]*v[0] + m[0][1]*v[1] + m[0][2]*v[2] + m[0][3]*v[3],
|
|||
|
|
m[1][0]*v[0] + m[1][1]*v[1] + m[1][2]*v[2] + m[1][3]*v[3],
|
|||
|
|
m[2][0]*v[0] + m[2][1]*v[1] + m[2][2]*v[2] + m[2][3]*v[3]
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
def multiply_matrices(self, m1, m2):
|
|||
|
|
"""两个4x4矩阵相乘"""
|
|||
|
|
def list_to_matrix(lst):
|
|||
|
|
return [
|
|||
|
|
[lst[0], lst[4], lst[8], lst[12]],
|
|||
|
|
[lst[1], lst[5], lst[9], lst[13]],
|
|||
|
|
[lst[2], lst[6], lst[10], lst[14]],
|
|||
|
|
[lst[3], lst[7], lst[11], lst[15]]
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def matrix_to_list(mat):
|
|||
|
|
return [
|
|||
|
|
mat[0][0], mat[1][0], mat[2][0], mat[3][0],
|
|||
|
|
mat[0][1], mat[1][1], mat[2][1], mat[3][1],
|
|||
|
|
mat[0][2], mat[1][2], mat[2][2], mat[3][2],
|
|||
|
|
mat[0][3], mat[1][3], mat[2][3], mat[3][3]
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
a = list_to_matrix(m1)
|
|||
|
|
b = list_to_matrix(m2)
|
|||
|
|
|
|||
|
|
result = [[0 for _ in range(4)] for _ in range(4)]
|
|||
|
|
for i in range(4):
|
|||
|
|
for j in range(4):
|
|||
|
|
for k in range(4):
|
|||
|
|
result[i][j] += a[i][k] * b[k][j]
|
|||
|
|
|
|||
|
|
return matrix_to_list(result)
|
|||
|
|
|
|||
|
|
def apply_transform_to_vertices(self, vertices, transform_matrix):
|
|||
|
|
"""对顶点应用变换矩阵"""
|
|||
|
|
if not transform_matrix:
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
transformed_vertices = []
|
|||
|
|
for vertex in vertices:
|
|||
|
|
transformed = self.multiply_matrix_vector(transform_matrix, vertex)
|
|||
|
|
transformed_vertices.append(transformed)
|
|||
|
|
|
|||
|
|
return transformed_vertices
|
|||
|
|
|
|||
|
|
def parse_tileset_json(self, tileset_path, parent_transform=None):
|
|||
|
|
"""解析tileset.json文件,收集B3DM文件和变换矩阵"""
|
|||
|
|
try:
|
|||
|
|
with open(tileset_path, 'r', encoding='utf-8') as f:
|
|||
|
|
tileset_data = json.load(f)
|
|||
|
|
|
|||
|
|
b3dm_files = []
|
|||
|
|
|
|||
|
|
def process_node(node, base_path, accumulated_transform):
|
|||
|
|
current_transform = node.get('transform')
|
|||
|
|
|
|||
|
|
if current_transform and accumulated_transform:
|
|||
|
|
final_transform = self.multiply_matrices(accumulated_transform, current_transform)
|
|||
|
|
elif current_transform:
|
|||
|
|
final_transform = current_transform
|
|||
|
|
else:
|
|||
|
|
final_transform = accumulated_transform
|
|||
|
|
|
|||
|
|
if 'content' in node and 'uri' in node['content']:
|
|||
|
|
uri = node['content']['uri']
|
|||
|
|
if uri.endswith('.b3dm'):
|
|||
|
|
full_path = os.path.join(base_path, uri)
|
|||
|
|
if os.path.exists(full_path):
|
|||
|
|
b3dm_files.append((full_path, final_transform))
|
|||
|
|
elif uri.endswith('.json'):
|
|||
|
|
sub_tileset_path = os.path.join(base_path, uri)
|
|||
|
|
if os.path.exists(sub_tileset_path):
|
|||
|
|
sub_files = self.parse_tileset_json(sub_tileset_path, final_transform)
|
|||
|
|
b3dm_files.extend(sub_files)
|
|||
|
|
|
|||
|
|
if 'children' in node:
|
|||
|
|
for child in node['children']:
|
|||
|
|
process_node(child, base_path, final_transform)
|
|||
|
|
|
|||
|
|
base_path = os.path.dirname(tileset_path)
|
|||
|
|
if 'root' in tileset_data:
|
|||
|
|
process_node(tileset_data['root'], base_path, parent_transform)
|
|||
|
|
|
|||
|
|
return b3dm_files
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解析tileset.json时出错: {e}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def parse_b3dm_file(self, file_path):
|
|||
|
|
"""解析B3DM文件"""
|
|||
|
|
try:
|
|||
|
|
with open(file_path, 'rb') as f:
|
|||
|
|
magic = f.read(4)
|
|||
|
|
if magic != b'b3dm':
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
version = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
byte_length = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
feature_table_json_byte_length = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
feature_table_binary_byte_length = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
batch_table_json_byte_length = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
batch_table_binary_byte_length = struct.unpack('<I', f.read(4))[0]
|
|||
|
|
|
|||
|
|
f.seek(28 + feature_table_json_byte_length + feature_table_binary_byte_length +
|
|||
|
|
batch_table_json_byte_length + batch_table_binary_byte_length)
|
|||
|
|
|
|||
|
|
gltf_data = f.read()
|
|||
|
|
return self.parse_gltf_data(gltf_data)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解析B3DM文件 {file_path} 失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def parse_gltf_data(self, gltf_data):
|
|||
|
|
"""解析glTF数据"""
|
|||
|
|
try:
|
|||
|
|
if gltf_data[:4] == b'glTF':
|
|||
|
|
return self.parse_glb_data(gltf_data)
|
|||
|
|
else:
|
|||
|
|
gltf_json = json.loads(gltf_data.decode('utf-8'))
|
|||
|
|
return self.extract_vertices_from_gltf(gltf_json, None)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解析glTF数据失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def parse_glb_data(self, glb_data):
|
|||
|
|
"""解析GLB格式的glTF数据"""
|
|||
|
|
try:
|
|||
|
|
magic = glb_data[:4]
|
|||
|
|
if magic != b'glTF':
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
version = struct.unpack('<I', glb_data[4:8])[0]
|
|||
|
|
total_length = struct.unpack('<I', glb_data[8:12])[0]
|
|||
|
|
|
|||
|
|
offset = 12
|
|||
|
|
json_data = None
|
|||
|
|
binary_data = None
|
|||
|
|
|
|||
|
|
while offset < len(glb_data):
|
|||
|
|
if offset + 8 > len(glb_data):
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
chunk_length = struct.unpack('<I', glb_data[offset:offset+4])[0]
|
|||
|
|
chunk_type = glb_data[offset+4:offset+8]
|
|||
|
|
chunk_data = glb_data[offset+8:offset+8+chunk_length]
|
|||
|
|
|
|||
|
|
if chunk_type == b'JSON':
|
|||
|
|
json_data = json.loads(chunk_data.decode('utf-8'))
|
|||
|
|
elif chunk_type == b'BIN\x00':
|
|||
|
|
binary_data = chunk_data
|
|||
|
|
|
|||
|
|
offset += 8 + chunk_length
|
|||
|
|
offset = (offset + 3) & ~3
|
|||
|
|
|
|||
|
|
if json_data:
|
|||
|
|
return self.extract_vertices_from_gltf(json_data, binary_data)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解析GLB数据失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def extract_vertices_from_gltf(self, gltf_json, binary_data):
|
|||
|
|
"""从glTF JSON中提取顶点数据"""
|
|||
|
|
vertices = []
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if 'extensionsUsed' in gltf_json and 'KHR_draco_mesh_compression' in gltf_json['extensionsUsed']:
|
|||
|
|
if DracoPy is None:
|
|||
|
|
print("警告: 检测到Draco压缩但DracoPy未安装")
|
|||
|
|
return vertices
|
|||
|
|
return self.extract_draco_vertices(gltf_json, binary_data)
|
|||
|
|
|
|||
|
|
if 'meshes' not in gltf_json:
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
for mesh in gltf_json['meshes']:
|
|||
|
|
for primitive in mesh['primitives']:
|
|||
|
|
if 'attributes' in primitive and 'POSITION' in primitive['attributes']:
|
|||
|
|
position_accessor_index = primitive['attributes']['POSITION']
|
|||
|
|
|
|||
|
|
if 'accessors' in gltf_json and position_accessor_index < len(gltf_json['accessors']):
|
|||
|
|
accessor = gltf_json['accessors'][position_accessor_index]
|
|||
|
|
buffer_view_index = accessor['bufferView']
|
|||
|
|
|
|||
|
|
if 'bufferViews' in gltf_json and buffer_view_index < len(gltf_json['bufferViews']):
|
|||
|
|
buffer_view = gltf_json['bufferViews'][buffer_view_index]
|
|||
|
|
buffer_index = buffer_view['buffer']
|
|||
|
|
byte_offset = buffer_view.get('byteOffset', 0) + accessor.get('byteOffset', 0)
|
|||
|
|
|
|||
|
|
if binary_data and buffer_index == 0:
|
|||
|
|
component_type = accessor['componentType']
|
|||
|
|
count = accessor['count']
|
|||
|
|
|
|||
|
|
if component_type == 5126: # FLOAT
|
|||
|
|
vertex_data = struct.unpack(f'<{count*3}f',
|
|||
|
|
binary_data[byte_offset:byte_offset+count*12])
|
|||
|
|
for i in range(0, len(vertex_data), 3):
|
|||
|
|
vertices.append([vertex_data[i], vertex_data[i+1], vertex_data[i+2]])
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"提取顶点数据失败: {e}")
|
|||
|
|
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
def extract_draco_vertices(self, gltf_json, binary_data):
|
|||
|
|
"""提取Draco压缩的顶点数据"""
|
|||
|
|
vertices = []
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if 'meshes' not in gltf_json:
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
for mesh in gltf_json['meshes']:
|
|||
|
|
for primitive in mesh['primitives']:
|
|||
|
|
if 'extensions' in primitive and 'KHR_draco_mesh_compression' in primitive['extensions']:
|
|||
|
|
draco_ext = primitive['extensions']['KHR_draco_mesh_compression']
|
|||
|
|
buffer_view_index = draco_ext['bufferView']
|
|||
|
|
|
|||
|
|
if 'bufferViews' in gltf_json and buffer_view_index < len(gltf_json['bufferViews']):
|
|||
|
|
buffer_view = gltf_json['bufferViews'][buffer_view_index]
|
|||
|
|
byte_offset = buffer_view.get('byteOffset', 0)
|
|||
|
|
byte_length = buffer_view['byteLength']
|
|||
|
|
|
|||
|
|
if binary_data:
|
|||
|
|
draco_data = binary_data[byte_offset:byte_offset+byte_length]
|
|||
|
|
|
|||
|
|
mesh_data = DracoPy.decode(draco_data)
|
|||
|
|
if hasattr(mesh_data, 'points'):
|
|||
|
|
points = mesh_data.points
|
|||
|
|
for point in points:
|
|||
|
|
vertices.append([float(point[0]), float(point[1]), float(point[2])])
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解码Draco数据失败: {e}")
|
|||
|
|
|
|||
|
|
return vertices
|
|||
|
|
|
|||
|
|
def calculate_triangle_angles(self, p1, p2, p3):
|
|||
|
|
"""计算三角形的三个内角(度)"""
|
|||
|
|
# 计算三边长度
|
|||
|
|
a = np.linalg.norm(p2 - p3) # 边a对应角A(p1)
|
|||
|
|
b = np.linalg.norm(p1 - p3) # 边b对应角B(p2)
|
|||
|
|
c = np.linalg.norm(p1 - p2) # 边c对应角C(p3)
|
|||
|
|
|
|||
|
|
# 避免除零错误
|
|||
|
|
if a == 0 or b == 0 or c == 0:
|
|||
|
|
return [0, 0, 0]
|
|||
|
|
|
|||
|
|
# 使用余弦定理计算角度
|
|||
|
|
try:
|
|||
|
|
# 角A = arccos((b²+c²-a²)/(2bc))
|
|||
|
|
cos_A = (b*b + c*c - a*a) / (2*b*c)
|
|||
|
|
cos_B = (a*a + c*c - b*b) / (2*a*c)
|
|||
|
|
cos_C = (a*a + b*b - c*c) / (2*a*b)
|
|||
|
|
|
|||
|
|
# 限制余弦值范围,避免数值误差
|
|||
|
|
cos_A = np.clip(cos_A, -1.0, 1.0)
|
|||
|
|
cos_B = np.clip(cos_B, -1.0, 1.0)
|
|||
|
|
cos_C = np.clip(cos_C, -1.0, 1.0)
|
|||
|
|
|
|||
|
|
angle_A = np.arccos(cos_A) * 180 / np.pi
|
|||
|
|
angle_B = np.arccos(cos_B) * 180 / np.pi
|
|||
|
|
angle_C = np.arccos(cos_C) * 180 / np.pi
|
|||
|
|
|
|||
|
|
return [angle_A, angle_B, angle_C]
|
|||
|
|
except:
|
|||
|
|
return [0, 0, 0]
|
|||
|
|
|
|||
|
|
def is_valid_triangle(self, p1, p2, p3, min_angle=10.0, max_aspect_ratio=10.0):
|
|||
|
|
"""验证三角形质量,基于角度约束和长宽比"""
|
|||
|
|
angles = self.calculate_triangle_angles(p1, p2, p3)
|
|||
|
|
|
|||
|
|
# 检查最小角度约束(参考C#代码中的10度限制)
|
|||
|
|
if min(angles) < min_angle:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 计算三边长度
|
|||
|
|
a = np.linalg.norm(p2 - p3)
|
|||
|
|
b = np.linalg.norm(p1 - p3)
|
|||
|
|
c = np.linalg.norm(p1 - p2)
|
|||
|
|
|
|||
|
|
# 检查长宽比约束
|
|||
|
|
max_side = max(a, b, c)
|
|||
|
|
min_side = min(a, b, c)
|
|||
|
|
if min_side > 0 and max_side / min_side > max_aspect_ratio:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def calculate_circumcenter_and_radius(self, p1, p2, p3):
|
|||
|
|
"""计算三角形外接圆圆心和半径(高精度算法)"""
|
|||
|
|
try:
|
|||
|
|
x1, y1 = p1[0], p1[1]
|
|||
|
|
x2, y2 = p2[0], p2[1]
|
|||
|
|
x3, y3 = p3[0], p3[1]
|
|||
|
|
|
|||
|
|
# 使用C#代码中的高精度外接圆计算公式
|
|||
|
|
d = 2 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2))
|
|||
|
|
|
|||
|
|
if abs(d) < 1e-10: # 三点共线
|
|||
|
|
return None, float('inf')
|
|||
|
|
|
|||
|
|
ux = ((x1*x1 + y1*y1) * (y2 - y3) + (x2*x2 + y2*y2) * (y3 - y1) + (x3*x3 + y3*y3) * (y1 - y2)) / d
|
|||
|
|
uy = ((x1*x1 + y1*y1) * (x3 - x2) + (x2*x2 + y2*y2) * (x1 - y3) + (x3*x3 + y3*y3) * (x2 - x1)) / d
|
|||
|
|
|
|||
|
|
# 计算半径
|
|||
|
|
radius = np.sqrt((ux - x1)**2 + (uy - y1)**2)
|
|||
|
|
|
|||
|
|
return np.array([ux, uy]), radius
|
|||
|
|
except:
|
|||
|
|
return None, float('inf')
|
|||
|
|
|
|||
|
|
def calculate_volume_delaunay(self, vertices, base_elevation=None, min_angle=10.0, use_quality_filter=True):
|
|||
|
|
"""使用Delaunay三角剖分计算体积"""
|
|||
|
|
if len(vertices) < 4:
|
|||
|
|
print("顶点数量不足,无法进行三角剖分")
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
points = np.array(vertices)
|
|||
|
|
|
|||
|
|
if base_elevation is None:
|
|||
|
|
base_elevation = np.min(points[:, 2])
|
|||
|
|
|
|||
|
|
print(f"Delaunay方法使用基准面高程: {base_elevation:.2f} 米")
|
|||
|
|
print(f"质量控制参数: 最小角度={min_angle}°, 质量过滤={'开启' if use_quality_filter else '关闭'}")
|
|||
|
|
|
|||
|
|
adjusted_points = points.copy()
|
|||
|
|
adjusted_points[:, 2] = np.maximum(points[:, 2] - base_elevation, 0)
|
|||
|
|
|
|||
|
|
print("正在进行Delaunay三角剖分...")
|
|||
|
|
tri = Delaunay(adjusted_points)
|
|||
|
|
|
|||
|
|
valid_simplices = []
|
|||
|
|
total_simplices = len(tri.simplices)
|
|||
|
|
|
|||
|
|
if use_quality_filter:
|
|||
|
|
print("正在进行三角形质量检查...")
|
|||
|
|
for simplex in tqdm(tri.simplices, desc="质量检查", unit="个"):
|
|||
|
|
p0 = adjusted_points[simplex[0]]
|
|||
|
|
p1 = adjusted_points[simplex[1]]
|
|||
|
|
p2 = adjusted_points[simplex[2]]
|
|||
|
|
p3 = adjusted_points[simplex[3]]
|
|||
|
|
|
|||
|
|
faces = [(p0, p1, p2), (p0, p1, p3), (p0, p2, p3), (p1, p2, p3)]
|
|||
|
|
valid_faces = 0
|
|||
|
|
|
|||
|
|
for face in faces:
|
|||
|
|
if self.is_valid_triangle(face[0], face[1], face[2], min_angle):
|
|||
|
|
valid_faces += 1
|
|||
|
|
|
|||
|
|
if valid_faces >= 3:
|
|||
|
|
valid_simplices.append(simplex)
|
|||
|
|
|
|||
|
|
print(f"质量过滤: {len(valid_simplices)}/{total_simplices} 个四面体通过质量检查")
|
|||
|
|
else:
|
|||
|
|
valid_simplices = tri.simplices
|
|||
|
|
|
|||
|
|
total_volume = 0.0
|
|||
|
|
valid_volume_count = 0
|
|||
|
|
|
|||
|
|
print(f"计算 {len(valid_simplices)} 个四面体的体积...")
|
|||
|
|
|
|||
|
|
for simplex in tqdm(valid_simplices, desc="计算四面体体积", unit="个"):
|
|||
|
|
p0 = adjusted_points[simplex[0]]
|
|||
|
|
p1 = adjusted_points[simplex[1]]
|
|||
|
|
p2 = adjusted_points[simplex[2]]
|
|||
|
|
p3 = adjusted_points[simplex[3]]
|
|||
|
|
|
|||
|
|
v1 = p1 - p0
|
|||
|
|
v2 = p2 - p0
|
|||
|
|
v3 = p3 - p0
|
|||
|
|
|
|||
|
|
det = np.linalg.det(np.array([v1, v2, v3]))
|
|||
|
|
volume = abs(det) / 6.0
|
|||
|
|
|
|||
|
|
if volume > 1e-12:
|
|||
|
|
total_volume += volume
|
|||
|
|
valid_volume_count += 1
|
|||
|
|
|
|||
|
|
print(f"有效体积计算: {valid_volume_count}/{len(valid_simplices)} 个四面体")
|
|||
|
|
|
|||
|
|
return total_volume
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Delaunay三角剖分计算体积失败: {e}")
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_and_filter_vertices(self, tileset_path):
|
|||
|
|
"""加载并过滤指定范围内的顶点"""
|
|||
|
|
print(f"开始处理tileset: {tileset_path}")
|
|||
|
|
|
|||
|
|
# 解析tileset获取所有B3DM文件
|
|||
|
|
b3dm_files = self.parse_tileset_json(tileset_path)
|
|||
|
|
print(f"找到 {len(b3dm_files)} 个B3DM文件")
|
|||
|
|
|
|||
|
|
if not b3dm_files:
|
|||
|
|
print("未找到任何B3DM文件")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 处理每个B3DM文件
|
|||
|
|
processed_count = 0
|
|||
|
|
total_vertices = 0
|
|||
|
|
filtered_count = 0
|
|||
|
|
|
|||
|
|
for i, (b3dm_file, transform_matrix) in enumerate(tqdm(b3dm_files, desc="处理B3DM文件", unit="文件")):
|
|||
|
|
# print(f"处理文件 {i+1}/{len(b3dm_files)}: {os.path.basename(b3dm_file)}")
|
|||
|
|
|
|||
|
|
vertices = self.parse_b3dm_file(b3dm_file)
|
|||
|
|
if vertices:
|
|||
|
|
# 应用变换矩阵
|
|||
|
|
if transform_matrix:
|
|||
|
|
vertices = self.apply_transform_to_vertices(vertices, transform_matrix)
|
|||
|
|
|
|||
|
|
# 过滤范围内的顶点
|
|||
|
|
for vertex in vertices:
|
|||
|
|
total_vertices += 1
|
|||
|
|
if self.is_point_in_bounds(vertex):
|
|||
|
|
self.filtered_vertices.append(vertex)
|
|||
|
|
filtered_count += 1
|
|||
|
|
|
|||
|
|
self.all_vertices.extend(vertices)
|
|||
|
|
processed_count += 1
|
|||
|
|
# tqdm.write(f" {os.path.basename(b3dm_file)}: 提取到 {len(vertices)} 个顶点")
|
|||
|
|
|
|||
|
|
print(f"\n成功处理 {processed_count}/{len(b3dm_files)} 个文件")
|
|||
|
|
print(f"总计提取 {total_vertices} 个顶点")
|
|||
|
|
print(f"范围内顶点 {filtered_count} 个")
|
|||
|
|
|
|||
|
|
return len(self.filtered_vertices) > 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
def calculate_volume(self, tileset_path, base_elevation=None, min_angle=10.0, use_quality_filter=True):
|
|||
|
|
"""计算指定范围内三维模型的体积
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
tileset_path: tileset.json文件路径
|
|||
|
|
base_elevation: 基准面高程
|
|||
|
|
min_angle: 最小角度约束(度)
|
|||
|
|
use_quality_filter: 是否启用质量过滤
|
|||
|
|
"""
|
|||
|
|
if not self.load_and_filter_vertices(tileset_path):
|
|||
|
|
print("未找到范围内的顶点数据")
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
print(f"\n开始计算体积,使用Delaunay三角剖分方法")
|
|||
|
|
print(f"参与计算的顶点数: {len(self.filtered_vertices)}")
|
|||
|
|
|
|||
|
|
points = np.array(self.filtered_vertices)
|
|||
|
|
if base_elevation is None:
|
|||
|
|
base_elevation = np.min(points[:, 2])
|
|||
|
|
|
|||
|
|
print(f"统一基准面高程: {base_elevation:.2f} 米")
|
|||
|
|
|
|||
|
|
volume = self.calculate_volume_delaunay(self.filtered_vertices, base_elevation, min_angle, use_quality_filter)
|
|||
|
|
|
|||
|
|
print(f"\n计算结果:")
|
|||
|
|
print(f"体积: {volume:.6f} 立方米")
|
|||
|
|
print(f"体积: {volume/1000000:.6f} 立方千米")
|
|||
|
|
|
|||
|
|
return volume
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
if len(sys.argv) < 3:
|
|||
|
|
print("用法: python volume_calculator.py <lboundary.json路径> <tileset.json路径> [基准面高程] [最小角度] [质量过滤]")
|
|||
|
|
print("基准面高程: 基准面高程(米),默认使用最低点")
|
|||
|
|
print("最小角度: 三角形最小角度约束(度),默认10.0")
|
|||
|
|
print("质量过滤: 是否启用质量过滤(true/false),默认true")
|
|||
|
|
print("示例: python volume_calculator.py boundary.json tileset.json 100.0 15.0 true")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
location_file = sys.argv[1]
|
|||
|
|
tileset_path = sys.argv[2]
|
|||
|
|
|
|||
|
|
# 解析可选参数
|
|||
|
|
base_elevation = None
|
|||
|
|
min_angle = 10.0
|
|||
|
|
use_quality_filter = True
|
|||
|
|
|
|||
|
|
if len(sys.argv) > 3:
|
|||
|
|
try:
|
|||
|
|
base_elevation = float(sys.argv[3])
|
|||
|
|
except ValueError:
|
|||
|
|
print("错误:基准面高程必须是数字")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if len(sys.argv) > 4:
|
|||
|
|
try:
|
|||
|
|
min_angle = float(sys.argv[4])
|
|||
|
|
except ValueError:
|
|||
|
|
print("错误:最小角度必须是数字")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if len(sys.argv) > 5:
|
|||
|
|
use_quality_filter = sys.argv[5].lower() in ['true', '1', 'yes', 'on']
|
|||
|
|
|
|||
|
|
if not os.path.exists(location_file):
|
|||
|
|
print(f"错误: 位置文件不存在 {location_file}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if not os.path.exists(tileset_path):
|
|||
|
|
print(f"错误: tileset文件不存在 {tileset_path}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
calculator = VolumeCalculator(location_file)
|
|||
|
|
volume = calculator.calculate_volume(tileset_path, base_elevation, min_angle, use_quality_filter)
|
|||
|
|
|
|||
|
|
if volume > 0:
|
|||
|
|
print("\n体积计算完成!")
|
|||
|
|
else:
|
|||
|
|
print("\n体积计算失败!")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|