Repository: mikedh/trimesh

Issue:
Is there a way to concentrate sampling of points on a trimesh.base.Trimesh mesh?
Hello,

I am able to sample a trimesh.base.Trimesh mesh surface very well using trimesh.sample.sample_surface() or trimesh.sample.sample_surface_even(). However, these sample the entire surface near evenly. 

I have a mesh such as the one below, which I want to sample with points :
![coarse](https://user-images.githubusercontent.com/75090879/126529724-2a6feadb-cdf3-4337-8cc2-196cdc57feda.png)

I want to sample this surface with points very finely at the curved regions, such as the radii (areas with fine mesh in the figure) and to have less points on the flat surfaces.

In practice, my mesh looks something like this:
![fine](https://user-images.githubusercontent.com/75090879/126529975-00ac1e8a-5144-4b0b-b1c8-a6b0a05b2dc3.png)

where there are fine triangles everywhere (the first image was only shown for clarity).

Is there a way to selective sample more points where the geometry is curved, and few where the geometry is flat?

Thanks very much!



Discussion:

Patch:
diff --git a/trimesh/base.py b/trimesh/base.py
index 6c0912a8..359e7fae 100644
--- a/trimesh/base.py
+++ b/trimesh/base.py
@@ -2241,7 +2241,7 @@ class Trimesh(Geometry3D):
         hull = convex.convex_hull(self)
         return hull
 
-    def sample(self, count, return_index=False):
+    def sample(self, count, return_index=False, face_weight=None):
         """
         Return random samples distributed across the
         surface of the mesh
@@ -2253,6 +2253,9 @@ class Trimesh(Geometry3D):
         return_index : bool
           If True will also return the index of which face each
           sample was taken from.
+        face_weight : None or len(mesh.faces) float
+          Weight faces by a factor other than face area.
+          If None will be the same as face_weight=mesh.area
 
         Returns
         ---------
@@ -2261,7 +2264,8 @@ class Trimesh(Geometry3D):
         face_index : (count, ) int
           Index of self.faces
         """
-        samples, index = sample.sample_surface(mesh=self, count=count)
+        samples, index = sample.sample_surface(
+            mesh=self, count=count, face_weight=face_weight)
         if return_index:
             return samples, index
         return samples
diff --git a/trimesh/sample.py b/trimesh/sample.py
index 01d6916b..ef312240 100644
--- a/trimesh/sample.py
+++ b/trimesh/sample.py
@@ -4,13 +4,14 @@ sample.py
 
 Randomly sample surface and volume of meshes.
 """
+
 import numpy as np
 
 from . import util
 from . import transformations
 
 
-def sample_surface(mesh, count):
+def sample_surface(mesh, count, face_weight=None):
     """
     Sample the surface of a mesh, returning the specified
     number of points
@@ -24,6 +25,9 @@ def sample_surface(mesh, count):
       Geometry to sample the surface of
     count : int
       Number of points to return
+    face_weight : None or len(mesh.faces) float
+      Weight faces by a factor other than face area.
+      If None will be the same as face_weight=mesh.area
 
     Returns
     ---------
@@ -33,15 +37,18 @@ def sample_surface(mesh, count):
       Indices of faces for each sampled point
     """
 
-    # len(mesh.faces) float, array of the areas
-    # of each face of the mesh
-    area = mesh.area_faces
-    # total area (float)
-    area_sum = np.sum(area)
-    # cumulative area (len(mesh.faces))
-    area_cum = np.cumsum(area)
-    face_pick = np.random.random(count) * area_sum
-    face_index = np.searchsorted(area_cum, face_pick)
+    if face_weight is None:
+        # len(mesh.faces) float, array of the areas
+        # of each face of the mesh
+        face_weight = mesh.area_faces
+
+    # cumulative sum of weights (len(mesh.faces))
+    weight_cum = np.cumsum(face_weight)
+
+    # last value of cumulative sum is total summed weight/area
+    face_pick = np.random.random(count) * weight_cum[-1]
+    # get the index of the selected faces
+    face_index = np.searchsorted(weight_cum, face_pick)
 
     # pull triangles into the form of an origin + 2 vectors
     tri_origins = mesh.triangles[:, 0]
diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py
index 507b1ded..238f2e68 100644
--- a/trimesh/scene/transforms.py
+++ b/trimesh/scene/transforms.py
@@ -543,6 +543,44 @@ class EnforcedForest(object):
 
         return True
 
+    def remove_node(self, u):
+        """
+        Remove a node from the forest.
+
+        Parameters
+        -----------
+        u : any
+          Hashable node key.
+
+        Returns
+        --------
+        changed : bool
+          Return if this operation changed anything.
+        """
+        # check if node is part of forest
+        if u not in self.node_data:
+            return False
+
+        # topology will change so clear cache
+        self._cache = {}
+
+        # delete all children's references and parent reference
+        children = [child for (child, parent) in self.parents.items() if parent == u]
+        for c in children:
+            del self.parents[c]
+        if u in self.parents:
+            del self.parents[u]
+
+        # delete edge data
+        edges = [(a, b) for (a, b) in self.edge_data if a == u or b == u]
+        for e in edges:
+            del self.edge_data[e]
+
+        # delete node data
+        del self.node_data[u]
+
+        return True
+
     def shortest_path(self, u, v):
         """
         Find the shortest path beween `u` and `v`.
diff --git a/trimesh/version.py b/trimesh/version.py
index 84fb14cc..ae9c5753 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.9.24'
+__version__ = '3.9.25'


Test patch:
diff --git a/tests/test_sample.py b/tests/test_sample.py
index 956485b6..27e8627f 100644
--- a/tests/test_sample.py
+++ b/tests/test_sample.py
@@ -20,6 +20,25 @@ class SampleTest(g.unittest.TestCase):
         distance = m.nearest.signed_distance(even)
         assert g.np.abs(distance).max() < 1e-4
 
+    def test_weights(self):
+        m = g.trimesh.creation.box()
+
+        # weigh all faces except first as zero
+        weights = g.np.zeros(len(m.faces))
+        weights[0] = 1.0
+
+        # sample with passed weights
+        points, fid = m.sample(count=100,
+                               return_index=True,
+                               face_weight=weights)
+        # all faces should be on single face
+        assert (fid == 0).all()
+
+        # oversample box to make sure weights aren't screwing
+        # up ability to get every face when weighted by area
+        assert set(g.np.unique(m.sample(
+            100000, return_index=True)[1])) == set(range(len(m.faces)))
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()
diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py
index 21728c59..7dcaa575 100644
--- a/tests/test_scenegraph.py
+++ b/tests/test_scenegraph.py
@@ -86,6 +86,23 @@ class GraphTests(g.unittest.TestCase):
         assert g.np.allclose(combine[:3, :3], rot[:3, :3])
         assert g.np.allclose(combine[:3, 3], trans)
 
+    def test_remove_node(self):
+        s = g.get_mesh("CesiumMilkTruck.glb")
+
+        assert len(s.graph.nodes_geometry) == 5
+        assert len(s.graph.nodes) == 9
+        assert len(s.graph.transforms.node_data) == 9
+        assert len(s.graph.transforms.edge_data) == 8
+        assert len(s.graph.transforms.parents) == 8
+
+        assert s.graph.transforms.remove_node("1")
+
+        assert len(s.graph.nodes_geometry) == 5
+        assert len(s.graph.nodes) == 9
+        assert len(s.graph.transforms.node_data) == 8
+        assert len(s.graph.transforms.edge_data) == 6
+        assert len(s.graph.transforms.parents) == 6
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()