diff --git a/setup.py b/setup.py
index 8fc8b2932d3a8d755dfe7f0c4ef4e332dce2941b..3624595a7dca53aacced312e24aee190cfdaf7dc 100644
--- a/setup.py
+++ b/setup.py
@@ -42,13 +42,17 @@ setup_kwds = dict(
         ],
         'cellImageQuantification': [
             'surfaceMeshCellCurvature = gnomon_package_tissueimagemesh.algorithm.cellImageQuantification.surfaceMeshCellCurvature',
+            'surfaceMeshCellLayer = gnomon_package_tissueimagemesh.algorithm.cellImageQuantification.surfaceMeshCellLayer',
+            'surfaceMeshCellProperty = gnomon_package_tissueimagemesh.algorithm.cellImageQuantification.surfaceMeshCellProperty'
         ],
         'meshFromImage': [
-            'imageSurfaceMesh = gnomon_package_tissueimagemesh.algorithm.meshFromImage.imageSurfaceMesh'
+            'imageSurfaceMesh = gnomon_package_tissueimagemesh.algorithm.meshFromImage.imageSurfaceMesh',
+            'segmentationSurfaceMesh = gnomon_package_tissueimagemesh.algorithm.meshFromImage.segmentationSurfaceMesh'
         ],
         'pointCloudQuantification': [
             'nucleiCurvatureNukem = gnomon_package_tissueimagemesh.algorithm.pointCloudQuantification.nucleiCurvatureNukem',
-            'nucleiLayerEstimation = gnomon_package_tissueimagemesh.algorithm.pointCloudQuantification.nucleiLayerEstimation'
+            'nucleiLayerEstimation = gnomon_package_tissueimagemesh.algorithm.pointCloudQuantification.nucleiLayerEstimation',
+            'surfaceMeshProperty = gnomon_package_tissueimagemesh.algorithm.pointCloudQuantification.surfaceMeshProperty'
         ],
     },
     keywords='',
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellCurvature.py b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellCurvature.py
index b291c79760cb291a430b2b4109a01e85a840c0a1..c76a31128b623cd6e7975c89186c1d5169c63a47 100644
--- a/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellCurvature.py
+++ b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellCurvature.py
@@ -7,7 +7,7 @@ import pandas as pd
 from dtkcore import d_real, d_inliststring
 
 from gnomon.utils import algorithmPlugin
-from gnomon.utils.decorators import cellImageInput, cellImageOutput, dataFrameOutput
+from gnomon.utils.decorators import cellImageInput, meshInput, cellImageOutput, dataFrameOutput
 from gnomon.core import gnomonAbstractCellImageQuantification
 
 from timagetk import TissueImage3D
@@ -18,6 +18,7 @@ from timagetk_geometry.features.tissue_mesh import compute_cell_curvature_featur
 
 
 @algorithmPlugin(version="0.3.0", coreversion="0.81.0", name="Surface Mesh Curvature")
+@meshInput('surface_topomesh', data_plugin="gnomonMeshDataPropertyTopomesh")
 @cellImageInput("tissue", data_plugin="gnomonCellImageDataTissueImage")
 @cellImageOutput("out_tissue", data_plugin="gnomonCellImageDataTissueImage")
 @dataFrameOutput('df', data_plugin="gnomonDataFrameDataPandas")
@@ -34,6 +35,7 @@ class surfaceMeshCellCurvature(gnomonAbstractCellImageQuantification):
         super().__init__()
 
         self.tissue = {}
+        self.surface_topomesh = {}
 
         self.out_tissue = {}
         self.df = {}
@@ -65,13 +67,16 @@ class surfaceMeshCellCurvature(gnomonAbstractCellImageQuantification):
             out_tissue.cells = deepcopy(tissue.cells)
             out_tissue.cells.image = out_tissue
 
-            surface_topomesh = tissue_image_surface_topomesh(
-                out_tissue,
-                resampling_voxelsize=self['resampling_voxelsize'],
-                surface_matching='cell',
-                orientation=(1 if self['orientation'] == "up" else -1),
-                down_facing_threshold=self['down_facing_threshold']
-            )
+            if time in self.surface_topomesh:
+                surface_topomesh = self.surface_topomesh[time]
+            else:
+                surface_topomesh = tissue_image_surface_topomesh(
+                    out_tissue,
+                    resampling_voxelsize=self['resampling_voxelsize'],
+                    surface_matching='cell',
+                    orientation=(1 if self['orientation'] == "up" else -1),
+                    down_facing_threshold=self['down_facing_threshold']
+                )
             compute_cell_curvature_features(out_tissue, surface_topomesh=surface_topomesh)
 
             self.out_tissue[time] = out_tissue
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellLayer.py b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellLayer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d65dfa410b80d80c6f6aa19e33721a0b5f4f879c
--- /dev/null
+++ b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellLayer.py
@@ -0,0 +1,89 @@
+import logging
+from copy import deepcopy
+
+import numpy as np
+import pandas as pd
+
+from dtkcore import d_real, d_inliststring
+
+from gnomon.utils import algorithmPlugin
+from gnomon.utils.decorators import cellImageInput, meshInput, cellImageOutput, dataFrameOutput
+from gnomon.core import gnomonAbstractCellImageQuantification
+
+from timagetk import TissueImage3D
+from timagetk.features.pandas_tools import cells_to_dataframe
+
+from timagetk_geometry.image_surface.tissue_image_mesh import tissue_image_surface_topomesh
+from timagetk_geometry.features.tissue_mesh import compute_cell_layer_feature
+
+
+@algorithmPlugin(version="0.3.0", coreversion="0.81.0", name="Surface Mesh Layer")
+@meshInput('surface_topomesh', data_plugin="gnomonMeshDataPropertyTopomesh")
+@cellImageInput("tissue", data_plugin="gnomonCellImageDataTissueImage")
+@cellImageOutput("out_tissue", data_plugin="gnomonCellImageDataTissueImage")
+@dataFrameOutput('df', data_plugin="gnomonDataFrameDataPandas")
+class surfaceMeshCellLayer(gnomonAbstractCellImageQuantification):
+    """Estimate the cell layer to which each cell in the image belong.
+
+    The method estimates the first layer of cells using a triangle mesh of the
+    tissue surface by selecting the nearest cells to at least one vertex of
+    the mesh. The layer information is then propagated to the rest of the
+    tissue through cell adjacencies.
+
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.tissue = {}
+        self.surface_topomesh = {}
+
+        self.out_tissue = {}
+        self.df = {}
+
+        self._parameters = {}
+        self._parameters['orientation'] = d_inliststring('Orientation', "up", ["up", "down"], "Whether to keep upper or lower part")
+        self._parameters['resampling_voxelsize'] = d_real("Resampling voxelsize", 1., 0.1, 5., 1, "Voxelsize to use for the resampling allowing to estimate the surface cell triangulation")
+        self._parameters['down_facing_threshold'] = d_real("Down threshold", 0., -1, 1., 2, "Angle threshold under with the surface (pointing downwards) will be removed")
+
+        self._parameter_groups = {}
+        for parameter_name in ['orientation','resampling_voxelsize', 'down_facing_threshold']:
+            self._parameter_groups[parameter_name] = 'surface_extraction'
+
+    def __del__(self):
+        pass
+
+    def run(self):
+        self.out_tissue = {}
+        self.df = {}
+
+        if len(self.tissue) == 0:
+            logging.error("Impossible to quantify! No segmented image provided")
+            return
+
+        for time in self.tissue.keys():
+            tissue = self.tissue[time]
+
+            out_tissue = TissueImage3D(tissue, not_a_label=0, background=1)
+            out_tissue.cells = deepcopy(tissue.cells)
+            out_tissue.cells.image = out_tissue
+
+            if time in self.surface_topomesh:
+                surface_topomesh = self.surface_topomesh[time]
+            else:
+                surface_topomesh = tissue_image_surface_topomesh(
+                    out_tissue,
+                    resampling_voxelsize=self['resampling_voxelsize'],
+                    surface_matching='cell',
+                    orientation=(1 if self['orientation'] == "up" else -1),
+                    down_facing_threshold=self['down_facing_threshold']
+                )
+            compute_cell_layer_feature(out_tissue, method='surface', surface_topomesh=surface_topomesh)
+
+            self.out_tissue[time] = out_tissue
+
+            self.df[time] = cells_to_dataframe(out_tissue.cells, cell_ids=out_tissue.cell_ids())
+            self.df[time]['time'] = time
+
+        if len(self.df) > 0:
+            self.df = {0: pd.concat([self.df[time] for time in self.df.keys()])}
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellProperty.py b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellProperty.py
new file mode 100644
index 0000000000000000000000000000000000000000..2371dc1fba6506139e13da87055350611d41cc79
--- /dev/null
+++ b/src/gnomon_package_tissueimagemesh/algorithm/cellImageQuantification/surfaceMeshCellProperty.py
@@ -0,0 +1,112 @@
+from copy import deepcopy
+
+import numpy as np
+import scipy.ndimage as nd
+import pandas as pd
+
+from dtkcore import d_inliststringlist, d_int
+
+import gnomon.core
+from gnomon.utils import algorithmPlugin
+from gnomon.utils.decorators import cellImageInput, meshInput, cellImageOutput, dataFrameOutput
+from gnomon.core import gnomonAbstractCellImageQuantification
+
+from timagetk import TissueImage3D
+from timagetk.features.pandas_tools import cells_to_dataframe
+
+from timagetk_geometry.features.cells import compute_cell_surface_center_feature
+
+@algorithmPlugin(version="0.3.0", coreversion="0.81.0", name="Surface Mesh Property")
+@meshInput('surface_topomesh', data_plugin="gnomonMeshDataPropertyTopomesh")
+@cellImageInput("tissue", data_plugin="gnomonCellImageDataTissueImage")
+@cellImageOutput('out_tissue', data_plugin="gnomonCellImageDataTissueImage")
+@dataFrameOutput('df', data_plugin="gnomonDataFrameDataPandas")
+class surfaceMeshCellProperty(gnomonAbstractCellImageQuantification):
+    """Transfer vertex attributes of a surface mesh to the nearest cells.
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.surface_topomesh = {}
+        self.tissue = {}
+        self.out_tissue = {}
+        self.df = {}
+
+        self._parameters = {}
+        self._parameters['attribute_names'] = d_inliststringlist("Attributes", [""], [""], "List of vertex attributes to transfer from the surface mesh")
+
+    def refreshParameters(self):
+        if len(self.surface_topomesh)>0:
+            surface_topomesh = list(self.surface_topomesh.values())[0]
+
+            attribute_names = self['attribute_names']
+            property_names = list(surface_topomesh.wisp_property_names(0))
+            if len(property_names) == 0:
+                property_names = [""]
+            attribute_names = [p for p in attribute_names if p in property_names]
+            if len(attribute_names) == 0:
+                attribute_names = property_names
+            self._parameters['attribute_names'].setValues(property_names)
+            self._parameters['attribute_names'].setValue(attribute_names)
+
+    def run(self):
+        self.out_tissue = {}
+        self.df = {}
+
+        for time in self.tissue.keys():
+            tissue = self.tissue[time]
+
+            surface_topomesh = self.surface_topomesh[time]
+
+            out_tissue = TissueImage3D(tissue, not_a_label=0, background=1)
+            out_tissue.cells = deepcopy(tissue.cells)
+            out_tissue.cells.image = out_tissue
+
+            cell_labels = out_tissue.cell_ids()
+            if surface_topomesh.has_wisp_property('cell', 0, is_computed=True):
+                vertex_cells = surface_topomesh.wisp_property('cell', 0).values(list(surface_topomesh.wisps(0)))
+            else:
+                if 'surface_center' in out_tissue.cells.feature_names():
+                    cell_surface_centers = out_tissue.cells.feature('surface_center')
+                else:
+                    cell_surface_centers = compute_cell_surface_center_feature(out_tissue)
+                cell_centers = np.array([cell_surface_centers[c] for c in cell_labels])
+
+                vertex_points = surface_topomesh.wisp_property('barycenter', 0).values(list(surface_topomesh.wisps(0)))
+                vertex_cell_distances = np.linalg.norm(vertex_points[:, np.newaxis] - cell_centers[np.newaxis], axis=-1)
+                vertex_cells = cell_labels[np.argmin(vertex_cell_distances, axis=-1)]
+
+                surface_topomesh.update_wisp_property('cell', 0, dict(zip(surface_topomesh.wisps(0), vertex_cells)))
+                surface_topomesh.update_wisp_property('label', 0, dict(zip(surface_topomesh.wisps(0), vertex_cells%256)))
+
+            for property_name in self['attribute_names']:
+                vertex_property = surface_topomesh.wisp_property(property_name, 0).values(list(surface_topomesh.wisps(0)))
+                property_values = np.unique(vertex_property)
+                property_is_binary = vertex_property.ndim == 1 and len(property_values) <= 2 and (0 in property_values or 1 in property_values)
+                cell_property = None
+                if property_is_binary:
+                    cell_property = (nd.sum(vertex_property, vertex_cells, index=cell_labels) > 0).astype(int)
+                else:
+                    if vertex_property.ndim == 1:
+                        cell_property = nd.mean(vertex_property, vertex_cells, index=cell_labels)
+                    elif vertex_property.ndim == 2:
+                        cell_property = list(np.transpose([
+                            nd.mean(vertex_property[:, k], vertex_cells, index=cell_labels)
+                            for k in range(vertex_property.shape[1])
+                        ]))
+                    elif vertex_property.ndim == 3:
+                        cell_property = list(np.transpose([[
+                            nd.mean(vertex_property[:, j, k], vertex_cells, index=cell_labels)
+                            for k in range(vertex_property.shape[2])
+                        ] for j in range(vertex_property.shape[1])], (2, 1, 0)))
+                if cell_property is not None:
+                    out_tissue.cells.set_feature(property_name, dict(zip(cell_labels, cell_property)))
+
+            self.out_tissue[time] = out_tissue
+
+            self.df[time] = cells_to_dataframe(out_tissue.cells, cell_ids=out_tissue.cell_ids())
+            self.df[time]['time'] = time
+
+        if len(self.df)>0:
+            self.df = {0: pd.concat([self.df[time] for time in self.df.keys()])}
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/imageSurfaceMesh.py b/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/imageSurfaceMesh.py
index 0cc29b48e19410b77f8cd5b2d45fc7bd98e20887..dd3874814773d1c70beaf4724478effcb22df377 100644
--- a/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/imageSurfaceMesh.py
+++ b/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/imageSurfaceMesh.py
@@ -11,10 +11,9 @@ from gnomon.utils import corePlugin
 
 from gnomon.utils.decorators import imageInput, meshOutput
 
