12 Visualizing Geological Cross Sections with PyVista#

Geological Cross Sections obtained from Geological Maps can be loaded and plotted with PyVista in GemGIS. The following illustrates how to get data from a Geological Map into GemGIS.

115fcee080f4400a87081467fd88c9b2

Data Preparation#

The data used for GemGIS is obtained from OpenDataNRW. It will be used under Datenlizenz Deutschland – Namensnennung – Version 2.0 (https://www.govdata.de/dl-de/by-2-0) with © Geowissenschaftliche Daten: Analoges Kartenwerk der Geologischen Karte von Nordrhein-Westfalen 1:100.000 (2020).

Geological Maps#

For the visualization of cross sections, the geological maps Münster and Rheine were taken as shown below.

802cdb08f6654608b3ce71d703c26973

Geological Cross Section#

From these maps, the geological cross sections were extracted with an image editing software, tilted so that they are completely horizontal and cropped to the extent of the cross section. The axes were also cut off.

d3f89cd6893c45d88dbb61387816fa6d 9d5792e606974fb1934cc547b24973dc

Traces of Geological Cross Sections#

In order to locate the cross sections in space, the traces of the profiles need to be digitized and the vertical ranges of the cross sections need to be provided as attributes of the shape file. This can be done for example in QGIS.

6ce461a294af4b4a92335a9336c2d105

d885c22a09b84b979c299187ca82e07d

Set File Paths and download Tutorial Data#

If you downloaded the latest GemGIS version from the Github repository, append the path so that the package can be imported successfully. Otherwise, it is recommended to install GemGIS via pip install gemgis and import GemGIS using import gemgis as gg. In addition, the file path to the folder where the data is being stored is set. The tutorial data is downloaded using Pooch (https://www.fatiando.org/pooch/latest/index.html) and stored in the specified folder. Use pip install pooch if Pooch is not installed on your system yet.

[1]:
import gemgis as gg

file_path ='data/12_visualizing_cross_sections_in_pyvista/'
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\numpy\.libs\libopenblas.FB5AE2TYXYH2IJRDKGDGQ3XBKLKTF43H.gfortran-win_amd64.dll
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\numpy\.libs\libopenblas64__v0.3.23-246-g3d31191b-gcc_10_3_0.dll
  warnings.warn("loaded more than 1 DLL from .libs:"
[2]:
gg.download_gemgis_data.download_tutorial_data(filename="12_visualizing_cross_sections_in_pyvista.zip", dirpath=file_path)
Downloading file '12_visualizing_cross_sections_in_pyvista.zip' from 'https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F/12_visualizing_cross_sections_in_pyvista.zip' to 'C:\Users\ale93371\Documents\gemgis1\docs\getting_started\tutorial\data\12_visualizing_cross_sections_in_pyvista'.

Loading Data#

The traces were saved as shape file and are now loaded as GeoDataFrame using GeoPandas.

[3]:
import geopandas as gpd

traces = gpd.read_file(file_path + 'traces.shp')
traces
[3]:
id zmax zmin name geometry
0 NaN 500 -6000 Muenster LINESTRING (32386148.890 5763304.720, 32393549...
1 NaN 500 -2000 Rheine LINESTRING (32402275.136 5761828.501, 32431165...

Create Mesh for one cross section#

A mesh can be created based on the LineStrings and the provided vertical extent using the function create_mesh_cross_section(..).

[30]:
mesh = gg.visualization.create_mesh_from_cross_section(linestring=traces.loc[0].geometry,
                                                       zmin=traces.loc[0]['zmin'],
                                                       zmax=traces.loc[0]['zmax'])

mesh
[30]:
PolyDataInformation
N Cells20
N Points22
N Strips0
X Bounds3.239e+07, 3.242e+07
Y Bounds5.717e+06, 5.763e+06
Z Bounds-6.000e+03, 5.000e+02
N Arrays0
[31]:
type(mesh)
[31]:
pyvista.core.pointset.PolyData

Plotting Mesh#

The mesh can now be plotted using PyVista. A new PyVista plotter is created. The cross section is correctly placed in space. However, no cross section data is yet shown on the mesh.

[32]:
import pyvista as pv

p = pv.Plotter()

p.add_mesh(mesh)

p.show_grid(color='black')
p.set_background(color='white')
p.show()
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\pyvista\jupyter\notebook.py:58: UserWarning: Failed to use notebook backend:

No module named 'trame'

Falling back to a static output.
  warnings.warn(
../../_images/getting_started_tutorial_12_visualizing_cross_sections_in_pyvista_11_1.png

Adding texture to mesh#

A texture can be applied to the mesh to display the cross section data. In this case, the image data of the cross section is used to drape it over the mesh.

[33]:
import rasterio

texture = rasterio.open(file_path + 'profile1.png')
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\rasterio\__init__.py:304: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix will be returned.
  dataset = DatasetReader(path, driver=driver, sharing=sharing, **kwargs)

Plotting texture#

The loaded image file contains the data of the cross section.

[34]:
from rasterio.plot import show

show(texture);
../../_images/getting_started_tutorial_12_visualizing_cross_sections_in_pyvista_15_0.png

Adding texture to mesh#

A PyVista texture will be created and passed to the add_mesh(...) function as shown below to drape the image data over the mesh.

[42]:
tex = pv.read_texture(file_path + 'profile1.png')

mesh.texture_map_to_plane(use_bounds=False,inplace=True)

p = pv.Plotter()
p.add_mesh(mesh)
p.add_mesh(mesh, texture=tex)


p.show_grid(color='black')
p.set_background(color='white')
p.show()
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\pyvista\jupyter\notebook.py:58: UserWarning: Failed to use notebook backend:

No module named 'trame'

Falling back to a static output.
  warnings.warn(
../../_images/getting_started_tutorial_12_visualizing_cross_sections_in_pyvista_17_1.png

Creating Meshes for multiple cross sections#

The functionality for one cross section can be extended to multiple cross sections. In this case, we are passing the entire GeoDataFrame containing the traces to the function create_meshes_from_cross_sections(..). The result is a list of PyVista PolyData datasets.

[36]:
meshes = gg.visualization.create_meshes_from_cross_sections(gdf=traces)
meshes
[36]:
[PolyData (0x2cc1fb393c0)
   N Cells:    20
   N Points:   22
   N Strips:   0
   X Bounds:   3.239e+07, 3.242e+07
   Y Bounds:   5.717e+06, 5.763e+06
   Z Bounds:   -6.000e+03, 5.000e+02
   N Arrays:   0,
 PolyData (0x2cc1fb39540)
   N Cells:    2
   N Points:   4
   N Strips:   0
   X Bounds:   3.240e+07, 3.243e+07
   Y Bounds:   5.762e+06, 5.814e+06
   Z Bounds:   -2.000e+03, 5.000e+02
   N Arrays:   0]

Plotting Meshes#

The meshes can now be plotted using PyVista. A new PyVista plotter is created. The cross sections are correctly placed in space.

[37]:
import pyvista as pv

p = pv.Plotter()

for i in range(len(meshes)):
    p.add_mesh(meshes[i])

p.show_grid(color='black')
p.set_background(color='white')
p.show()
C:\Users\ale93371\Anaconda3\envs\pygeomechanical\lib\site-packages\pyvista\jupyter\notebook.py:58: UserWarning: Failed to use notebook backend:

No module named 'trame'

Falling back to a static output.
  warnings.warn(
../../_images/getting_started_tutorial_12_visualizing_cross_sections_in_pyvista_21_1.png

Adding Texture to Meshes#

As for the previous example, the textures are loaded via the path of the image files. Therefore, the function create_file_paths is used to create a list of file paths.

NB: The order of the traces in the passed GeoDataFrame must be identical to the order of the loaded images!

[38]:
source_files = gg.raster.create_filepaths(dirpath = file_path, search_criteria='profile*.png')
source_files
[38]:
['C:\\Users\\ale93371\\Documents\\gemgis1\\docs\\getting_started\\tutorial\\data\\12_visualizing_cross_sections_in_pyvista\\profile1.png',
 'C:\\Users\\ale93371\\Documents\\gemgis1\\docs\\getting_started\\tutorial\\data\\12_visualizing_cross_sections_in_pyvista\\profile2.png']

A list of textures is then created from each source image file.

[39]:
textures = [pv.Texture(file) for file in source_files]
textures
[39]:
[Texture (0x2cb84f9da80)
   Components:   4
   Cube Map:     False
   Dimensions:   6952, 771,
 Texture (0x2cc1fb3a2c0)
   Components:   4
   Cube Map:     False
   Dimensions:   7047, 293]

Plotting Meshes with Textures#

The cross sections are finally plotted at their true locations in space with the textures draped over them.

[40]:
import pyvista as pv

p = pv.Plotter()

for i in range(len(meshes)):
    p.add_mesh(meshes[i], texture=textures[i])

p.show_grid(color='black')
p.set_background(color='white')
p.show()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[40], line 6
      3 p = pv.Plotter()
      5 for i in range(len(meshes)):
----> 6     p.add_mesh(meshes[i], texture=textures[i])
      8 p.show_grid(color='black')
      9 p.set_background(color='white')

File ~\Anaconda3\envs\pygeomechanical\lib\site-packages\pyvista\plotting\plotter.py:3450, in BasePlotter.add_mesh(self, mesh, color, style, scalars, clim, show_edges, edge_color, point_size, line_width, opacity, flip_scalars, lighting, n_colors, interpolate_before_map, cmap, label, reset_camera, scalar_bar_args, show_scalar_bar, multi_colors, name, texture, render_points_as_spheres, render_lines_as_tubes, smooth_shading, split_sharp_edges, ambient, diffuse, specular, specular_power, nan_color, nan_opacity, culling, rgb, categories, silhouette, use_transparency, below_color, above_color, annotations, pickable, preference, log_scale, pbr, metallic, roughness, render, component, emissive, copy_mesh, backface_params, show_vertices, **kwargs)
   3448     raise TypeError(f'Invalid texture type ({type(texture)})')
   3449 if mesh.GetPointData().GetTCoords() is None:
-> 3450     raise ValueError(
   3451         'Input mesh does not have texture coordinates to support the texture.'
   3452     )
   3453 actor.texture = texture
   3454 # Set color to white by default when using a texture

ValueError: Input mesh does not have texture coordinates to support the texture.
[ ]: