简介:一份面向GIS学习者和水文分析初学者的DEM河流提取实操资源。项目基于数字高程模型,结合D8流向法与流量累积算法,演示从原始高程数据到河流网络识别的完整流程,适合在课程设计、环境科学或水资源管理项目中参考使用。压缩包共81个文件,大小3.12MB,主要包含C#源码工程(.cs、.sln、.csproj)、可执行程序(.exe、.dll)、DEM数据文本(.txt)以及多组阈值对比结果截图(.jpg)等,既可直接运行观察效果,也便于二次开发调参。内容预览显示程序可输出坡度图、不同阈值下的河流提取图与河流全图,并附有程序指南和算法说明文档,对流程和参数设置做了细致讲解。已有538人学习下载,适合需要快速掌握基于地形数据的水系提取方法、并希望获得可运行代码与可视化参考的学习者。
1. 基于DEM数据的河流提取:别把阈值一设就交差
大多数第一次做基于DEM数据的河流提取的人,都会把重心放在跑通流程上:填洼、流向、流量累积、阈值、矢量化,一条龙下来看着挺像样,可真拿去和实际河流对一下,发现不是多了一排平行线,就是上游河源对不上。这个场景说明一个事实:DEM河流提取的真正难点,从来不是流程跑不跑得通,而是每一步的参数和边界在哪里。本文会把这条流水线的原理、可复现的脚本、阈值换算方法和典型翻车现场一次讲清楚。适合刚接触水文分析、或者已经会点GIS但总被结果质量困扰的从业者。
2. 河流提取的原理框架:从栅格高程到河网的五个环节
水文分析提取河流,不是直接从DEM里找蓝色的线,而是通过模拟雨水在地表怎么流,把“水走过的路径”累积成河网。整条流水线可以拆成五个环节:数据预处理、填洼、流向计算、流量累积、阈值提取和矢量化。每一步都在回答一个具体问题,漏掉任何一环,结果都会出问题。
2.1 填洼:为什么DEM不能直接用
原始DEM里到处都是局部低点:山脚的凹坑、数据采集时的噪声、喀斯特地区的真实落水洞。如果不处理,流向计算会在这些低点断掉——水到了坑里就出不来了,后面累积量全是零,河网直接从中间消失。填洼的作用就是把这些低点垫高,让地表径流能一路流到流域出口。
填洼有两种思路:无限制填洼会把所有洼地都抬到周围最低出口的高度,适合大多数山地流域;限制深度填洼只填充低于某个阈值的洼地,超过阈值就保留,适合有明显真实洼地(比如岩溶区、采矿区)的地方。另一个思路叫breach(下切),不开挖整体填平,而是把洼地边缘切开一条沟让水流走,对地形改变更小,但实现麻烦一些。
我一般先跑无限制填洼看一眼结果:如果流域边界和已知分水岭差异很大,再换限制深度或breach。工具上可以直接用桌面GIS的Hydrology工具集,也可以用脚本调用开源算法库,效果没有本质差别。
2.2 流向与流量累积:D8算法在算什么
填洼之后的第一件事是算流向。最常用的D8算法,就是把每个像元和周围8个邻居比较,找到坡度最大的那个方向,把水全部流向那里。这个方向编码为1、2、4、8、16、32、64、128这些2的幂次,便于用位运算存储。虽然简单,但D8有一个天生缺陷:水流只能走8个方向,所以提取出的河道往往呈锯齿状,在陡坡段还容易出现平行线。
流量累积是顺着流向把上游来水像元数累加起来。一个像元的累积值含义是“有多少个像元的水最终经过了这里”,用累积值乘以单像元面积,就得到汇水面积。这一步的输出直接决定河网长什么样:累积量大的地方是主河道,积累量小的上游是沟谷。阈值提取的本质上就是在这张累积量栅格上划一条线,超过阈值才算河。
2.3 工具链选型:开源脚本与桌面GIS怎么配合
做河流提取常见的工具组合有三种:一是桌面GIS软件里现成的水文工具条,点击式操作,适合快速验证和教学;二是在其模型构建器里把流程串起来,方便批量处理;三是完全用开源命令行加脚本,适合大批量流域处理、或者要接进自动化流程的情况。
我自己的习惯是:探索阶段用桌面GIS点几遍,看不同阈值下的结果趋势;定型后写成脚本,因为河流提取经常要对几十个流域挨个跑,手工点一遍太伤眼睛。开源方案里,水文分析工具链比较成熟,支持填洼、D8流向、多流向算法、累积量和河网矢量化,输入输出都走标准栅格格式,和GDAL配合得很顺。
下面是直接用命令行跑完整流程的示例,适合先验证数据:
whitebox_tools -r FillDepressionsWangLiu -i dem.tif -o filled.tif whitebox_tools -r D8Pointer -i filled.tif -o flow_dir.tif whitebox_tools -r D8FlowAccumulation -i flow_dir.tif -o accum.tif --out_type=cells whitebox_tools -r ExtractStreams -i accum.tif -o streams.tif --threshold=1000命令逻辑很直观:先填洼,再算流向,再算累积量,最后用阈值1000做二值化。这里每个步骤的输出都是下一步的输入,顺序不能乱。命令行工具的好处是每个参数都能看帮助文档,翻车时可以单独重跑某一步,不用从头再来。填洼和流向交给命令行,后面要叠加坡度、土地利用这些因子做可变阈值时,再用脚本处理。
3. 本地跑通最小提取流程:可直接复用的Python脚本
这章给一套能直接跑的脚本。以某山地小流域为例,DEM分辨率30米,范围约300平方公里。目标是把从填洼到矢量化整条线串起来,并且每一步都落盘保存,方便排查。
3.1 预处理:投影统一与DEM异常值清理
拿到DEM的第一件事不是填洼,而是检查投影。很多公开DEM是经纬度坐标,直接拿去算流向,累积量里的“像元数”和真实面积会随纬度变化,阈值根本没法统一。我一般统一投影到等面积投影,比如UTM或者适合当地纬度的Albers,这样每个像元的面积才是固定的。
检查投影用一条命令就能看到关键信息:
gdalinfo dem.tif | grep -E "PROJCRS|GEOGCRS|Pixel Size|Data type"看输出里是否有投影坐标系信息、像元尺寸单位是米还是度。如果是经纬度,先重投影再往下走:
gdalwarp -overwrite -t_srs "EPSG:32650" -r bilinear -of GTiff dem.tif dem_utm.tif重投影的插值方法选双线性就行,河流提取对高程精度不是特别敏感,不必用更重的三次卷积。另外要注意DEM里的异常值:沿海或内陆湖泊区域常有大面积零值或负值,这些不是真实地形,填洼时会被当成巨大的洼地全部填平,导致出现一块假平原。我建议把这些区域的像元直接设成无效值,或者在填洼前用掩膜裁掉。
3.2 完整提取脚本:填洼、流向、累积量、阈值、矢量化
下面是基于开源水文库的Python写法,注释里标注了每一步的物理含义。不同版本的函数名稍有差异,以你本地的接口文档为准。
import whitebox_workflows as wbw wbe = wbw.WbEnvironment(wd="./work") wbe.verbose = False # 不滚动刷屏 dem = wbe.read_raster("dem_utm.tif") # 预处理完的DEM,单位米 # 第1步:填洼,选无限制填洼处理一般山地流域 filled = wbe.fill_depressions_wang_liu(dem) wbe.write_raster(filled, "filled.tif", compress=True) # 第2步:D8流向 flow_dir = wbe.d8_pointer(filled) wbe.write_raster(flow_dir, "flow_dir.tif", compress=True) # 第3步:流量累积,用像元数表示汇水面积 accum = wbe.d8_flow_accumulation(flow_dir, out_type="cells") wbe.write_raster(accum, "accum.tif", compress=True) # 第4步:阈值提取,这里先用经验值500 streams = wbe.extract_streams(accum, threshold=500) wbe.write_raster(streams, "streams.tif", compress=True) # 第5步:栅格河网转矢量,simplify可减少折线节点数 wbe.raster_to_vector(streams, "streams.shp", simplify=True)这段脚本覆盖了从填洼到矢量化的全部环节。fill_depressions_wang_liu是无限制填洼,优点是实现稳定、参数少;如果DEM里存在真实的封闭洼地,比如岩溶地貌,就不能用这个,要改用带深度限制的填洼函数,否则会把落水洞全部抹平。d8_pointer输出的是流向编码栅格,你可以把结果上色看方向是否合理,这是排查平行河问题最直接的入口。d8_flow_accumulation的out_type决定累积量是像元数还是真实面积,提取河网时用像元数就够,但后面算集水面积阈值时要换算。raster_to_vector会自动按8邻域连通关系把河网转成折线,周围只有一个连接点的像元会被当作河道末端。
这一步得到的矢量还比较粗糙,别急着入库。先叠加到遥感影像上看一眼:主河道走向对不对、支流是不是太密、有没有横穿山脊的离谱线段。这些检查比任何参数调优都重要。
3.3 阈值怎么定:按目标汇水面积反推并做密度校验
阈值是最让人犯难的一个参数,因为它是纯经验值。没有“正确答案”只能说“合理范围”,但可以按物理含义做一个基准换算:你先确定想保留的最小河道对应的汇水面积,再除以单像元面积,就得到累积量阈值。
target_area_km2 = 1.0 # 希望保留1平方公里以上集水区的河道 pixel_size_m = 30 # DEM分辨率 # 单像元面积 = 分辨率平方,再换算成平方公里 cell_area_km2 = (pixel_size_m ** 2) / 1_000_000 threshold = int(target_area_km2 / cell_area_km2) print(threshold) # 1111这里算出来是1111,意思是累积量超过1111个像元的格网单元会被识别成河道。这个值的物理含义是“至少汇集了1平方公里降雨的路径才能算河”。实际使用时,通常在这个基准值附近试几档:500、1000、2000都跑一遍,对比河网密度和真实情况的匹配度。
密度校验是一个很多人不知道的技巧:把矢量河网总长度除以流域面积,得到河网密度。和同区域公开数据的经验值对比,如果明显偏大,说明阈值取低了,河道碎得不成样子;如果偏小,说明阈值太高,上游被砍得太多。这一步比肉眼判断阈值靠谱得多,尤其是面对几万平方公里的流域时。
4. 参数调优:让提取结果从“能出图”到“能入库”
跑通流程只是起点。真正决定提取质量的是参数怎么组合:流向算法、阈值形式、后处理策略。这章讲三组最常调的参数,以及它们在不同地形下的表现。
4.1 流向算法:D8与Dinf怎么选
D8是行业默认算法,但不是唯一选择。Dinf(无限方向)把水流按坡向分配到两个相邻像元之间,不再局限于8个方向,在缓坡和平原地区表现更自然。两者的取舍可以看这张表:
| 对比项 | D8 | Dinf |
|---|---|---|
| 流向自由度 | 8个方向 | 连续方向 |
| 主河道稳定性 | 好,结果干净 | 稍差,细节更多 |
| 缓坡平原表现 | 容易走直线、平行线 | 更接近真实漫流 |
| 下游河道宽度 | 偏窄 | 偏宽,更符合实际 |
| 参数敏感度 | 低 | 高,阈值要跟着调 |
如果你处理的是山地丘陵流域,D8已经足够,主河道提取非常稳定;如果流域里有大面积平原或者洪泛区,建议试试Dinf,你会发现平原上的河道不再是一条笔直的斜线,而是有弯曲、有分叉。
换算法不复杂,就是把流向和累积量两步换成对应的多流向函数。但要注意:Dinf提取的河网细碎得多,阈值通常要往上调,否则会出现大量没意义的细沟。我自己一般先用D8确定河网大致形态,再对不同区段用Dinf做局部精细提取。
4.2 可变阈值:按坡度区分山区与平原的河道标准
单一阈值最大的弱点是“一刀切”:在山区,500像元对应的面积已经足够形成明显河道;到了平原,同样的累积量可能只是一片漫流。一个实用做法是按坡度分两个阈值,陡坡区域用低阈值,平地用高阈值。
import numpy as np from osgeo import gdal # 读取累积量和坡度栅格,都转为numpy数组 accum_arr = accum.ReadAsArray() slope_arr = slope.ReadAsArray() # 坡度大于5度的区域当山区处理,阈值1000;其余区域阈值3000 threshold_map = np.where(slope_arr > 5, 1000, 3000) stream_mask = accum_arr >= threshold_map这段代码的核心是按地形条件生成一个“阈值栅格”,再拿累积量去比较。坡度阈值5度不是固定的,南方丘陵和西北干旱区差异很大,我一般先用坡度直方图看分布,把分位数在60%到80%之间的坡度值作为分界。生成stream_mask后,再转成栅格并矢量化,就能得到同时适应山区和平原的河网。要注意的是,两个区域的接合处可能出现河道不连续,因为阈值突变导致一边有河一边没有。处理办法是在坡度分界带做个缓冲区,让阈值平滑过渡,避免硬切。
4.3 矢量后处理:短线剔除与并行伪河道清理
栅格转矢量后,总有一堆长度几十米的短线,这是DEM噪声和流向抖动造成的伪河道。后处理的目标不是删“短”的线,而是删“没有汇水意义”的线。用投影坐标系算实际长度,再设定一个最小长度阈值。
import geopandas as gpd gdf = gpd.read_file("streams.shp") # 确认是投影坐标系,长度单位是米 gdf["length_m"] = gdf.geometry.length # 剔除200米以下的碎段,阈值根据分辨率和地形调整 gdf = gdf[gdf["length_m"] >= 200] # 如果有河道分级结果,可以按级别过滤,保留主河道 if "strahler" in gdf.columns: gdf = gdf[gdf["strahler"] >= 1] gdf.to_file("streams_clean.shp", driver="ESRI Shapefile")这里有一个血泪教训:不要为了图好看把最小长度设太大。我见过有人把500米以下的河道全删了,结果一条30公里长的真实支流被拦腰截成两段,因为它在某处穿过一个狭长的洼地,被分成了三段,中间那段只有300米。正确的做法是先做河网连接,把靠得近的线段合并,再按合并后的长度过滤。geopandas做这种操作比较方便,但要注意合并逻辑一定要跟流向一致,不能把上下游倒过来拼。
5. 河流提取避坑手册:五个高频翻车现场
这章整理几个最常见的失败模式,每一条都是“现象、原因、解决”三段式,按这个顺序去排查自己的数据通常能定位到问题。
5.1 提取结果全是平行直线河
现象:主河道两侧出现一排方向几乎一致的平行短线,尤其在陡坡段最明显,看起来像梳子齿。原因:DEM分辨率不够,河道在栅格上被“压扁”成一条陡峭的对角线,D8流向算法只能沿8个方向走,多次累积后就形成平行折线。解决:先用双线性重采样把DEM细化到更高分辨率,再用Dinf算法替代D8;最后用短线段剔除做清理。如果平行线仍然存在,多半是原始DEM本身有系统误差,需要换数据源。
5.2 河网断头,上游河源大量缺失
现象:主河道在下游连续,但一到上游就断成一段一段,或者整个河源区域什么都没有。原因:阈值定太高把上游低累积量区域全滤掉了;另一种可能是填洼不彻底,一些浅洼地没被填平,导致水在半路停下。解决:先用百分位法看累积量分布,把阈值设在P85到P95之间而不是拍脑袋;再检查填洼结果,如果填洼后仍有零星低值区,把填洼函数改成不限制深度版本重跑。断头严重的,可以分水系分级设阈值,源头区域单独用低一档的阈值提取再合并。
5.3 平原地区出现大面积怪异河网
现象:在几乎平的耕地上提取出一大片扇形、环形、放射状的河网,甚至出现“倒流”的假象。原因:D8在坡度为0的平坦区域没有明确方向,任何一点微小噪声就会决定水流方向,累积量在平面上随机分布,河网自然乱成一团。解决:平原区不要用D8,改用Dinf,并把坡度很小的区域单独处理;更可靠的办法是结合真实水系数据做“河道烧录”——把已知河道位置的高程压低1到3米,强制水流沿真实路线走。这个操作在人工灌区几乎是必须的,因为真实水系已经被人为改造,和DEM地形不再吻合。
注意:河道烧录会改变地形,做径流模拟时不要在原DEM上做,要保留一份未烧录的副本。
5.4 提取结果与实际河道整体偏移
现象:提取的河道走势和真实河流平行,看起来像把真实河道平移了几百米。原因:最常见的是坐标系不一致,DEM是CGCS2000,河流矢量是WGS84,两者在局部会偏移几十到几百米;其次是DEM数据本身有垂直误差,或者是旧地形数据,而河流已经自然改道。解决:第一步统一坐标系,用控制点检查两个图层同名点的偏移量;第二步对比两个数据源的时间,如果是旧DEM对不上新河道,这属于数据时效问题,不是算法问题。分析用途下,偏移超过两个像元就该修正,否则河网和流域边界叠出来都是错的。
5.5 填洼后地形失真,汇水面积对不上
现象:计算出的流域面积比官方数据小了10%到20%,或者流域边界明显偏移。原因:无限制填洼把所有洼地都填了,如果流域内有大型湖泊、水库这类真实蓄水体,填洼会把它们变成平地,从而移动分水岭位置。解决:填洼前先识别真实水体,把水体区域在高程数据里保留甚至压低,再进行填洼;流域边界提取时,用填洼后的流向而非原始DEM算分水岭。蓄水体面积占比大的流域,这一步的误差直接影响汇水面积计算,不要忽略。
6. 进阶:用真实水系验证提取质量并输出分级河网
到了这一步,你的河网已经能看了,但还不能说“提取对了”。验证的方法是把提取结果和公开水系数据做定量对比,用三个指标衡量质量:命中率、长度比、河源命中率。命中率指提取河道与真实水系相交部分的长度占比,反映“该有的河有没有”;长度比指提取河道总长度与真实水系总长度的比值,反映“是不是多画了太多假河”;河源命中率是上游起点与真实河源的吻合程度。
用geopandas做相交对比很直接:先统一投影,再用空间叠加求两个图层的交线长度。交线越长,说明提取结果与真实水系越吻合。如果交线占比高但总长度比远大于1,多半是伪河道太多,回到阈值上调;如果交线占比低,先查坐标系,再回头检查预处理。
验证通过后,可以给河网做分级。最常用的是Strahler分级:没有支流的河道为1级,两条1级汇成2级,两条2级汇成3级,以此类推。分级不仅是好看,它直接对应河道的汇水能力,后续做缓冲区分析、河道宽度估算、生态流量计算都能用上。
# 在已有二值河网栅格基础上做Strahler分级 ordered = wbe.strahler_order(streams) wbe.write_raster(ordered, "strahler.tif", compress=True)分完级再转矢量,属性表里就带上了级别字段。渲染时把1级河画细线、主干河画粗线,图面会清楚很多。入库前反复确认阈值与分级值,因为我做这个方向最大的教训就是:流程跑通后的兴奋期,最容易轻视验证环节,等发现结果用不了再返工,时间已经花掉一倍。希望帮到你。
本文还有配套的精品资源,点击获取