-from timagetk_geometry.image_surface.image_mesh import image_surface_topomesh
+from timagetk_geometry.image_surface.image_mesh import image_surface_topomesh, up_facing_surface_topomesh
 
 from cellcomplex.property_topomesh.analysis import compute_topomesh_property
-from cellcomplex.property_topomesh.extraction import cut_surface_topomesh
 from cellcomplex.property_topomesh.creation import triangle_topomesh
 from cellcomplex.utils import array_dict
 
@@ -29,8 +28,10 @@ class imageSurfaceMesh(gnomonAbstractMeshFromImage):
     filter of standard deviation gaussian_sigma and the result is thresholded
     by the value of the threshold parameter. The obtained binary mask is
     meshed using a Marching Cubes algorithm and decimated to reach the target
-    edge_length. Only the top or bottom part of the mesh is kept depending on
-    the orientation parameter, up to a given cut_level.
+    edge_length. Only the largest connected component o the top or bottom part
+    of the mesh is kept depending on the orientation parameter, after removing
+    the parts beyond a given threshold of vertical orientation of the surface
+    normal vectors.
     """
 
     def __init__(self):
@@ -46,13 +47,13 @@ class imageSurfaceMesh(gnomonAbstractMeshFromImage):
 
         self._parameters['gaussian_sigma'] = d_real('Sigma', 2, 0., 20., 2, "Standard deviation of the Gaussian kernel used to smooth signal")
         self._parameters['orientation'] = d_inliststring('Orientation', "up", ["up", "down"], "Whether to keep upper or lower part")
-        self._parameters['cut_level'] = d_real('Cut level', 0, 0, 50, 1, "Level of the z cut")
+        self._parameters['down_threshold'] = d_real('Orientation threshold', 0, -1, 1, 1, "The z coordinate below which a normal vector is considered to face downwards")
         self._parameters['edge_length'] = d_real('Edge length', 10, 0, 20, 1, "Maximal length of mesh edges")
 
         self._parameter_groups = {}
         for parameter_name in ['resampling_voxelsize', 'gaussian_sigma']:
             self._parameter_groups[parameter_name] = 'image_preprocessing'
-        for parameter_name in ['orientation', 'cut_level']:
+        for parameter_name in ['orientation', 'down_threshold']:
             self._parameter_groups[parameter_name] = 'surface_cutting'
 
     def refreshParameters(self):
@@ -79,16 +80,12 @@ class imageSurfaceMesh(gnomonAbstractMeshFromImage):
                 self._parameters['threshold'].setMax(255)
             elif img.dtype == np.uint16:
                 self._parameters['threshold'].setMax(65535)
-            self._parameters['threshold'].setValue(1.2*threshold_otsu(img.get_array()))
+            self._parameters['threshold'].setValue(int(np.round(1.2*threshold_otsu(img.get_array()))))
 
             self._parameters['resampling_voxelsize'].setMin(0)
             self._parameters['resampling_voxelsize'].setMax(10*np.max(img.voxelsize))
             self._parameters['resampling_voxelsize'].setValue(4*np.mean(img.voxelsize))
 
-            self._parameters['cut_level'].setMin(0.)
-            self._parameters['cut_level'].setMax(img.shape[0]*img.voxelsize[0])
-            self._parameters['cut_level'].setValue(img.shape[0]*img.voxelsize[0]/2.)
-
     def run(self):
 
         self.topomesh = {}
@@ -109,9 +106,13 @@ class imageSurfaceMesh(gnomonAbstractMeshFromImage):
                 target_length=self['edge_length']
             )
 
-            topomesh = cut_surface_topomesh(topomesh, z_cut=self['cut_level'], below=self['orientation']=="up")
-            # topomesh = up_facing_surface_topomesh(topomesh, normal_method="orientation")
-
+            topomesh = up_facing_surface_topomesh(
+                topomesh,
+                upwards=self['orientation']=="up",
+                down_facing_threshold=self['down_threshold'],
+                normal_method='orientation',
+                connected=True
+            )
             compute_topomesh_property(topomesh, 'vertices', 2)
 
             points = topomesh.wisp_property('barycenter', 0).values(list(topomesh.wisps(0)))
@@ -120,9 +121,6 @@ class imageSurfaceMesh(gnomonAbstractMeshFromImage):
             triangles = point_mapping.values(triangles)
 
             topomesh = triangle_topomesh(triangles, points)
-            #def cut_surface_topomesh(input_topomesh, z_cut=0, below=True):
-            #nuclei_sigma=2., density_voxelsize=1., intensity_threshold=2000., microscope_orientation=1, maximal_length=10., remeshing_iterations=10,
-            #erosion_radius=0.0, smoothing=50, decimation=100):
 
             for degree in [1, 2, 3]:
                 compute_topomesh_property(topomesh, 'barycenter', degree)
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/segmentationSurfaceMesh.py b/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/segmentationSurfaceMesh.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d4cd4fe43df392ff33cd635407638dc404e3dad
--- /dev/null
+++ b/src/gnomon_package_tissueimagemesh/algorithm/meshFromImage/segmentationSurfaceMesh.py
@@ -0,0 +1,69 @@
+import numpy as np
+
+from skimage.filters.thresholding import threshold_otsu
+
+from dtkcore import d_int, d_inliststring, d_real
+
+import gnomon.core
+from gnomon.core import gnomonAbstractMeshFromImage
+
+from gnomon.utils import corePlugin
+
+from gnomon.utils.decorators import cellImageInput, meshOutput
+
+from timagetk_geometry.image_surface.tissue_image_mesh import tissue_image_surface_topomesh
+
+from cellcomplex.property_topomesh.analysis import compute_topomesh_property
+
+
+@corePlugin(version="0.3.0", coreversion="0.81.0", name="Segmentation Surface Mesh")
+@cellImageInput("seg_img", data_plugin='gnomonCellImageDataTissueImage')
+@meshOutput("topomesh", data_plugin="gnomonMeshDataPropertyTopomesh")
+class segmentationSurfaceMesh(gnomonAbstractMeshFromImage):
+    """Reconstruct the surface on an segmented image as a triangular mesh.
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.seg_img = {}
+        self.topomesh = {}
+
+        self._parameters = {}
+        self._parameters['resampling_voxelsize'] = d_real("Resampling voxelsize", 0, 0, 1, 2, "Cubic voxelsize in which to resample the image before mesh extraction")
+        self._parameters['orientation'] = d_inliststring('Orientation', "up", ["up", "down"], "Whether to keep upper or lower part")
+
+        self._parameter_groups = {}
+
+    def refreshParameters(self):
+        if len(self.seg_img)>0:
+            seg_img = list(self.seg_img.values())[0]
+
+            self._parameters['resampling_voxelsize'].setMin(0)
+            self._parameters['resampling_voxelsize'].setMax(10*np.max(seg_img.voxelsize))
+            self._parameters['resampling_voxelsize'].setValue(4*np.mean(seg_img.voxelsize))
+
+    def run(self):
+        self.topomesh = {}
+
+        for time in self.seg_img.keys():
+            seg_img = self.seg_img[time]
+
+            resampling_voxelsize = None if self['resampling_voxelsize'] == 0 else self['resampling_voxelsize']
+            topomesh = tissue_image_surface_topomesh(
+                seg_img=seg_img,
+                resampling_voxelsize=resampling_voxelsize,
+                orientation=(1 if self['orientation']=="up" else -1),
+                surface_matching='cell',
+                padding=False,
+            )
+
+            for degree in [1, 2, 3]:
+                compute_topomesh_property(topomesh, 'barycenter', degree)
+
+            for degree in [0, 1, 2, 3]:
+                positions = topomesh.wisp_property('barycenter', degree)
+                for k, dim in enumerate(['x', 'y', 'z']):
+                    topomesh.update_wisp_property('barycenter_'+dim, degree, dict(zip(positions.keys(), positions.values()[:, k])))
+
+            self.topomesh[time] = topomesh
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiCurvatureNukem.py b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiCurvatureNukem.py
index 1570e37eb7be202182ed2ddc11ff32c6ab00cfaa..00e6d39588ae635006537b849d73d47aa6ec3c12 100644
--- a/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiCurvatureNukem.py
+++ b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiCurvatureNukem.py
@@ -2,15 +2,17 @@ from copy import deepcopy
 
 import numpy as np
 import pandas as pd
