侧边栏壁纸
博主头像
YouGIS博文 - YouGIS顽石工坊 博主等级

行动起来,活在当下

  • 累计撰写 71 篇文章
  • 累计创建 97 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

DEM视域分析(Viewshed):视线追踪原理与Python实现

Administrator
2026-08-03 / 0 评论 / 0 点赞 / 17 阅读 / 0 字

主页:yougis.com.cn
博文:
blog.yougis.com.cn
工具:
yougis.com.cn

GIS数据:yougis.com.cn/res/home

qr-wechat.jpg

扫码获取更多精彩内容

gh_1b60b293e57f_430.jpg

一键获取海量空间数据

概述

视域分析(Viewshed)是DEM最具实用价值的应用之一——计算在指定观测点观察时,地表的可见/遮挡区域分布。本文从视线追踪算法原理到代码实现,完整讲解单点视域、累积视域和最优选址。

数据基础:ASTER GDEM V003,30m分辨率,WGS84(EPSG:4326),int16,NoData=-9999。全国34省已裁切,覆盖N18°~N53°、E73°~E134°。


1. 原理

1.1 视线追踪(Line of Sight)

核心算法是视线追踪。设观测点O、目标点T,沿O→T方向的中间像素序列为 P_1, P_2, …, P_n。

视线上 P_i 处的高程(线性插值):

los_h(P_i) = h_O + (h_T - h_O) × |O→P_i| / |O→T|

其中 h_O = DEM[O] + 观测高度,h_T = DEM[T] + 目标高度。

判断规则:如果存在 i ∈ {1, …, n-1} 使得 DEM[P_i] > los_h(P_i),则T不可见

1.2 计算复杂度

方法

复杂度

说明

朴素算法

O(N^1.5)

每个像素独立追踪视线

R2算法

O(N)

极坐标射线扫描,增量计算

参考面算法

O(N)

预计算参考面(Wang et al., 2000)

省级DEM(~5000×5000),GDAL单点视域约30秒~2分钟。

1.3 地球曲率校正

搜索半径>10km时需考虑地球曲率。距观测点d处,目标点相对切平面的下降量:

Δh = d² / (2R) ≈ d² / 12742000  (R≈6371km)

距离

下降量

5km

2.0m

10km

7.8m

15km

17.6m

30km

70.6m

标准大气折射使视线弯曲向下,等效地球半径增大约1/6,实际校正系数为0.85714(=1/1.1667)。


2. 关键参数

2.1 参数清单

参数

含义

推荐值

观测高度(Observer Offset)

观测点离地面高度

人眼1.7m,瞭望塔15~25m,基站30~50m

目标高度(Target Offset)

目标像素离地面高度

地面0m,手机1.5m

搜索半径(Max Distance)

最大计算范围

5~30km

方位角范围(Azimuth)

水平观测扇区

0~360°全方向

俯仰角范围(Vertical Angle)

垂直观测范围

-90°~+90°

曲率校正(Curvature Coeff)

地球曲率+大气折射

0.85714

2.2 观测高度与视域关系

高度越高视域越大,但边际效益递减。山区30m DEM上典型规律:

观测高度

相对可见面积

备注

1.7m

1.0(基准)

人眼

8m

~1.3×

监控杆

15m

~1.6×

瞭望塔

30m

~1.9×

通信塔

50m

~2.1×

高塔

100m

~2.3×

边际效益很小


3. Python实现

3.1 GDAL命令行(推荐)

# 基本视域
gdaldem viewshed dem.tif viewshed.tif \
    -ox 114.5 -oy 30.5 \   # 观测点经纬度
    -oz 1.7 \               # 观测高度(m)
    -md 10000               # 最大搜索半径(m)

# 通信基站(30m高,15km半径,含曲率校正)
gdaldem viewshed dem.tif vs_celltower.tif \
    -ox 114.5 -oy 30.5 \
    -oz 30 -md 15000 \
    -cc 0.85714

# 瞭望塔(15m高,12km半径)
gdaldem viewshed dem.tif vs_watchtower.tif \
    -ox 114.5 -oy 30.5 \
    -oz 15 -md 12000 \
    -cc 0.85714

3.2 WhiteboxTools

from whitebox import WhiteboxTools

wbt = WhiteboxTools()
wbt.set_workdir("/path/to/data/")

wbt.viewshed(
    "dem.tif",
    "viewshed.tif",
    "--viewpoint 2500,1800",   # 像素坐标(row,col)
    max_dist=10000              # 最大距离(m)
)

3.3 numpy手写版(原理理解用)

import numpy as np
import rasterio

def viewshed_numpy(dem_path, vp_row, vp_col, obs_height=1.7,
                   max_dist_m=10000, target_height=0.0,
                   curvature=True):
    """
    视域分析(numpy手写版)
    
    参数:
        dem_path: DEM文件路径
        vp_row, vp_col: 观测点像素坐标
        obs_height: 观测高度(m)
        max_dist_m: 最大搜索半径(m)
        target_height: 目标高度(m)
        curvature: 是否进行地球曲率校正
    
    返回:
        visibility: 可见性矩阵(1=可见, 0=不可见)
    """
    with rasterio.open(dem_path) as src:
        dem = src.read(1).astype(np.float64)
        nodata = src.nodata
        if nodata is not None:
            dem[dem == nodata] = -9999
        cellsize_m = abs(src.res[0]) * 111320.0  # 度→米
    
    rows, cols = dem.shape
    max_pixels = int(max_dist_m / cellsize_m)
    
    # 观测点高程 + 观测高度
    h_obs = dem[vp_row, vp_col] + obs_height
    R_eff = 6371000.0 * 1.1667  # 大气折射等效地球半径
    
    visible = np.zeros_like(dem, dtype=np.uint8)
    visible[vp_row, vp_col] = 1
    
    r_min = max(0, vp_row - max_pixels)
    r_max = min(rows, vp_row + max_pixels + 1)
    c_min = max(0, vp_col - max_pixels)
    c_max = min(cols, vp_col + max_pixels + 1)
    
    for r in range(r_min, r_max):
        for c in range(c_min, c_max):
            if r == vp_row and c == vp_col:
                continue
            
            # 距离计算
            dr = r - vp_row
            dc = c - vp_col
            dist_m = np.sqrt(dr**2 + dc**2) * cellsize_m
            if dist_m > max_dist_m:
                continue
            
            h_target = dem[r, c] + target_height
            
            # 曲率校正:目标点下降量
            if curvature:
                h_target -= dist_m**2 / (2 * R_eff)
            
            # 生成视线上的采样点
            n_samples = max(int(dist_m / cellsize_m), 2)
            line_r = np.linspace(vp_row, r, n_samples + 1).astype(int)
            line_c = np.linspace(vp_col, c, n_samples + 1).astype(int)
            
            # 视线高程(线性插值)
            t = np.linspace(0, 1, n_samples + 1)
            los_h = h_obs + (h_target - h_obs) * t
            
            # 曲率校正:视线上每点的下降量
            if curvature:
                dist_along = t * dist_m
                los_h -= dist_along**2 / (2 * R_eff)
            
            # 检查遮挡
            terrain_h = dem[line_r, line_c]
            # 排除端点(起点和终点本身)
            blocked = np.any(terrain_h[1:-1] > los_h[1:-1] + 0.01)
            
            visible[r, c] = 0 if blocked else 1
    
    return visible


def save_viewshed(visible, ref_path, out_path):
    with rasterio.open(ref_path) as src:
        profile = src.profile.copy()
    profile.update(dtype='uint8', nodata=255)
    with rasterio.open(out_path, 'w', **profile) as dst:
        dst.write(visible.astype(np.uint8), 1)

注意:numpy版O(N^1.5),仅用于理解。生产环境使用GDAL或WhiteboxTools。

3.4 可视化

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

def plot_viewshed(dem_path, viewshed_path, vp_row, vp_col):
    fig, axes = plt.subplots(1, 3, figsize=(20, 7))

    with rasterio.open(dem_path) as src:
        dem = src.read(1).astype(float)
        dem[dem == src.nodata] = np.nan
    with rasterio.open(viewshed_path) as src:
        vis = src.read(1)

    # DEM
    ax = axes[0]
    im = ax.imshow(dem, cmap='terrain')
    ax.plot(vp_col, vp_row, 'rv', markersize=14, zorder=5)
    ax.set_title('DEM高程', fontsize=14, fontweight='bold')
    plt.colorbar(im, ax=ax, shrink=0.7, label='m')

    # 视域二值图
    ax = axes[1]
    cmap = ListedColormap(['#bdc3c7', '#27ae60'])
    ax.imshow(vis, cmap=cmap, vmin=0, vmax=1)
    ax.plot(vp_col, vp_row, 'rv', markersize=14, zorder=5)
    ax.set_title('视域(绿=可见 灰=遮挡)', fontsize=14, fontweight='bold')

    # 叠加(DEM半透明 + 视域高亮)
    ax = axes[2]
    ax.imshow(dem, cmap='gray')
    masked = np.ma.masked_where(vis == 0, vis)
    ax.imshow(masked, cmap=ListedColormap(['#2ecc71']), alpha=0.5)
    ax.plot(vp_col, vp_row, 'r^', markersize=14, zorder=5)
    ax.set_title('叠加效果', fontsize=14, fontweight='bold')

    plt.tight_layout()
    plt.savefig('viewshed_result.png', dpi=150, bbox_inches='tight')
    plt.show()

viewshed_4panel.png

4. 累积视域与最优选址

4.1 累积视域(Cumulative Viewshed)

