附录E:实施参考(GDAL/PROJ示例)

本附录提供GDAL和PROJ库的实用代码示例,涵盖Python编程接口、命令行工具使用以及实际应用场景。示例从基础操作到高级应用,帮助读者快速掌握地图投影和坐标转换的实践技能。


E.1 Python环境配置

E.1.1 安装GDAL和PROJ

通过PyPI安装

# 安装GDAL
pip install GDAL

# 安装pyproj(PROJ的Python接口)
pip install pyproj

通过Conda安装(推荐)

# 创建新环境
conda create -n geospatial python=3.11

# 激活环境
conda activate geospatial

# 安装包
conda install -c conda-forge gdal pyproj rasterio

E.1.2 版本检查

# 检查GDAL版本
from osgeo import gdal
print(f"GDAL Version: {gdal.__version__}")

# 检查PROJ版本
import pyproj
print(f"PROJ Version: {pyproj.proj_version_str}")

E.2 坐标参考系统管理(PROJ/PyProj)

E.2.1 基础查询与验证

E.2.1.1 查询CRS信息

from pyproj import CRS

# 通过EPSG代码创建CRS
crs_4326 = CRS.from_epsg(4326)
crs_3857 = CRS.from_epsg(3857)

# 获取CRS信息
print(f"EPSG:4326 - {crs_4326.name}")
print(f"类型: {crs_4326.type_info}")
print(f"椭球体: {crs_4326.ellipsoid.name}")
print(f"单位: {crs_4326.axis_info[0].unit_name}")

# 判断CRS是否为地理坐标系统
print(f"是否为地理坐标系统: {crs_4326.is_geographic}")
print(f"是否为投影坐标系统: {crs_3857.is_projected}")

E.2.1.2 通过WKT创建CRS

# 通过WKT字符串创建CRS
wkt = """
GEOGCS["WGS 84",
    DATUM["World Geodetic System 1984",
        SPHEROID["WGS 84",6378137,298.257223563,
            AUTHORITY["EPSG","7030"]],
        AUTHORITY["EPSG","6326"]],
    PRIMEM["Greenwich",0,
        AUTHORITY["EPSG","8901"]],
    UNIT["degree",0.0174532925199433,
        AUTHORITY["EPSG","9122"]],
    AUTHORITY["EPSG","4326"]]
"""
crs_from_wkt = CRS.from_wkt(wkt)
print(f"EPSG代码: {crs_from_wkt.to_epsg()}")

E.2.1.3 通过PROJ字符串创建CRS

# 传统PROJ4字符串
proj_string = "+proj=longlat +datum=WGS84 +no_defs"
crs_from_proj = CRS.from_proj4(proj_string)
print(f"CRS名称: {crs_from_proj.name}")

E.2.2 坐标变换

E.2.2.1 单点转换

from pyproj import Transformer

# 创建变换器
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)

# 转换单个坐标点(北京天安门)
lon, lat = 116.4074, 39.9042
x, y = transformer.transform(lon, lat)

print(f"经纬度: ({lon}, {lat})")
print(f"Web Mercator: ({x:.2f}, {y:.2f})")

E.2.2.2 批量坐标转换

# 准备多个坐标点
coords = [
    (116.4074, 39.9042),  # 北京天安门
    (121.4737, 31.2304),  # 上海外滩
    (113.2644, 23.1291),  # 广州塔
]

# 批量转换
utm_zone = 50  # 中国大部分地区在50N
transformer_utm = Transformer.from_crs("EPSG:4326", f"EPSG:326{utm_zone}", always_xy=True)

results = [transformer_utm.transform(lon, lat) for lon, lat in coords]

for (lon, lat), (x, y) in zip(coords, results):
    print(f"({lon:.4f}, {lat:.4f}) -> UTM{utm_zone}N: ({x:.2f}, {y:.2f})")

E.2.2.3 NumPy数组转换(高效)

import numpy as np
from pyproj import Transformer

# 创建大数组
n = 1000000
lons = np.linspace(110, 120, n)
lats = np.linspace(30, 40, n)
coords = np.column_stack([lons, lats])

# 转换
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
x, y = transformer.transform(coords[:, 0], coords[:, 1])

print(f"转换了{n:,}个坐标点")
print(f"x范围: [{x.min():.2f}, {x.max():.2f}]")
print(f"y范围: [{y.min():.2f}, {y.max():.2f}]")

E.2.3 中国区域常用CRS变换

E.2.3.1 WGS84到CGCS2000

from pyproj import Transformer

# WGS84 (EPSG:4326) 到 CGCS2000 (EPSG:4490)
transformer = Transformer.from_crs("EPSG:4326", "EPSG:4490", always_xy=True)

# 广州塔坐标
lon, lat = 113.31917, 23.10427
lon_cgcs, lat_cgcs = transformer.transform(lon, lat)