-from cellcomplex.property_topomesh.utils.pandas_tools import topomesh_to_dataframe
-from dtkcore import d_inliststring
-from dtkcore import d_int
+
+from dtkcore import d_inliststring, d_int
+
 from gnomon.utils import algorithmPlugin
-from gnomon.utils.decorators import dataFrameOutput
-from gnomon.utils.decorators import imageInput
-from gnomon.utils.decorators import pointCloudInput
-from gnomon.utils.decorators import pointCloudOutput
+from gnomon.utils.decorators import imageInput, meshInput, pointCloudInput
+from gnomon.utils.decorators import dataFrameOutput, pointCloudOutput
+
 from gnomon.core import gnomonAbstractPointCloudQuantification
+
+from cellcomplex.property_topomesh.utils.pandas_tools import topomesh_to_dataframe
+
 from tissue_nukem_3d.nuclei_mesh_tools import nuclei_surface_topomesh_curvature
 
 
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiLayerEstimation.py b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiLayerEstimation.py
index e3b851c3d589261895972fab3df129556336ff6e..76314ff0387585b3fe1c76c48f174be6cf66410c 100644
--- a/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiLayerEstimation.py
+++ b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/nucleiLayerEstimation.py
@@ -6,7 +6,7 @@ import pandas as pd
 from dtkcore import d_inliststring, d_int
 
 from gnomon.utils import algorithmPlugin
