marcomusy / vedo

A python module for scientific analysis of 3D data based on VTK and Numpy

Home Page:https://vedo.embl.es

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What pointdata are filled with in cut mesh?

sergei9838 opened this issue · comments

Hi, Marco,
I cut a mesh with added point id's saved to pointdata:

C = Cube().clean()
C.pointdata['ids'] = np.arange(C.nvertices)
# The upper face vertices:
up_idx = [i for i in range(C.nvertices) if C.vertices[i,1] > 0]
print(up_idx)
# [2, 3, 5, 6]
print([C.pointdata['ids'][i] for i in up_idx])
# [2, 3, 5, 6]  # OK

Now I cut the upped half of the cube. This adds me 4 new vertices on the cutting plane XZ:

Cup = C.clone().cut_with_plane(normal=(0,1,0))
cup_idx = [i for i in range(Cup.nvertices) if Cup.vertices[i,1] > 0]
print(cup_idx)  # these are the original vertices' ids
# [0, 1, 4, 5] 

We see that the original vertices were renumbered. Do the original vertices retain their ids?

print([Cup.pointdata['ids'][i] for i in cup_idx])
# [2, 3, 5, 6] 

Yes, these are retained, as expected.

What about the newly added vertices?

cut = [i for i in range(C.nvertices) if Cup.vertices[i,1] == 0]
print(cut)
# [2, 3, 6, 7]

Do they also have id's?

print(Cup.pointdata['ids'][cut])
# [2 2 7 5]  # ???

So their point data has been filled with some gibberish... Who cares, you ask?

What I am trying to achieve is to cut a mesh from the original large mesh, do some manipulations with its points and then glue it back to the original mesh. Since the vertex numeration changes for the cut mesh, the idea was to update the original vertices by their id's preserved in pointdata['ids']

Cup.shift((0,0.5,0))
Cnew = C.clone()
for i in range(Cup.nvertices):
    Cnew.vertices[Cup.pointdata['ids'][i]] = Cup.vertices[i]
show(Cnew.c("wheat")).close()
Screenshot 2024-05-14 at 17 32 30

The result is not a stretched cube, because 2 is the id of the original and also 2 added vertices. I thought that for the new vertices `ids' would just not be defined. As it looks, I need to chase the added vertices and alter their fake ids for this to work:

for i in cut:
    Cup.pointdata['ids'][i] = -1
Cnew = C.clone()
for i in range(Cup.nvertices):
    if Cup.pointdata['ids'][i] >=0:
        Cnew.vertices[Cup.pointdata['ids'][i]] = Cup.vertices[i]

show(Cnew.c("wheat"), axes=1).close()
Screenshot 2024-05-14 at 17 57 10

Are the added vertices in the cut mesh marked somewhere somehow? It is easy to identify these for cutting with plane, but kind of ugly solution for mesh cutting mesh:
before altering the cut mesh,
new_vert_ids = [i for i in range(Cup.nvertices) if Point(Cup.vertices[i]).distance_to(C) > 0]

And BTW, why such a strange colouring? c("wheat") seems to be ignored for half of the cube...

Hi Sergei, what about this solution:

from vedo import *

C = Cube().clean()
C.flat().lw(1).c("wheat").alpha(0.5)
C.pointdata['pids'] = np.arange(C.nvertices)

Cup = C.clone().cut_with_plane(normal=(0,1,0.2))
Cup.pointdata.remove('pids')
Cup.interpolate_data_from(C, radius=0.001, kernel="linear", null_value=-1)

show([[C,   C.labels("id"),   C.labels("pids", c='red', scale=0.05)], 
      [Cup, Cup.labels("id"), Cup.labels("pids", c='red', scale=0.05)]], N=2, axes=1)

Screenshot from 2024-05-14 19-15-50

the strange non-wheat half is due to the fact that phong shading needs normals, but because you called .clean() these are lost, the .flat() fixes it.

Thank you, Marco!
That’s doable as well, apart that I will need to convert the interpolated data to integers to be able to use these as indices. I just hoped that the original vertices are recorded somewhere in the cut mesh' data, but if this info is not recorded, perhaps, a good solution would be:

C = Cube().clean()
Cverts = Points(C.vertices)
C.pointdata['pids'] = np.arange(C.nvertices)

Cup = C.clone().cut_with_plane(normal=(0,1,0))
# getting the original vertices:
orig_verts = [i for i in range(C.nvertices) if Point(Cup.vertices[i]).distance_to(Cverts) < 1e-3]

# transform the cut mesh:
Cup.shift(0,0.5,0)
# glue it back:
for i in orig_verts:
    C.vertices[Cup.pointdata['pids'][i]] = Cup.vertices[i]

C.compute_normals()
Cup.compute_normals()
show(C.alpha(0.5).c("wheat"), Cup.c("yellow").alpha(0.5), axes=1).close()

Thank you for explaining the colouring! It seems also that pointdata are interpreted somehow by the plotter. Compare:

C = Cube().clean().compute_normals()
show(C).close()
Screenshot 2024-05-15 at 12 07 15

and with pointdata added:

C.pointdata['rand'] = np.round(np.random.uniform(0,1,8), 2)
show(C, C.labels("rand", scale=0.05)).close()
Screenshot 2024-05-15 at 12 28 52

Looks like a kind of inverted heat map. If another pointdata are added, the plot seems to ignore it.

Hi Sergei is not your solution equivalent to mine?
you just need to extract the newly formed numpy array and cast it to int.

When some point data is present it triggers a default (ugly) colormapping.
you can cange that interactively by clicking on the mesh and press 4.

In principle, yes: just 2 operations shorter - no erasing of pids, no conversion to int. But I learnt a new function: interpolate_data_from! ))
Thank you for your support, Marco!

If there is an interest, I have created a SubMesh class which keeps memory of the original mesh and allows (even interactively) for the sub mesh to glue back to the original mesh.

class SubMesh:
    """
    Cut out a submesh and glue it back, possibly with updated vertices, to the original mesh.
    The number and ordering of vertices in the resultant mesh are preserved.
    Class properties:
    * original_mesh
    * submesh: sub-mesh cut from original_mesh
    * mesh: the resultant mesh with submesh glued back
    * old_pids: indices of the submesh vertices which are also the mesh vertices
    * new_pids: indices of the new vertices added to submesh along the cut lines
    * cut: the pointcloud of the new vertices
    * dist2cut: distances from the original vertices in the submesh (old_pids) to the cut
    """
    def __init__(self, msh: Mesh, cut_fn_name: str, **kwargs):
        """
        :param msh: a Mesh
        :param cut_fn_name: Mesh method name to cut the Mesh
        :param kwargs: keyworded arguments to the cut meshod
        """
        self.original_mesh = msh
        self.mesh = msh.clone()
        self.mesh.pointdata['pids'] = np.arange(self.mesh.nvertices)
        self.submesh = getattr(self.mesh.clone(), cut_fn_name)(**kwargs)
        verts = Points(self.mesh.vertices)
        self.old_pids = []
        self.new_pids = []
        for i, v in enumerate(self.submesh.vertices):
            if Point(v).distance_to(verts) < 1e-3:
                self.old_pids.append(i)
            else:
                self.new_pids.append(i)
        self.cut = Points(self.submesh.vertices[self.new_pids])
        self.dist2cut = dict()

    def glue_(self, radius, align):
        """
        Glue submesh with possibly modified vertex positions back to the original mesh.
        :param radius: smoothing radius. The vertices of submesh which were originally
        at the distance smaller than radius, are interpolated between the original and new positions proportionally
        to the distance
        :param align: align the cut of submesh to the cut of the original mesh before gluing
        :return: mesh with submesh glued back
        """
        sm = self.submesh.clone()
        if align:
            sm.align_with_landmarks(self.submesh.vertices[self.new_pids], self.cut.vertices, rigid=True)
        if radius > 0:
            if len(self.dist2cut) == 0:  # pre-compute the distances for interactive gluing
                for i in self.old_pids:
                    pos = self.original_mesh.vertices[self.submesh.pointdata['pids'][i]]
                    self.dist2cut[i] = Point(pos).distance_to(self.cut).item()
            for i in self.old_pids:
                d = min(self.dist2cut[i] / radius, 1.)
                self.mesh.vertices[self.submesh.pointdata['pids'][i]] = (
                        d * sm.vertices[i] + (1-d) * self.original_mesh.vertices[self.submesh.pointdata['pids'][i]])
        else:
            for i in self.old_pids:
                self.mesh.vertices[self.submesh.pointdata['pids'][i]] = sm.vertices[i]

        self.mesh.pointdata.remove('pids')

    def glue(self, radius: float=0, mesh_col="wheat", align=False, interactive=False):
        """
        Glue submesh with possibly modified vertex positions back to the original mesh.
        :param radius: smoothing radius. The vertices of submesh which were originally
        at the distance smaller than radius, are interpolated between the original and new positions proportionally
        to the distance
        :param mesh_col: colour of the mesh in the plot
        :param align: align the cut of submesh to the cut of the original mesh before gluing
        :param interactive: open an interactive plot to adjust the smoothing radius
        :return: mesh with submesh glued back
        """
        self.glue_(radius=radius, align=align)
        if not interactive:
            return
        else:
            if len(self.dist2cut) == 0:  # pre-compute the distances for interactive gluing
                for i in self.old_pids:
                    pos = self.original_mesh.vertices[self.submesh.pointdata['pids'][i]]
                    self.dist2cut[i] = Point(pos).distance_to(self.cut).item()

            self.plt = Plotter()
            self.plt += self.mesh.c(mesh_col)

            def stitch(widget, event):
                self.glue_(radius=widget.value**2, align=align)
                self.plt -= self.mesh
                self.plt += self.mesh.c(mesh_col)

            self.plt.add_slider(
                stitch,
                value=radius,
                xmin=0,
                xmax=np.array(list(self.dist2cut.values())).max()**0.5,
                pos="bottom",
                title="Smoothing radius",
            )
            self.plt.show(interactive=True).close()

Example:

S = Sphere(r=1, res=50)
box = Cube(side=1.5).wireframe()
cups = SubMesh(S, 'cut_with_box', bounds=box, invert=True)
cups.submesh.scale(1.2)  # alter the submesh
show(cups.original_mesh.alpha(0.5), cups.submesh.c("yellow"), cups.cut).close()
Screenshot 2024-05-16 at 09 35 41

Now glue the sub mesh back without smoothing:

cups.glue()
show(cups.mesh.alpha(1).c('yellow')).close()
Screenshot 2024-05-16 at 09 38 05

Smooth the seam interactively:

cups.glue(radius=0.2, mesh_col="coral", interactive=True)
Screenshot 2024-05-16 at 09 39 39

A 'man' example:

man = Mesh(dataurl+"man.vtk").rotate_x(-90).color('w')
# show(man).close()

cut_height = 1.20
head = SubMesh(man, 'cut_with_plane', origin=(0, cut_height, 0), normal=(0, 1, 0))
show(head.submesh.c("y"), head.original_mesh.alpha(0.5), head.cut).close()
Screenshot 2024-05-16 at 12 49 04
# modify the head:
head.submesh.scale(1.2, origin=(0,cut_height,0)).shift((0, 0.05, 0))
head.glue()
show(head.mesh).close()
Screenshot 2024-05-16 at 12 49 50
head.glue(radius=0.2)
show(head.mesh).close()
Screenshot 2024-05-16 at 12 50 15
head.glue(radius=0.05, align=True)  # eliminate the vertical shift
show(head.mesh).close()
Screenshot 2024-05-16 at 12 50 36
head.glue(interactive=True)
Screenshot 2024-05-16 at 12 51 22

Thanks! This is quite impressive!
You guys are getting better than me with vedo lol.
Ill have better into it over the weekend.

Thanks! This is quite impressive! You guys are getting better than me with vedo lol. Ill have better into it over the weekend.

Marco, it's a joy to work with vedo! Thank you so much for your efforts! 👏