Surface vertex classification#
In this notebook we demonstrate how to retrieve surface/vertex measurements to classify vertices on the surface. The used example data is derived from AV Luque and JV Veenvliet (2023) licensed CC-BY. See the creating_surfaces for how to create the surface from raw imaging data.
See also
You need to additionally install the napari-accelerated-pixel-and-object-classification plugin, e.g. by calling mamba install napari-accelerated-pixel-and-object-classification -c conda-forge
from the terminal.
import napari_process_points_and_surfaces as nppas
import napari
import apoc
import numpy as np
We will be working with a simple geometry (i.e., an ellipsoid) to demonstrate the curvature property and the different settings.
surface = nppas.gastruloid()
surface
The nppas gastruloid example is derived from AV Luque and JV Veenvliet (2023) which is licensed CC-BY (https://creativecommons.org/licenses/by/4.0/legalcode) and can be downloaded from here: https://zenodo.org/record/7603081
nppas.SurfaceTuple
|
Quantification#
We can create a table (pandas Dataframe) like this.
requested_measurements = [nppas.Quality.AREA,
nppas.Quality.ASPECT_RATIO,
nppas.Quality.GAUSS_CURVATURE,
nppas.Quality.MEAN_CURVATURE,
nppas.Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL,
nppas.Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL,
]
df = nppas.surface_quality_table(surface, requested_measurements)
df
c:\structure\code\vedo\vedo\pointcloud.py:526: FutureWarning: `rcond` parameter will change to the default of machine precision times ``max(M, N)`` where M and N are the input matrix dimensions.
To use the future default and silence this warning we advise to pass `rcond=None`, to keep using the old, explicitly pass `rcond=-1`.
C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f
vertex_index | Quality.AREA | Quality.ASPECT_RATIO | Quality.GAUSS_CURVATURE | Quality.MEAN_CURVATURE | Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL | Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL | |
---|---|---|---|---|---|---|---|
0 | 0 | 29.997389 | 1.600400 | 0.030287 | 0.000490 | 0.000257 | 0.000019 |
1 | 1 | 46.087046 | 1.602183 | 0.011136 | 0.000018 | 0.000250 | 0.000019 |
2 | 2 | 35.886338 | 1.400599 | 0.012633 | 0.000142 | 0.000253 | 0.000019 |
3 | 3 | 22.887296 | 1.751932 | 0.036979 | 0.000548 | 0.000379 | 0.000019 |
4 | 4 | 29.952347 | 1.220882 | 0.010277 | 0.000047 | 0.000391 | 0.000019 |
... | ... | ... | ... | ... | ... | ... | ... |
3319 | 3319 | 25.079661 | 1.340802 | 0.031878 | 0.000606 | 0.000168 | 0.000019 |
3320 | 3320 | 47.213916 | 1.254924 | 0.004615 | 0.000003 | 0.000169 | 0.000019 |
3321 | 3321 | 35.964707 | 1.140267 | 0.015661 | 0.000198 | 0.000163 | 0.000019 |
3322 | 3322 | 45.673529 | 1.189562 | 0.011380 | 0.000100 | 0.000152 | 0.000019 |
3323 | 3323 | 30.105530 | 1.151230 | 0.014396 | 0.000190 | 0.000163 | 0.000019 |
3324 rows × 7 columns
To get an overview about measurements, we can summarize them:
df.describe().T
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
vertex_index | 3324.0 | 1661.500000 | 9.597005e+02 | 0.000000 | 830.750000 | 1661.500000 | 2492.250000 | 3323.000000 |
Quality.AREA | 3324.0 | 33.753233 | 1.079078e+01 | 5.677486 | 26.694735 | 32.956835 | 39.255080 | 125.564101 |
Quality.ASPECT_RATIO | 3324.0 | 7.126810 | 8.990960e+01 | 1.038034 | 1.292444 | 1.437911 | 1.648299 | 3421.965459 |
Quality.GAUSS_CURVATURE | 3324.0 | 0.016958 | 3.527488e-02 | -1.031106 | 0.005509 | 0.013645 | 0.024739 | 0.348243 |
Quality.MEAN_CURVATURE | 3324.0 | 0.000383 | 7.652709e-03 | -0.028035 | -0.000135 | 0.000010 | 0.000270 | 0.426018 |
Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL | 3324.0 | 0.000258 | 6.857515e-05 | 0.000152 | 0.000214 | 0.000241 | 0.000275 | 0.000545 |
Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL | 3324.0 | 0.000019 | 3.388642e-21 | 0.000019 | 0.000019 | 0.000019 | 0.000019 | 0.000019 |
From that table, we can extract a single column as list.
sp_curvature = list(df['Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL'])
sp_curvature[:5]
[0.0002572409483117654,
0.00025042866718866794,
0.0002531992227477513,
0.00037887302875519685,
0.00039058363169657424]
Visualizing measurements#
To visualize the measurements, we need to attach them to the surface:
quantified_surface = nppas.set_vertex_values(surface, sp_curvature)
The visualization can be customized as well, e.g. by changing the view angle and the colormap.
quantified_surface.azimuth = -135
quantified_surface
nppas.SurfaceTuple
|
quantified_surface.cmap = 'jet'
quantified_surface
nppas.SurfaceTuple
|
Interacting with surface data in Napari#
We now open Napari to interact with the data
viewer = napari.Viewer(ndisplay=3)
viewer.camera.angles = (40, -30, 55)
surface_layer = viewer.add_surface(surface, colormap='hsv')
surface_layer.properties = df.to_dict(orient='list')
surface_layer.features = df
from napari_skimage_regionprops import add_table
add_table(surface_layer, viewer)
Napari status bar display of label properties disabled because https://github.com/napari/napari/issues/5417 and https://github.com/napari/napari/issues/4342
<napari_skimage_regionprops._table.TableWidget at 0x2c7b7ebd5e0>
viewer.window.add_dock_widget(nppas.SurfaceAnnotationWidget(viewer))
<napari._qt.widgets.qt_viewer_dock_widget.QtViewerDockWidget at 0x2c7b92c8700>
Manual annotation#
Use the napari window and the Surface Annotation Widget to draw with value 2 and 3 in concave and convex region on the surface.
napari.utils.nbscreenshot(viewer)
from napari_accelerated_pixel_and_object_classification._surface_vertex_classifier import SurfaceVertexClassifierWidget
viewer.window.add_dock_widget(SurfaceVertexClassifierWidget(viewer))
<napari._qt.widgets.qt_viewer_dock_widget.QtViewerDockWidget at 0x2c7b9330d30>
Selected surface layer: surface
Selected measurements: ['Quality.AREA', 'Quality.ASPECT_RATIO', 'Quality.GAUSS_CURVATURE', 'Quality.MEAN_CURVATURE', 'Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL', 'Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL']
selected properties Quality.AREA Quality.ASPECT_RATIO Quality.GAUSS_CURVATURE \
0 29.997389 1.600400 0.030287
1 46.087046 1.602183 0.011136
2 35.886338 1.400599 0.012633
3 22.887296 1.751932 0.036979
4 29.952347 1.220882 0.010277
... ... ... ...
3319 25.079661 1.340802 0.031878
3320 47.213916 1.254924 0.004615
3321 35.964707 1.140267 0.015661
3322 45.673529 1.189562 0.011380
3323 30.105530 1.151230 0.014396
Quality.MEAN_CURVATURE Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL \
0 0.000490 0.000257
1 0.000018 0.000250
2 0.000142 0.000253
3 0.000548 0.000379
4 0.000047 0.000391
... ... ...
3319 0.000606 0.000168
3320 0.000003 0.000169
3321 0.000198 0.000163
3322 0.000100 0.000152
3323 0.000190 0.000163
Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL
0 0.000019
1 0.000019
2 0.000019
3 0.000019
4 0.000019
... ...
3319 0.000019
3320 0.000019
3321 0.000019
3322 0.000019
3323 0.000019
[3324 rows x 6 columns]
As there are no 0 in the annotated classes, the minimum is subtracted
annotated classes [0. 0. 0. ... 0. 0. 0.]
RFC predictions finished. [2. 2. 3. ... 3. 2. 3.]
Train classifier#
Next, we can use the Surface Vertex Classification widget to train a classifier.
napari.utils.nbscreenshot(viewer)
Apply classifier#
The classifier is an apoc TableRowClassifier, which can be applied to the table/DataFrame above.
table_row_classifier = apoc.TableRowClassifier(opencl_filename='table_row_classifier.cl')
result = table_row_classifier.predict(df)
result
array([1, 1, 2, ..., 2, 1, 2], dtype=uint32)
df['classification'] = result
df
vertex_index | Quality.AREA | Quality.ASPECT_RATIO | Quality.GAUSS_CURVATURE | Quality.MEAN_CURVATURE | Quality.SPHERE_FITTED_CURVATURE_HECTA_VOXEL | Quality.SPHERE_FITTED_CURVATURE_KILO_VOXEL | _CLUSTER_ID | classification | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 29.997389 | 1.600400 | 0.030287 | 0.000490 | 0.000257 | 0.000019 | 2.0 | 1 |
1 | 1 | 46.087046 | 1.602183 | 0.011136 | 0.000018 | 0.000250 | 0.000019 | 2.0 | 1 |
2 | 2 | 35.886338 | 1.400599 | 0.012633 | 0.000142 | 0.000253 | 0.000019 | 3.0 | 2 |
3 | 3 | 22.887296 | 1.751932 | 0.036979 | 0.000548 | 0.000379 | 0.000019 | 3.0 | 2 |
4 | 4 | 29.952347 | 1.220882 | 0.010277 | 0.000047 | 0.000391 | 0.000019 | 3.0 | 2 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
3319 | 3319 | 25.079661 | 1.340802 | 0.031878 | 0.000606 | 0.000168 | 0.000019 | 3.0 | 2 |
3320 | 3320 | 47.213916 | 1.254924 | 0.004615 | 0.000003 | 0.000169 | 0.000019 | 2.0 | 1 |
3321 | 3321 | 35.964707 | 1.140267 | 0.015661 | 0.000198 | 0.000163 | 0.000019 | 3.0 | 2 |
3322 | 3322 | 45.673529 | 1.189562 | 0.011380 | 0.000100 | 0.000152 | 0.000019 | 2.0 | 1 |
3323 | 3323 | 30.105530 | 1.151230 | 0.014396 | 0.000190 | 0.000163 | 0.000019 | 3.0 | 2 |
3324 rows × 9 columns
annotated_surface = nppas.set_vertex_values(surface, result)
annotated_surface.azimuth = -135
annotated_surface
nppas.SurfaceTuple
|