def cumulative_viewshed(dem_path, viewpoints, out_path,
                        max_dist=10000, obs_h=15):
    """多观测点累积视域"""
    wbt = WhiteboxTools()
    cumul = None

    for i, (r, c) in enumerate(viewpoints):
        tmp = f"tmp_vs_{i}.tif"
        wbt.viewshed(dem_path, tmp, f"--viewpoint {r},{c}",
                     max_dist=max_dist)
        with rasterio.open(tmp) as src:
            v = src.read(1).astype(np.int16)
        cumul = v if cumul is None else cumul + v

    # 保存
    with rasterio.open(dem_path) as src:
        profile = src.profile.copy()
    profile.update(dtype='int16', nodata=-1)
    with rasterio.open(out_path, 'w', **profile) as dst:
        dst.write(cumul.astype('int16'), 1)

    # 统计
    total = cumul.size
    covered = (cumul > 0).sum()
    multi = (cumul >= 2).sum()
    print(f"候选点数: {len(viewpoints)}")
    print(f"覆盖率: {covered/total*100:.1f}%")
    print(f"双重覆盖: {multi/total*100:.1f}%")
    return cumul

4.2 贪心最优选址

def greedy_optimal_placement(dem_path, candidates,
                              target_coverage=0.95,
                              max_observers=15,
                              max_dist=10000, obs_h=15):
    """
    贪心算法:从候选点中选择最少观测点实现目标覆盖率
    
    参数:
        candidates: 候选点列表[(row,col), ...]
        target_coverage: 目标覆盖率
        max_observers: 最大观测点数
    
    返回:
        selected: 选中的观测点索引
    """
    wbt = WhiteboxTools()

    # 预计算各候选点视域
    vsheds = {}
    for i, (r, c) in enumerate(candidates):
        tmp = f"tmp_cand_{i}.tif"
        wbt.viewshed(dem_path, tmp, f"--viewpoint {r},{c}",
                     max_dist=max_dist)
        with rasterio.open(tmp) as src:
            vsheds[i] = src.read(1).astype(bool)

    selected = []
    covered = np.zeros_like(vsheds[0], dtype=bool)

    for step in range(max_observers):
        best_i, best_gain = None, 0
        for i, vs in vsheds.items():
            if i in selected:
                continue
            gain = (vs & ~covered).sum()
            if gain > best_gain:
                best_gain, best_i = gain, i

        if best_i is None:
            break

        selected.append(best_i)
        covered |= vsheds[best_i]
        cov = covered.sum() / covered.size
        print(f"  Step {step+1}: 选点{candidates[best_i]}, "
              f"新增{best_gain}像素, 累计覆盖率={cov:.1%}")

        if cov >= target_coverage:
            print(f"  达到目标覆盖率{target_coverage:.0%}!")
            break

    return selected

5. 应用场景

viewshed_detail.png

领域

应用

典型参数

通信工程

5G基站/微波中继站选址

obs=30m, dist=15km, 曲率校正

森林防火

瞭望塔布设

obs=15m, dist=12km

军事

雷达阵地/哨所选址

obs=10~30m, dist=20~50km

旅游

观景台选址

obs=1.7m, dist=5~10km

环保

风电场/高压线视觉影响

obs=1.7m, dist=10~15km

监控

城市天网摄像头

obs=8m, dist=1~3km


viewshed_3d.png

6. 软件工具对照

工具

命令/路径

曲率校正

最优选址

GDAL

gdaldem viewshed

✅ (-cc)

ArcGIS

Surface → Viewshed

Viewshed优化工具

ArcGIS Pro

Visibility

QGIS/GRASS

r.los / r.viewshed

WhiteboxTools

viewshed


7. 数据获取

本文分析基于 ASTER GDEM V003(30m精度),全国34省已裁切处理完毕。

方式一:公众号私信

关键词

获取内容

示例

DEM-省份名

省级裁切成果

DEM-云南

DEM-N{纬度}E{经度}

原始分幅

DEM-N25E100

方式二:YouGIS数据助手


参考文献

  1. Franklin, W.R. & Ray, C.K. (1994). Higher isn’t necessarily better: Visibility algorithms and experiments. Advances in GIS Research, 751-770.

  2. Wang, J., Robinson, G.J. & White, K. (2000). Generating viewsheds without using sightlines. PE&RS, 66(1):87-90.

  3. De Floriani, L. & Magillo, P. (2003). Algorithms for visibility computation on terrains: a survey. Environment and Planning B, 30(5):709-728.

  4. NASA/METI. ASTER GDEM V003 (2019). https://asterweb.jpl.nasa.gov/gdem.asp

  5. Lindsay, J.B. (2016). WhiteboxTools: An open-source platform for geospatial analysis. https://whiteboxgeo.com


数据来源:ASTER GDEM V003(NASA/METI),YouGIS顽石整理加工与分享。

0

评论区