坡度坡向tif生成

This commit is contained in:
liyubo 2026-01-29 11:51:20 +08:00
parent c5eeb87488
commit 9a09c1e1cf
8 changed files with 1931 additions and 326 deletions

View File

@ -145,11 +145,7 @@ class MinIO3DTilesManager:
visited = set() visited = set()
# 下载入口文件 # 下载入口文件
entry_local_path = self.get_local_path( entry_local_path = self.get_local_path(entry_bucket, entry_path, save_dir)
entry_bucket, entry_path,
entry_bucket, entry_dir,
save_dir
)
success, result = self.download_file(entry_bucket, entry_path, entry_local_path) success, result = self.download_file(entry_bucket, entry_path, entry_local_path)
if not success: if not success:
@ -352,39 +348,23 @@ class MinIO3DTilesManager:
except Exception as e: except Exception as e:
return None return None
def get_local_path(self, bucket_name, object_name, base_bucket, base_object, save_dir): def get_local_path(self, bucket_name, object_name, save_dir):
"""生成保持目录结构的本地路径""" """生成保持目录结构的本地路径"""
clean_bucket = self.clean_filename(bucket_name) clean_bucket = self.clean_filename(bucket_name)
bucket_dir = clean_bucket
if bucket_name == base_bucket and base_object: path_parts = object_name.split('/')
base_dir = os.path.dirname(base_object) cleaned_parts = []
for part in path_parts:
cleaned_part = self.clean_filename(part)
if cleaned_part:
cleaned_parts.append(cleaned_part)
if base_dir: if cleaned_parts:
if object_name.startswith(base_dir): cleaned_relative = '/'.join(cleaned_parts)
relative_path = object_name[len(base_dir):].lstrip('/\\') local_path = os.path.join(save_dir, clean_bucket, cleaned_relative)
else:
relative_path = object_name
else:
relative_path = object_name
else: else:
relative_path = object_name local_path = os.path.join(save_dir, clean_bucket)
if relative_path:
path_parts = relative_path.split('/')
cleaned_parts = []
for part in path_parts:
cleaned_part = self.clean_filename(part)
if cleaned_part:
cleaned_parts.append(cleaned_part)
if cleaned_parts:
cleaned_relative = '/'.join(cleaned_parts)
local_path = os.path.join(save_dir, bucket_dir, cleaned_relative)
else:
local_path = os.path.join(save_dir, bucket_dir)
else:
local_path = os.path.join(save_dir, bucket_dir)
return os.path.normpath(local_path) return os.path.normpath(local_path)
@ -437,11 +417,7 @@ class MinIO3DTilesManager:
print(f"下载文件:{file_id}") print(f"下载文件:{file_id}")
visited.add(file_id) visited.add(file_id)
local_path = self.get_local_path( local_path = self.get_local_path(file_bucket, file_path, save_dir)
file_bucket, file_path,
base_bucket, base_dir,
save_dir
)
self.download_file(file_bucket, file_path, local_path) self.download_file(file_bucket, file_path, local_path)

View File

