Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix vedo api #88

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
153 changes: 112 additions & 41 deletions docs/connected_component_labeling.ipynb

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions napari_process_points_and_surfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
smooth_pointcloud_moving_least_squares_2d_radius,
smooth_pointcloud_moving_least_squares_2d,
reconstruct_surface_from_pointcloud,
connected_component_labeling
connected_component_labeling,
split_mesh
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems unrelated to the PR title. Also does it break backwards compatibility?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haesleinhuepf Thanks for looking at this. I thought about it for a while and came to the conclusion that it really is a breaking change, already from the vedo side. The original compute_connectivity function allowed to retrieve a per-vertex label which indicated to which object it belonged, which we could then nicely display as the vertex color.

In the new version, the id of each vertex isn't public anymore and vedo just returns a list [mesh1, mesh2, ...] of the connected components :/

To answer your question: The function mesh.compute_connectivity() has been renamed in vedo to mesh.split() so I'd consider that as part of an API change, and it has been done so in a breaking way.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we could use the split function to implement what has been there before, correct? I'm asking because I'm using this stuff in projects. Removing it is no option

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we could use the split function to implement what has been there before, correct? I'm asking because I'm using this stuff in projects. Removing it is no option

I'm not sure, tbh. The split function never exposes which vertex belongs to which object. -_-

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it gives lists of vertices grouped, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns a list of meshes. Each mesh consists of vertices [0, n_mesh_i]

)

from ._utils import isotropic_scale_surface
Expand Down Expand Up @@ -116,7 +117,7 @@ def gastruloid() -> "napari.types.SurfaceData":
def _vedo_ellipsoid() -> "napari.types.SurfaceData":
import vedo
shape = vedo.shapes.Ellipsoid().scale(10)
return (shape.points(), np.asarray(shape.faces()))
return (shape.vertices, np.asarray(shape.cells))