print(f"WGS84: ({lon:.8f}, {lat:.8f})")
print(f"CGCS2000: ({lon_cgcs:.8f}, {lat_cgcs:.8f})")
print(f"差异: ({abs(lon-lon_cgcs)*111320*math.cos(math.radians(lat)):.3f}m, {abs(lat-lat_cgcs)*110574:.3f}m)")

E.2.3.2 CGCS2000到UTM投影

# 判断UTM带
def get_utm_zone(lon):
    return int((lon + 180) / 6) + 1

lon, lat = 113.31917, 23.10427
utm_zone = get_utm_zone(lon)
epsg_utm = f"EPSG:326{utm_zone}"  # 北半球

transformer = Transformer.from_crs("EPSG:4490", epsg_utm, always_xy=True)
easting, northing = transformer.transform(lon, lat)

print(f"UTM Zone: {utm_zone}N")
print(f"Easting: {easting:.2f}m")
print(f"Northing: {northing:.2f}m")

E.2.4 高级变换:网格基准变换

# 使用网格进行高精度基准变换(如果可用)
# 注意:需要安装PROJ资源文件(+nadgrids)

try:
    # NAD83到WGS84使用NTv2网格
    transformer = Transformer.from_pipeline(
        "+proj=pipeline +step +proj unitconvert +xy_in=deg +xy_out=rad "
        "+step +proj=hgridshifts +grids=ntv2_0.gsb "
        "+step +proj unitconvert +xy_in=rad +xy_out=deg"
    )

    lon, lat = -75, 40  # 纽约附近
    lon_trans, lat_trans = transformer.transform(lon, lat)
    print(f"基准变换: ({lon}, {lat}) -> ({lon_trans:.10f}, {lat_trans:.10f})")
except Exception as e:
    print(f"网格变换可能需要额外资源: {e}")

E.2.5 边界与区域转换

from shapely.geometry import Polygon, box
from pyproj import CRS, Transformer
import numpy as np

# 创建一个矩形区域
min_lon, min_lat = 116.0, 39.0
max_lon, max_lat = 117.0, 40.0
polygon = box(min_lon, min_lat, max_lon, max_lat)

# 转换为UTM并计算面积
utm_zone = get_utm_zone((min_lon + max_lon) / 2)
epsg_utm = f"EPSG:326{utm_zone}"

transformer = Transformer.from_crs("EPSG:4326", epsg_utm, always_xy=True)

# 提取边界并转换
coords = list(polygon.exterior.coords)
transformed_coords = [transformer.transform(lon, lat) for lon, lat in coords]

# 创建变换后的多边形
utm_polygon = Polygon(transformed_coords)
area_km2 = utm_polygon.area / 1_000_000

print(f"区域面积: {area_km2:.2f} km²")

E.3 栅格数据处理(GDAL)

E.3.1 读取栅格数据

E.3.1.1 基本读取

from osgeo import gdal, osr
import numpy as np

# 打开GeoTIFF文件
dataset = gdal.Open('input.tif')

if dataset:
    # 获取基本信息
    print(f"驱动: {dataset.GetDriver().ShortName}")
    print(f"宽度: {dataset.RasterXSize}")
    print(f"高度: {dataset.RasterYSize}")
    print(f"波段数: {dataset.RasterCount}")

    # 获取地理转换参数(仿射变换)
    geotransform = dataset.GetGeoTransform()
    print(f"原点X: {geotransform[0]:.2f}")
    print(f"X分辨率: {geotransform[1]:.2f}")
    print(f"旋转: {geotransform[2]:.2f}")
    print(f"原点Y: {geotransform[3]:.2f}")
    print(f"旋转: {geotransform[4]:.2f}")
    print(f"Y分辨率: {geotransform[5]:.2f}")

    # 获取投影
    prj = dataset.GetProjection()
    srs = osr.SpatialReference(wkt=prj)
    print(f"CRS: {srs.GetAttrValue('projcs') or srs.GetAttrValue('geogcs')}")

    # 读取第一个波段
    band = dataset.GetRasterBand(1)
    data = band.ReadAsArray()
    print(f"数据形状: {data.shape}")
    print(f"数据类型: {gdal.GetDataTypeName(band.DataType)}")

dataset = None  # 关闭数据集

E.3.1.2 子区域读取(高效处理大文件)

import numpy as np

dataset = gdal.Open('large_raster.tif')

# 定义子区域
xoff, yoff = 1000, 1000  # 起始位置(像素)
xsize, ysize = 512, 512   # 读取大小

# 读取子区域
band = dataset.GetRasterBand(1)
subdata = band.ReadAsArray(xoff, yoff, xsize, ysize)

# 计算子区域的地理坐标
geotransform = dataset.GetGeoTransform()
sub_min_x = geotransform[0] + xoff * geotransform[1]
sub_max_y = geotransform[3] + yoff * geotransform[5]
sub_max_x = sub_min_x + xsize * geotransform[1]
sub_min_y = sub_max_y + ysize * geotransform[5]

