How to extract edges from polydata as connected features?

Question:

I have a polydata structure and its extracted edges but computed with extract_feature_edges function as unconnected cells (separated lines).

Is it possible to connect those cells (lines) from their common points and then get the different features (lands, islands such as what you can see in the image – Antartica, Australia, … – BTW they are paleo continents)?

In resume, I would like to extract from my grid and its edges the different land parts as separate polydata. I have tried with the python module shapely and the polygonize function, it works but not with 3D coordinates (https://shapely.readthedocs.io/en/latest/reference/shapely.polygonize.html).

import pyvista as pv

! wget -q -nc https://thredds-su.ipsl.fr/thredds/fileServer/ipsl_thredds/brocksce/pyvista/mesh.vtk
mesh = pv.PolyData('mesh.vtk')
edges = mesh.extract_feature_edges(boundary_edges=True)

pl = pv.Plotter()

pl.add_mesh(pv.Sphere(radius=0.999, theta_resolution=360, phi_resolution=180))
pl.add_mesh(mesh, show_edges=True, edge_color="gray")
pl.add_mesh(edges, color="red", line_width=2)

viewer = pl.show(jupyter_backend='pythreejs', return_viewer=True)
display(viewer)

enter image description here

Any idea?

Asked By: PBrockmann

||

Answers:

Here is a solution using vtk.vtkStripper() to join contiguous segments into polylines.
See thread from https://discourse.vtk.org/t/get-a-continuous-line-from-a-polydata-structure/9864

import pyvista as pv
import vtk
import random

! wget -q -nc https://thredds-su.ipsl.fr/thredds/fileServer/ipsl_thredds/brocksce/pyvista/mesh3D.vtk
mesh = pv.PolyData('mesh3D.vtk')
edges = mesh.extract_feature_edges(boundary_edges=True)

pl = pv.Plotter()

pl.add_mesh(pv.Sphere(radius=0.999, theta_resolution=360, phi_resolution=180))
pl.add_mesh(mesh, show_edges=True, edge_color="gray")

regions = edges.connectivity()
regCount = len(set(regions.get_array("RegionId")))

connectivityFilter = vtk.vtkPolyDataConnectivityFilter()
stripper = vtk.vtkStripper()

for r in range(regCount):
    connectivityFilter.SetInputData(edges)
    connectivityFilter.SetExtractionModeToSpecifiedRegions()
    connectivityFilter.InitializeSpecifiedRegionList()
    connectivityFilter.AddSpecifiedRegion(r)
    connectivityFilter.Update()

    if r == 11:
        p = pv.PolyData(connectivityFilter.GetOutput())
        p.save('poly1.vtk')
    
    stripper.SetInputData(connectivityFilter.GetOutput())
    stripper.SetJoinContiguousSegments(True)
    stripper.Update()
    reg = stripper.GetOutput()
    
    random_color = "#"+''.join([random.choice('0123456789ABCDEF') for i in range(6)])
    pl.add_mesh(reg, color=random_color, line_width=4)

viewer = pl.show(jupyter_backend='trame', return_viewer=True)
display(viewer)
Answered By: PBrockmann

This has come up before in github discussions. The conclusion was that PyVista doesn’t have anything built-in to reorder edges, but there might be third-party libraries that can do this (this answer mentioned libigl, but I have no experience with that).

I have some ideas on how to tackle this, but there are concerns about the applicability of such a helper in the generic case. In your specific case, however, we know that every edge is a closed loop, and that there aren’t very many of them, so we don’t have to worry about performance (and especially memory footprint) that much.

Here’s a manual approach to reordering the edges by building an adjacency graph and walking until we end up where we started on each loop:

from collections import defaultdict

import pyvista as pv

# load example mesh
mesh = pv.read('mesh.vtk')

# get edges
edges = mesh.extract_feature_edges(boundary_edges=True)

# build undirected adjacency graph from edges (2-length lines)
# (potential performance improvement: use connectivity to only do this for each closed loop)
# (potentially via calling edges.split_bodies())
lines = edges.lines.reshape(-1, 3)[:, 1:]
adjacency = defaultdict(set)  # {2: {1, 3}, ...} if there are lines from point 2 to point 1 and 3
for first, second in lines:
    adjacency[first].add(second)
    adjacency[second].add(first)

# start looping from whichever point, keep going until we run out of adjacent points
points_left = set(range(edges.n_points))
loops = []
while points_left:
    point = points_left.pop()  # starting point for next loop
    loop = [point]
    loops.append(loop)
    while True:
        # keep walking the loop
        neighb = adjacency[point].pop()
        loop.append(neighb)
        if neighb == loop[0]:
            # this loop is done
            break
        # make sure we never backtrack
        adjacency[neighb].remove(point)
        # bookkeeping
        points_left.discard(neighb)
        point = neighb

# assemble new lines based on the existing ones, flatten
lines = sum(([len(loop)] + loop for loop in loops), [])

# overwrite the lines in the original edges; optionally we could create a copy here
edges.lines = lines

# edges are long, closed loops by construction, so it's probably correct
# plot each curve with an individual colour just to be safe
plotter = pv.Plotter()
plotter.add_mesh(pv.Sphere(radius=0.999))
plotter.add_mesh(edges, scalars=range(edges.n_cells), line_width=3, show_scalar_bar=False)
plotter.enable_anti_aliasing('msaa')
plotter.show()

This code replaces your original 1760 2-length lines with 14 larger lines defining each loop. You have to be a bit careful, though: north of Australia you have a loop that self-intersects:

white globe with colourful continent edges drawn; an 8-shaped self-intersecting path is also visible

The intersection point appears 4 times instead of 2. This means that my brute-force solver doesn’t give a well-defined result: it will choose at the intersection randomly, and if by (bad) luck we start the loop from the intersection point the algorithm will probably fail. Making it more robust is left as an exercise to the reader (my comment about splitting the edges into individual ones could help with this issue).

Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.