def _vedo_stanford_bunny() -> "napari.types.SurfaceData":
Expand Down Expand Up @@ -207,7 +208,7 @@ def surface_to_binary_volume(surface: "napari.types.SurfaceData", as_large_as_im
_init_viewer(viewer)

my_mesh = vedo.mesh.Mesh((surface[0], surface[1]))
vertices = my_mesh.points() # get coordinates of surface vertices
vertices = my_mesh.vertices # get coordinates of surface vertices

# get bounding box of mesh
boundaries_l = np.min(vertices + 0.5, axis=0).astype(int)
Expand Down Expand Up @@ -303,7 +304,7 @@ def all_labels_to_surface(labels: "napari.types.LabelsData", add_label_id_as_val
surface = set_vertex_values(surface, all_values)

return surface
#(mesh.points(), np.asarray(mesh.faces()), mesh.pointdata['OriginalMeshID'])
#(mesh.vertices, np.asarray(mesh.cells), mesh.pointdata['OriginalMeshID'])

# alias
marching_cubes = all_labels_to_surface
Expand Down Expand Up @@ -744,7 +745,7 @@ def fill_holes(surface: "napari.types.SurfaceData", size_limit: float = 100) ->
mesh = vedo.mesh.Mesh((surface[0], surface[1]))
mesh.fill_holes(size=size_limit)

return (mesh.points(), np.asarray(mesh.faces()))
return (mesh.vertices, np.asarray(mesh.cells))

def _check_open3d():
try:
Expand Down
12 changes: 6 additions & 6 deletions napari_process_points_and_surfaces/_quantification.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ def add_quality(surface: "napari.types.SurfaceData", quality_id: Quality = Quali
else:
raise NotImplementedError(f"Quality {quality_id} is not implemented.")

vertices = np.asarray(mesh2.points())
faces = np.asarray(mesh2.faces())
vertices = np.asarray(mesh2.vertices)
faces = np.asarray(mesh2.cells)

return SurfaceTuple((vertices, faces, values))

Expand Down Expand Up @@ -272,7 +272,7 @@ def add_curvature_scalars(surface: "napari.types.SurfaceData",
mesh.compute_curvature(method=used_method)
values = mesh.pointdata[curvature_id.name]

return SurfaceTuple((mesh.points(), np.asarray(mesh.faces()), values))
return SurfaceTuple((mesh.vertices, np.asarray(mesh.cells), values))

@register_function(menu="Measurement maps > Surface curvature (sphere-fitted, nppas)")
def add_spherefitted_curvature(surface: "napari.types.SurfaceData", radius: float = 1.0) -> List["napari.types.LayerDataTuple"]:
Expand Down Expand Up @@ -319,7 +319,7 @@ def _add_spherefitted_curvature(surface: "napari.types.SurfaceData", radius: flo
residues = np.zeros(mesh.npoints)
for idx in range(mesh.npoints):

patch = vedo.pointcloud.Points(mesh.closest_point(mesh.points()[idx], radius=radius))
patch = vedo.pointcloud.Points(mesh.closest_point(mesh.vertices[idx], radius=radius))

try:
s = vedo.pointcloud.fit_sphere(patch)
Expand All @@ -337,8 +337,8 @@ def _add_spherefitted_curvature(surface: "napari.types.SurfaceData", radius: flo
properties_curvature_layer = {'name': 'curvature', 'colormap': 'viridis'}
properties_residues_layer = {'name': 'fit residues', 'colormap': 'magma'}

layer1 = (SurfaceTuple((mesh.points(), np.asarray(mesh.faces()), curvature)), properties_curvature_layer, 'surface')
layer2 = (SurfaceTuple((mesh.points(), np.asarray(mesh.faces()), residues)), properties_residues_layer, 'surface')
layer1 = (SurfaceTuple((mesh.vertices, np.asarray(mesh.cells), curvature)), properties_curvature_layer, 'surface')
layer2 = (SurfaceTuple((mesh.vertices, np.asarray(mesh.cells), residues)), properties_residues_layer, 'surface')

return [layer1, layer2]

Expand Down
2 changes: 1 addition & 1 deletion napari_process_points_and_surfaces/_tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def test_curvature():
import numpy as np

shape = vedo.shapes.Ellipsoid()
surface_data = (shape.points(), np.asarray(shape.faces()))
surface_data = (shape.vertices, np.asarray(shape.cells))

add_curvature_scalars(surface_data, Curvature.Gauss_Curvature)
add_curvature_scalars(surface_data, Curvature.Mean_Curvature)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ def test_connected_components():
image[20:29, 20:29, 20:29] = 2

surface = nppas.all_labels_to_surface(image)
connected_components = nppas.connected_component_labeling(surface)
connected_components = nppas.split_mesh(surface)
assert len(connected_components) == 2
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved

assert len(np.unique(connected_components[2])) == 2
labelled_components = nppas.connected_component_labeling(surface)
assert len(np.unique((labelled_components[2]))) == 2

def test_decimate():
import napari_process_points_and_surfaces as nppas
Expand Down Expand Up @@ -208,3 +210,6 @@ def test_create_convex_hull_from_points():
# gastruloid = nppas.gastruloid()
#
# nppas.show(gastruloid)

if __name__ == '__main__':
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
test_connected_components()
94 changes: 76 additions & 18 deletions napari_process_points_and_surfaces/_vedo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from napari_tools_menu import register_function
import numpy as np
from typing import List

def to_vedo_mesh(surface):
_hide_vtk_warnings()
Expand Down Expand Up @@ -67,7 +68,7 @@ def _repr_html_(self):
# mesh statistics
bounds = "<br/>".join(["{min_x:.3f}...{max_x:.3f}".format(min_x=min_x, max_x=max_x) for min_x, max_x in zip(mesh.bounds()[::2], mesh.bounds()[1::2])])
average_size = "{size:.3f}".format(size=mesh.average_size())
center_of_mass = ",".join(["{size:.3f}".format(size=x) for x in mesh.centerOfMass()])
center_of_mass = ",".join(["{size:.3f}".format(size=x) for x in mesh.center_of_mass()])
scale = ",".join(["{size:.3f}".format(size=x) for x in mesh.scale()])
histogram = ""
min_max = ""
Expand Down Expand Up @@ -104,13 +105,13 @@ def _repr_html_(self):
"<td style=\"text-align: center; vertical-align: top;\">",
help_text,
"<table>",
"<tr><td>origin (z/y/x)</td><td>" + str(mesh.origin()).replace(" ", "&nbsp;") + "</td></tr>",
#"<tr><td>origin (z/y/x)</td><td>" + str(mesh.origin()).replace(" ", "&nbsp;") + "</td></tr>",
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
"<tr><td>center of mass(z/y/x)</td><td>" + center_of_mass.replace(" ", "&nbsp;") + "</td></tr>",
"<tr><td>scale(z/y/x)</td><td>" + scale.replace(" ", "&nbsp;") + "</td></tr>",
"<tr><td>bounds (z/y/x)</td><td>" + str(bounds).replace(" ", "&nbsp;") + "</td></tr>",
"<tr><td>average size</td><td>" + str(average_size) + "</td></tr>",
"<tr><td>number of vertices</td><td>" + str(mesh.npoints) + "</td></tr>",
"<tr><td>number of faces</td><td>" + str(len(mesh.faces())) + "</td></tr>",
"<tr><td>number of faces</td><td>" + str(len(mesh.cells)) + "</td></tr>",
min_max,
"</table>",
histogram,
Expand All @@ -124,13 +125,13 @@ def _repr_html_(self):

def to_napari_surface_data(vedo_mesh, values=None):
if values is None:
return SurfaceTuple((vedo_mesh.points(), np.asarray(vedo_mesh.faces())))
return SurfaceTuple((vedo_mesh.vertices, np.asarray(vedo_mesh.cells)))
else:
return SurfaceTuple((vedo_mesh.points(), np.asarray(vedo_mesh.faces()), values))
return SurfaceTuple((vedo_mesh.vertices, np.asarray(vedo_mesh.cells), values))


def to_napari_points_data(vedo_points):
return vedo_points.points()
return vedo_points.vertices


def _hide_vtk_warnings():
Expand Down Expand Up @@ -184,26 +185,83 @@ def remove_duplicate_vertices(surface: "napari.types.SurfaceData", viewer: "napa


@register_function(menu="Surfaces > Connected components labeling (vedo, nppas)")
def connected_component_labeling(surface: "napari.types.SurfaceData", viewer: "napari.Viewer" = None) -> "napari.types.SurfaceData":
def connected_component_labeling(
surface: "napari.types.SurfaceData",
viewer: "napari.Viewer" = None) -> 'napari.types.SurfaceData':
from ._utils import _init_viewer
import vtk
import vtk.util.numpy_support as vtk_to_np

_init_viewer(viewer)

mesh = to_vedo_mesh(surface)

# Set up connectivity filter
conn = vtk.vtkConnectivityFilter()
conn.SetInputData(mesh.dataset)
conn.SetExtractionModeToAllRegions()

# Enable coloring of regions
conn.ColorRegionsOn()
conn.Update()
result = conn.GetOutput()

region_ids = vtk_to_np.vtk_to_numpy(
result.GetCellData().GetArray("RegionId"))

vertex_labels = np.zeros(mesh.npoints, dtype=np.uint32)
vertex_labels[np.asarray(mesh.cells)[:, 0]] = region_ids
vertex_labels[np.asarray(mesh.cells)[:, 1]] = region_ids
vertex_labels[np.asarray(mesh.cells)[:, 2]] = region_ids

return SurfaceTuple((
surface[0],
np.asarray(surface[1]),
vertex_labels))


def split_mesh(
surface: "napari.types.SurfaceData"
) -> list:
"""
Split a mesh into its connected components.

See Also
--------
..[0] https://vedo.embl.es/docs/vedo/mesh.html#Mesh.split
"""
Determine the connected components of a surface mesh.
mesh = to_vedo_mesh(surface)
meshes = mesh.split()

meshes_out = [
SurfaceTuple(to_napari_surface_data(mesh)) for mesh in meshes
]

return meshes_out


@register_function(menu="Surfaces > Split mesh (vedo, nppas)")
def _split_mesh(
surface: "napari.types.SurfaceData",
viewer: "napari.Viewer" = None
) -> List["napari.types.LayerDataTuple"]:
"""
Split a mesh into its connected components.

See Also
--------
..[0] https://vedo.embl.es/docs/vedo/mesh.html#Mesh.compute_connectivity
..[0] https://vedo.embl.es/docs/vedo/mesh.html#Mesh.split

"""
from ._quantification import set_vertex_values
from ._utils import _init_viewer
_init_viewer(viewer)

mesh = to_vedo_mesh(surface)
mesh.compute_connectivity()
region_id = mesh.pointdata["RegionId"]

mesh_out = to_napari_surface_data(mesh)
mesh_out = set_vertex_values(mesh_out, region_id)
meshes = split_mesh(surface)

return mesh_out
return [(mesh,
{'colormap': 'hsv',
'name': 'Component ' + str(i)},
'surface') for i, mesh in enumerate(meshes)]


@register_function(menu="Surfaces > Smooth (moving least squares, vedo, nppas)")
Expand Down Expand Up @@ -411,7 +469,7 @@ def subdivide_adaptive(surface: "napari.types.SurfaceData",

if maximum_edge_length == 0:
maximum_edge_length = mesh_in.diagonal_size(
) / np.sqrt(mesh_in._data.GetNumberOfPoints()) / number_of_iterations
) / np.sqrt(mesh_in.npoints) / number_of_iterations

mesh_out = mesh_in.subdivide(
number_of_iterations, method=2, mel=maximum_edge_length)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ install_requires =
napari-tools-menu>=0.1.14
napari-time-slicer>=0.4.5
napari-workflows>=0.2.3
vedo>=2022.4.1
vedo>=2022.5.0
napari-skimage-regionprops>=0.5.5
pandas
imageio!=2.22.1
Expand Down
Loading