print(f"子区域空间范围:")
print(f"X: [{sub_min_x:.2f}, {sub_max_x:.2f}]")
print(f"Y: [{sub_min_y:.2f}, {sub_max_y:.2f}]")

dataset = None

E.3.2 栅格重投影

E.3.2.1 基本重投影

from osgeo import gdal

# 打开输入数据
dataset = gdal.Open('input_4326.tif')

# 执行重投影
output_file = 'output_3857.tif'
gdal.Warp(
    output_file,
    dataset,
    dstSRS='EPSG:3857',           # 目标CRS
    resampleAlg=gdal.GRA_Bilinear, # 双线性插值
    xRes=30, yRes=30               # 输出分辨率(米)
)

print(f"重投影完成: {output_file}")

E.3.2.2 保持原始分辨率重投影

# 计算原始分辨率
dataset = gdal.Open('input.tif')
geotransform = dataset.GetGeoTransform()
pixel_size_x = abs(geotransform[1])
pixel_size_y = abs(geotransform[5])

# 重投影(自动分辨率计算)
gdal.Warp(
    'output_resamp.tif',
    dataset,
    dstSRS='EPSG:3857',
    dstAT=True,  # 调整变换以保持最佳分辨率
    targetAlignedPixels=True
)

dataset = None

E.3.2.3 重采样方法选择

方法 gdal常量 使用场景
最近邻 GRA_NearestNeighbour 分类数据,保持原始值
双线性 GRA_Bilinear 连续数据,平滑过渡
三次卷积 GRA_Cubic 质量优先,计算量较大
Lanczos GRA_Lanczos 高质量重采样
# 高质量重采样
gdal.Warp(
    'output_high_quality.tif',
    input_dataset,
    dstSRS='EPSG:3857',
    resampleAlg=gdal.GRA_Cubic,
    warpOptions=["NUM_THREADS=ALL_CPUS"]
)

E.3.3 栅格裁剪与裁剪

E.3.3.1 按边界框裁剪

from osgeo import gdal, osr

dataset = gdal.Open('input.tif')

# 定义裁剪边界(投影坐标)
min_x, max_x = 116.0, 117.0
min_y, max_y = 39.0, 40.0

# 如果裁剪范围与输入CRS不同,需要先转换
# 此处假设为相同CRS

output_file = 'clipped.tif'
gdal.Translate(
    output_file,
    dataset,
    projWin=[min_x, max_y, max_x, min_y],  # [x_min, y_max, x_max, y_min]
    outputBounds=[min_x, min_y, max_x, max_y]
)

dataset = None

E.3.3.2 按矢量多边形裁剪

from osgeo import gdal, ogr

# 打开裁剪矢量
shapefile = 'clip_polygon.shp'
vector_ds = ogr.Open(shapefile)
vector_layer = vector_ds.GetLayer()

# 执行裁剪
gdal.Warp(
    'clipped_by_vector.tif',
    'input.tif',
    cutlineDSName=shapefile,
    cutlineLayer=vector_layer.GetName(),
    cropToCutline=True,
    dstNodata=-9999  # 裁剪区域外的填充值
)

vector_ds = None

E.3.4 栅格金字塔构建

from osgeo import gdal

dataset = gdal.Open('input.tif', gdal.GA_Update)

# 构建金字塔层级
# 层级: [2, 4, 8, 16, 32] 意味着构建2倍、4倍等下采样
gdal.BuildOverviews(
    dataset,
    overviewlist=[2, 4, 8, 16, 32],
    resamplingmethod='AVERAGE'  # 平均值下采样
)

# 验证金字塔
print("金字塔层级:")
for i, overview in enumerate(dataset.GetRasterBand(1).GetOverviews()):
    print(f"  层级 {i+1}: {overview.XSize} x {overview.YSize}")

dataset = None

E.3.5 创建栅格数据

import numpy as np
from osgeo import gdal, osr

# 创建示例数据
width, height = 1000, 1000
data = np.random.rand(height, width) * 100

# 设置地理范围和投影
min_x, min_y = 116.0, 39.0
pixel_size = 0.001  # 约100米
max_x = min_x + width * pixel_size
max_y = min_y + height * pixel_size

# 设置投影
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
prj = srs.ExportToWkt()

# 创建GeoTIFF
driver = gdal.GetDriverByName('GTiff')
dataset = driver.Create('output.tif', width, height, 1, gdal.GDT_Float64)

# 设置地理变换和投影
dataset.SetGeoTransform([min_x, pixel_size, 0, max_y, 0, -pixel_size])
dataset.SetProjection(prj)

# 写入数据
band = dataset.GetRasterBand(1)
band.SetNoDataValue(-9999)
band.WriteArray(data)

# 计算统计数据并压缩
band.FlushCache()
band.ComputeStatistics(False)

dataset = None

print("栅格文件创建完成: output.tif")

E.3.6 多波段处理(RGB合成)

from osgeo import gdal

# 打开多个单波段文件(如红绿蓝)
red_band = gdal.Open('red.tif').GetRasterBand(1)
green_band = gdal.Open('green.tif').GetRasterBand(1)
blue_band = gdal.Open('blue.tif').GetRasterBand(1)

# 创建输出文件(3波段)
driver = gdal.GetDriverByName('GTiff')
width, height = red_band.XSize, red_band.YSize
output = driver.Create('rgb.tif', width, height, 3, gdal.GDT_Byte)

# 复制地理变换和投影
input_ds = gdal.Open('red.tif')
output.SetGeoTransform(input_ds.GetGeoTransform())
output.SetProjection(input_ds.GetProjection())

# 写入波段
output.GetRasterBand(1).WriteArray(red_band.ReadAsArray())
output.GetRasterBand(2).WriteArray(green_band.ReadAsArray())
output.GetRasterBand(3).WriteArray(blue_band.ReadAsArray())

# 设置波段名称
output.GetRasterBand(1).SetDescription('Red')
output.GetRasterBand(2).SetDescription('Green')
output.GetRasterBand(3).SetDescription('Blue')

output = None
print("RGB合成完成: rgb.tif")

E.4 矢量数据处理(GDAL/OGR)

E.4.1 读取矢量数据

E.4.1.1 遍历要素

from osgeo import ogr

# 打开Shapefile
driver = ogr.GetDriverByName('ESRI Shapefile')
data_source = driver.Open('input.shp', 0)  # 0: 只读

if data_source:
    layer = data_source.GetLayer()
    layer_defn = layer.GetLayerDefn()

    print(f"坐标系: {layer.GetSpatialRef().GetName()}")
    print(f"要素数量: {layer.GetFeatureCount()}")
    print(f"字段数: {layer_defn.GetFieldCount()}")

    # 遍历前5个要素
    for i, feature in enumerate(layer):
        if i >= 5:
            break

        geometry = feature.GetGeometryRef()
        print(f"\n要素 {i}:")
        print(f"  几何类型: {geometry.GetGeometryName()}")
        print(f"  WKT: {geometry.ExportToWkt()}")

        # 读取属性
        for j in range(layer_defn.GetFieldCount()):
            field_defn = layer_defn.GetFieldDefn(j)
            field_name = field_defn.GetName()
            field_value = feature.GetField(field_name)
            print(f"  {field_name}: {field_value}")

data_source = None

E.4.1.2 空间查询

data_source = driver.Open('input.shp')
layer = data_source.GetLayer()

# 创建查询空间(点)
from osgeo import ogr
query_point = ogr.CreateGeometryFromWkt('POINT(116.4074 39.9042)')

# 设置空间过滤器
layer.SetSpatialFilter(query_point)

# 查询结果
print("包含该点的要素:")
for feature in layer:
    geometry = feature.GetGeometryRef()
    print(f"ID: {feature.GetFID()}, 几何: {geometry.GetGeometryName()}")

data_source = None

E.4.2 创建矢量数据

E.4.2.1 创建点要素

from osgeo import ogr, osr

# 创建输出文件
driver = ogr.GetDriverByName('ESRI Shapefile')
data_source = driver.CreateDataSource('output_points.shp')

# 设置投影
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)

# 创建图层
layer = data_source.CreateLayer('points', srs, ogr.wkbPoint)

# 创建字段
layer.CreateField(ogr.FieldDefn('name', ogr.OFTString))
layer.CreateField(ogr.FieldDefn('population', ogr.OFTInteger))

# 创建要素
cities = [
    ('北京', 116.4074, 39.9042, 21540000),
    ('上海', 121.4737, 31.2304, 24870000),
    ('广州', 113.2644, 23.1291, 18000000),
]

for name, lon, lat, pop in cities:
    feature = ogr.Feature(layer.GetLayerDefn())
    feature.SetField('name', name)
    feature.SetField('population', pop)

    # 创建几何
    point = ogr.Geometry(ogr.wkbPoint)
    point.AddPoint(lon, lat)
    feature.SetGeometry(point)

    layer.CreateFeature(feature)
    feature = None  # 释放资源

data_source = None
print(f"创建要素完成: output_points.shp")

E.4.2.2 创建多边形要素

data_source = driver.CreateDataSource('output_polygons.shp')
srs = osr.SpatialReference()
srs.ImportFromEPSG(3857)

layer = data_source.CreateLayer('polygons', srs, ogr.wkbPolygon)
layer.CreateField(ogr.FieldDefn('name', ogr.OFTString))
layer.CreateField(ogr.FieldDefn('area_km2', ogr.OFTReal))

# 创建多边形
polygon = ogr.Geometry(ogr.wkbPolygon)
ring = ogr.Geometry(ogr.wkbLinearRing)

