Tutorial to convert geospatial data (Shapefile) to 3D data (VTK) with Python, Geopandas & Pyvista

ConvertgeospatialdataShapefileto3DdataVTKwithPythonGeopandasandPyvista.png

In our perspective, 3D visualization of geospatial data has been a long desired feature that has been covered in some features from SAGA GIS or in some plugins from QGIS. This time we developed a Python script that converts point / line / polygon ESRI shapefiles  (or any vector file) to unstructured grid Vtk format type (Vtu) by the use of the Python libraries Geopandas and Pyvista. The tutorial has files, scripts, and videos that show the whole procedure with some remarks on the software and spatial files and a discussion about the nature of the spatial files that presents some challenges in the data conversion.

Notes for this tutorial:

  • All 3d geometries are related to the "Elev" attribute and the geometry field of the Pandas dataframe.

  • Lines shapefiles must be single parts

  • Polygons should not have holes or they wont be considered. 

  • Polygons are exported as lines not as polygons due to the fact that Vtks were designed for convex polygons having poor performance with geospatial polygons.

As regular Shapefile and Vtk data format users, we know that the potential for the conversion is huge; however we consider this tutorial a good starting point for future improvement or the application in particular cases.

Know more about the VTK format on this link:

https://lorensen.github.io/VTKExamples/site/VTKFileFormats/


Tutorial

Scripts

This is the Python code related to the conversion:

#import required packages
import itertools
import numpy as np
import pyvista as pv
import geopandas as gpd
#for windows users
from shapely import speedups
speedups.disable()
C:\Users\GIDA2\miniconda3\lib\site-packages\geopandas\_compat.py:84: UserWarning: The Shapely GEOS version (3.4.3-CAPI-1.8.3 r4285) is incompatible with the GEOS version PyGEOS was compiled with (3.8.1-CAPI-1.13.3). Conversions between both will be slow.
  warnings.warn(
#create geodataframes from all shapefiles
pointDf = gpd.read_file('../Shps/wellPoints.shp')
lineDf = gpd.read_file('../Shps/contoursLines.shp')
polyDf = gpd.read_file('../Shps/lakesPolygons.shp')

For point type geometry

#create emtpy lists to collect point information
cellSec = []
cellTypeSec = []
pointSec = []

#iterate over the points
i = 0
for index, valuesx in pointDf.iterrows():
    x, y, z = pointDf.loc[index].geometry.x, pointDf.loc[index].geometry.y, pointDf.loc[index].Elev
    pointSec.append([x,y,z])
    cellTypeSec.append([1])
    cellSec.append([1,i])
    i+=1

#convert list to numpy arrays
cellArray = np.array(cellSec)
cellTypeArray = np.array(cellTypeSec)
pointArray = np.array(pointSec)

#create the unstructured grid object
pointUgrid = pv.UnstructuredGrid(cellArray,cellTypeArray,pointArray)

#we can add some values to the point
pointUgrid.cell_arrays["Elev"] = pointDf.Elev.values

#plot and save as vtk
pointUgrid.plot()
pointUgrid.save('../Vtks/wellPoint.vtu',binary=False)
output_3_0.png

For line type geometry

#create emtpy dict to store the partial unstructure grids
lineTubes = {}

#iterate over the points
for index, values in lineDf.iterrows():
    cellSec = []
    linePointSec = []

    #iterate over the geometry coords
    zipObject = zip(values.geometry.xy[0],values.geometry.xy[1],itertools.repeat(values.Elev))
    for linePoint in zipObject:
        linePointSec.append([linePoint[0],linePoint[1],linePoint[2]])

    #get the number of vertex from the line and create the cell sequence
    nPoints = len(list(lineDf.loc[index].geometry.coords))
    cellSec = [nPoints] + [i for i in range(nPoints)]

    #convert list to numpy arrays
    cellSecArray = np.array(cellSec)
    cellTypeArray = np.array([4])
    linePointArray = np.array(linePointSec)

    partialLineUgrid = pv.UnstructuredGrid(cellSecArray,cellTypeArray,linePointArray)   
    #we can add some values to the point
    partialLineUgrid.cell_arrays["Elev"] = values.Elev
    lineTubes[str(index)] = partialLineUgrid

#merge all tubes and export resulting vtk
lineBlocks = pv.MultiBlock(lineTubes)
lineGrid = lineBlocks.combine()
lineGrid.save('../Vtks/contourLine.vtk',binary=False)
lineGrid.plot()
output_5_0.png

For poly type geometry

#create emtpy dict to store the partial unstructure grids
polyTubes = {}

#iterate over the points
for index, values in polyDf.iterrows():
    cellSec = []
    linePointSec = []

    #iterate over the geometry coords
    zipObject = zip(values.geometry.exterior.xy[0],
                    values.geometry.exterior.xy[1],
                    itertools.repeat(values.Elev))
    for linePoint in zipObject:
        linePointSec.append([linePoint[0],linePoint[1],linePoint[2]])

    #get the number of vertex from the line and create the cell sequence
    nPoints = len(list(polyDf.loc[index].geometry.exterior.coords))
    cellSec = [nPoints] + [i for i in range(nPoints)]

    #convert list to numpy arrays
    cellSecArray = np.array(cellSec)
    cellTypeArray = np.array([4])
    linePointArray = np.array(linePointSec)

    partialPolyUgrid = pv.UnstructuredGrid(cellSecArray,cellTypeArray,linePointArray)   
    #we can add some values to the point
    partialPolyUgrid.cell_arrays["Elev"] = values.Elev
    #    partialPolyUgrid.save('../vtk/partiallakePoly.vtk',binary=False)
    polyTubes[str(index)] = partialPolyUgrid

#merge all tubes and export resulting vtk
polyBlocks = pv.MultiBlock(polyTubes)
polyGrid = polyBlocks.combine()
polyGrid.save('../Vtks/lakePolyasLines.vtk',binary=False)
polyGrid.plot()
output_7_0.png

Input data

You can download the input files from this link.

1 Comment

Saul Montoya

Saul Montoya es Ingeniero Civil graduado de la Pontificia Universidad Católica del Perú en Lima con estudios de postgrado en Manejo e Ingeniería de Recursos Hídricos (Programa WAREM) de la Universidad de Stuttgart con mención en Ingeniería de Aguas Subterráneas y Hidroinformática.

 

Suscribe to our online newsletter

Subscribe for free newsletter, receive news, interesting facts and dates of our courses in water resources.