十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DEM坡度计算六种算法详解:从差分模板到Python落地

DEM坡度计算六种算法详解:从差分模板到Python落地 简介针对地理信息系统与遥感领域中的坡度和地形分析需求这份资源以程序源代码形式提供六种坡度计算方法涵盖简单差分、二阶差分、三阶反距离平方权差分、三阶反距离权差分、三阶不带权差分以及边框差分等主流算法。每种算法均有可运行的实现并附带实验说明文档与示例栅格数据便于读者对照数字高程模型DEM理解不同方法的数学原理、适用场景及精度差异。源码采用常见GIS编程方式组织可帮助测绘地理信息、环境科学、土木工程等方向的学生和研发人员快速完成算法验证、参数对比与二次开发降低从理论到代码落地的门槛。压缩包以rar格式打包体积仅约1.53MB文件明细未单独标注但内容紧凑足以支撑坡度计算实验与教学演示。目前已有2038人学习下载适合作为课程设计、毕业设计或工程应用中的参考实现。1. 从DEM到坡度图为什么同一个地形会有六种算法做地形分析的人多半有过这种经历同一块DEM用ArcGIS的Slope工具和用QGIS算出来的坡度图放到PS里一叠明暗纹理对不上再换一套代码自己算数值又差了一截。这不是工具出了问题而是坡度计算本身就没有唯一答案——坡度的本质是地表曲面在某一点的倾斜程度但曲面在离散栅格上只有有限个采样点不同算法对“如何用周围像素推断这一点”的假设不同结果自然不同。这次拆解的资源是一份实验配套程序包含六种坡度计算方法的完整实现简单差分、二阶差分、三阶反距离平方权差分、三阶反距离权差分、三阶不带权差分、边框差分。数据侧附带raster1.txt栅格文件和slope calculate源码文档实验五 六种坡度计算方法程序.docx里给出了算法推导和实现步骤。对GIS开发、遥感数据处理和土木工程里做地形建模的人来说这套代码的价值不只是跑通一个Slope工具而是把ArcGIS背后藏着的算子选择逻辑摊开来看每种方法的差分模板长什么样、对噪声的容忍度如何、边界像素怎么处理、什么地形适合什么权重。后面几章会把这六种算法逐个拆开从差分模板的数学形式讲起落到Python实现上最后给出验证方法和选型建议。代码用numpy手写核心算子不依赖GDAL内置的slope函数这样每个中间量都可以打印出来核对。2. 六种坡度算法的差分模板从一阶到三阶的演进逻辑2.1 坡度的数学定义与栅格离散化连续地表上坡度是高度函数在水平面上的最大下降率数学上定义为梯度向量的模slope arctan( sqrt( (dz/dx)^2 (dz/dy)^2 ) )但DEM是一个离散栅格每个像元只记录一个高程值导数只能通过邻域像素的差分来近似。中心像元周围有8个邻域怎么组合这8个差值就衍生出了不同算法。把这个问题抽象成模板卷积用一个3x3或2x2的权重核分别对高程矩阵做x方向和y方向的加权求和得到dz/dx和dz/dy再合成坡度。理解了这个框架六种算法就变成了六组权重系数的问题。下文将逐一给出模板这些模板是后面写代码时的核心依据。2.2 简单差分最小窗口的代价简单差分只用中心像元东、西两个方向或南、北的高程差计算x方向导数。以3x3窗口为例设中心像元高程为e其周围8个像元按位置命名为a b c d e f g h i简单差分的x方向导数为(f - d) / (2 * cell_size)y方向导数为(h - b) / (2 * cell_size)。这个模板只有两个有效权重计算量最小但对噪声完全没有抑制能力。如果DEM里有一个孤立的异常高程点简单差分会把异常直接放大成一个大坡度值。实际使用中除非DEM已经做过严格的平滑滤波否则不推荐用这个算法出正式成果。2.3 二阶差分引入曲率信息二阶差分在计算中心像元导数时把对角方向的像元也纳入考量公式变为dz/dx ( (c f i) - (a d g) ) / (6 * cell_size) dz/dy ( (g h i) - (a b c) ) / (6 * cell_size)模板形式是3x3全窗口每行列求和后做差。相比简单差分二阶差分多利用了6个像素的高程信息对孤立噪声的敏感度有所下降因为它本质上是在做一次局部平均后再差分。缺点是当真实地形是一个陡坎时二阶差分会在陡坎边缘产生过冲——把坡度算得比实际更陡。2.4 三阶不带权差分Horn算法的雏形三阶不带权差分是最常被实现的方法也是很多GIS软件里默认Slope算子的基础。它的x方向模板为-1 0 1 -1 0 1 -1 0 1归一化系数是1 / (6 * cell_size)。y方向模板为转置-1 -1 -1 0 0 0 1 1 1这个模板的特点是给左、右两列或上、下两行的所有像元相同的权重。从数值分析角度看它相当于在3x3窗口内先对每一列做平均再对列均值做中心差分等价于对数据做了轻微平滑因此比简单差分稳定得多。2.5 三阶反距离平方权差分与三阶反距离权差分这两个算法引入了距离衰减的权重思想。3x3窗口中对角像元到中心像元的距离是正交像元的√2倍。如果权重与距离平方成反比那么对角像元的权重就是正交像元的一半。据此构造的x方向模板为-1 0 1 -2 0 2 -1 0 1归一化系数为1 / (8 * cell_size)。权重用1/d²d取√2时权重为1/2恰好落到模板的四角。三阶反距离权差分则把距离指数从2改成1即权重与距离成反比对角像元权重为1/√2。模板为-1/√2 0 1/√2 -1 0 1 -1/√2 0 1/√2归一化系数需要重新计算后文代码里会给出数值实现。反距离平方权对近处像元更敏感适合地形起伏剧烈、细节丰富的区域反距离权则稍微平滑一些适合有噪声的数据。2.6 边框差分边界策略的兜底方案任何模板卷积在图像边缘都会遇到窗口越界的问题。边框差分不是一个独立的坡度公式而是一组边界处理策略当中心像元位于栅格的第一行、最后一行、第一列或最后一列时退化为可用邻域的差分。具体来说对于左边界像元x方向差分只用右边的像元对(f - e)对于右边界只用左边的(e - d)。上下边界的y方向同理。角点位置则只有一个方向可计算另一方向导数置0。这种处理方式在栅格尺寸较小时影响明显比如raster1.txt里如果是一个20x20的小DEM边框像元占比将近20%边界策略会直接决定坡度图边缘的形态。3. Python实现六种坡度算法从卷积模板到坡度图输出3.1 环境准备与数据载入实现采用Python numpy rasterio的组合。numpy负责矩阵运算rasterio负责读取DEM文件和写GeoTIFF。如果本地没有rasterio也可以用numpy直接读文本格式的DEMraster1.txt就是典型的ASCII Grid格式。创建虚拟环境并安装依赖python -m venv slope_env source slope_env/bin/activate # Windows下用 slope_env\Scripts\activate pip install numpy rasterio matplotlib读取DEM数据的代码import numpy as np import rasterio def load_dem(path): 读取GeoTIFF或ASCII Grid格式的DEM返回高程矩阵和像元尺寸 with rasterio.open(path) as src: dem src.read(1).astype(np.float64) cell_size src.res[0] # 假设x、y方向分辨率一致 nodata src.nodata if nodata is not None: dem[dem nodata] np.nan return dem, cell_size这段代码把无值像元统一置为NaN后续所有计算都要对NaN做掩膜处理否则差分结果会被无值区域污染。3.2 核心算子六种算法的统一实现把六种算法的权重模板存入字典用scipy.ndimage.convolve或手写滑动窗口实现卷积。这里选择手写实现因为方便在中间步骤打印x、y方向导数便于调试import numpy as np def slope_by_template(dem, cell_size, method): 根据差分模板计算坡度 method: simple | second | third_plain | third_idw2 | third_idw1 # 六种算法的 x 方向差分模板 templates_x { simple: np.array([[0, 0, 0], [-1, 0, 1], [0, 0, 0]]), second: np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]), third_plain: np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]]), third_idw2: np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]), third_idw1: np.array([[-1/np.sqrt(2), 0, 1/np.sqrt(2)], [-1, 0, 1], [-1/np.sqrt(2), 0, 1/np.sqrt(2)]]), } if method not in templates_x: raise ValueError(f未知方法: {method}) # 权重归一化系数 norm { simple: 2.0 * cell_size, second: 6.0 * cell_size, third_plain: 6.0 * cell_size, third_idw2: 8.0 * cell_size, third_idw1: (2 2 * np.sqrt(2)) * cell_size, }[method] template_x templates_x[method] template_y template_x.T rows, cols dem.shape dzdx np.zeros_like(dem) dzdy np.zeros_like(dem) # 遍历每个像元跳过NaN for i in range(1, rows - 1): for j in range(1, cols - 1): window dem[i-1:i2, j-1:j2] if np.isnan(window).any(): continue dzdx[i, j] np.sum(window * template_x) / norm dzdy[i, j] np.sum(window * template_y) / norm # 坡度 arctan(sqrt(dzdx^2 dzdy^2)) slope np.arctan(np.sqrt(dzdx**2 dzdy**2)) return slope, dzdx, dzdy这段代码的核心逻辑是对每一个像元提取3x3邻域与模板做逐元素乘法再求和得到该点的方向导数。norm系数里的third_idw1需要解释一下模板中四个角权重绝对值为1/√2四个边权重绝对值为1总和为4×(1/√2) 4×1 2√2 4 ≈ 6.828归一化时用这个数除以cell_size。3.3 二阶差分的独立实现上面代码里second和third_plain看起来模板形式一样都是列和行求和后做差但二者其实有区别。second的模板是x方向只对三行做差分、每行权重相同对应的是前一章里二阶差分的公式third_plain的模板第一行和第三行是反号的对应Horn算法。为清晰起见二阶差分单独写一个函数def slope_second_order(dem, cell_size): 二阶差分先对邻域做平均再计算中心差分 from scipy.ndimage import uniform_filter # 先对DEM做3x3均值平滑再对平滑结果做中心差分 smooth uniform_filter(dem, size3, modenearest) dzdx (smooth[1:-1, 2:] - smooth[1:-1, :-2]) / (2 * cell_size) dzdy (smooth[2:, 1:-1] - smooth[:-2, 1:-1]) / (2 * cell_size) # 中心像元的坡度 rows, cols dem.shape slope np.zeros_like(dem) slope[1:-1, 1:-1] np.arctan(np.sqrt(dzdx**2 dzdy**2)) return slope这里用uniform_filter先做平滑再做差分效果上等于先对九个像元求均值再算中心差分。这样写的好处是计算效率远高于逐像元循环在处理大DEM时能明显感受到速度差异。3.4 边框差分的边界填充策略slope_by_template函数里循环从range(1, rows-1)开始意味着最外圈像元的坡度始终为0这在面积计算时会少算边缘贡献。边框差分要单独处理外圈def slope_with_frame(dem, cell_size, methodthird_idw2): 边框差分中心区域用指定模板边界区域退化为单边差分 rows, cols dem.shape slope, _, _ slope_by_template(dem, cell_size, method) # 先算内部 # 上边界y方向只能用下方像元x方向用中心差分 for j in range(1, cols-1): dzdx (dem[0, j1] - dem[0, j-1]) / (2 * cell_size) dzdy (dem[1, j] - dem[0, j]) / cell_size slope[0, j] np.arctan(np.sqrt(dzdx**2 dzdy**2)) # 下边界 for j in range(1, cols-1): dzdx (dem[rows-1, j1] - dem[rows-1, j-1]) / (2 * cell_size) dzdy (dem[rows-1, j] - dem[rows-2, j]) / cell_size slope[rows-1, j] np.arctan(np.sqrt(dzdx**2 dzdy**2)) # 左边界和右边界同理 for i in range(1, rows-1): dzdx (dem[i, 1] - dem[i, 0]) / cell_size dzdy (dem[i1, 0] - dem[i-1, 0]) / (2 * cell_size) slope[i, 0] np.arctan(np.sqrt(dzdx**2 dzdy**2)) for i in range(1, rows-1): dzdx (dem[i, cols-1] - dem[i, cols-2]) / cell_size dzdy (dem[i1, cols-1] - dem[i-1, cols-1]) / (2 * cell_size) slope[i, cols-1] np.arctan(np.sqrt(dzdx**2 dzdy**2)) # 四个角点只有一个方向有值 slope[0, 0] np.arctan(np.sqrt(((dem[1, 0] - dem[0, 0]) / cell_size)**2 ((dem[0, 1] - dem[0, 0]) / cell_size)**2)) # 其余三个角点同理省略 return slope边界处理的本质是对窗口外的值做镜像填充。对于上边界的像元dzdy只有半差分可用这里直接用dem[1, j] - dem[0, j]作为分母为cell_size的单边差分相当于假设边界外侧是平面。这个假设在大范围缓坡地形下误差不大在陡峭山区则会让边界坡度偏大或偏小需要根据实际情况决定是否对边界做掩膜处理。4. 参数对照、地形适应性分析与实验数据验证4.1 六种算法的参数汇总基于前两章的推导和实现把六种算法的关键参数整理如下表格。这张表在选型时可以直接拿来对照。算法窗口大小x方向模板3x3归一化分母噪声敏感度适用地形简单差分2x2有效[-1, 0, 1]2·cellsize高平滑人工地形二阶差分3x3每行[-1,0,1]6·cellsize中预处理后的DEM三阶不带权3x3每行[-1,0,1]6·cellsize低通用默认三阶反距离平方权3x3四角±1边±28·cellsize较低起伏剧烈区域三阶反距离权3x3四角±1/√2(22√2)·cellsize低有噪声的山区边框差分动态视位置而定可变视所在边界小尺寸栅格归一化分母决定了坡度输出的量级。如果分母用错比如把third_idw2的分母写成6·cellsize坡度会整体偏大在平坦区域会算出虚假的1-2度起伏。经验做法是先对一个已知坡度的合成DEM跑一遍验证输出值是否在预期范围。4.2 合成DEM验证方案的实操为了验证代码正确性构造一个已知坡度的斜面def make_synthetic_dem(size30, slope_x0.1, slope_y0.05): 生成一个坡度为 arctan(sqrt(0.1²0.05²)) 的斜面DEM x np.arange(size) * 1.0 # 像元大小设为1 y np.arange(size) * 1.0 xx, yy np.meshgrid(x, y) dem slope_x * xx slope_y * yy return dem # 期望坡度弧度 import math expected_slope math.atan(math.sqrt(0.1**2 0.05**2)) print(f期望坡度: {math.degrees(expected_slope):.4f}°)用raster1.txt里的真实数据跑之前先跑这个合成DEM。理想情况下所有算法都应该输出接近期望坡度的值差异只在千分位级别。如果三阶类算法输出的值偏离超过0.01度基本可以断定是模板符号方向写反了——这是手写卷积最常犯的错比如把模板写成[1, 0, -1]而不是[-1, 0, 1]x方向导数符号反转合成坡度的arctan值不变因为平方但配合y方向就可能算出完全错误的方向和大小。4.3 raster1.txt的实际跑分与算法差异分析raster1.txt的数据量不大直接读取并运行全部六种算法import numpy as np def read_ascii_grid(path): 读取ESRI ASCII Grid格式 with open(path) as f: for _ in range(6): # 跳过文件头 header f.readline().split() if header[0] ncols: ncols int(header[1]) elif header[0] nrows: nrows int(header[1]) elif header[0] cellsize: cellsize float(header[1]) elif header[0] NODATA_value: nodata float(header[1]) data np.loadtxt(f).reshape(nrows, ncols) data[data nodata] np.nan return data, cellsize dem, cell read_ascii_grid(raster1.txt) print(fDEM尺寸: {dem.shape}, 像元大小: {cell}) print(f高程范围: {np.nanmin(dem):.2f} ~ {np.nanmax(dem):.2f}) # 跑六种算法 methods [simple, second, third_plain, third_idw2, third_idw1] for m in methods: slope, _, _ slope_by_template(dem, cell, m) print(f{m}: 平均坡度 {np.nanmean(slope):.4f} rad, f最大坡度 {np.nanmax(slope):.4f} rad)运行结果通常会发现两类规律。第一simple方法的最大坡度会显著高于其他方法这正是噪声放大的体现第二third_idw2和third_plain的平均坡度差异在个位数百分比以内但在山谷线和山脊线位置会出现局部差异。这类差异对地质灾害评估和径流分析影响很大——山脊线上的微小坡度差异可能改变汇水方向的判定。4.4 常见实现误用与调试排查实现过程中有几个容易踩的坑值得单独说。第一个坑是像元尺寸的单位混用。DEM的cellsize如果单位是度经纬度坐标而高程单位是米算出来的坡度量纲完全错误。处理方法是先把DEM投影到平面坐标系或者对经纬度数据乘以cos(纬度)做近似修正。第二个坑是NaN像元的传播。前面代码里np.isnan(window).any()会跳过窗口内任何含NaN的像元导致NaN周围一圈的坡度全部缺失。如果数据里有大量无值区域输出的坡度图会出现空洞。常见做法是先用最近邻插值或反距离权重插值填充无值区域再做坡度计算。第三个坑是坡度输出单位的混淆。np.arctan返回的是弧度而业务上通常用度表示。转换公式是np.rad2deg(slope)。在ArcGIS里如果geomorphometric参数设成degree输出范围是0-90设成radian则是0-1.57。自己的代码里一定要写清楚返回单位否则下游做坡度分级就会出错。第四个坑是模板符号方向与坐标系y轴朝向。栅格数据的y轴方向通常是从北到南即第一行是北边。如果模板的y方向定义反了dzdy符号会翻转直接影响坡向计算。检查方法是构造一个南高北低的合成DEM理想的dzdy应为负值如果算出来是正值把模板转置即可。5. 把算法封装进工程代码的四个实践技巧5.1 用字典分发器替代if-else链前面slope_by_template函数里用了templates_x[method]做分发这个模式在工程里值得继续推广。当方法数量增长到十几个时可以用一个注册表把算法名映射到函数避免修改主函数逻辑SLOPE_METHODS {} def register_slope_method(name): def decorator(func): SLOPE_METHODS[name] func return func return decorator register_slope_method(third_idw2) def slope_idw2(dem, cell_size): # 具体实现 pass def compute_slope(dem, cell_size, method): return SLOPE_METHODS[method](dem, cell_size)这样扩展新算法时不需要改动调用方代码后续如果要集成到Web服务或批处理脚本里会顺手很多。5.2 结果质量检查双指标拿到坡度图后用两个指标做快速质检。第一个是平坦区域的坡度分布——找一块人工平整区域比如机场跑道或大型建筑用地统计坡度均值如果超过0.5度说明算法或数据有系统误差。第二个是坡度的空间自相关性相邻像元的坡度差应当平滑过渡如果出现棋盘格状分布说明差分模板的权重比例有问题。5.3 批量处理中的内存优化大DEM的坡度计算容易撑爆内存。slope_by_template里创建了dzdx和dzdy两个和DEM等大的float64数组一个10000x10000的DEM就要占用800MB×2。优化做法是在循环里直接累加slope数组不保留中间导数矩阵或用np.float32存储中间结果。精度损失在坡度计算中可以忽略因为arctan之后的结果对输入的小扰动不够敏感。5.4 坡度分级可视化的一段代码坡度计算完输出一张分级图方便直接评估算法间的视觉差异import matplotlib.pyplot as plt def classify_slope(slope_deg, levels[0, 5, 10, 15, 25, 35, 90]): 按国标坡度分级平坡0-5, 缓坡5-15, 斜坡15-25, 陡坡25-35, 急坡35-90 import matplotlib.colors as mcolors cmap mcolors.ListedColormap([#1a9850, #91cf60, #d9ef8b, #fee08b, #fc8d59, #d73027]) return np.digitize(slope_deg, levels) - 1, cmap slope_deg np.rad2deg(slope) classified, cmap classify_slope(slope_deg) plt.imshow(classified, cmapcmap, vmin0, vmax5) plt.colorbar(ticks[0.5, 1.5, 2.5, 3.5, 4.5], labelSlope Grade) plt.title(Slope Classification) plt.savefig(slope_classified.png, dpi150, bbox_inchestight)运行这段代码会把raster1.txt对应的坡度分级图保存为PNG把六种算法的输出并排放在一起能很直观地看到三阶反距离平方权在陡坡区域比三阶不带权多划分出多少急坡单元——这个差异在数值统计里容易被忽略但在图上非常明显。本文还有配套的精品资源点击获取
返回列表