# 多边形顶点(顺时针)
ring.AddPoint(12960000, 4850000)
ring.AddPoint(12965000, 4850000)
ring.AddPoint(12965000, 4855000)
ring.AddPoint(12960000, 4855000)
ring.AddPoint(12960000, 4850000)  # 闭合

polygon.AddGeometry(ring)

# 创建要素
feature = ogr.Feature(layer.GetLayerDefn())
feature.SetField('name', '区域A')
feature.SetField('area_km2', polygon.GetArea() / 1_000_000)
feature.SetGeometry(polygon)

layer.CreateFeature(feature)

data_source = None

E.4.3 矢量重投影

E.4.3.1 使用ogr2ogr重投影

# 命令行方式(推荐)
import os

os.system('ogr2ogr -t_srs EPSG:3857 output_3857.shp input_4326.shp')

# Python API方式
from osgeo import ogr, osr

# 打开输入数据
in_ds = ogr.Open('input.shp')
in_layer = in_ds.GetLayer()
in_srs = in_layer.GetSpatialRef()

# 设置输出CRS
out_srs = osr.SpatialReference()
out_srs.ImportFromEPSG(3857)

# 创建坐标变换
coord_transform = osr.CoordinateTransformation(in_srs, out_srs)

# 创建输出数据
out_driver = ogr.GetDriverByName('ESRI Shapefile')
out_ds = out_driver.CreateDataSource('output.shp')
out_layer = out_ds.CreateLayer('output', out_srs, in_layer.GetGeomType())

# 创建字段
in_layer_defn = in_layer.GetLayerDefn()
for i in range(in_layer_defn.GetFieldCount()):
    out_layer.CreateField(in_layer_defn.GetFieldDefn(i))

# 转换要素
for in_feat in in_layer:
    out_feat = ogr.Feature(out_layer.GetLayerDefn())

    # 复制属性
    for i in range(in_layer_defn.GetFieldCount()):
        out_feat.SetField(i, in_feat.GetField(i))

    # 转换几何
    geom = in_feat.GetGeometryRef()
    geom.Transform(coord_transform)
    out_feat.SetGeometry(geom)

    out_layer.CreateFeature(out_feat)

in_ds = None
out_ds = None

E.4.4 空间分析

E.4.4.1 缓冲区分析

from osgeo import ogr

data_source = ogr.Open('input.shp')
layer = data_source.GetLayer()

# 创建缓冲区输出
out_driver = ogr.GetDriverByName('GeoJSON')
out_ds = out_driver.CreateDataSource('buffered.json')
out_layer = out_ds.CreateLayer('buffered', layer.GetSpatialRef())

# 缓冲距离(单位与CRS相同,如米)
buffer_distance = 1000

for feature in layer:
    geom = feature.GetGeometryRef()
    buffered_geom = geom.Buffer(buffer_distance)

    out_feat = ogr.Feature(out_layer.GetLayerDefn())
    out_feat.SetGeometry(buffered_geom)
    out_layer.CreateFeature(out_feat)

data_source = None
out_ds = None

E.4.4.2 求交分析

# 打开两个图层
roads_ds = ogr.Open('roads.shp')
roads_layer = roads_ds.GetLayer()

buildings_ds = ogr.Open('buildings.shp')
buildings_layer = buildings_ds.GetLayer()

# 创建输出
out_ds = ogr.GetDriverByName('GeoJSON').CreateDataSource('intersect.json')
out_layer = out_ds.CreateLayer('intersect', roads_layer.GetSpatialRef())

# 求交
for road_feat in roads_layer:
    road_geom = road_feat.GetGeometryRef()

    for build_feat in buildings_layer:
        build_geom = build_feat.GetGeometryRef()

        # 几何交集
        inter_geom = road_geom.Intersection(build_geom)

        if inter_geom.IsEmpty():
            continue

        out_feat = ogr.Feature(out_layer.GetLayerDefn())
        out_feat.SetGeometry(inter_geom)
        out_layer.CreateFeature(out_feat)

roads_ds = None
buildings_ds = None
out_ds = None

E.5 GDAL命令行工具

E.5.1 gdalinfo - 栅格信息查询

# 基本信息查询
gdalinfo input.tif

# 查询坐标系统详细信息
gdalinfo -proj4 input.tif

# 查询最小最大值
gdalinfo -mm input.tif

# 查询统计数据(近似)
gdalinfo -stats input.tif

# 仅查询元数据
gdalinfo -nomd input.tif

# 输出JSON格式
gdalinfo -json input.tif > info.json

E.5.2 gdal_translate - 格式转换

# 栅格格式转换
gdal_translate -of GTiff input.jpg output.tif

# 指定子区域(xoff yoff xsize ysize)
gdaltranslate -srcwin 0 0 1000 1000 input.tif output_subset.tif

# 裁剪矩形
gdal_translate -projwin 116.0 40.0 117.0 39.0 input.tif output_clipped.tif

# 压缩选项
gdal_translate -co COMPRESS=LZW -co TILED=YES -co BIGTIFF=YES input.tif output_compressed.tif

# 调整分辨率(放大)
gdal_translate -outsize 2000% 2000% input.tif output_scaled_up.tif

# 调整分辨率(缩小)
gdal_translate -outsize 50% 50% input.tif output_scaled_down.tif

E.5.3 gdalwarp - 重投影与处理

# 基本重投影
gdalwarp -t_srs EPSG:3857 input_4326.tif output_3857.tif

# 指定输出分辨率
gdalwarp -t_srs EPSG:3857 -tr 30 30 input.tif output_resampled.tif

# 重采样方法
gdalwarp -t_srs EPSG:3857 -r cubic input.tif output_cubic.tif
gdalwarp -t_srs EPSG:3857 -r near input.tif output_near.tif

# 裁剪重投影
gdalwarp -t_srs EPSG:3857 -te 12960000 4850000 12970000 4860000 input.tif output_clipped.tif
# -顺序: xmin ymin xmax ymax

# 重投影并裁剪(先投影后裁剪)
gdalwarp -t_srs EPSG:3857 -crop_to_cutline -cutline input_polygon.shp input.tif output.tif

# 多线程处理
gdalwarp -t_srs EPSG:3857 -wo NUM_THREADS=ALL_CPUS input.tif output.tif

# 目标对齐像素(瓦片对齐)
gdalwarp -t_srs EPSG:3857 -dstnodata -9999 -targetAlignedPixels input.tif output.tif

E.5.4 ogr2ogr - 矢量转换

# 格式转换
ogr2ogr -f "GeoJSON" output.json input.shp

# 重投影
ogr2ogr -t_srs EPSG:3857 output_3857.shp input_4326.shp

# 保持属性表结构
ogr2ogr -f "Esri Shapefile" -preserve_fid output.shp input.geojson

# 属性过滤(SQL)
ogr2ogr -where "population > 1000000" output.shp input.shp

# 使用空间查询
ogr2ogr -spat 116 39 118 41 output.shp input.shp

# 仅几何(无属性)
ogr2ogr -select "" output.shp input.shp

# 仅特定字段
ogr2ogr -select "name,population" output.shp input.shp

# 追加到现有文件
ogr2ogr -append -update output.shp input_new.shp

# 覆盖现有文件
ogr2ogr -overwrite output.shp input.shp

E.5.5 gdaladdo - 构建金字塔

# 构建金字塔
gdaladdo -r average input.tif 2 4 8 16 32

# 生成外部金字塔
gdaladdo -r average --config USE_RRD YES input.tif 2 4 8 16

# 仅验证是否已有金字塔
gdaladdo -r nearest input.tif --clean

E.6 实际应用案例

E.6.1 案例1:Web地图瓦片生成

场景说明:生成Web格式的地图瓦片(TMS/XYZ格式)

import math
from osgeo import gdal

def lonlat_to_tile(lon, lat, zoom):
    """将经纬度转换为瓦片坐标"""
    n = 2.0 ** zoom
    xtile = int((lon + 180.0) / 360.0 * n)
    ytile = int((1.0 - math.log(math.tan(math.radians(lat)) + 1.0/math.cos(math.radians(lat))) / math.pi) / 2.0 * n)
    return xtile, ytile

def tile_to_lonlat(xtile, ytile, zoom):
    """将瓦片坐标转换为经纬度"""
    n = 2.0 ** zoom
    lon = xtile / n * 360.0 - 180.0
    lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
    lat = math.degrees(lat_rad)
    return lon, lat

def generate_tile(input_raster, output_tile, x, y, zoom, size=256):
    """生成单个瓦片"""
    # 计算瓦片边界(Web Mercator,EPSG:3857)
    lon_max, lat_max = tile_to_lonlat(x, y, zoom)
    lon_min, lat_min = tile_to_lonlat(x + 1, y + 1, zoom)

    # 创建临时VRT
    vrt_options = gdal.WarpOptions(
        format='VRT',
        outputBounds=[lon_min, lat_min, lon_max, lat_max],
        xRes=size, yRes=size,
        dstSRS='EPSG:3857',
        resampleAlg=gdal.GRA_Bilinear
    )

    # 执行重投影并输出PNG
    gdal.Warp(
        output_tile,
        input_raster,
        options=gdal.WarpOptions(
            format='PNG',
            xRes=size,
            yRes=size,
            outputBounds=[lon_min, lat_min, lon_max, lat_max],
            dstSRS='EPSG:3857',
            resampleAlg=gdal.GRA_Bilinear
        )
    )

# 示例:生成缩放级别12的瓦片
input_raster = 'base_map.tif'
zoom = 12
x, y = lonlat_to_tile(116.4074, 39.9042, zoom)  # 北京

generate_tile(input_raster, f'tile_{zoom}_{x}_{y}.png', x, y, zoom)