-from gnomon.utils.decorators import pointCloudInput, imageInput, pointCloudOutput, dataFrameOutput
+from gnomon.utils.decorators import pointCloudInput, imageInput, meshInput, pointCloudOutput, dataFrameOutput
 from gnomon.core import gnomonAbstractPointCloudQuantification
 
 from timagetk.components.spatial_image import SpatialImage
@@ -15,6 +15,7 @@ from timagetk_geometry.features.nuclei import nuclei_layer
 
 @algorithmPlugin(version="0.3.0", coreversion="0.81.0", name="Nuclei L1 Estimation")
 @imageInput('img', data_plugin='gnomonImageDataMultiChannelImage')
+@meshInput('surface_topomesh', data_plugin="gnomonMeshDataPropertyTopomesh")
 @pointCloudInput("df", data_plugin="gnomonPointCloudDataPandas")
 @pointCloudOutput('out_df', data_plugin="gnomonPointCloudDataPandas")
 @dataFrameOutput('data_df', data_plugin="gnomonDataFrameDataPandas")
@@ -31,6 +32,7 @@ class nucleiLayerEstimation(gnomonAbstractPointCloudQuantification):
         super().__init__()
 
         self.img = {}
+        self.surface_topomesh = {}
         self.df = {}
         self.out_df = {}
         self.data_df = {}
