Repository: mikedh/trimesh
Pull request #1324 (2021-08-15 17:45:06)

calculate integral mean curvature
This function calculates the integral mean curvature of a mesh. With this addition, all four Minkowski functionals for a surface mesh embedded in 3d can be calculated by Trimesh. (Volume, Area, Integral Mean Curvature, Euler number)

In the most general case, the lengths of the two numpy arrays,
self.face_adjacency_angles
and
self.edges_unique_length,

may not be equal. This could be checked with an 'assert', if necessary.


Patch:
diff --git a/trimesh/base.py b/trimesh/base.py
index 359e7fae..5ee5c52e 100644
--- a/trimesh/base.py
+++ b/trimesh/base.py
@@ -1449,6 +1449,21 @@ class Trimesh(Geometry3D):
         populate = self.face_adjacency_radius
         return self._cache['face_adjacency_span']
 
+    @caching.cache_decorator
+    def integral_mean_curvature(self):
+        """
+        The integral mean curvature, or the surface integral of the mean curvature.
+
+        Returns
+        ---------
+        area : float
+          Integral mean curvature of mesh
+        """
+        edges_length = np.linalg.norm(np.subtract(
+            *self.vertices[self.face_adjacency_edges.T]), axis=1)
+        imc = (self.face_adjacency_angles * edges_length).sum() * 0.5
+        return imc
+
     @caching.cache_decorator
     def vertex_adjacency_graph(self):
         """


Test patch:
diff --git a/tests/test_integralmeancurvature.py b/tests/test_integralmeancurvature.py
new file mode 100644
index 00000000..2fc5e7b8
--- /dev/null
+++ b/tests/test_integralmeancurvature.py
@@ -0,0 +1,45 @@
+try:
+    from . import generic as g
+except BaseException:
+    import generic as g
+
+
+class IntegralMeanCurvatureTest(g.unittest.TestCase):
+
+    def test_IMCsphere(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        for radius in [0.1, 1.0, 3.1459, 29.20]:
+            m = g.trimesh.creation.icosphere(subdivisions=4, radius=radius)
+            IMC = m.integral_mean_curvature
+            ref = 4 * g.np.pi * radius
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+    def test_IMCcapsule(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        radius = 1.2
+        for aspect_ratio in [0.0, 0.5, 1.0, 4.0, 100]:
+            L = aspect_ratio * radius
+            m = g.trimesh.creation.capsule(height=L, radius=radius, count=[64, 64])
+            IMC = m.integral_mean_curvature
+            ref = g.np.pi * (L + 4 * radius)
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+    def test_IMCbox(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        n_tests = 4
+        extents = 1 - g.np.random.random((n_tests, 3))
+        for extent in extents:
+            m = g.trimesh.creation.box(extents=extent)
+            IMC = m.integral_mean_curvature
+            # only the right angle (=pi/2) edges contribute,
+            # edges that lie on the faces of the box do not
+            ref = 4 * extent.sum() * g.np.pi / 2 * 0.5
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+
+if __name__ == '__main__':
+    g.trimesh.util.attach_to_log()
+    g.unittest.main()