print(f"生成瓦片: tile_{zoom}_{x}_{y}.png")

E.6.2 案例2:多源数据融合

场景说明:融合多个数据源(栅格+矢量)并进行统一投影

from osgeo import gdal, ogr, osr
import numpy as np

def create_base_raster(output_file, min_x, max_x, min_y, max_y, resolution=30):
    """创建基础栅格"""
    width = int((max_x - min_x) / resolution)
    height = int((max_y - min_y) / resolution)

    driver = gdal.GetDriverByName('GTiff')
    dataset = driver.Create(
        output_file,
        width,
        height,
        1,  # 单波段
        gdal.GDT_Float64
    )

    # 设置地理信息和投影
    dataset.SetGeoTransform([min_x, resolution, 0, max_y, 0, -resolution])

    srs = osr.SpatialReference()
    srs.ImportFromEPSG(3857)
    dataset.SetProjection(srs.ExportToWkt())

    # 初始化数据
    data = np.zeros((height, width), dtype=np.float64)
    band = dataset.GetRasterBand(1)
    band.WriteArray(data)
    band.SetNoDataValue(-9999)

    dataset = None
    return output_file

def rasterize_vector(raster_file, vector_file, burn_value=1):
    """将矢量栅格化到栅格"""
    # 打开栅格
    raster_ds = gdal.Open(raster_file, gdal.GA_Update)
    band = raster_ds.GetRasterBand(1)

    # 打开矢量
    vector_ds = ogr.Open(vector_file)
    vector_layer = vector_ds.GetLayer()

    # 栅格化
    gdal.RasterizeLayer(
        raster_ds,
        [1],
        vector_layer,
        burn_values=[burn_value]
    )

    raster_ds = None
    vector_ds = None

# 创建融合范围(北京区域)
min_x, max_x = 12900000, 13000000  # Web Mercator范围
min_y, max_y = 4850000, 4950000
resolution = 100

# 1. 创建基础栅格
base_raster = 'base_fusion.tif'
create_base_raster(base_raster, min_x, max_x, min_y, max_y, resolution)

# 2. 叠加矢量数据(如道路)
# 假设有roads.shp(已重投影到EPSG:3857)
# rasterize_vector(base_raster, 'roads.shp', burn_value=1)

# 3. 叠加另一栅格(如高程模型)
# elevation_ds = gdal.Open('dem.tif')
# warped_elevation = 'dem_warped.tif'
# gdal.Warp(warped_elevation, elevation_ds,
#           dstSRS='EPSG:3857',
#           outputBounds=[min_x, min_y, max_x, max_y],
#           xRes=resolution, yRes=resolution)

# 4. 数值计算栅格数据
ds = gdal.Open(base_raster, gdal.GA_Update)
band = ds.GetRasterBand(1)
data = band.ReadAsArray()

# 示例:添加随机噪声模拟高程
data += np.random.rand(*data.shape) * 10

band.WriteArray(data)
ds = None

print(f"数据融合完成: {base_raster}")

E.6.3 案例3:大数据批处理

场景说明:批量处理大量栅格文件(重投影+压缩)

import os
import glob
from osgeo import gdal
from concurrent.futures import ProcessPoolExecutor, as_completed

def process_raster(input_file, output_dir, target_crs='EPSG:3857'):
    """处理单个栅格文件"""
    basename = os.path.basename(input_file)
    output_file = os.path.join(output_dir, f'processed_{basename}')

    try:
        # 重投影并压缩
        gdal.Warp(
            output_file,
            input_file,
            dstSRS=target_crs,
            resampleAlg=gdal.GRA_Bilinear,
            creationOptions=[
                'COMPRESS=LZW',
                'TILED=YES',
                'BIGTIFF=IF_NEEDED'
            ],
            warpOptions=['NUM_THREADS=1']  # 每个进程单线程
        )
        print(f"完成: {output_file}")
        return output_file
    except Exception as e:
        print(f"错误处理 {input_file}: {e}")
        return None

def batch_process(input_dir, output_dir, num_workers=4):
    """批量处理"""
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)

    # 获取输入文件列表
    input_files = glob.glob(os.path.join(input_dir, '*.tif'))

    print(f"找到 {len(input_files)} 个文件待处理")

    # 多进程并行处理
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        futures = [executor.submit(process_raster, f, output_dir) for f in input_files]

        # 等待所有任务完成
        results = [future.result() for future in as_completed(futures)]

    success_count = sum(1 for r in results if r is not None)
    print(f"处理完成: {success_count}/{len(input_files)} 个文件")

# 使用示例
batch_process('input_tifs', 'output_tifs', num_workers=4)

E.6.4 案例4:动态投影服务

场景说明:根据用户请求动态重投影并返回数据

from flask import Flask, request, send_file
from osgeo import gdal
import tempfile
import os

app = Flask(__name__)