@ -778,7 +778,319 @@ def parse_tileset(tileset_path, region_coords=None, enable_enhancement=True, deb
return [] return []
# ========== DEM生成函数 ========== # ========== DEM生成函数 ==========
def points_to_dem(points, output_dem_path, pixel_size=None, quality='medium'): def points_to_dem(points, output_dem_path, pixel_size=None, quality='high', min_resolution=10):
"""
将点云转换为DEM确保足够的网格分辨率用于坡度坡向计算
:param points: 点云数据 [(lon, lat, height), ...]
:param output_dem_path: 输出DEM文件路径
:param pixel_size: 像素大小如果为None则自动计算
:param quality: 质量等级 'low'|'medium'|'high'|'terrain'地形分析专用
:param min_resolution: 最小网格分辨率像素数用于保证坡度计算
"""
import time
import os
import numpy as np
from osgeo import gdal, osr
from scipy.interpolate import griddata
import warnings
if len(points) == 0:
raise ValueError("无点云数据无法生成DEM")
start_time = time.time()
# 转换为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()
lon_range = max_lon - min_lon
lat_range = max_lat - min_lat
print(f"[DEM生成] 点云范围:")
print(f" 经度: {min_lon:.6f}° ~ {max_lon:.6f}° (范围: {lon_range:.6f}°)")
print(f" 纬度: {min_lat:.6f}° ~ {max_lat:.6f}° (范围: {lat_range:.6f}°)")
print(f" 高程: {heights.min():.2f}m ~ {heights.max():.2f}m")
print(f" 点数: {len(points):,}")
# 自动确定像素大小(优化版本)
if pixel_size is None:
# 根据数据量自动确定
if quality == 'terrain': # 地形分析专用
# 确保足够的网格分辨率用于坡度计算
target_pixels = max(min_resolution, int(np.sqrt(len(points)) / 2))
# 根据数据范围计算像素大小
lon_pixel = lon_range / target_pixels
lat_pixel = lat_range / target_pixels
# 取较小的像素大小以保证分辨率
pixel_size = min(lon_pixel, lat_pixel)
# 设置像素大小范围限制
pixel_size = max(pixel_size, 0.000001) # 最小约0.1米
pixel_size = min(pixel_size, 0.0005) # 最大约55米
elif quality == 'high':
pixel_size = max(0.00001, lon_range / 100) # 至少1.1米最多100像素
elif quality == 'medium':
pixel_size = max(0.00002, lon_range / 50) # 至少2.2米最多50像素
else: # low
pixel_size = max(0.00005, lon_range / 20) # 至少5.6米最多20像素
# 计算网格尺寸
width = max(10, int(lon_range / pixel_size) + 1)
height = max(10, int(lat_range / pixel_size) + 1)
# 确保网格大小符合最小分辨率要求
if width * height < min_resolution * min_resolution:
print(f"[DEM生成] 警告:网格分辨率不足 ({width}x{height}),自动调整...")
# 重新计算像素大小以满足最小分辨率
target_cells = min_resolution * min_resolution
target_pixel_size = np.sqrt(lon_range * lat_range / target_cells)
pixel_size = max(0.000001, min(pixel_size, target_pixel_size))
width = max(min_resolution, int(lon_range / pixel_size) + 1)
height = max(min_resolution, int(lat_range / pixel_size) + 1)
# 扩展数据范围以增加边缘像素(有助于坡度计算)
expand_factor = 1.1 # 扩展10%
min_lon_exp = min_lon - lon_range * (expand_factor - 1) / 2
max_lon_exp = max_lon + lon_range * (expand_factor - 1) / 2
min_lat_exp = min_lat - lat_range * (expand_factor - 1) / 2
max_lat_exp = max_lat + lat_range * (expand_factor - 1) / 2
print(f"[DEM生成] 网格设置:")
print(f" 像素大小: {pixel_size:.6f}° (~{pixel_size*111320:.1f}米)")
print(f" 网格尺寸: {width} × {height}")
print(f" 总像素数: {width * height:,}")
print(f" 点云密度: {len(points)/(width*height):.2f} 点/像素")
# 创建网格
x_grid = np.linspace(min_lon_exp, max_lon_exp, width)
y_grid = np.linspace(max_lat_exp, min_lat_exp, height) # 纬度从上到下
xi, yi = np.meshgrid(x_grid, y_grid)
# 插值 - 优化版本
print("[DEM生成] 开始插值计算...")
# 检查数据分布
grid_points = np.column_stack([xi.flatten(), yi.flatten()])
data_points = np.column_stack([lons, lats])
# 使用KD树加速最近邻查询
try:
from scipy.spatial import cKDTree
tree = cKDTree(data_points)
dists, _ = tree.query(grid_points, k=1)
max_dist = np.max(dists)
print(f"[DEM生成] 最大最近邻距离: {max_dist*111320:.1f}")
except:
pass
# 尝试IDW插值适用于地形
try:
if len(points) > 100:
print("[DEM生成] 使用IDW插值...")
zi = idw_interpolation(lons, lats, heights, xi, yi, power=2)
else:
# 数据太少,使用线性+最近邻
zi = griddata((lons, lats), heights, (xi, yi), method='linear', fill_value=np.nan)
except Exception as e:
print(f"[DEM生成] IDW插值失败: {e},使用线性插值")
zi = griddata((lons, lats), heights, (xi, yi), method='linear', fill_value=np.nan)
# 处理空白区域
nan_mask = np.isnan(zi)
if np.any(nan_mask):
nan_count = np.sum(nan_mask)
nan_percent = nan_count / (width * height) * 100
print(f"[DEM生成] 插值空白: {nan_count:,} 像素 ({nan_percent:.1f}%)")
if nan_percent < 50: # 空白区域少于50%
# 使用最近邻填充空白
zi_nn = griddata((lons, lats), heights, (xi, yi), method='nearest')
zi[nan_mask] = zi_nn[nan_mask]
else:
print("[DEM生成] 警告:空白区域过多,使用最近邻插值")
zi = griddata((lons, lats), heights, (xi, yi), method='nearest')
# 平滑处理(可选,有助于坡度计算)
if quality in ['terrain', 'high']:
try:
from scipy.ndimage import gaussian_filter
sigma = 0.5 # 高斯滤波参数
zi = gaussian_filter(zi, sigma=sigma, mode='nearest')
print(f"[DEM生成] 应用高斯平滑 (sigma={sigma})")
except:
pass
# 创建GeoTIFF
print("[DEM生成] 创建GeoTIFF文件...")
driver = gdal.GetDriverByName("GTiff")
# 压缩选项
if quality in ['terrain', 'high']:
options = ["COMPRESS=DEFLATE", "PREDICTOR=3", "ZLEVEL=6", "TILED=YES", "BLOCKXSIZE=256", "BLOCKYSIZE=256"]
else:
options = ["COMPRESS=LZW", "TILED=YES"]
dem_ds = driver.Create(output_dem_path, width, height, 1, gdal.GDT_Float32, options)
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_exp, pixel_size, 0,
max_lat_exp, 0, -pixel_size
]
dem_ds.SetGeoTransform(geotransform)
# 写入数据
band = dem_ds.GetRasterBand(1)
band.WriteArray(zi)
band.SetNoDataValue(-9999.0)
band.SetDescription("Elevation (meters)")
band.SetUnitType("meters")
# 计算统计信息(处理可能的统计问题)
print("[DEM生成] 计算统计信息...")
band.FlushCache()
try:
band.ComputeStatistics(False)
print("[DEM生成] 统计信息计算完成")
except RuntimeError as e:
print(f"[DEM生成] 警告:统计信息计算失败: {e}")
# 手动计算统计信息
valid_mask = zi != -9999.0
if np.any(valid_mask):
valid_data = zi[valid_mask]
stats = [
float(np.min(valid_data)),
float(np.max(valid_data)),
float(np.mean(valid_data)),
float(np.std(valid_data))
]
band.SetStatistics(*stats)
print(f"[DEM生成] 手动设置统计信息:")
print(f" 最小值: {stats[0]:.2f}m")
print(f" 最大值: {stats[1]:.2f}m")
print(f" 平均值: {stats[2]:.2f}m")
print(f" 标准差: {stats[3]:.2f}m")
# 构建金字塔
if quality in ['terrain', 'high']:
print("[DEM生成] 构建金字塔...")
gdal.SetConfigOption('COMPRESS_OVERVIEW', 'DEFLATE')
dem_ds.BuildOverviews("AVERAGE", [2, 4, 8, 16])
dem_ds = None # 关闭文件
# 验证结果
elapsed_time = time.time() - start_time
file_size_mb = os.path.getsize(output_dem_path) / (1024 * 1024) if os.path.exists(output_dem_path) else 0
# 重新打开验证
try:
ds = gdal.Open(output_dem_path, gdal.GA_ReadOnly)
if ds:
band = ds.GetRasterBand(1)
actual_width = ds.RasterXSize
actual_height = ds.RasterYSize
print(f"[DEM生成] 验证结果:")
print(f" 实际尺寸: {actual_width} × {actual_height}")
print(f" 文件大小: {file_size_mb:.2f} MB")
print(f" 处理时间: {elapsed_time:.1f}")
# 检查是否适合坡度计算
if actual_width >= 10 and actual_height >= 10:
print(f"[DEM生成] ✓ DEM分辨率适合坡度坡向计算")
else:
print(f"[DEM生成] ⚠ DEM分辨率较低坡度计算可能不准确")
ds = None
except:
print(f"[DEM生成] 完成! 文件: {output_dem_path}")
return output_dem_path
def idw_interpolation(x, y, z, xi, yi, power=2, radius=None):
"""
反距离权重插值
"""
import numpy as np
from scipy.spatial import cKDTree
x = np.asarray(x)
y = np.asarray(y)
z = np.asarray(z)
xi = np.asarray(xi)
yi = np.asarray(yi)
# 展平网格
xi_flat = xi.flatten()
yi_flat = yi.flatten()
# 创建KD树
tree = cKDTree(np.column_stack([x, y]))
# 设置搜索半径
if radius is None:
# 自动确定半径平均点间距的3倍
from scipy.spatial.distance import pdist
if len(x) > 1:
distances = pdist(np.column_stack([x, y]))
radius = np.mean(distances) * 3
else:
radius = 0.01 # 默认值
# 查询最近邻点
if len(z) > 50: # 数据较多时,限制搜索点数
k = min(12, len(z))
dists, idxs = tree.query(np.column_stack([xi_flat, yi_flat]), k=k, distance_upper_bound=radius)
else:
dists, idxs = tree.query(np.column_stack([xi_flat, yi_flat]), k=len(z))
# IDW插值
zi_flat = np.zeros(len(xi_flat))
for i in range(len(xi_flat)):
valid_mask = idxs[i] < len(z)
if np.any(valid_mask):
valid_dists = dists[i][valid_mask]
valid_idx = idxs[i][valid_mask]
# 避免除零
valid_dists = np.maximum(valid_dists, 1e-10)
# 计算权重
weights = 1.0 / (valid_dists ** power)
weights = weights / np.sum(weights)
# 加权平均
zi_flat[i] = np.sum(z[valid_idx] * weights)
else:
zi_flat[i] = np.nan
# 恢复形状
zi = zi_flat.reshape(xi.shape)
return zi
def points_to_dem1(points, output_dem_path, pixel_size=None, quality='medium'):
""" """
将点云转换为DEM 将点云转换为DEM
:param quality: 质量等级 'low'|'medium'|'high' :param quality: 质量等级 'low'|'medium'|'high'

View File

