Un po' di tempo fa ho affrontato in un mio articolo il problema dell'aggiunta della coordinata altimetrica ad un dataset di punti usando QGIS. In questo JupyterNotebook affronterò il problema usando Python.

Prima di iniziare¶

Librerie¶

Per raggiungere l'obiettivo userò essenzialmente geopandas, shapely e rasterio. In verità tra gli import troverai anche pathlib e matplotlib ma non sono essenziali per la corretta riuscita del procedimento.

In [1]:
import pathlib

import pandas as pd
import geopandas as gpd
import rasterio as rio
from rasterio.plot import show, show_hist
import matplotlib.pyplot as plt
from shapely.geometry import Point

Fonti dati¶

Come DEM ho usato un tile del progetto TIN Italy a sua volta ritagliato e su cui ho generato con QGIS quattro punti random.

In [2]:
sample_data = pathlib.Path.cwd().parent.parent.joinpath('sample_data/add_z')
dem = sample_data.joinpath('clip_dem.tif')
points = sample_data.joinpath('points.shp')

Contenuti¶

  • 1. Quota altimetrica costante
  • 2. Quota altimetrica da DEM
  • 2bis. Quota altimetrica da DEM usando rioxarray
  • Conclusione

1. Quota altimetrica costante¶

E' il caso più semplice: ho un certo insieme di punti e tutti devono acquisire la stezza Z.

In [3]:
point_data = gpd.read_file(points)
point_data
Out[3]:
fid geometry
0 1.0 POINT (405270.221 4512122.298)
1 2.0 POINT (405226.798 4512675.034)
2 3.0 POINT (404962.921 4512294.350)
3 4.0 POINT (404991.893 4512570.669)
In [4]:
point_data.plot(figsize=(10, 10))
Out[4]:
<Axes: >
No description has been provided for this image
In [5]:
point_data.has_z
Out[5]:
0    False
1    False
2    False
3    False
dtype: bool

Usando has_z ho verificato se i quattro punti sono effettivamente piani e quel False per ognuno di loro mi conferma che lo sono. Non resta che aggiungere la terza quota.

In [6]:
point_z_list = []

for _index, row in point_data.iterrows():
    point_geometry = row.geometry.coords
    # Estraggo la coppia di coordinate
    coordinates_plane = point_geometry[0]
    # Creo una lista con la coppia di coordinate ed aggiungo la z
    coordinates_3d = list(coordinates_plane)
    coordinates_3d.append(10)
    # Converto la lista in PointZ ed aggiungo l'oggetto alla lista di punti
    point_geometry_3d = Point(tuple(coordinates_3d))
    point_z_list.append([row.fid, point_geometry_3d])

# Ricostruisco il GeoDataFrame di punti
df = pd.DataFrame(point_z_list)
df.rename(columns={0:'fid', 1: 'geometry'}, inplace=True)
gdf = gpd.GeoDataFrame(df, geometry=df.geometry, crs=point_data.crs)
gdf
Out[6]:
fid geometry
0 1.0 POINT Z (405270.221 4512122.298 10.000)
1 2.0 POINT Z (405226.798 4512675.034 10.000)
2 3.0 POINT Z (404962.921 4512294.350 10.000)
3 4.0 POINT Z (404991.893 4512570.669 10.000)
In [7]:
gdf.has_z
Out[7]:
0    True
1    True
2    True
3    True
dtype: bool

Interrogando i dati nuovamente con has_z risultano essere 3D.

2. Quota altimetrica da DEM¶

Caso più complesso e sicuramente corrispondente alla realtà.

Il primo step è leggere la fonte dati raster. In verità dovrei leggere anche la fonte vettoriale ma siccome l'ho già fatto nell'esempio precedente evito di duplicare il passaggio.

In [8]:
raster_data = rio.open(dem)
raster_data
Out[8]:
<open DatasetReader name='/media/max/Windows11/DEV/MIO/PyGIS_blog/sample_data/add_z/clip_dem.tif' mode='r'>
In [9]:
figure, axes = plt.subplots(figsize=(10, 10))
show(
    source=raster_data,
    cmap='terrain'
)
No description has been provided for this image
Out[9]:
<Axes: >
In [10]:
show_hist(
    source=raster_data,
    title='Distribuzione delle quote',
    bins=10,
    alpha=0.5,
)
No description has been provided for this image