@app.route('/api/warp')
def warp_raster():
    """栅格重投影API"""
    # 获取参数
    input_file = request.args.get('input')
    target_crs = request.args.get('crs', 'EPSG:3857')
    resolution = float(request.args.get('resolution', 30))

    if not input_file or not os.path.exists(input_file):
        return "错误: 输入文件不存在", 400

    # 创建临时输出文件
    with tempfile.NamedTemporaryFile(suffix='.tif', delete=False) as tmp:
        output_file = tmp.name

    try:
        # 重投影
        gdal.Warp(
            output_file,
            input_file,
            dstSRS=target_crs,
            xRes=resolution,
            yRes=resolution,
            resampleAlg=gdal.GRA_Bilinear
        )

        # 返回文件
        return send_file(output_file, mimetype='image/tiff')

    except Exception as e:
        return f"错误: {str(e)}", 500

@app.route('/api/info')
def raster_info():
    """栅格信息查询API"""
    input_file = request.args.get('input')

    if not input_file or not os.path.exists(input_file):
        return "错误: 输入文件不存在", 400

    ds = gdal.Open(input_file)
    if not ds:
        return "错误: 无法打开文件", 400

    info = {
        'driver': ds.GetDriver().ShortName,
        'size': [ds.RasterXSize, ds.RasterYSize],
        'bands': ds.RasterCount,
        'projection': ds.GetProjection(),
        'geotransform': ds.GetGeoTransform()
    }

    ds = None
    return info

if __name__ == '__main__':
    app.run(debug=True, port=5000)

E.7 错误处理与最佳实践

E.7.1 常见错误与解决方案

E.7.1.1 投影错误

# 错误: 无法识别EPSG代码
try:
    crs = CRS.from_epsg(999999)  # 不存在
except Exception as e:
    print(f"CRS错误: {e}")
    # 解决方案: 检查EPSG代码是否正确

E.7.1.2 数据类型不匹配

# 错误: 栅格数据类型不兼容
dataset = gdal.Open('input.tif')
if dataset:
    band = dataset.GetRasterBand(1)
    data_type = band.DataType

    # 确保输出数据类型兼容
    driver = gdal.GetDriverByName('GTiff')
    output = driver.Create('output.tif', width, height, 1, data_type)  # 使用相同类型
    output = None
dataset = None

E.7.1.3 内存溢出

# 对大文件逐块处理,避免内存溢出
def process_large_raster(input_file, output_file, block_size=1024):
    dataset = gdal.Open(input_file)
    width, height = dataset.RasterXSize, dataset.RasterYSize

    driver = gdal.GetDriverByName('GTiff')
    output = driver.Create(output_file, width, height, 1, gdal.GDT_Float32)
    output.SetGeoTransform(dataset.GetGeoTransform())
    output.SetProjection(dataset.GetProjection())

    output_band = output.GetRasterBand(1)

    # 逐块处理
    for y in range(0, height, block_size):
        for x in range(0, width, block_size):
            block_width = min(block_size, width - x)
            block_height = min(block_size, height - y)

            data = dataset.GetRasterBand(1).ReadAsArray(x, y, block_width, block_height)

            # 处理数据(例如:乘以常数)
            processed_data = data * 1.5

            output_band.WriteArray(processed_data, x, y)

    dataset = None
    output = None

E.7.2 性能优化建议

1. 批量处理优于单个处理

# 推荐: 批量转换
gdal.Warp('output.tif', ['input1.tif', 'input2.tif'], ...)

# 避免: 逐个转换
gdal.Warp('output1.tif', 'input1.tif', ...)
gdal.Warp('output2.tif', 'input2.tif', ...)

2. 使用金字塔加速显示

gdaladdo -r average input.tif 2 4 8 16

3. 选择合适的压缩格式

# 连续数据: LZW
# 分类数据: DEFLATE或无压缩
# 网络: JPEG(如果质量可接受)

4. 多线程处理

gdalwarp --config GDAL_NUM_THREADS ALL_CPUS -t_srs EPSG:3857 input.tif output.tif

E.8 参考资源

E.8.1 官方文档

  • GDAL官网: https://gdal.org
  • PROJ官网: https://proj.org
  • PyProj文档: https://pyproj4.github.io/pyproj/
  • OSGeo wiki: https://trac.osgeo.org/

E.8.2 示例数据

  • Natural Earth数据: https://www.naturalearthdata.com/
  • OpenStreetMap: https://www.openstreetmap.org/
  • USGS Earth Explorer: https://earthexplorer.usgs.gov/

说明:本附录提供了GDAL和PROJ库的综合实践示例,涵盖从基础操作到复杂应用的完整流程。示例采用Python编程语言,配合命令行工具,旨在帮助读者快速掌握地图投影和坐标转换的实用技能。对于生产环境应用,建议进一步阅读官方文档并关注性能优化和错误处理。


This site uses Just the Docs, a documentation theme for Jekyll.