@ -14,33 +14,34 @@ earthwork_bp = Blueprint("earthwork", url_prefix="")
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 全局变量
_calculator_point_cloud = None
_calculator_3d_tiles = None
_data_source_3d_tiles = None
# 初始化函数 # 初始化函数
async def init_app(): def init_app(url, type = "3dtiles"):
"""初始化应用""" """初始化应用"""
global _data_source_3d_tiles, _calculator_3d_tiles, _calculator_point_cloud data_source = None
calculator_3d_tiles = None
calculator_point_cloud = None
try: try:
# 配置数据源
tileset_path = "./data/3dtiles/tileset.json"
# 初始化数据源 # 初始化数据源
_data_source_3d_tiles = TilesetDataSource(tileset_path) data_source = TilesetDataSource(url)
await _data_source_3d_tiles.initialize() data_source.dowload_map_data(url)
# 初始化计算器-3dTiles if type == "3dtiles" :
_calculator_3d_tiles = EarthworkCalculator3dTiles(_data_source_3d_tiles) # 初始化计算器-3dTiles
calculator_3d_tiles = EarthworkCalculator3dTiles(data_source)
# 初始化计算器-点云 elif type == "pointcloud" :
point_cloud_path = "./data/pointCloud/simulated_points.laz" # 初始化计算器-点云
_calculator_point_cloud = EarthworkCalculatorPointCloud(point_cloud_path) calculator_point_cloud = EarthworkCalculatorPointCloud(data_source.tileset_path)
else :
logger.info(f"不支持的3d地图数据格式:{type}")
raise
logger.info("土方量计算器初始化完成") logger.info("土方量计算器初始化完成")
return {
"data_source":data_source,
"calculator_3d_tiles":calculator_3d_tiles,
"calculator_point_cloud":calculator_point_cloud
}
except ImportError as e: except ImportError as e:
logger.error(f"依赖库缺失: {str(e)}") logger.error(f"依赖库缺失: {str(e)}")
raise raise
@ -56,12 +57,26 @@ async def calc_earthwork(request: Request):
请求参数示例: 请求参数示例:
{ {
"polygonCoords": [[120.1, 30.1], [120.2, 30.1], [120.2, 30.2], [120.1, 30.2]], "polygonCoords": [
"designElevation": 50.0, [
115.70440468338526,
30.77363140345639
],
[
115.70443054007985,
30.773510462589584
],
[
115.70459702429197,
30.77360789911405
]
],
"designElevation": 100,
"algorithm": "tin", "algorithm": "tin",
"resolution": 1.0, "resolution": 1,
"crs": "EPSG:4326", "crs": "EPSG:4326",
"interpolationMethod": "linear" "interpolationMethod": "linear",
"url": "http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/hbgldk/yzk/20260113/3D/terra_b3dms/tileset.json"
} }
""" """
try: try:
@ -73,11 +88,14 @@ async def calc_earthwork(request: Request):
# 2. 提取参数 # 2. 提取参数
polygon_coords = data.get("polygonCoords") polygon_coords = data.get("polygonCoords")
design_elevation = data.get("designElevation") design_elevation = data.get("designElevation")
url = data.get("url")
if not polygon_coords: if not polygon_coords:
return _error_response("多边形坐标不能为空", 400) return _error_response("多边形坐标不能为空", 400)
if design_elevation is None: if design_elevation is None:
return _error_response("设计高程不能为空", 400) return _error_response("设计高程不能为空", 400)
if url is None:
return _error_response("地图不能为空", 400)
# 3. 可选参数 # 3. 可选参数
algorithm = data.get("algorithm", "tin") algorithm = data.get("algorithm", "tin")
@ -102,13 +120,13 @@ async def calc_earthwork(request: Request):
return _error_response("分辨率必须在0-100米之间", 400) return _error_response("分辨率必须在0-100米之间", 400)
# 5. 确保计算器已初始化 # 5. 确保计算器已初始化
if _calculator_3d_tiles is None: app_info = init_app(url)
await init_app() calculator_3d_tiles = app_info.get("calculator_3d_tiles")
# 6. 执行计算 # 6. 执行计算
algorithm_type = AlgorithmType(algorithm) algorithm_type = AlgorithmType(algorithm)
result = await _calculator_3d_tiles.calculate( result = await calculator_3d_tiles.calculate(
polygon_coords=polygon_coords, polygon_coords=polygon_coords,
design_elevation=design_elevation, design_elevation=design_elevation,
algorithm=algorithm_type, algorithm=algorithm_type,
@ -144,6 +162,10 @@ async def validate_earthwork(request: Request):
if not polygon_coords: if not polygon_coords:
return _error_response("多边形坐标不能为空", 400) return _error_response("多边形坐标不能为空", 400)
url = data.get("url")
if url is None:
return _error_response("地图不能为空", 400)
# 3. 参数验证 # 3. 参数验证
if len(polygon_coords) < 3: if len(polygon_coords) < 3:
return _error_response("多边形至少需要3个点", 400) return _error_response("多边形至少需要3个点", 400)
@ -153,11 +175,11 @@ async def validate_earthwork(request: Request):
polygon_coords.append(polygon_coords[0]) polygon_coords.append(polygon_coords[0])
# 4. 确保计算器已初始化 # 4. 确保计算器已初始化
if _calculator_3d_tiles is None: app_info = init_app(url)
await init_app() calculator_3d_tiles = app_info.get("calculator_3d_tiles")
# 5. 执行验证 # 5. 执行验证
validation_result = await _calculator_3d_tiles.validate(polygon_coords) validation_result = await calculator_3d_tiles.validate(polygon_coords)
# 6. 返回结果 # 6. 返回结果
return _success_response(validation_result) return _success_response(validation_result)
@ -242,9 +264,7 @@ async def batch_calc_earthwork(request: Request):
if len(calculations) > 100: if len(calculations) > 100:
return _error_response("批量计算数量超过限制最多100个", 400) return _error_response("批量计算数量超过限制最多100个", 400)
# 2. 确保计算器已初始化
if _calculator_3d_tiles is None:
await init_app()
# 3. 执行批量计算 # 3. 执行批量计算
results = [] results = []
@ -255,8 +275,9 @@ async def batch_calc_earthwork(request: Request):
# 提取参数 # 提取参数
polygon_coords = calc_data.get("polygonCoords") polygon_coords = calc_data.get("polygonCoords")
design_elevation = calc_data.get("designElevation") design_elevation = calc_data.get("designElevation")
url = calc_data.get("url")
if not polygon_coords or design_elevation is None: if not polygon_coords or design_elevation is None or url is None:
errors.append({ errors.append({
"index": i, "index": i,
"error": "缺少必要参数" "error": "缺少必要参数"
@ -281,10 +302,14 @@ async def batch_calc_earthwork(request: Request):
crs = calc_data.get("crs", "EPSG:4326") crs = calc_data.get("crs", "EPSG:4326")
interpolation_method = calc_data.get("interpolationMethod", "linear") interpolation_method = calc_data.get("interpolationMethod", "linear")
# 2. 确保计算器已初始化
app_info = init_app(url)
calculator_3d_tiles = app_info.get("calculator_3d_tiles")
# 执行计算 # 执行计算
algorithm_type = AlgorithmType(algorithm) algorithm_type = AlgorithmType(algorithm)
result = await _calculator_3d_tiles.calculate( result = await calculator_3d_tiles.calculate(
polygon_coords=polygon_coords, polygon_coords=polygon_coords,
design_elevation=design_elevation, design_elevation=design_elevation,
algorithm=algorithm_type, algorithm=algorithm_type,
@ -338,19 +363,22 @@ async def calc_earthwork_point_cloud(request: Request):
polygon_coords = data.get("polygonCoords") # 计算区域多边形坐标 polygon_coords = data.get("polygonCoords") # 计算区域多边形坐标
design_elev = data.get("designElevation") # 设计高程 design_elev = data.get("designElevation") # 设计高程
crs = data.get("crs", "EPSG:4326") # 坐标系默认WGS84 crs = data.get("crs", "EPSG:4326") # 坐标系默认WGS84
url = data.get("url")
if url is None:
return _error_response("地图不能为空", 400)
# 2. 确保计算器已初始化 # 2. 确保计算器已初始化
if _calculator_point_cloud is None: app_info = init_app(url)
await init_app() calculator_point_cloud = app_info.get("calculator_point_cloud")
result = _calculator_point_cloud.calculate_earthwork(polygon_coords=polygon_coords, design_elev=design_elev, crs=crs) result = calculator_point_cloud.calculate_earthwork(polygon_coords=polygon_coords, design_elev=design_elev, crs=crs)
# 3. 处理结果 # 3. 处理结果
if not result["success"]: if not result["success"]:
return _error_response(result["error"], 400) return _error_response(result["error"], 400)
# 4. 格式化结果 # 4. 格式化结果
formatted_result = _calculator_point_cloud.format_result(result) formatted_result = calculator_point_cloud.format_result(result)
# 5. 返回成功响应 # 5. 返回成功响应
return _success_response(formatted_result) return _success_response(formatted_result)

View File

@ -8,6 +8,7 @@ import logging
from enum import Enum from enum import Enum
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import math import math
from pyproj import Geod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,13 +38,13 @@ class EarthworkResult3dTiles:
"""转换为字典""" """转换为字典"""
return { return {
"volume": { "volume": {
"cut": round(self.cut_volume, 3), "cut": round(self.cut_volume, 8),
"fill": round(self.fill_volume, 3), "fill": round(self.fill_volume, 8),
"net": round(self.net_volume, 3), "net": round(self.net_volume, 8),
"unit": "" "unit": ""
}, },
"area": { "area": {
"value": round(self.area, 3), "value": round(self.area, 8),
"unit": "" "unit": ""
}, },
"elevation": { "elevation": {
@ -94,67 +95,375 @@ class TerrainDataSource(ABC):
pass pass
class GeometryUtils: class GeometryUtils:
"""几何计算工具类""" """地理空间几何计算工具类(支持经纬度坐标)"""
@staticmethod def __init__(self, source_crs: str = "EPSG:4326", target_crs: str = "EPSG:3857"):
def calculate_polygon_area(polygon_coords: List[List[float]]) -> float: """
"""计算多边形面积(平面面积)""" 初始化
polygon_np = np.array(polygon_coords) Args:
x = polygon_np[:, 0] source_crs: 源坐标系通常是EPSG:4326
y = polygon_np[:, 1] target_crs: 目标投影坐标系用于平面计算
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) """
self.source_crs = source_crs
self.target_crs = target_crs
self.geod = Geod(ellps="WGS84")
@staticmethod # 创建坐标转换器
def is_point_in_polygon(point: np.ndarray, polygon: np.ndarray) -> bool: self.transformer_to_proj = Transformer.from_crs(
"""判断点是否在多边形内""" source_crs, target_crs, always_xy=True
from matplotlib.path import Path )
path = Path(polygon) self.transformer_to_geo = Transformer.from_crs(
return path.contains_point(point) target_crs, source_crs, always_xy=True
)
def calculate_polygon_area(self, polygon_coords: List[List[float]]) -> float:
"""
计算多边形的地面实际面积平方米
Args:
polygon_coords: 经纬度坐标列表 [[lon1, lat1], ...]
Returns:
面积平方米
"""
if len(polygon_coords) < 3:
return 0.0
# 确保多边形闭合
closed_coords = self._ensure_closed_polygon(polygon_coords)
# 提取经纬度
lons = [coord[0] for coord in closed_coords]
lats = [coord[1] for coord in closed_coords]
# 使用测地线计算面积
area, _ = self.geod.polygon_area_perimeter(lons, lats)
return abs(area)
def is_point_in_polygon(self, point: Tuple[float, float],
polygon_coords: List[List[float]],
use_spherical: bool = True) -> bool:
"""
判断点是否在多边形内支持地球表面判断
Args:
point: 点坐标 (lon, lat)
polygon_coords: 多边形顶点坐标
use_spherical: 是否使用球面算法
Returns:
是否在多边形内
"""
if len(polygon_coords) < 3:
return False
if use_spherical:
# 方法1球面射线法更准确
return self._is_point_in_polygon_spherical(point, polygon_coords)
else:
# 方法2投影到平面后判断更快
return self._is_point_in_polygon_planar(point, polygon_coords)
def calculate_triangle_area(self, points: np.ndarray) -> float:
"""
计算三角形的地面面积平方米
Args:
points: 3×2数组每行是[lon, lat]
Returns:
三角形地面面积平方米
"""
if points.shape != (3, 2):
raise ValueError("需要3个点的坐标")
# 转换为球面坐标计算
lons = points[:, 0]
lats = points[:, 1]
# 使用球面三角形面积公式
R = 6378137.0 # WGS84地球半径
# 转换为弧度
lon_rad = np.radians(lons)
lat_rad = np.radians(lats)
# 计算球面三角形的面积
# 使用L'Huilier公式
a = self._spherical_distance(lon_rad[0], lat_rad[0], lon_rad[1], lat_rad[1])
b = self._spherical_distance(lon_rad[1], lat_rad[1], lon_rad[2], lat_rad[2])
c = self._spherical_distance(lon_rad[2], lat_rad[2], lon_rad[0], lat_rad[0])
@staticmethod
def calculate_triangle_area(points: np.ndarray) -> float:
"""计算三角形面积"""
a = np.linalg.norm(points[0] - points[1])
b = np.linalg.norm(points[1] - points[2])
c = np.linalg.norm(points[2] - points[0])
s = (a + b + c) / 2 s = (a + b + c) / 2
return np.sqrt(s * (s - a) * (s - b) * (s - c))
@staticmethod # 防止数值误差
def create_grid(polygon: np.ndarray, resolution: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: tan_e2 = np.tan(s/2) * np.tan((s-a)/2) * np.tan((s-b)/2) * np.tan((s-c)/2)
"""创建规则格网""" tan_e2 = max(tan_e2, 0) # 避免负值
x_min, y_min = polygon.min(axis=0)
x_max, y_max = polygon.max(axis=0)
# 扩展一个格网单元 if tan_e2 > 0:
x_min -= resolution E = 4 * np.arctan(np.sqrt(tan_e2))
x_max += resolution else:
y_min -= resolution E = 0
y_max += resolution
x_grid = np.arange(x_min, x_max + resolution, resolution) area = R * R * E
y_grid = np.arange(y_min, y_max + resolution, resolution) return area
xx, yy = np.meshgrid(x_grid, y_grid)
return xx, yy, x_grid, y_grid def create_grid(self, polygon_coords: List[List[float]],
resolution_m: float,
use_projection: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
创建规则格网地面距离为单位的网格
@staticmethod Args:
def interpolate_grid(xx: np.ndarray, yy: np.ndarray, polygon_coords: 多边形坐标
points: np.ndarray, method: str = 'linear') -> np.ndarray: resolution_m: 网格分辨率
"""格网插值""" use_projection: 是否使用投影坐标系
from scipy.interpolate import LinearNDInterpolator, CloughTocher2DInterpolator
grid_points = np.column_stack([xx.ravel(), yy.ravel()]) Returns:
xx, yy: 网格坐标
grid_coords_geo: 网格点的地理坐标
"""
if use_projection:
# 方法1投影到平面坐标系创建网格
return self._create_grid_projected(polygon_coords, resolution_m)
else:
# 方法2直接在经纬度上创建近似网格小区域可用
return self._create_grid_geographic(polygon_coords, resolution_m)
def interpolate_grid(self, xx: np.ndarray, yy: np.ndarray,
points: np.ndarray,
method: str = 'linear',
return_geo: bool = False) -> np.ndarray:
"""
格网插值
Args:
xx, yy: 网格坐标投影坐标系
points: 已知点每行是[lon, lat, elevation][x_proj, y_proj, elevation]
method: 插值方法 'linear' 'cubic'
return_geo: 是否返回地理坐标
Returns:
插值后的高程网格
"""
# 确保points是投影坐标
if points.shape[1] != 3:
raise ValueError("points应为3列: x, y, z")
# 如果输入是地理坐标,转换为投影坐标
if np.max(np.abs(points[:, 0])) > 180: # 粗略判断
# 已经是投影坐标
points_proj = points
else:
# 转换为投影坐标
x_proj, y_proj = self.transformer_to_proj.transform(
points[:, 0], points[:, 1]
)
points_proj = np.column_stack([x_proj, y_proj, points[:, 2]])
# 创建插值器
if method == 'linear': if method == 'linear':
interpolator = LinearNDInterpolator(points[:, :2], points[:, 2]) interpolator = LinearNDInterpolator(
points_proj[:, :2],
points_proj[:, 2],
fill_value=np.nan
)
elif method == 'cubic': elif method == 'cubic':
interpolator = CloughTocher2DInterpolator(points[:, :2], points[:, 2]) interpolator = CloughTocher2DInterpolator(
points_proj[:, :2],
points_proj[:, 2],
fill_value=np.nan
)
else: else:
raise ValueError(f"不支持的插值方法: {method}") raise ValueError(f"不支持的插值方法: {method}")
# 插值
grid_points = np.column_stack([xx.ravel(), yy.ravel()])
elevations = interpolator(grid_points) elevations = interpolator(grid_points)
return elevations.reshape(xx.shape) result = elevations.reshape(xx.shape)
if return_geo:
# 如果需要,将网格点转回地理坐标
lon_grid, lat_grid = self.transformer_to_geo.transform(
xx.ravel(), yy.ravel()
)
lon_grid = lon_grid.reshape(xx.shape)
lat_grid = lat_grid.reshape(xx.shape)
return result, lon_grid, lat_grid
return result
# ============ 私有方法 ============
def _ensure_closed_polygon(self, coords: List[List[float]]) -> List[List[float]]:
"""确保多边形闭合"""
if len(coords) >= 3:
# 使用 numpy 比较
if not np.array_equal(coords[0], coords[-1]):
return coords + [coords[0]]
return coords
def _spherical_distance(self, lon1_rad: float, lat1_rad: float,
lon2_rad: float, lat2_rad: float) -> float:
"""计算球面两点间角距离"""
dlon = lon2_rad - lon1_rad
dlat = lat2_rad - lat1_rad
a = np.sin(dlat/2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon/2)**2
return 2 * np.arcsin(np.sqrt(a))
def _is_point_in_polygon_spherical(self, point: Tuple[float, float],
polygon_coords: List[List[float]]) -> bool:
"""球面射线法判断点是否在多边形内"""
lon_p, lat_p = point
closed_polygon = self._ensure_closed_polygon(polygon_coords)
# 将多边形的边转换为球面大圆弧
crossings = 0
n = len(closed_polygon) - 1
for i in range(n):
lon1, lat1 = closed_polygon[i]
lon2, lat2 = closed_polygon[i + 1]
# 检查射线是否与边相交(近似算法)
# 简化:使用平面近似,对小区域足够准确
if ((lat1 > lat_p) != (lat2 > lat_p)) and \
(lon_p < (lon2 - lon1) * (lat_p - lat1) / (lat2 - lat1) + lon1):
crossings += 1
return crossings % 2 == 1
def _is_point_in_polygon_planar(self, point: Tuple[float, float],
polygon_coords: List[List[float]]) -> bool:
"""投影到平面后判断"""
# 转换为投影坐标
point_proj = np.array(self.transformer_to_proj.transform(point[0], point[1])).reshape(1, 2)
polygon_proj = np.array([
self.transformer_to_proj.transform(lon, lat)
for lon, lat in polygon_coords
])
# 使用平面方法判断
path = Path(polygon_proj)
return path.contains_point(point_proj[0])
def _create_grid_projected(self, polygon_coords: List[List[float]],
resolution_m: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""在投影坐标系中创建网格"""
# 将多边形转换为投影坐标
polygon_proj = []
for lon, lat in polygon_coords:
x, y = self.transformer_to_proj.transform(lon, lat)
polygon_proj.append([x, y])
polygon_proj = np.array(polygon_proj)
# 计算边界框
x_min, y_min = polygon_proj.min(axis=0)
x_max, y_max = polygon_proj.max(axis=0)
# 扩展半个网格
x_min -= resolution_m / 2
x_max += resolution_m / 2
y_min -= resolution_m / 2
y_max += resolution_m / 2
# 创建网格
x_grid = np.arange(x_min, x_max + resolution_m, resolution_m)
y_grid = np.arange(y_min, y_max + resolution_m, resolution_m)
xx, yy = np.meshgrid(x_grid, y_grid)
# 将网格点转回地理坐标
grid_coords_geo = []
for x, y in zip(xx.ravel(), yy.ravel()):
lon, lat = self.transformer_to_geo.transform(x, y)
grid_coords_geo.append([lon, lat])
grid_coords_geo = np.array(grid_coords_geo).reshape(xx.shape[0], xx.shape[1], 2)
return xx, yy, grid_coords_geo
def _create_grid_geographic(self, polygon_coords: List[List[float]],
resolution_m: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""在经纬度坐标系中创建近似网格"""
# 计算中心点
lons = [coord[0] for coord in polygon_coords]
lats = [coord[1] for coord in polygon_coords]
center_lon = np.mean(lons)
center_lat = np.mean(lats)
# 计算经纬度到米的换算系数
lat_rad = np.radians(center_lat)
meters_per_degree_lon = 111319.9 * np.cos(lat_rad)
meters_per_degree_lat = 111000.0
# 计算边界框(米)
x_min_m, x_max_m, y_min_m, y_max_m = 1e9, -1e9, 1e9, -1e9
for lon, lat in polygon_coords:
x_m = (lon - center_lon) * meters_per_degree_lon
y_m = (lat - center_lat) * meters_per_degree_lat
x_min_m = min(x_min_m, x_m)
x_max_m = max(x_max_m, x_m)
y_min_m = min(y_min_m, y_m)
y_max_m = max(y_max_m, y_m)
# 扩展半个网格
x_min_m -= resolution_m / 2
x_max_m += resolution_m / 2
y_min_m -= resolution_m / 2
y_max_m += resolution_m / 2
# 创建网格(米)
x_grid_m = np.arange(x_min_m, x_max_m + resolution_m, resolution_m)
y_grid_m = np.arange(y_min_m, y_max_m + resolution_m, resolution_m)
# 转换为经纬度
x_grid_lon = center_lon + x_grid_m / meters_per_degree_lon
y_grid_lat = center_lat + y_grid_m / meters_per_degree_lat
xx, yy = np.meshgrid(x_grid_lon, y_grid_lat)
# 网格坐标(经纬度)
grid_coords_geo = np.dstack([xx, yy])
return xx, yy, grid_coords_geo
def get_polygon_bounds(self, polygon_coords: List[List[float]],
in_meters: bool = False) -> dict:
"""
获取多边形边界信息
Args:
polygon_coords: 多边形坐标
in_meters: 是否返回米为单位
Returns:
边界信息字典
"""
lons = [coord[0] for coord in polygon_coords]
lats = [coord[1] for coord in polygon_coords]
bounds = {
'min_lon': min(lons),
'max_lon': max(lons),
'min_lat': min(lats),
'max_lat': max(lats),
'center_lon': (min(lons) + max(lons)) / 2,
'center_lat': (min(lats) + max(lats)) / 2
}
if in_meters:
# 计算实际尺寸(米)
center_lat = bounds['center_lat']
lat_rad = np.radians(center_lat)
meters_per_degree_lon = 111319.9 * np.cos(lat_rad)
meters_per_degree_lat = 111000.0
bounds['width_m'] = (bounds['max_lon'] - bounds['min_lon']) * meters_per_degree_lon
bounds['height_m'] = (bounds['max_lat'] - bounds['min_lat']) * meters_per_degree_lat
bounds['area_m2'] = self.calculate_polygon_area(polygon_coords)
return bounds
class EarthworkCalculator3dTiles: class EarthworkCalculator3dTiles:
"""土方量计算器""" """土方量计算器"""
@ -167,6 +476,7 @@ class EarthworkCalculator3dTiles:
data_source: 地形数据源 data_source: 地形数据源
""" """
self.data_source = data_source self.data_source = data_source
self.geometryUtils = GeometryUtils()
self._transformer_cache = {} self._transformer_cache = {}
async def calculate(self, async def calculate(self,
@ -183,7 +493,7 @@ class EarthworkCalculator3dTiles:
polygon_coords: 多边形坐标 polygon_coords: 多边形坐标
design_elevation: 设计高程 design_elevation: 设计高程
algorithm: 计算算法 algorithm: 计算算法
resolution: 格网分辨率 resolution: 格网分辨率()
target_crs: 目标坐标系 target_crs: 目标坐标系
interpolation_method: 插值方法 interpolation_method: 插值方法
@ -195,7 +505,7 @@ class EarthworkCalculator3dTiles:
points = await self.data_source.get_points_in_polygon(polygon_coords) points = await self.data_source.get_points_in_polygon(polygon_coords)
if points.size == 0: if points.size == 0:
raise ValueError("区域内没有找到高程数据") raise ValueError("区域内没有找到顶点数据")
# 2. 坐标转换 # 2. 坐标转换
points = await self._transform_coordinates(points, target_crs) points = await self._transform_coordinates(points, target_crs)
@ -250,10 +560,10 @@ class EarthworkCalculator3dTiles:
polygon_np = np.array(polygon_coords) polygon_np = np.array(polygon_coords)
# 创建格网 # 创建格网
xx, yy, x_grid, y_grid = GeometryUtils.create_grid(polygon_np, resolution) xx, yy, x_grid, y_grid = self.geometryUtils.create_grid(polygon_np, resolution)
# 插值 # 插值
natural_elevations = GeometryUtils.interpolate_grid(xx, yy, points, interpolation_method) natural_elevations = self.geometryUtils.interpolate_grid(xx, yy, points, interpolation_method)
# 初始化挖填量 # 初始化挖填量
cut_volume = 0.0 cut_volume = 0.0
@ -273,7 +583,7 @@ class EarthworkCalculator3dTiles:
# 检查格网中心点是否在多边形内 # 检查格网中心点是否在多边形内
cell_center = cell_corners.mean(axis=0) cell_center = cell_corners.mean(axis=0)
if not GeometryUtils.is_point_in_polygon(cell_center, polygon_np): if not self.geometryUtils.is_point_in_polygon(cell_center, polygon_np):
continue continue
# 获取格网四个角点的高程 # 获取格网四个角点的高程
@ -301,7 +611,7 @@ class EarthworkCalculator3dTiles:
cut_volume += abs(height_diff) * cell_area cut_volume += abs(height_diff) * cell_area
# 计算统计信息 # 计算统计信息
area = GeometryUtils.calculate_polygon_area(polygon_coords) area = self.geometryUtils.calculate_polygon_area(polygon_coords)
mask = ~np.isnan(natural_elevations) mask = ~np.isnan(natural_elevations)
valid_elevations = natural_elevations[mask] valid_elevations = natural_elevations[mask]
@ -342,14 +652,16 @@ class EarthworkCalculator3dTiles:
triangle_center = triangle_points.mean(axis=0)[:2] triangle_center = triangle_points.mean(axis=0)[:2]
# 检查三角形中心是否在多边形内 # 检查三角形中心是否在多边形内
if not GeometryUtils.is_point_in_polygon(triangle_center, polygon_np): if not self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np):
continue continue
# 计算三角形面积 # 计算三角形面积
area = GeometryUtils.calculate_triangle_area(triangle_points[:, :2]) area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
if math.isnan(area) :
continue
total_area += area total_area += area
# 计算平均高程(使用三个顶点的高程) # 计算平均高程(使用三个顶点的高程)
avg_elevation = triangle_points[:, 2].mean() avg_elevation = triangle_points[:, 2].mean()
# 计算挖填量 # 计算挖填量
@ -358,9 +670,13 @@ class EarthworkCalculator3dTiles:
fill_volume += height_diff * area fill_volume += height_diff * area
else: else:
cut_volume += abs(height_diff) * area cut_volume += abs(height_diff) * area
# if math.isnan(cut_volume) :
# print("cut_volume变为nan")
# if math.isnan(fill_volume) :
# print("fill_volume变为nan")
# 计算统计信息 # 计算统计信息
area = GeometryUtils.calculate_polygon_area(polygon_coords) area = self.geometryUtils.calculate_polygon_area(polygon_coords)
return EarthworkResult3dTiles( return EarthworkResult3dTiles(
cut_volume=cut_volume, cut_volume=cut_volume,
@ -397,11 +713,11 @@ class EarthworkCalculator3dTiles:
triangle_points = points[simplex] triangle_points = points[simplex]
triangle_center = triangle_points.mean(axis=0)[:2] triangle_center = triangle_points.mean(axis=0)[:2]
if not GeometryUtils.is_point_in_polygon(triangle_center, polygon_np): if not self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np):
continue continue
# 计算三角形面积 # 计算三角形面积
area = GeometryUtils.calculate_triangle_area(triangle_points[:, :2]) area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
total_area += area total_area += area
# 对于每个三角形,计算三棱柱体积 # 对于每个三角形,计算三棱柱体积
@ -418,7 +734,7 @@ class EarthworkCalculator3dTiles:
# 计算边的平均挖填高度 # 计算边的平均挖填高度
avg_height = (abs(height_i) + abs(height_j)) / 2 avg_height = (abs(height_i) + abs(height_j)) / 2
# 计算边的面积假设边宽度为resolution # 计算边的面积(假设边宽度为resolution)
edge_area = edge_length * resolution edge_area = edge_length * resolution
if height_i > 0 or height_j > 0: if height_i > 0 or height_j > 0:
@ -426,7 +742,7 @@ class EarthworkCalculator3dTiles:
else: else:
cut_volume += avg_height * edge_area cut_volume += avg_height * edge_area
area = GeometryUtils.calculate_polygon_area(polygon_coords) area = self.geometryUtils.calculate_polygon_area(polygon_coords)
return EarthworkResult3dTiles( return EarthworkResult3dTiles(
cut_volume=cut_volume, cut_volume=cut_volume,
@ -477,7 +793,7 @@ class EarthworkCalculator3dTiles:
validation_result = { validation_result = {
"polygon_valid": len(polygon_coords) >= 3, "polygon_valid": len(polygon_coords) >= 3,
"area": GeometryUtils.calculate_polygon_area(polygon_coords), "area": self.geometryUtils.calculate_polygon_area(polygon_coords),
"points_available": points.size > 0, "points_available": points.size > 0,
"points_count": points.shape[0] if points.size > 0 else 0, "points_count": points.shape[0] if points.size > 0 else 0,
"data_quality": "good" if points.shape[0] > 100 else "poor", "data_quality": "good" if points.shape[0] > 100 else "poor",

1062
b3dm/slope_aspect_tif.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -174,7 +174,7 @@ async def preload_3dtiles(request: Request):
# 创建并启动线程 # 创建并启动线程
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
thread1 = threading.Thread(target=TerrainCalculator.preload_3dtiles, args=(vector.url)) thread1 = threading.Thread(target=TerrainCalculator.preload_3dtiles, args=(vector.url,))
# 启动线程 # 启动线程
thread1.start() thread1.start()
url_prefix = extract_and_rebuild_url(vector.url) url_prefix = extract_and_rebuild_url(vector.url)

View File

@ -3,58 +3,39 @@ from typing import List, Tuple, Dict, Any
import logging import logging
import os import os
import uuid import uuid
from b3dm.data_3dtiles_manager import MinIO3DTilesManager
import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem
import b3dm.slope_aspect_img as slope_aspect_img import b3dm.slope_aspect_img as slope_aspect_img
import b3dm.slope_aspect_tif as slope_aspect_tif import b3dm.slope_aspect_tif as slope_aspect_tif
from b3dm.tileset_data_source import TilesetDataSource
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ENDPOINT_URL = "222.212.85.86:9000" _data_source = None
ACCESS_KEY = "WuRenJi"
SECRET_KEY = "WRJ@2024"
class TerrainCalculator: class TerrainCalculator:
"""地形坡度和坡向计算器""" """地形坡度和坡向计算器"""
def preload_3dtiles(url) : def preload_3dtiles(url: str) :
# 下载3dtiles地图数据 # 下载3dtiles地图数据
manager = MinIO3DTilesManager( _data_source = TilesetDataSource(url)
endpoint_url=ENDPOINT_URL, _data_source.dowload_map_data(url)
access_key=ACCESS_KEY,
secret_key=SECRET_KEY, if not _data_source.tileset_path :
secure=False
)
script_dir = os.path.dirname(os.path.abspath(__file__))
success, entry_local_path = manager.download_full_tileset(
tileset_url=url,
save_dir=f"data_3dtiles",
region_filter=None
)
if not success :
logger.info(f"下载地图数据失败: {url}") logger.info(f"下载地图数据失败: {url}")
return "下载地图数据失败", None return "下载地图数据失败", None
def generate_slopeAspect_3d_overlook(region_coords, url, overall_3d_png_name, minio_sub_path) : def generate_slopeAspect_3d_overlook(region_coords, url, overall_3d_png_name, minio_sub_path) :
# 下载3dtiles地图数据 # 下载3dtiles地图数据
manager = MinIO3DTilesManager( _data_source = TilesetDataSource(url)
endpoint_url=ENDPOINT_URL, _data_source.dowload_map_data(url)
access_key=ACCESS_KEY,
secret_key=SECRET_KEY, if not _data_source.tileset_path :
secure=False
)
script_dir = os.path.dirname(os.path.abspath(__file__))
success, entry_local_path = manager.download_full_tileset(
tileset_url=url,
save_dir=f"data_3dtiles",
region_filter=None
)
if not success :
logger.info(f"下载地图数据失败: {url},{region_coords}") logger.info(f"下载地图数据失败: {url},{region_coords}")
return "下载地图数据失败", None return "下载地图数据失败", None
tileset_path = entry_local_path tileset_path = _data_source.tileset_path
script_dir = os.path.dirname(os.path.abspath(__file__))
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif") dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords) data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
@ -66,8 +47,8 @@ class TerrainCalculator:
slope_aspect_img.read_slope_aspect_by_dem(dem_path, overall_3d_png_path) slope_aspect_img.read_slope_aspect_by_dem(dem_path, overall_3d_png_path)
logger.info(f"生成成功: {url},{region_coords},{overall_3d_png_path}") logger.info(f"生成成功: {url},{region_coords},{overall_3d_png_path}")
entry_bucket, _ = manager.parse_minio_url(url); entry_bucket, _ = _data_source.parse_minio_url(url);
success, minio_path = manager.upload_file(entry_bucket, f"{minio_sub_path}/{overall_3d_png_name}", overall_3d_png_path) success, minio_path = _data_source.upload_file(entry_bucket, f"{minio_sub_path}/{overall_3d_png_name}", overall_3d_png_path)
if success : if success :
return "生成成功", minio_path return "生成成功", minio_path
else : else :
@ -75,36 +56,28 @@ class TerrainCalculator:
def generate_slopeAspect_tif(region_coords, url, slope_aspect_tif_name, minio_sub_path) : def generate_slopeAspect_tif(region_coords, url, slope_aspect_tif_name, minio_sub_path) :
# 下载3dtiles地图数据 # 下载3dtiles地图数据
manager = MinIO3DTilesManager( _data_source = TilesetDataSource(url)
endpoint_url=ENDPOINT_URL, _data_source.dowload_map_data(url)
access_key=ACCESS_KEY,
secret_key=SECRET_KEY, if not _data_source.tileset_path :
secure=False
)
script_dir = os.path.dirname(os.path.abspath(__file__))
success, entry_local_path = manager.download_full_tileset(
tileset_url=url,
save_dir=f"data_3dtiles",
region_filter=None
)
if not success :
logger.info(f"下载地图数据失败: {url},{region_coords}") logger.info(f"下载地图数据失败: {url},{region_coords}")
return "下载地图数据失败", None return "下载地图数据失败", None
tileset_path = entry_local_path tileset_path = _data_source.tileset_path
script_dir = os.path.dirname(os.path.abspath(__file__))
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif") dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords) data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
if not os.path.exists(dem_path) : if not os.path.exists(dem_path) :
logger.info(f"生成坡度坡向俯视图失败: {url},{region_coords}") logger.info(f"生成坡度坡向tif失败: {url},{region_coords}")
return "生成坡度坡向俯视图失败", None return "生成坡度坡向tif失败", None
slope_aspect_tif_path = os.path.join(script_dir, slope_aspect_tif_name) slope_aspect_tif_path = os.path.join(script_dir, slope_aspect_tif_name)
slope_aspect_tif.create_slope_aspect(dem_path, 'combined', slope_aspect_tif_path) slope_aspect_tif.create_slope_aspect(dem_path, 'combined', slope_aspect_tif_path)
logger.info(f"生成成功: {url},{region_coords},{slope_aspect_tif_path}") logger.info(f"生成成功: {url},{region_coords},{slope_aspect_tif_path}")
entry_bucket, _ = manager.parse_minio_url(url); entry_bucket, _ = _data_source.parse_minio_url(url);
success, minio_path = manager.upload_file(entry_bucket, f"{minio_sub_path}/{slope_aspect_tif_name}", slope_aspect_tif_path) success, minio_path = _data_source.upload_file(entry_bucket, f"{minio_sub_path}/{slope_aspect_tif_name}", slope_aspect_tif_path)
if success : if success :
return "生成成功", minio_path return "生成成功", minio_path
else : else :

View File

@ -5,148 +5,71 @@ import asyncio
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import logging import logging
import os import os
from pathlib import Path
from b3dm.data_3dtiles_manager import MinIO3DTilesManager
import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ENDPOINT_URL = "222.212.85.86:9000"
ACCESS_KEY = "WuRenJi"
SECRET_KEY = "WRJ@2024"
class TilesetDataSource: class TilesetDataSource:
"""使用py3dtiles库的数据源""" """使用py3dtiles库的数据源"""
def __init__(self, tileset_path: str, cache_size: int = 1000): def __init__(self, url: str, cache_size: int = 1000):
self.tileset_path = os.path.abspath(tileset_path) self.url = url
self.tileset_dir = os.path.dirname(self.tileset_path) self.tileset_path = None
self.cache_size = cache_size self.tileset_dir = None
self._tileset = None
self._point_cache = {}
self._executor = ThreadPoolExecutor(max_workers=4)
self._crs = "EPSG:4979" self._crs = "EPSG:4979"
async def initialize(self): def parse_minio_url(self, url):
"""初始化""" manager = MinIO3DTilesManager(
try: endpoint_url=ENDPOINT_URL,
# 尝试导入py3dtiles access_key=ACCESS_KEY,
try: secret_key=SECRET_KEY,
import py3dtiles secure=False
from py3dtiles.tileset import TileSet )
except ImportError: return manager.parse_minio_url(url)
logger.warning("py3dtiles未安装将使用简化数据源")
raise ImportError("请安装py3dtiles: pip install py3dtiles")
loop = asyncio.get_event_loop() def upload_file(self, bucket_name, object_name, file_path):
self._tileset = await loop.run_in_executor( manager = MinIO3DTilesManager(
self._executor, endpoint_url=ENDPOINT_URL,
TileSet.from_file, access_key=ACCESS_KEY,
self.tileset_path secret_key=SECRET_KEY,
) secure=False
)
flag, path = manager.upload_file(bucket_name, object_name, file_path)
if flag :
os.remove(file_path)
return flag, path
logger.info(f"py3dtiles数据源初始化完成: {self.tileset_path}")
except Exception as e: def dowload_map_data(self, url: str) :
logger.error(f"py3dtiles初始化失败: {str(e)}") # 下载3dtiles地图数据
# 回退到简化数据源 manager = MinIO3DTilesManager(
self._tileset = None endpoint_url=ENDPOINT_URL,
access_key=ACCESS_KEY,
secret_key=SECRET_KEY,
secure=False
)
success, tileset_path = manager.download_full_tileset(
tileset_url=url,
save_dir=f"data_3dtiles",
region_filter=None
)
if success :
self.tileset_path = os.path.abspath(tileset_path)
self.tileset_dir = os.path.dirname(tileset_path)
async def get_points_in_polygon(self, async def get_points_in_polygon(self, polygon_coords, z_range=None):
polygon_coords: List[List[float]], """获取多边形内的点数据"""
z_range: Optional[Tuple[float, float]] = None) -> np.ndarray: points = data_3dtiles_to_dem.parse_tileset(self.tileset_path, polygon_coords)
"""获取点数据""" return np.array(points)
if self._tileset is None:
# 使用简化数据源
return await self._get_simulated_points(polygon_coords, z_range)
try:
# 使用py3dtiles API获取数据
points = []
polygon_np = np.array(polygon_coords)
# 遍历tileset中的所有tile
for tile in self._tileset.root_tile.traverse():
tile_points = self._extract_tile_points(tile)
if tile_points.size > 0:
# 筛选多边形内的点
points_in_polygon = self._filter_points_by_polygon(tile_points, polygon_np)
if points_in_polygon.size > 0:
points.append(points_in_polygon)
if points:
all_points = np.vstack(points)
if z_range:
mask = (all_points[:, 2] >= z_range[0]) & (all_points[:, 2] <= z_range[1])
all_points = all_points[mask]
return all_points
return np.array([])
except Exception as e:
logger.error(f"py3dtiles获取数据失败: {str(e)}")
return await self._get_simulated_points(polygon_coords, z_range)
def _extract_tile_points(self, tile) -> np.ndarray:
"""从tile提取点数据"""
try:
if hasattr(tile, 'content') and tile.content:
# 尝试获取点数据
if hasattr(tile.content, 'points'):
return tile.content.points.positions
elif hasattr(tile.content, 'body'):
# 处理其他格式
return np.array([])
return np.array([])
except:
return np.array([])
def _filter_points_by_polygon(self, points: np.ndarray, polygon: np.ndarray) -> np.ndarray:
"""筛选多边形内的点"""
from matplotlib.path import Path
if points.size == 0:
return points
path = Path(polygon[:, :2])
mask = path.contains_points(points[:, :2])
return points[mask]
async def _get_simulated_points(self,
polygon_coords: List[List[float]],
z_range: Optional[Tuple[float, float]] = None) -> np.ndarray:
"""获取模拟点数据"""
# 同上一个类的生成模拟数据方法
polygon_np = np.array(polygon_coords)
if polygon_np.shape[0] < 3:
return np.array([])
x_min, y_min = polygon_np.min(axis=0)
x_max, y_max = polygon_np.max(axis=0)
grid_size = 100
x = np.linspace(x_min, x_max, grid_size)
y = np.linspace(y_min, y_max, grid_size)
xx, yy = np.meshgrid(x, y)
points = np.column_stack([xx.ravel(), yy.ravel()])
np.random.seed(42)
base_elevation = 50.0
terrain_variation = 10.0
z = base_elevation + terrain_variation * np.sin(0.1 * xx).ravel() * np.cos(0.1 * yy).ravel()
points = np.column_stack([points[:, 0], points[:, 1], z])
from matplotlib.path import Path
path = Path(polygon_np[:, :2])
mask = path.contains_points(points[:, :2])
points = points[mask]
if z_range:
mask = (points[:, 2] >= z_range[0]) & (points[:, 2] <= z_range[1])
points = points[mask]
logger.info(f"生成 {points.shape[0]} 个模拟点")
return points
async def get_data_bounds(self) -> Dict[str, List[float]]: async def get_data_bounds(self) -> Dict[str, List[float]]:
"""获取数据边界""" """获取数据边界"""
if self._tileset is None:
await self.initialize()
bounds = { bounds = {
"min": [float('inf'), float('inf'), float('inf')], "min": [float('inf'), float('inf'), float('inf')],
@ -165,3 +88,18 @@ class TilesetDataSource:
def get_crs(self) -> str: def get_crs(self) -> str:
return self._crs return self._crs
async def main() :
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SCRIPT_PAR_DIR = os.path.dirname(SCRIPT_DIR)
tileset_path = os.path.join(SCRIPT_PAR_DIR, "data/3dtiles/tileset.json")
data_source_3d_tiles = TilesetDataSource(tileset_path)
# tileSet = TileSet()
# path = Path(tileset_path)
# tileset_data = tileSet.from_file(path)
print("====================================================")
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())