L'istogramma delle quote ci consente di capire che c'è una forte presenza di quote zero che corrispondono all'azzurro del raster precedentemente stampato.

In [11]:
point_z_list = []

for _index, row in point_data.iterrows():
    point_geometry = row.geometry.xy
    # Estraggo la coppia di coordinate
    x_coords = point_geometry[0][0]
    y_coords = point_geometry[1][0]
    # Individuo la riga e la colonna del raster in cui ricade
    # la coppia di coordinate.
    line, column = raster_data.index(x_coords, y_coords)
    # Estraggo il valore della cella dall'intersezione della
    # riga e colonna individuate al passaggio precedente.
    # E' il valore di quota che sto cercando.
    pixel_value = raster_data.read(1)[line, column]
    # Creo il PointZ ed aggiungo l'oggetto alla lista di punti
    point_geometry_3d = Point((x_coords, y_coords, pixel_value))
    point_z_list.append([row.fid, point_geometry_3d])

# Ricostruisco il GeoDataFrame di punti
df = pd.DataFrame(point_z_list)
df.rename(columns={0:'fid', 1: 'geometry'}, inplace=True)
gdf = gpd.GeoDataFrame(df, geometry=df.geometry, crs=point_data.crs)
gdf
Out[11]:
fid geometry
0 1.0 POINT Z (405270.221 4512122.298 53.347)
1 2.0 POINT Z (405226.798 4512675.034 0.000)
2 3.0 POINT Z (404962.921 4512294.350 65.504)
3 4.0 POINT Z (404991.893 4512570.669 18.646)
In [12]:
gdf.has_z
Out[12]:
0    True
1    True
2    True
3    True
dtype: bool

Anche in questo caso la verifica sulla tridimensionalità del GeoDataFrame ha dato esito positivo.

2bis. Quota altimetrica da DEM usando rioxarray¶

Aggiornamento del 07-04-2022

Procedura simile alla precedente con la sola differenza legata al tipo di libreria con cui vado a leggere il DEM; andrò ad usare infatti rioxarray estensione di xarray.

In [13]:
import rioxarray as rxr

new_raster_data = rxr.open_rasterio(dem).squeeze()
new_raster_data
Out[13]:
<xarray.DataArray (y: 72, x: 55)>
[3960 values with dtype=float32]
Coordinates:
    band         int64 1
  * x            (x) float64 4.049e+05 4.05e+05 4.05e+05 ... 4.055e+05 4.055e+05
  * y            (y) float64 4.513e+06 4.513e+06 ... 4.512e+06 4.512e+06
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Area
    _FillValue:     -9999.0
    scale_factor:   1.0
    add_offset:     0.0