@@ -71,6 +73,11 @@ class nucleiLayerEstimation(gnomonAbstractPointCloudQuantification):
             nuclei_points = df[['center_'+dim for dim in 'xyz']].values
             nuclei_positions = dict(zip(df['label'].values, nuclei_points))
 
+            if (time in self.surface_topomesh) and self.surface_topomesh[time] is not None:
+                surface_topomesh = self.surface_topomesh[time]
+            else:
+                surface_topomesh = None
+
             if (time in self.img) and self.img[time] is not None:
                 img = self.img[time]
                 if 'surface_channel' in self._parameters.keys():
@@ -83,8 +90,10 @@ class nucleiLayerEstimation(gnomonAbstractPointCloudQuantification):
                 img = SpatialImage(np.zeros(shape), voxelsize=(1., 1., 1.))
                 surface_mode = "density"
 
+            print(surface_topomesh)
             cell_layer, surface_topomesh = nuclei_layer(
                 nuclei_positions,
+                surface_topomesh=surface_topomesh,
                 nuclei_img=img,
                 upwards=(self['orientation'] == 'upright'),
                 surface_mode=surface_mode,
diff --git a/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/surfaceMeshProperty.py b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/surfaceMeshProperty.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a61becb6bbe3ae3d5e553de5557a694c53fbcfb
--- /dev/null
+++ b/src/gnomon_package_tissueimagemesh/algorithm/pointCloudQuantification/surfaceMeshProperty.py
@@ -0,0 +1,99 @@
+from copy import deepcopy
+
+import numpy as np
+import scipy.ndimage as nd
+import pandas as pd
+
+from dtkcore import d_inliststringlist, d_int
+
+import gnomon.core
+from gnomon.utils import algorithmPlugin
+from gnomon.utils.decorators import pointCloudInput, imageInput, meshInput, pointCloudOutput, dataFrameOutput
+from gnomon.core import gnomonAbstractPointCloudQuantification
+
+
+@algorithmPlugin(version="0.3.0", coreversion="0.81.0", name="Nuclei Mesh Property")
+@meshInput('surface_topomesh', data_plugin="gnomonMeshDataPropertyTopomesh")
+@pointCloudInput("df", data_plugin="gnomonPointCloudDataPandas")
+@pointCloudOutput('out_df', data_plugin="gnomonPointCloudDataPandas")
+@dataFrameOutput('data_df', data_plugin="gnomonDataFrameDataPandas")
+class surfaceMeshProperty(gnomonAbstractPointCloudQuantification):
+    """Transfer vertex attributes of a surface mesh to the nearest points.
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.surface_topomesh = {}
+        self.df = {}
+        self.out_df = {}
+        self.data_df = {}
+
+        self._parameters = {}
+        self._parameters['attribute_names'] = d_inliststringlist("Attributes", [""], [""], "List of vertex attributes to transfer from the surface mesh")
+
+    def refreshParameters(self):
+        if len(self.surface_topomesh)>0:
+            surface_topomesh = list(self.surface_topomesh.values())[0]
+
+            attribute_names = self['attribute_names']
+            property_names = list(surface_topomesh.wisp_property_names(0))
+            if len(property_names) == 0:
+                property_names = [""]
+            attribute_names = [p for p in attribute_names if p in property_names]
+            if len(attribute_names) == 0:
+                attribute_names = property_names
+            self._parameters['attribute_names'].setValues(property_names)
+            self._parameters['attribute_names'].setValue(attribute_names)
+
+    def run(self):
+        self.data_df = {}
+        self.out_df = {}
+
+        for time in self.df.keys():
+            df = self.df[time]
+
+            surface_topomesh = self.surface_topomesh[time]
+
+            out_df = deepcopy(df)
+
+            cell_labels = df['label'].values
+            if surface_topomesh.has_wisp_property('cell', 0, is_computed=True):
+                vertex_cells = surface_topomesh.wisp_property('cell', 0).values(list(surface_topomesh.wisps(0)))
+            else:
+                cell_centers = df[['center_'+dim for dim in 'xyz']].values
+
+                vertex_points = surface_topomesh.wisp_property('barycenter', 0).values(list(surface_topomesh.wisps(0)))
+                vertex_cell_distances = np.linalg.norm(vertex_points[:, np.newaxis] - cell_centers[np.newaxis], axis=-1)
+                vertex_cells = cell_labels[np.argmin(vertex_cell_distances, axis=-1)]
+
+                surface_topomesh.update_wisp_property('cell', 0, dict(zip(surface_topomesh.wisps(0), vertex_cells)))
+                surface_topomesh.update_wisp_property('label', 0, dict(zip(surface_topomesh.wisps(0), vertex_cells%256)))
+
+            for property_name in self['attribute_names']:
+                vertex_property = surface_topomesh.wisp_property(property_name, 0).values(list(surface_topomesh.wisps(0)))
+                property_values = np.unique(vertex_property)
+                property_is_binary = vertex_property.ndim == 1 and len(property_values) <= 2 and (0 in property_values or 1 in property_values)
+                if property_is_binary:
+                    out_df[property_name] = (nd.sum(vertex_property, vertex_cells, index=cell_labels) > 0).astype(int)
+                else:
+                    if vertex_property.ndim == 1:
+                        out_df[property_name] = nd.mean(vertex_property, vertex_cells, index=cell_labels)
+                    elif vertex_property.ndim == 2:
+                        out_df[property_name] = list(np.transpose([
+                            nd.mean(vertex_property[:, k], vertex_cells, index=cell_labels)
+                            for k in range(vertex_property.shape[1])
+                        ]))
+                    elif vertex_property.ndim == 3:
+                        out_df[property_name] = list(np.transpose([[
+                            nd.mean(vertex_property[:, j, k], vertex_cells, index=cell_labels)
+                            for k in range(vertex_property.shape[2])
+                        ] for j in range(vertex_property.shape[1])], (2, 1, 0)))
+
+            self.out_df[time] = out_df
+
+            self.data_df[time] = deepcopy(out_df)
+            self.data_df[time]['time'] = time
+
+        if len(self.data_df)>0:
+            self.data_df = {0: pd.concat([self.data_df[time] for time in self.data_df.keys()])}