在地理空间分析中,数字高程模型(DEM)作为基础性数据,广泛应用于地形建模、水文过程模拟以及自然灾害预警等多个领域。本文从工程实践出发,系统梳理DEM数据处理的关键流程与核心算法原理,并结合Python与GDAL库实现完整的实操案例,内容涵盖数据读取、预处理、地形特征提取及优化策略等环节,适用于从事GIS开发和空间数据分析的技术人员参考学习。
DEM数据处理的目标是将原始的高程栅格转化为具有实际应用价值的地形信息产品,其典型工作流遵循“数据输入 → 预处理 → 核心计算 → 后处理 → 结果输出”的闭环结构:
GDAL作为地理栅格数据处理的事实标准库,具备强大的多格式兼容能力,能够高效完成各类DEM文件的读写操作。
关键代码示例:
from osgeo import gdal
import numpy as np
import matplotlib.pyplot as plt
# 注册所有驱动并打开DEM文件
gdal.AllRegister()
dem_path = "./data/dem.tif" # 请替换为实际路径
dataset = gdal.Open(dem_path, gdal.GA_ReadOnly)
# 提取元数据信息
rows = dataset.RasterYSize # 图像行数
cols = dataset.RasterXSize # 图像列数
proj = dataset.GetProjection() # 投影定义
geotrans = dataset.GetGeoTransform() # 仿射变换参数
band = dataset.GetRasterBand(1) # 获取第一个波段(通常为高程)
dem_data = band.ReadAsArray(0, 0, cols, rows) # 读取整个高程数组
nodata = band.GetNoDataValue() # 获取无效值标识
# 像素坐标转地理坐标的函数
def pixel2geo(row, col, geotrans):
"""将像素行列坐标转换为地理坐标"""
x = geotrans[0] + col * geotrans[1] + row * geotrans[2]
y = geotrans[3] + col * geotrans[4] + row * geotrans[5]
return (x, y)
# 释放资源,避免文件被锁定
dataset = None
注意事项:
conda install -c conda-forge gdal
由于植被遮挡、传感器故障等原因,DEM常存在无数据区域(即“空洞”),需通过插值方法进行修复。不同算法适用于不同场景:
| 算法类型 | 核心原理 | 适用场景 | 优点 |
|---|---|---|---|
| 线性插值法 | 沿行和列方向分别进行线性插值后取平均 | 小范围空洞修复 | 实现简单、运算速度快 |
| 反距离权重法(IDW) | 根据邻近有效点的距离加权估算当前点高程 | 中等规模空洞 | 较好保留局部地形形态特征 |
| 克里金法 | 基于空间自相关性构建变异函数进行最优估计 | 对精度要求较高的项目 | 提供无偏最优预测结果 |
| TFDM扩散模型 | 利用地形特征引导的生成式扩散机制补全缺失区域 | 复杂地形下的大面积空洞 | 重建精度高(实验测得MAE约为28.91m) |
线性插值实现代码(适用于快速修复):
def fill_dem_hole(dem_data, nodata):
"""采用双向线性插值填充DEM中的空洞"""
hole_mask = dem_data == nodata
if not np.any(hole_mask):
return dem_data
filled_data = dem_data.copy()
# 按行方向插值
for i in range(rows):
row_data = filled_data[i]
hole_cols = np.where(row_data == nodata)[0]
for col in hole_cols:
left = col - 1
while left >= 0 and row_data[left] == nodata:
left -= 1
right = col + 1
while right < cols and row_data[right] == nodata:
right += 1
def dem_chunk_process(dem_path, chunk_size=512, output_path="./output/dem_processed.tif"):
"""
对大规模DEM数据进行分块处理
chunk_size: 每个分块的像素尺寸,默认为512x512
"""
当处理高分辨率或大范围的DEM数据时,由于数据体量庞大,直接加载整个数据集容易引发内存溢出。为此,采用分块处理策略成为关键的性能优化手段。通过将原始DEM划分为多个较小的子区域(块),逐块读取、计算与写入,可显著降低内存占用。
将大尺度DEM按照预设的块大小(如512×512像元)分割成若干子块,对每个子块独立执行所需运算(如空洞填补、坡度坡向计算等)。在处理过程中需特别注意相邻块之间的边界衔接问题,避免因局部计算导致结果出现“阶梯效应”或边缘不连续现象。通常可通过重叠缓冲区(overlap buffer)机制,在每块周围扩展若干行/列数据以保证差分运算的准确性,最终合并时裁剪掉冗余边缘。
# 使用GDAL打开原始DEM文件
dataset = gdal.Open(dem_path, gdal.GA_ReadOnly)
band = dataset.GetRasterBand(1)
geotransform = dataset.GetGeoTransform()
projection = dataset.GetProjection()
cols = dataset.RasterXSize
rows = dataset.RasterYSize
nodata = band.GetNoDataValue()
# 创建输出TIFF文件(带地理信息)
driver = gdal.GetDriverByName("GTiff")
out_dataset = driver.Create(output_path, cols, rows, 2, gdal.GDT_Float32, options=["COMPRESS=DEFLATE"])
out_dataset.SetGeoTransform(geotransform)
out_dataset.SetProjection(projection)
out_band_slope = out_dataset.GetRasterBand(1)
out_band_aspect = out_dataset.GetRasterBand(2)
out_band_slope.SetNoDataValue(nodata)
out_band_aspect.SetNoDataValue(nodata)
# 遍历所有分块
for y in range(0, rows, chunk_size):
for x in range(0, cols, chunk_size):
# 计算当前块的实际大小(防止越界)
width = min(chunk_size, cols - x)
height = min(chunk_size, rows - y)
# 读取当前块的高程数据
block_data = band.ReadAsArray(x, y, width, height).astype(np.float32)
# 执行空洞填补
filled_block = fill_dem_hole(block_data, nodata)
# 计算坡度和坡向
slope_block, aspect_block = calculate_slope_aspect(filled_block)
# 写入结果到对应波段
out_band_slope.WriteArray(slope_block, x, y)
out_band_aspect.WriteArray(aspect_block, x, y)
# 刷新磁盘缓存并关闭文件
out_band_slope.FlushCache()
out_band_aspect.FlushCache()
del dataset, out_dataset
坡度和坡向是基于数字高程模型(DEM)提取的核心地形参数。其中,坡度反映地表单元的倾斜程度,而坡向表示该单元最大倾斜方向的角度,通常用于水文分析、光照模拟和地貌分类等应用。
本方法采用3×3邻域窗口的中心差分法进行计算,利用周围八个像元的高程值估算中心点的表面梯度。
坡度计算公式:
Slope = arctan(√(fx + fy))
其中,fx 表示东西方向(x轴)上的高程变化率,fy 表示南北方向(y轴)上的高程变化率,二者通过3×3窗口内加权差分得到。
坡向计算公式:
Aspect = arctan2(fy, -fx)
计算结果以弧度表示,需转换为角度制,并映射至0°–360°范围,其中0°代表正北方向,顺时针递增。
conda install -c conda-forge gdal
def calculate_slope_aspect(dem_data, cell_size=30):
"""
计算坡度(单位:度)和坡向(单位:度)
cell_size: 网格分辨率,单位为米,默认使用SRTM标准分辨率30米
"""
# 定义用于计算偏导数的卷积核
kernel_fx = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
kernel_fy = np.array([[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]])
# 对输入DEM进行边缘填充,防止边界卷积出错
dem_padded = np.pad(dem_data, pad_width=1, mode='edge')
# 使用互相关计算x和y方向的梯度分量
fx = np.correlate(dem_padded.ravel(), kernel_fx.ravel(), mode='valid').reshape(dem_data.shape) / (8 * cell_size)
fy = np.correlate(dem_padded.ravel(), kernel_fy.ravel(), mode='valid').reshape(dem_data.shape) / (8 * cell_size)
# 计算坡度(将弧度转换为角度)
slope = np.arctan(np.sqrt(fx**2 + fy**2)) * 180 / np.pi
# 计算坡向并调整至0°–360°范围
aspect = np.arctan2(fy, -fx) * 180 / np.pi
aspect[aspect < 0] += 360 # 将负角度转换为等效正角度
return slope, aspect
# 执行坡度与坡向计算
slope, aspect = calculate_slope_aspect(dem_filled, cell_size=30)
def fill_dem_hole(dem_data, nodata=-9999):
"""
基于行列双线性插值填补DEM中的无效值空洞
"""
filled_data = np.copy(dem_data)
rows, cols = filled_data.shape
# 行方向插值(水平填补)
for i in range(rows):
row_data = filled_data[i, :]
hole_cols = np.where(row_data == nodata)[0]
for col in hole_cols:
left = col - 1
while left >= 0 and row_data[left] == nodata:
left -= 1
right = col + 1
while right < cols and row_data[right] == nodata:
right += 1
if left >= 0 and right < cols:
# 线性插值填补
filled_data[i, col] = row_data[left] + (row_data[right] - row_data[left]) * (col - left) / (right - left)
# 列方向插值(垂直方向补充,修正行插值未覆盖的空洞)
for j in range(cols):
col_data = filled_data[:, j]
hole_rows = np.where(col_data == nodata)[0]
for row in hole_rows:
up = row - 1
while up >= 0 and col_data[up] == nodata:
up -= 1
down = row + 1
while down < rows and col_data[down] == nodata:
down += 1
if up >= 0 and down < rows:
filled_data[row, j] = col_data[up] + (col_data[down] - col_data[up]) * (row - up) / (down - up)
return filled_data
# 执行空洞填补操作
dem_filled = fill_dem_hole(dem_data, nodata)
[此处为图片3]
chunk_size: 子块的尺寸(以像素为单位),默认设定为 512×512
# 打开DEM数据文件并读取相关信息
dataset = gdal.Open(dem_path, gdal.GA_ReadOnly)
rows = dataset.RasterYSize # 获取影像行数
cols = dataset.RasterXSize # 获取影像列数
band = dataset.GetRasterBand(1) # 获取第一个波段
geotrans = dataset.GetGeoTransform() # 仿射变换参数
proj = dataset.GetProjection() # 投影信息
# 初始化输出文件,使用GTiff格式
driver = gdal.GetDriverByName("GTiff")
out_dataset = driver.Create(output_path, cols, rows, 1, gdal.GDT_Float32)
out_dataset.SetGeoTransform(geotrans) # 设置地理变换
out_dataset.SetProjection(proj) # 设置投影
out_band = out_dataset.GetRasterBand(1)
# 按照指定块大小进行分块处理
for i in range(0, rows, chunk_size):
for j in range(0, cols, chunk_size):
# 确定当前子块的实际维度,防止越界
chunk_rows = min(chunk_size, rows - i)
chunk_cols = min(chunk_size, cols - j)
# 读取当前区域的数据块
chunk_data = band.ReadAsArray(j, i, chunk_cols, chunk_rows)
# 对子块执行空值填充处理
chunk_filled = fill_dem_hole(chunk_data, band.GetNoDataValue())
# 将处理后的数据写入输出文件
out_band.WriteArray(chunk_filled, j, i)
# 清除缓存并释放资源
out_band.FlushCache()
out_dataset = None
dataset = None
print(f"分块处理完成,输出文件已保存至:{output_path}")
# 调用函数执行分块处理流程
dem_chunk_process(dem_path, chunk_size=512)
四、结果可视化与精度验证
4.1 三维地形图绘制(基于Matplotlib)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.colors import LightSource
def visualize_dem_3d(dem_data, geotrans):
"""
实现DEM数据的三维地形展示
"""
# 生成空间坐标网格
x = np.linspace(geotrans[0], geotrans[0] + cols * geotrans[1], cols)
y = np.linspace(geotrans[3], geotrans[3] + rows * geotrans[5], rows)
X, Y = np.meshgrid(x, y)
# 配置光源方向与立体渲染效果
ls = LightSource(azdeg=270, altdeg=20)
rgb = ls.shade(dem_data, cmap=plt.cm.gist_earth, vert_exag=0.1, blend_mode='soft')
# 创建三维绘图对象
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, dem_data,
rstride=1, cstride=1,
facecolors=rgb,
linewidth=0,
antialiased=False,
shade=False)
# 设置标题和坐标轴标签
ax.set_title('DEM 3D Terrain Visualization', fontsize=14)
ax.set_xlabel('X (m)', fontsize=12)
ax.set_ylabel('Y (m)', fontsize=12)
ax.set_zlabel('Elevation (m)', fontsize=12)
plt.tight_layout()
plt.show()
# 执行三维可视化操作
visualize_dem_3d(dem_filled, geotrans)
conda install -c conda-forge gdal
4.2 坡度与坡向联合可视化(多子图布局)
def visualize_slope_aspect(slope, aspect):
"""
对坡度和坡向结果进行二维图形化显示
"""
为确保处理结果的可靠性,需通过以下定量指标进行验证:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 坡向图
im2 = ax2.imshow(aspect, cmap='hsv', vmin=0, vmax=360)
ax2.set_title('Aspect (Degree)', fontsize=14)
plt.colorbar(im2, ax=ax2, label='Aspect')
# 坡度图
im1 = ax1.imshow(slope, cmap='YlOrRd', vmin=0, vmax=60)
ax1.set_title('Slope (Degree)', fontsize=14)
plt.colorbar(im1, ax=ax1, label='Slope')
执行最终可视化操作:
plt.tight_layout() plt.show() # 调用函数进行显示 visualize_slope_aspect(slope, aspect)
DEM数据处理的关键在于在精度与效率之间取得平衡。对于基础应用场景,利用GDAL与numpy即可实现高效的数据处理流程;而在复杂地形条件下,则需要引入分块处理机制以及高级插值算法来提升结果质量。本文所构建的技术路线涵盖了从数据读取、处理到可视化的完整链条,相关代码具备良好的可移植性,可直接应用于实际工程项目中。
扫码加好友,拉您进群



收藏