xarray.DataArray
  • y: 72
  • x: 55
  • ...
    [3960 values with dtype=float32]
    • band
      ()
      int64
      1
      array(1)
    • x
      (x)
      float64
      4.049e+05 4.05e+05 ... 4.055e+05
      array([404941.257791, 404951.883373, 404962.508955, 404973.134536,
             404983.760118, 404994.3857  , 405005.011282, 405015.636864,
             405026.262445, 405036.888027, 405047.513609, 405058.139191,
             405068.764773, 405079.390355, 405090.015936, 405100.641518,
             405111.2671  , 405121.892682, 405132.518264, 405143.143845,
             405153.769427, 405164.395009, 405175.020591, 405185.646173,
             405196.271755, 405206.897336, 405217.522918, 405228.1485  ,
             405238.774082, 405249.399664, 405260.025245, 405270.650827,
             405281.276409, 405291.901991, 405302.527573, 405313.153155,
             405323.778736, 405334.404318, 405345.0299  , 405355.655482,
             405366.281064, 405376.906645, 405387.532227, 405398.157809,
             405408.783391, 405419.408973, 405430.034555, 405440.660136,
             405451.285718, 405461.9113  , 405472.536882, 405483.162464,
             405493.788045, 405504.413627, 405515.039209])
    • y
      (y)
      float64
      4.513e+06 4.513e+06 ... 4.512e+06
      array([4512752.806342, 4512743.597027, 4512734.387712, 4512725.178397,
             4512715.969081, 4512706.759766, 4512697.550451, 4512688.341135,
             4512679.13182 , 4512669.922505, 4512660.71319 , 4512651.503874,
             4512642.294559, 4512633.085244, 4512623.875928, 4512614.666613,
             4512605.457298, 4512596.247983, 4512587.038667, 4512577.829352,
             4512568.620037, 4512559.410722, 4512550.201406, 4512540.992091,
             4512531.782776, 4512522.57346 , 4512513.364145, 4512504.15483 ,
             4512494.945515, 4512485.736199, 4512476.526884, 4512467.317569,
             4512458.108253, 4512448.898938, 4512439.689623, 4512430.480308,
             4512421.270992, 4512412.061677, 4512402.852362, 4512393.643047,
             4512384.433731, 4512375.224416, 4512366.015101, 4512356.805785,
             4512347.59647 , 4512338.387155, 4512329.17784 , 4512319.968524,
             4512310.759209, 4512301.549894, 4512292.340578, 4512283.131263,
             4512273.921948, 4512264.712633, 4512255.503317, 4512246.294002,
             4512237.084687, 4512227.875372, 4512218.666056, 4512209.456741,
             4512200.247426, 4512191.03811 , 4512181.828795, 4512172.61948 ,
             4512163.410165, 4512154.200849, 4512144.991534, 4512135.782219,
             4512126.572903, 4512117.363588, 4512108.154273, 4512098.944958])
    • spatial_ref
      ()
      int64
      0
      crs_wkt :
      PROJCS["WGS 84 / UTM zone 33N",GEOGCS["WGS 84",DATUM["WGS_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"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32633"]]
      semi_major_axis :
      6378137.0
      semi_minor_axis :
      6356752.314245179
      inverse_flattening :
      298.257223563
      reference_ellipsoid_name :
      WGS 84
      longitude_of_prime_meridian :
      0.0
      prime_meridian_name :
      Greenwich
      geographic_crs_name :
      WGS 84
      horizontal_datum_name :
      World Geodetic System 1984
      projected_crs_name :
      WGS 84 / UTM zone 33N
      grid_mapping_name :
      transverse_mercator
      latitude_of_projection_origin :
      0.0
      longitude_of_central_meridian :
      15.0
      false_easting :
      500000.0
      false_northing :
      0.0
      scale_factor_at_central_meridian :
      0.9996
      spatial_ref :
      PROJCS["WGS 84 / UTM zone 33N",GEOGCS["WGS 84",DATUM["WGS_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"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32633"]]
      GeoTransform :
      404935.945 10.625581818181937 0.0 4512757.411 0.0 -9.209315277778337
      array(0)
    • x
      PandasIndex
      PandasIndex(Index([ 404941.2577909091, 404951.88337272726,  404962.5089545455,
             404973.13453636365,  404983.7601181818,        404994.3857,
              405005.0112818182,  405015.6368636364, 405026.26244545454,
              405036.8880272727, 405047.51360909094,  405058.1391909091,
             405068.76477272727, 405079.39035454544, 405090.01593636366,
             405100.64151818183,        405111.2671, 405121.89268181816,
              405132.5182636364, 405143.14384545456,  405153.7694272727,
              405164.3950090909,  405175.0205909091,  405185.6461727273,
             405196.27175454545,  405206.8973363636, 405217.52291818184,
                    405228.1485,  405238.7740818182, 405249.39966363634,
             405260.02524545457, 405270.65082727274,  405281.2764090909,
              405291.9019909091,  405302.5275727273, 405313.15315454546,
             405323.77873636363,  405334.4043181818,        405345.0299,
              405355.6554818182, 405366.28106363636,  405376.9066454545,
             405387.53222727275,  405398.1578090909,  405408.7833909091,
             405419.40897272725,  405430.0345545455, 405440.66013636364,
              405451.2857181818,        405461.9113,  405472.5368818182,
             405483.16246363637, 405493.78804545454,  405504.4136272727,
             405515.03920909093],
            dtype='float64', name='x'))
    • y
      PandasIndex
      PandasIndex(Index([4512752.8063423615,  4512743.597027084,  4512734.387711806,
              4512725.178396529,   4512715.96908125,  4512706.759765972,
              4512697.550450695,  4512688.341135417, 4512679.1318201395,
              4512669.922504862,  4512660.713189583,  4512651.503874306,
              4512642.294559028,   4512633.08524375,  4512623.875928473,
              4512614.666613195,  4512605.457297917,  4512596.247982639,
              4512587.038667361,  4512577.829352084,  4512568.620036806,
              4512559.410721528,  4512550.201406251,  4512540.992090972,
             4512531.7827756945,  4512522.573460417,  4512513.364145139,
              4512504.154829862,  4512494.945514584,  4512485.736199306,
              4512476.526884028,   4512467.31756875, 4512458.1082534725,
              4512448.898938195,  4512439.689622917,   4512430.48030764,
              4512421.270992361,  4512412.061677083,  4512402.852361806,
              4512393.643046528,   4512384.43373125,  4512375.224415973,
              4512366.015100695,  4512356.805785417,  4512347.596470139,
              4512338.387154861,  4512329.177839584,  4512319.968524306,
              4512310.759209028,  4512301.549893751,  4512292.340578472,
              4512283.131263195,  4512273.921947917,  4512264.712632639,
              4512255.503317362,  4512246.294002084, 4512237.0846868055,
              4512227.875371528,   4512218.66605625,  4512209.456740973,
              4512200.247425695,  4512191.038110417,   4512181.82879514,
              4512172.619479861, 4512163.4101645835,  4512154.200849306,
              4512144.991534028,  4512135.782218751,  4512126.572903473,
              4512117.363588194,  4512108.154272917,  4512098.944957639],
            dtype='float64', name='y'))
  • AREA_OR_POINT :
    Area
    _FillValue :
    -9999.0
    scale_factor :
    1.0
    add_offset :
    0.0
In [14]:
new_point_z_list = []

for _index, row in point_data.iterrows():
    point_geometry = row.geometry.xy
    # Estraggo la coppia di coordinate
    x_coords = point_geometry[0][0]
    y_coords = point_geometry[1][0]

    # Individuo la riga e la colonna del DataArray in cui ricade
    # la coppia di coordinate.
    value = new_raster_data.sel(x=x_coords, y=y_coords, method="nearest")
    # Estraggo il valore della cella dall'intersezione della
    # riga e colonna individuate al passaggio precedente.
    # E' il valore di quota che sto cercando.
    pixel_value = value.data
    # Creo il PointZ ed aggiungo l'oggetto alla lista di punti
    point_geometry_3d = Point((x_coords, y_coords, pixel_value))
    new_point_z_list.append([row.fid, point_geometry_3d])

# Ricostruisco il GeoDataFrame di punti
new_df = pd.DataFrame(new_point_z_list)
new_df.rename(columns={0:'fid', 1: 'geometry'}, inplace=True)
new_gdf = gpd.GeoDataFrame(new_df, geometry=new_df.geometry, crs=point_data.crs)
new_gdf
Out[14]:
fid geometry
0 1.0 POINT Z (405270.221 4512122.298 53.347)
1 2.0 POINT Z (405226.798 4512675.034 0.000)
2 3.0 POINT Z (404962.921 4512294.350 65.504)
3 4.0 POINT Z (404991.893 4512570.669 18.646)

Conclusione¶

Quelli riportati sono solo due dei metodi possibili per raggiungere l'obiettivo, sono quelli che ho individuato io ma potrebbero essercene altri. Nota bene che per 2 è indispensabile che raster e punti abbiano lo stesso sistema di riferimento!