Repository: mikedh/trimesh
Pull request #1851 (2023-02-22 18:10:03)

Reduce name collisions to speed up loading of gltfs
`unique_name` can become incredibly slow because of [this loop](https://github.com/mikedh/trimesh/blob/main/trimesh/util.py#L2437) if given many colliding names. For example, if loading a model without geometry or node names, or with many repeated names, `unique_name` will identify that there is a conflict and then try appending and then incrementing an integer until the name is unique. For later names, when there are already many with appended integers, this approaches a double for loop over all the names.

The change here adds an optional argument to `unique_name` that can allow it to keep track of how far it has incremented for different input names and start from this point, significantly speeding it up in certain cases.

Description from first attempt
~~The changes here append a unique integer to each name before checking it's uniqueness, to significantly reduce the number of name collisions. The side effect is that all the geometry and node names have an integer appended. If it is important to preserve the original names if at all possible, another option would be to create a unique name generator that keeps track of base names that have been seen before with a counter for each.~~


Patch:
diff --git a/README.md b/README.md
index be65d759..5a2fd164 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![trimesh](https://trimsh.org/images/logotype-a.svg)](http://trimsh.org)
 
 -----------
-[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)  [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/trimesh/trimesh?label=docker)](https://hub.docker.com/repository/docker/trimesh/trimesh/)
+[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)
 
 
 Trimesh is a pure Python (2.7-3.5+) library for loading and using [triangular meshes](https://en.wikipedia.org/wiki/Triangle_mesh) with an emphasis on watertight surfaces. The goal of the library is to provide a full featured and well tested Trimesh object which allows for easy manipulation and analysis, in the style of the Polygon object in the [Shapely library](https://github.com/Toblerity/Shapely).
diff --git a/examples/sinter.py b/examples/sinter.py
index a5c57918..c534f8ce 100644
--- a/examples/sinter.py
+++ b/examples/sinter.py
@@ -17,7 +17,7 @@ models = os.path.abspath(os.path.join(
     os.path.expanduser(os.path.dirname(__file__)), '..', 'models'))
 
 
-def collect_meshes(count=None, max_size=20.0):
+def collect_meshes(count=None):
     """
     Collect single body watertight meshes from our
     models folder.
@@ -41,8 +41,7 @@ def collect_meshes(count=None, max_size=20.0):
             pass
         for ori in scene.geometry.values():
             if (not isinstance(ori, trimesh.Trimesh) or
-                    not ori.is_watertight or ori.volume < 0.001 or
-                    ori.extents.max() > max_size):
+                    not ori.is_watertight or ori.volume < 0.001):
                 continue
 
             # split into single body meshes
@@ -59,17 +58,16 @@ def collect_meshes(count=None, max_size=20.0):
 
 if __name__ == '__main__':
 
-    size = 10.0
     # get some sample data
-    meshes = collect_meshes(max_size=size)
+    meshes = collect_meshes(count=100)
 
     print('loaded {} meshes'.format(len(meshes)))
 
     # place the meshes into the volume
     with Profiler() as P:
         placed, transforms, consume = packing.meshes(
-            meshes, size=[size] * 3, spacing=0.1)
-    P.print(show_all=True)
+            meshes, spacing=0.05)
+    P.print()
 
     # none of the placed meshes should have overlapping AABB
     assert not packing.bounds_overlap([i.bounds for i in placed])
@@ -85,3 +83,6 @@ if __name__ == '__main__':
         plane_origin=concat.bounds[0],
         plane_normal=[0, 0, 1],
         heights=np.linspace(0.0, 10.0, 100))
+
+    # show a cross section in the middle
+    sections[50].show()
diff --git a/trimesh/comparison.py b/trimesh/comparison.py
index c29cec9a..7cbbce5a 100644
--- a/trimesh/comparison.py
+++ b/trimesh/comparison.py
@@ -15,7 +15,7 @@ from .constants import tol
 # how many significant figures to use for each
 # field of the identifier based on hand-tuning
 id_sigfig = np.array(
-    [4,   # area
+    [4,  # area
      10,  # euler number
      5,  # area/volume ratio
      2,  # convex/mesh area ratio
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index f46b9a7f..dc2489a8 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -1365,7 +1365,7 @@ def _read_buffers(header,
     # load data from accessors into Trimesh objects
     meshes = collections.OrderedDict()
 
-    names_original = collections.defaultdict(list)
+    start_increments = {}
 
     for index, m in enumerate(header.get("meshes", [])):
 
@@ -1389,9 +1389,8 @@ def _read_buffers(header,
                 attr = p['attributes']
                 # create a unique mesh name per- primitive
                 name = m.get('name', 'GLTF')
-                names_original[index].append(name)
                 # make name unique across multiple meshes
-                name = unique_name(name, meshes)
+                name = unique_name(name, meshes, start_increments)
 
                 if mode == _GL_LINES:
                     # load GL_LINES into a Path object
@@ -1463,7 +1462,6 @@ def _read_buffers(header,
                     # each primitive gets it's own Trimesh object
                     if len(m["primitives"]) > 1:
                         kwargs['metadata']['from_gltf_primitive'] = True
-                        name = unique_name(name, meshes)
                     else:
                         kwargs['metadata']['from_gltf_primitive'] = False
 
@@ -1545,9 +1543,13 @@ def _read_buffers(header,
     # to a dict comprehension as it will be checking
     # the mutated dict in every loop
     name_index = {}
+    start_increments = {}
     for i, n in enumerate(nodes):
         name_index[unique_name(
-            n.get('name', str(i)), name_index)] = i
+            n.get('name', str(i)),
+            name_index,
+            start_increments
+        )] = i
     # invert the dict so we can look up by index
     # node index (int) : name (str)
     names = {v: k for k, v in name_index.items()}
diff --git a/trimesh/exchange/stl.py b/trimesh/exchange/stl.py
index 813ed7e4..4ff5018a 100644
--- a/trimesh/exchange/stl.py
+++ b/trimesh/exchange/stl.py
@@ -17,7 +17,7 @@ _stl_dtype = np.dtype([('normals', '<f4', (3)),
                        ('attributes', '<u2')])
 # define a numpy datatype for the header of a binary STL file
 _stl_dtype_header = np.dtype([('header', np.void, 80),
-                              ('face_count', '<u4')])
+                              ('face_count', '<i4')])
 
 
 def load_stl(file_obj, **kwargs):
diff --git a/trimesh/path/packing.py b/trimesh/path/packing.py
index e2138da0..5709c7ea 100644
--- a/trimesh/path/packing.py
+++ b/trimesh/path/packing.py
@@ -71,6 +71,7 @@ class RectangleBin:
           Position of insertion in the tree or None
           if the insertion was unsuccessful.
         """
+
         for child in self.child:
             # try inserting into child cells
             attempt = child.insert(size=size, rotate=rotate)
diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py
index 4bb888b8..4ec5c9f4 100644
--- a/trimesh/path/polygons.py
+++ b/trimesh/path/polygons.py
@@ -885,91 +885,39 @@ def projected(mesh,
     return polygon
 
 
-def second_moments(polygon, centered=False):
+def second_moment(coords):
     """
-    Calculate the second moments of area of a polygon
+    Calculate the second moment of area of a polygon
     from the boundary.
 
     Parameters
     ------------
     coords : (n, 2) float or Polygon
       Closed polygon.
-    centered: bool
-      Get second moments for a frame with origin at the centroid
-      and perform a principal axis transformation.
 
     Returns
     ----------
     moments : (3,) float
-      The values of `[Ixx, Iyy, Ixy]`
-    principal_moments : (2,) float
-      Principal second moments of inertia: `[Imax, Imin]`
-      Only returned if `centered`.
-    alpha : float
-      Angle by which the polygon needs to be rotated, so the
-      principal axis align with the X and Y axis.
-      Only returned if `centered`.
-    transform : (3, 3) float
-      Transformation matrix which rotates the polygon by alpha.
-      Only returned if `centered`.
+      The values of `[Ix, Iy, Ixy]`
     """
+    if hasattr(coords, 'exterior'):
+        # if we have been passed a shapely.geometry.Polygon
+        exterior = second_moment(np.array(coords.exterior.coords))
+        interiors = np.sum([second_moment(np.array(i.coords))
+                            for i in coords.interiors],
+                           axis=0)
+        return exterior - interiors
 
-    transform = np.eye(3)
-    if centered:
-        # calculate centroid and move polygon
-        transform[:2, 2] = - np.array(polygon.centroid.coords)
-        polygon = transform_polygon(polygon, transform)
-
-    # start with the exterior
-    coords = np.array(polygon.exterior.coords)
+    coords = np.asanyarray(coords, dtype=np.float64)
     # shorthand the coordinates
     x1, y1 = np.vstack((coords[-1], coords[:-1])).T
     x2, y2 = coords.T
     # do vectorized operations
     v = x1 * y2 - x2 * y1
-    Ixx = np.sum(v * (y1 * y1 + y1 * y2 + y2 * y2)) / 12.0
-    Iyy = np.sum(v * (x1 * x1 + x1 * x2 + x2 * x2)) / 12.0
-    Ixy = np.sum(v * (x1 * y2 + 2 * x1 * y1 + 2 * x2 * y2 + x2 * y1)) / 24.0
-
-    for interior in polygon.interiors:
-        coords = np.array(interior.coords)
-        # shorthand the coordinates
-        x1, y1 = np.vstack((coords[-1], coords[:-1])).T
-        x2, y2 = coords.T
-        # do vectorized operations
-        v = x1 * y2 - x2 * y1
-        Ixx -= np.sum(v * (y1 * y1 + y1 * y2 + y2 * y2)) / 12.0
-        Iyy -= np.sum(v * (x1 * x1 + x1 * x2 + x2 * x2)) / 12.0
-        Ixy -= np.sum(v * (x1 * y2 + 2 * x1 * y1 +
-                      2 * x2 * y2 + x2 * y1)) / 24.0
-
-    moments = [Ixx, Iyy, Ixy]
-
-    if not centered:
-        return moments
-
-    # get the principal moments
-    root = np.sqrt(((Iyy - Ixx) / 2.0)**2 + Ixy**2)
-    Imax = (Ixx + Iyy) / 2.0 + root
-    Imin = (Ixx + Iyy) / 2.0 - root
-    principal_moments = [Imax, Imin]
-
-    # do the principal axis transform
-    if np.isclose(Ixy, 0.0, atol=1e-12):
-        alpha = 0
-    elif np.isclose(Ixx, Iyy):
-        # prevent division by 0
-        alpha = 0.25 * np.pi
-    else:
-        alpha = 0.5 * np.arctan(2.0 * Ixy / (Ixx - Iyy))
-
-    # construct transformation matrix
-    cos_alpha = np.cos(alpha)
-    sin_alpha = np.sin(alpha)
-
-    transform[0, 0] = cos_alpha
-    transform[1, 1] = cos_alpha
-    transform[0, 1] = - sin_alpha
-    transform[1, 0] = sin_alpha
-
-    return moments, principal_moments, alpha, transform
+    Ix = v * (y1 * y1 + y1 * y2 + y2 * y2)
+    Iy = v * (x1 * x1 + x1 * x2 + x2 * x2)
+    Ixy = v * (x1 * y2 + 2 * x1 * y1 + 2 * x2 * y2 + x2 * y1)
+    # divide by constants and return
+    return np.array([Ix.sum() / 12.0,
+                     Iy.sum() / 12.0,
+                     Ixy.sum() / 24.0], dtype=np.float64)
diff --git a/trimesh/util.py b/trimesh/util.py
index 0891ce43..26b12b03 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -2360,7 +2360,7 @@ def to_ascii(text):
     return str(text)
 
 
-def is_ccw(points, return_all=False):
+def is_ccw(points):
     """
     Check if connected 2D points are counterclockwise.
 
@@ -2368,41 +2368,28 @@ def is_ccw(points, return_all=False):
     -----------
     points : (n, 2) float
       Connected points on a plane
-    return_all : bool
-      Return polygon area and centroid or just counter-clockwise.
 
     Returns
     ----------
     ccw : bool
       True if points are counter-clockwise
-    area : float
-      Only returned if `return_centroid`
-    centroid : (2,) float
-      Centroid of the polygon.
     """
-    points = np.array(points, dtype=np.float64)
-
-    if len(points.shape) != 2 or points.shape[1] != 2:
-        raise ValueError('only defined for `(n, 2)` points')
-
-    # the "shoelace formula"
-    product = np.subtract(*(points[:-1, [1, 0]] * points[1:]).T)
-    # the area of the polygon
-    area = product.sum() / 2.0
-    # check the sign of the area
-    ccw = area < 0.0
-
-    if not return_all:
-        return ccw
+    points = np.asanyarray(points, dtype=np.float64)
 
-    # the centroid of the polygon uses the same formula
-    centroid = ((points[:-1] + points[1:]) *
-                product.reshape((-1, 1))).sum(axis=0) / (6.0 * area)
+    if (len(points.shape) != 2 or points.shape[1] != 2):
+        raise ValueError('CCW is only defined for 2D')
+    xd = np.diff(points[:, 0])
+    # sum along axis=1 with a dot product
+    yd = np.dot(np.column_stack((
+        points[:, 1],
+        points[:, 1])).reshape(-1)[1:-1].reshape((-1, 2)), [1, 1])
+    area = np.sum(xd * yd) * .5
+    ccw = area < 0
 
-    return ccw, area, centroid
+    return ccw
 
 
-def unique_name(start, contains):
+def unique_name(start, contains, start_increments={}):
     """
     Deterministically generate a unique name not
     contained in a dict, set or other grouping with
@@ -2415,6 +2402,9 @@ def unique_name(start, contains):
       Initial guess for name.
     contains : dict, set, or list
       Bundle of existing names we can *not* use.
+    start_increments : dict
+      Maps name starts encountered before to increments in
+      order to speed up finding a unique name.
 
     Returns
     ---------
@@ -2427,13 +2417,13 @@ def unique_name(start, contains):
             start not in contains):
         return start
 
-    # start checking with zero index
-    increment = 0
+    # start checking with zero index unless found
+    increment = start_increments.get(start, 0)
     if start is not None and len(start) > 0:
         formatter = start + '_{}'
         # split by our delimiter once
         split = start.rsplit('_', 1)
-        if len(split) == 2:
+        if len(split) == 2 and increment == 0:
             try:
                 # start incrementing from the existing
                 # trailing value
@@ -2450,6 +2440,7 @@ def unique_name(start, contains):
     for i in range(increment + 1, 2 + increment + len(contains)):
         check = formatter.format(i)
         if check not in contains:
+            start_increments[start] = i
             return check
 
     # this should really never happen since we looped
diff --git a/trimesh/version.py b/trimesh/version.py
index 69dcfd87..88d90c3e 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,4 +1,4 @@
-__version__ = '3.20.1'
+__version__ = '3.20.0'
 
 if __name__ == '__main__':
     # print version if run directly i.e. in a CI script


Test patch:
diff --git a/tests/generic.py b/tests/generic.py
index 4b757758..76a83aa4 100644
--- a/tests/generic.py
+++ b/tests/generic.py
@@ -172,48 +172,14 @@ io_wrap = trimesh.util.wrap_as_stream
 
 def random(*args, **kwargs):
     """
-    A random function always seeded from the same value so tests
-    can use random data but they execute mostly deterministically
-    in a large test matrix.
+    A random function always seeded from the same value.
 
-    Drop-in replacement for `np.random.random(*args, **kwargs)`
+    Replaces: np.random.random(*args, **kwargs)
     """
     state = np.random.RandomState(seed=1)
     return state.random_sample(*args, **kwargs)
 
 
-def random_transforms(count, translate=1000):
-    """
-    Deterministic generation of random transforms so unit
-    tests have limited levels of flake.
-
-    Parameters
-    -------------
-    count : int
-      Number of repeatable but random-ish transforms to generate
-    translate : float
-      The scale of translation to apply.
-
-    Yields
-    ------------
-    transform : (4, 4) float
-      Homogenous transformation matrix
-    """
-    quaternion = random((count, 3))
-    translate = (random((count, 3)) - 0.5) * float(translate)
-
-    for quat, trans in zip(quaternion, translate):
-        matrix = tf.random_rotation_matrix(rand=quat)
-        matrix[:3, 3] = trans
-        yield matrix
-
-
-# random should be deterministic
-assert np.allclose(random(10), random(10))
-assert np.allclose(list(random_transforms(10)),
-                   list(random_transforms(10)))
-
-
 def _load_data():
     """
     Load the JSON files from our truth directory.
diff --git a/tests/helpers/id_helper.py b/tests/helpers/id_helper.py
index d0116863..fb5f09a9 100644
--- a/tests/helpers/id_helper.py
+++ b/tests/helpers/id_helper.py
@@ -72,9 +72,8 @@ def permutations(mesh,
         identifiers.append(identifier)
 
         if (time.time() - start) > cutoff:
-            print('bailing for time:{} count:{}'.format(
-                time.time() - start,
-                i))
+            print('bailing for time:{} count:{}'.format(time.time() - start,
+                                                        i))
             return np.array(identifiers)
 
     return np.array(identifiers)
diff --git a/tests/test_align.py b/tests/test_align.py
index 8fa42fb8..b8c266b2 100644
--- a/tests/test_align.py
+++ b/tests/test_align.py
@@ -20,8 +20,8 @@ class AlignTests(g.unittest.TestCase):
         # start with some edge cases and make sure the transform works
         target = g.np.array([0, 0, -1], dtype=g.np.float64)
         vectors = g.np.vstack((
-            g.trimesh.unitize(g.random((1000, 3)) - .5),
-            g.random((1000, 3)) - .5,
+            g.trimesh.unitize(g.np.random.random((1000, 3)) - .5),
+            g.np.random.random((1000, 3)) - .5,
             [-target, target],
             g.trimesh.util.generate_basis(target),
             [[7.12106798e-07, -7.43194705e-08, 1.00000000e+00],
diff --git a/tests/test_arc.py b/tests/test_arc.py
index 5aba967b..9c3df525 100644
--- a/tests/test_arc.py
+++ b/tests/test_arc.py
@@ -34,11 +34,11 @@ class ArcTests(g.unittest.TestCase):
         min_angle = g.np.radians(2)
         count = 1000
 
-        center_3D = (g.random((count, 3)) - .5) * 50
+        center_3D = (g.np.random.random((count, 3)) - .5) * 50
         center_2D = center_3D[:, 0:2]
-        radii = g.np.clip(g.random(count) * 100, min_angle, g.np.inf)
+        radii = g.np.clip(g.np.random.random(count) * 100, min_angle, g.np.inf)
 
-        angles = g.random((count, 2)) * \
+        angles = g.np.random.random((count, 2)) * \
             (g.np.pi - min_angle) + min_angle
         angles = g.np.column_stack((g.np.zeros(count),
                                     g.np.cumsum(angles, axis=1)))
diff --git a/tests/test_base.py b/tests/test_base.py
index 722daaee..5a6a52e3 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -35,7 +35,7 @@ class MeshTests(g.unittest.TestCase):
         assert g.np.isclose(m.volume, pre_vol)
 
         # add some unreferenced vertices
-        m.vertices = g.np.vstack((m.vertices, g.random((100, 3))))
+        m.vertices = g.np.vstack((m.vertices, g.np.random.random((100, 3))))
         assert len(m.vertices) == pre_len + 100
         assert g.np.isclose(m.volume, pre_vol)
 
diff --git a/tests/test_bounds.py b/tests/test_bounds.py
index a8340485..9208bfe2 100644
--- a/tests/test_bounds.py
+++ b/tests/test_bounds.py
@@ -69,7 +69,7 @@ class BoundsTest(g.unittest.TestCase):
         """
         for dimension in [3, 2]:
             for i in range(25):
-                points = g.random((10, dimension))
+                points = g.np.random.random((10, dimension))
                 to_origin, extents = g.trimesh.bounds.oriented_bounds(points)
 
                 assert g.trimesh.util.is_shape(to_origin,
@@ -93,7 +93,7 @@ class BoundsTest(g.unittest.TestCase):
     def test_2D(self):
         for theta in g.np.linspace(0, g.np.pi * 2, 2000):
             # create some random rectangular-ish 2D points
-            points = g.random((10, 2)) * [5, 1]
+            points = g.np.random.random((10, 2)) * [5, 1]
 
             # save the basic AABB of the points before rotation
             rectangle_pre = points.ptp(axis=0)
@@ -207,7 +207,7 @@ class BoundsTest(g.unittest.TestCase):
             # transform box randomly in rotation and translation
             mat = g.trimesh.transformations.random_rotation_matrix()
             # translate in box -100 : +100
-            mat[:3, 3] = (g.random(3) - .5) * 200
+            mat[:3, 3] = (g.np.random.random(3) - .5) * 200
 
             # source mesh to check
             b = g.trimesh.creation.box(extents=extents,
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 4119f80f..2d402f36 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -26,7 +26,7 @@ class CacheTest(g.unittest.TestCase):
 
             # generate test data and perform numpy operations
             a = g.trimesh.caching.tracked_array(
-                g.random(TEST_DIM))
+                g.np.random.random(TEST_DIM))
             modified = [hash(a)]
             a[0][0] = 10
             modified.append(hash(a))
@@ -95,7 +95,7 @@ class CacheTest(g.unittest.TestCase):
         g.trimesh.caching.hash_fast = original
 
     def test_contiguous(self):
-        a = g.random((100, 3))
+        a = g.np.random.random((100, 3))
         t = g.trimesh.caching.tracked_array(a)
 
         original = g.trimesh.caching.hash_fast
@@ -120,7 +120,7 @@ class CacheTest(g.unittest.TestCase):
         """
         d = g.trimesh.caching.DataStore()
 
-        d['hi'] = g.random(100)
+        d['hi'] = g.np.random.random(100)
         hash_initial = hash(d)
         # mutate internal data
         d['hi'][0] += 1
diff --git a/tests/test_color.py b/tests/test_color.py
index 51027148..de1dc33a 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -93,30 +93,31 @@ class VisualTest(g.unittest.TestCase):
         m.visual.vertex_colors[1] = test_color_transparent
         assert m.visual.transparency
 
-        test = (g.random((len(m.faces), 4)) * 255).astype(g.np.uint8)
+        test = (g.np.random.random((len(m.faces), 4)) * 255).astype(g.np.uint8)
         m.visual.face_colors = test
         assert bool((m.visual.face_colors == test).all())
         assert m.visual.kind == 'face'
 
-        test = (g.random((len(m.vertices), 4))
+        test = (g.np.random.random((len(m.vertices), 4))
                 * 255).astype(g.np.uint8)
         m.visual.vertex_colors = test
         assert bool((m.visual.vertex_colors == test).all())
         assert m.visual.kind == 'vertex'
 
-        test = (g.random(4) * 255).astype(g.np.uint8)
+        test = (g.np.random.random(4) * 255).astype(g.np.uint8)
         m.visual.face_colors = test
         assert bool((m.visual.vertex_colors == test).all())
         assert m.visual.kind == 'face'
-        m.visual.vertex_colors[0] = [0, 0, 0, 0]
+        m.visual.vertex_colors[0] = (
+            g.np.random.random(4) * 255).astype(g.np.uint8)
         assert m.visual.kind == 'vertex'
 
-        test = (g.random(4) * 255).astype(g.np.uint8)
+        test = (g.np.random.random(4) * 255).astype(g.np.uint8)
         m.visual.vertex_colors = test
         assert bool((m.visual.face_colors == test).all())
         assert m.visual.kind == 'vertex'
-        m.visual.face_colors[:2] = (
-            g.random((2, 4)) * 255).astype(g.np.uint8)
+        m.visual.face_colors[0] = (
+            g.np.random.random(4) * 255).astype(g.np.uint8)
         assert m.visual.kind == 'face'
 
     def test_smooth(self):
diff --git a/tests/test_export.py b/tests/test_export.py
index 386e2a6d..48ab5a35 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -264,7 +264,7 @@ class ExportTest(g.unittest.TestCase):
         RET_COUNT = 5
 
         # a path that doesn't exist
-        nonexists = '/banana{}'.format(g.random())
+        nonexists = '/banana{}'.format(g.np.random.random())
         assert not g.os.path.exists(nonexists)
 
         # loadable OBJ model
diff --git a/tests/test_geom.py b/tests/test_geom.py
index a82d291e..0e4c86d7 100644
--- a/tests/test_geom.py
+++ b/tests/test_geom.py
@@ -12,8 +12,8 @@ class GeomTests(g.unittest.TestCase):
     def test_triangulate(self):
         from trimesh.geometry import triangulate_quads as tq
         # create some triangles and quads
-        tri = (g.random((100, 3)) * 100).astype(g.np.int64)
-        quad = (g.random((100, 4)) * 100).astype(g.np.int64)
+        tri = (g.np.random.random((100, 3)) * 100).astype(g.np.int64)
+        quad = (g.np.random.random((100, 4)) * 100).astype(g.np.int64)
 
         # should just exit early for triangles
         assert g.np.allclose(tri, tq(tri.tolist()))
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index 2bd6a220..e018e59f 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -695,7 +695,7 @@ class GLTFTest(g.unittest.TestCase):
         # test concatenation with texture
         m = g.get_mesh('fuze.obj')
 
-        colors = (g.random(
+        colors = (g.np.random.random(
             (len(m.vertices), 4)) * 255).astype(g.np.uint8)
 
         # set the color vertex attribute
diff --git a/tests/test_grouping.py b/tests/test_grouping.py
index c23eddcc..2fbb0236 100644
--- a/tests/test_grouping.py
+++ b/tests/test_grouping.py
@@ -195,7 +195,7 @@ class GroupTests(g.unittest.TestCase):
     def test_cluster(self):
         # create some random points stacked with some zeros to cluster
         points = g.np.vstack(((
-            g.random((10000, 3)) * 5).astype(g.np.int64),
+            g.np.random.random((10000, 3)) * 5).astype(g.np.int64),
             g.np.zeros((100, 3))))
         # should be at least one cluster
         assert len(g.trimesh.grouping.clusters(points, .01)) > 0
@@ -304,7 +304,7 @@ class GroupTests(g.unittest.TestCase):
 
     def test_unique_ordered_rows(self):
         # check the ordering of unique_rows
-        v = g.random((100000, 3))
+        v = g.np.random.random((100000, 3))
         v = g.np.vstack((v, v, v, v))
 
         # index, inverse
diff --git a/tests/test_identifier.py b/tests/test_identifier.py
index 60706af5..3df80a4a 100644
--- a/tests/test_identifier.py
+++ b/tests/test_identifier.py
@@ -7,6 +7,7 @@ except BaseException:
 class IdentifierTest(g.unittest.TestCase):
 
     def test_identifier(self, count=25):
+
         meshes = g.np.append(list(g.get_meshes(
             only_watertight=True, split=True, min_volume=0.001)),
             g.get_mesh('fixed_top.ply'))
@@ -19,8 +20,8 @@ class IdentifierTest(g.unittest.TestCase):
             g.log.info('Trying hash at %d random transforms', count)
             hashed = []
             identifier = []
-            for transform in g.random_transforms(count):
-                permutated = mesh.copy().apply_transform(transform)
+            for _ in range(count):
+                permutated = mesh.permutate.transform()
                 hashed.append(permutated.identifier_hash)
                 identifier.append(permutated.identifier)
 
@@ -36,13 +37,10 @@ class IdentifierTest(g.unittest.TestCase):
                             str(g.np.array(debug, dtype=g.np.int64)))
                 raise ValueError('values differ after transform!')
 
-            # stretch the mesh by a small amount
-            stretched = mesh.copy().apply_scale(
-                [0.99974507, 0.9995662, 1.0029832])
-            if hashed[-1] == stretched.identifier_hash:
-                raise ValueError(
-                    'Hashes on %s didn\'t change after stretching',
-                    mesh.metadata['file_name'])
+            if hashed[-1] == permutated.permutate.noise(
+                    mesh.scale / 100.0).identifier_hash:
+                raise ValueError('Hashes on %s didn\'t change after noise!',
+                                 mesh.metadata['file_name'])
 
     def test_scene_id(self):
         """
diff --git a/tests/test_inertia.py b/tests/test_inertia.py
index 601b4eaa..64f43b8d 100644
--- a/tests/test_inertia.py
+++ b/tests/test_inertia.py
@@ -112,7 +112,7 @@ class InertiaTest(g.unittest.TestCase):
                     matrix = g.trimesh.transformations.random_rotation_matrix()
                     p.primitive.transform = matrix
                 elif hasattr(p.primitive, 'center'):
-                    p.primitive.center = g.random(3)
+                    p.primitive.center = g.np.random.random(3)
 
     def test_tetrahedron(self):
         # Based on the 'numerical example' of the paper:
diff --git a/tests/test_integralmeancurvature.py b/tests/test_integralmeancurvature.py
index 58937fff..4f98f593 100644
--- a/tests/test_integralmeancurvature.py
+++ b/tests/test_integralmeancurvature.py
@@ -32,7 +32,7 @@ class IntegralMeanCurvatureTest(g.unittest.TestCase):
         # how close do we need to be - relative tolerance
         tol = 1e-3
         n_tests = 4
-        extents = 1 - g.random((n_tests, 3))
+        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
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 4166bc17..4d361d3d 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -19,7 +19,7 @@ class MargeTest(g.unittest.TestCase):
 
         # stack a bunch of unreferenced vertices
         m.vertices = g.np.vstack((
-            m.vertices, g.random((10000, 3))))
+            m.vertices, g.np.random.random((10000, 3))))
         assert m.euler_number == 2
         assert m.vertices.shape == (10008, 3)
         assert m.referenced_vertices.sum() == 8
diff --git a/tests/test_mutate.py b/tests/test_mutate.py
index 71d3ff8d..747515b0 100644
--- a/tests/test_mutate.py
+++ b/tests/test_mutate.py
@@ -143,21 +143,21 @@ class MutateTests(g.unittest.TestCase):
 
         # ray.intersects_id
         centre = mesh.vertices.mean(axis=0)
-        origins = g.random((100, 3)) * 1000
+        origins = g.np.random.random((100, 3)) * 1000
         directions = g.np.copy(origins)
         directions[:50, :] -= centre
         directions[50:, :] += centre
         mesh.ray.intersects_id(origins, directions)
 
         # nearest.vertex
-        points = g.random((500, 3)) * 100
+        points = g.np.random.random((500, 3)) * 100
         mesh.nearest.vertex(points)
 
         # section
         section_count = 20
-        origins = g.random((section_count, 3)) * 100
-        normals = g.random((section_count, 3)) * 100
-        heights = g.random((10,)) * 100
+        origins = g.np.random.random((section_count, 3)) * 100
+        normals = g.np.random.random((section_count, 3)) * 100
+        heights = g.np.random.random((10,)) * 100
         for o, n in zip(origins, normals):
             # try slicing at random origin and at center mass
             mesh.slice_plane(o, n)
diff --git a/tests/test_nsphere.py b/tests/test_nsphere.py
index f773a862..c33d322c 100644
--- a/tests/test_nsphere.py
+++ b/tests/test_nsphere.py
@@ -27,7 +27,7 @@ class NSphereTest(g.unittest.TestCase):
         # check minimum n-sphere for points in 2, 3, 4 dimensions
         for d in [2, 3, 4]:
             for i in range(5):
-                points = g.random((100, d))
+                points = g.np.random.random((100, d))
                 C, R = g.trimesh.nsphere.minimum_nsphere(points)
                 R_check = ((points - C)**2).sum(axis=1).max() ** .5
                 assert len(C) == d
@@ -38,7 +38,7 @@ class NSphereTest(g.unittest.TestCase):
         # make sure created spheres are uv sphere
         m = g.trimesh.creation.uv_sphere()
         # move the mesh around for funsies
-        m.apply_translation(g.random(3))
+        m.apply_translation(g.np.random.random(3))
         m.apply_transform(
             g.trimesh.transformations.random_rotation_matrix())
         # all vertices should be on nsphere
diff --git a/tests/test_packing.py b/tests/test_packing.py
index ecf7315c..0f698df7 100644
--- a/tests/test_packing.py
+++ b/tests/test_packing.py
@@ -107,7 +107,7 @@ class PackingTest(g.unittest.TestCase):
         paths = [g.trimesh.load_path(i) for i in polygons]
 
         with g.Profiler() as P:
-            r, tf, consume = packing.paths(paths, spacing=0.02)
+            r, tf, consume = packing.paths(paths)
         print(P.output_text())
         # number of paths inserted
         count = consume.sum()
@@ -179,8 +179,8 @@ class PackingTest(g.unittest.TestCase):
                 extents = []
                 for i in ori:
                     extents.append(
-                        g.np.roll(i, int(g.random() * 10)) +
-                        g.random(3))
+                        g.np.roll(i, int(g.np.random.random() * 10)) +
+                        g.np.random.random(3))
                 extents = g.np.array(extents)
 
                 bounds, consume = packing.rectangles(extents)
@@ -238,15 +238,13 @@ class PackingTest(g.unittest.TestCase):
                 density.append(viz.volume / viz.bounding_box.volume)
         print(P.output_text())
 
-    def test_meshes(self, count=20):
+    def test_meshes(self):
         from trimesh.path import packing
         # create some random rotation boxes
         meshes = [g.trimesh.creation.box(
-            extents=extents,
-            transform=transform)
-            for transform, extents in zip(
-            g.random_transforms(count),
-            (g.random((count, 3)) + 1) * 10)]
+            extents=g.np.random.random(3),
+            transform=g.tf.random_rotation_matrix())
+            for _ in range(20)]
         packed, transforms, consume = packing.meshes(
             meshes, spacing=0.01)
         scene = g.trimesh.Scene(packed)
diff --git a/tests/test_points.py b/tests/test_points.py
index cb2387bd..459ba984 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -12,7 +12,7 @@ class PointsTest(g.unittest.TestCase):
         """
         shape = (100, 3)
         # random points
-        points = g.random(shape)
+        points = g.np.random.random(shape)
         # make sure randomness never gives duplicates by offsetting
         points += g.np.arange(shape[0]).reshape((-1, 1))
 
@@ -35,7 +35,7 @@ class PointsTest(g.unittest.TestCase):
         assert hash(cloud) == initial_hash
 
         # set some random colors
-        cloud.colors = g.random((shape[0], 4))
+        cloud.colors = g.np.random.random((shape[0], 4))
         # remove the duplicates we created
         cloud.merge_vertices()
 
@@ -80,8 +80,8 @@ class PointsTest(g.unittest.TestCase):
         vertices and no faces for some unknowable reason
         """
 
-        v = g.random((1000, 3))
-        v[g.np.floor(g.random(90) * len(v)).astype(int)] = v[0]
+        v = g.np.random.random((1000, 3))
+        v[g.np.floor(g.np.random.random(90) * len(v)).astype(int)] = v[0]
 
         mesh = g.trimesh.Trimesh(v)
 
@@ -94,7 +94,7 @@ class PointsTest(g.unittest.TestCase):
             # create a random rotation
             matrix = g.trimesh.transformations.random_rotation_matrix()
             # create some random points in spacd
-            p = g.random((1000, 3))
+            p = g.np.random.random((1000, 3))
             # make them all lie on the XY plane so we know
             # the correct normal to check against
             p[:, 2] = 0
@@ -116,7 +116,7 @@ class PointsTest(g.unittest.TestCase):
             matrices = [g.trimesh.transformations.random_rotation_matrix()
                         for _ in range(nb_points_sets)]
             # create some random points in spacd
-            p = g.random((nb_points_sets, 1000, 3))
+            p = g.np.random.random((nb_points_sets, 1000, 3))
             # make them all lie on the XY plane so we know
             # the correct normal to check against
             p[..., 2] = 0
@@ -171,7 +171,7 @@ class PointsTest(g.unittest.TestCase):
         for dimension in [2, 3]:
             for count in [2, 10, 100]:
                 for i in range(10):
-                    points = g.random((count, dimension))
+                    points = g.np.random.random((count, dimension))
 
                     # find a path that visits every point quickly
                     idx, dist = g.trimesh.points.tsp(points, start=0)
@@ -225,7 +225,7 @@ class PointsTest(g.unittest.TestCase):
         # initial color CRC
         initial = hash(p.visual)
         # set to random colors
-        p.colors = g.random(
+        p.colors = g.np.random.random(
             (len(p.vertices), 4))
         # visual CRC should have changed
         assert hash(p.visual) != initial
@@ -255,8 +255,8 @@ class PointsTest(g.unittest.TestCase):
         assert mask.shape == (200,)
 
     def test_add_operator(self):
-        points_1 = g.random((10, 3))
-        points_2 = g.random((20, 3))
+        points_1 = g.np.random.random((10, 3))
+        points_2 = g.np.random.random((20, 3))
         colors_1 = [[123, 123, 123, 255]] * len(points_1)
         colors_2 = [[255, 0, 123, 255]] * len(points_2)
 
diff --git a/tests/test_polygons.py b/tests/test_polygons.py
index 285047c4..72472d2e 100644
--- a/tests/test_polygons.py
+++ b/tests/test_polygons.py
@@ -90,7 +90,7 @@ class PolygonTests(g.unittest.TestCase):
         m = g.trimesh.creation.icosphere(subdivisions=4)
 
         p = [g.trimesh.path.polygons.projected(m, normal=n)
-             for n in g.random((100, 3))]
+             for n in g.np.random.random((100, 3))]
 
         # sphere projection should never have interiors
         assert all(len(i.interiors) == 0 for i in p)
@@ -144,28 +144,6 @@ class PolygonTests(g.unittest.TestCase):
                 holes = []
             return Polygon(shell=shell, holes=holes)
 
-        def poly_corner(bh):
-            # return a rectangle with one corner
-            # at the origin and the rest in positive space
-            shell = rectangle(bh)
-            shell += shell.min(axis=0)
-            return Polygon(shell=shell)
-
-        def poly_doublecorner(bh):
-            # returns two equal sized rectangles as one polygon
-            # Same as poly_corner(), but the rectangle gets
-            # mirrored by 180 deg at the origin
-            # This puts the centroid in the origin with Ixy != 0
-            shell_1 = rectangle(bh)
-            shell_1 += shell_1.min(axis=0)
-            shell_2 = - shell_1
-            shell = g.np.concatenate((shell_1[2:, :],
-                                      shell_1[:2, :],
-                                      shell_2[2:, :],
-                                      shell_2[:2, :]
-                                      ), axis=0)
-            return Polygon(shell=shell)
-
         def truth(bh, bhi=None):
             # return the analytical second moment of area
             # for a rectangle with centroid at the origin
@@ -183,91 +161,46 @@ class PolygonTests(g.unittest.TestCase):
             # check a rectangle with one corner
             # at the origin and the rest in positive space
             b, h = bh
-            return g.np.array([b * h**3 / 3.0,
-                               h * b**3 / 3.0,
-                               0.5 * b**2 * 0.5 * h**2], dtype=g.np.float64)
-
-        from trimesh.path.polygons import second_moments
-        from trimesh.path.polygons import transform_polygon
+            return g.np.array([b * h**3,
+                               h * b**3], dtype=g.np.float64) / 3.0
 
+        from trimesh.path.polygons import second_moment
         from shapely.geometry import Polygon
-
         heights = g.np.array([[0.01, 0.01],
                               [1, 1],
                               [10, 2],
                               [3, 21]])
         for bh in heights:
             # check the second moment of a rectangle
-            # as polygon is already centered, centered doesn't have any effect
-            O_moments, O_principal_moments, O_alpha, O_transform = second_moments(
-                poly(bh), centered=True)
+            # as both a numpy array and as a Polygon
+            a = second_moment(rectangle(bh))
+            b = second_moment(poly(bh))
             # check against wikipedia
             t = truth(bh)
-            # for a centered rectangle, the principal axis are alread aligned
-            # with the frame axis
-            assert g.np.allclose(O_moments, t)
-            assert g.np.any(g.np.isclose(O_moments, O_principal_moments[0]))
-            assert g.np.isclose(O_moments[2], 0)  # Ixy = 0
-            assert g.np.isclose(O_alpha, 0)
-            assert g.np.allclose(O_transform, g.np.eye(3))
-
-            # now check a rectangle with the corner, so Ixy != 0
-
-            # First we test with centering. The results should be same as
-            # with the initally centered rectangles
-            C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
-                poly_corner(bh), centered=True)
-            assert g.np.allclose(O_moments, C_moments)
-            assert g.np.allclose(O_principal_moments, C_principal_moments)
-            assert g.np.isclose(O_alpha, C_alpha)
-            assert g.np.allclose(O_transform[:, :2], C_transform[:, :2])
-
-            # Now without centering
-            moments = second_moments(poly_corner(bh), centered=False)
-            t = truth_corner(bh)
-            assert g.np.allclose(moments, t)
-
-            # Now we will get the transform for a double rectangle. Then we will apply
-            # the transform and test if Ixy == 0, alpha == 0 etc.
-            C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
-                poly_doublecorner(bh), centered=True)
-            # apply the outputted transform to the polygon
-            T_polygon = transform_polygon(poly_doublecorner(bh), C_transform)
-            # call the function on the transformed polygon
-            T_moments, T_principal_moments, T_alpha, T_transform = second_moments(
-                T_polygon, centered=True)
-            assert g.np.any(g.np.isclose(T_moments, C_principal_moments[0]))
-            assert g.np.allclose(C_principal_moments, T_principal_moments)
-            assert g.np.isclose(T_alpha, 0, atol=1e-7)
-            assert g.np.allclose(T_transform, g.np.eye(3))
-
-            # check polygons with interior
+            assert g.np.allclose(a, b)
+            assert g.np.allclose(a, t)
+
+            # now add some interiors
             for bhi in heights:
                 # only check if interior is smaller than exterior
                 if not (bhi < bh).all():
                     continue
 
                 # check a rectangle with interiors
-                c = second_moments(poly(bh, bhi), centered=False)
+                c = second_moment(poly(bh, bhi))
                 t = truth(bh, bhi)
                 assert g.np.allclose(c, t)
 
-    def test_native_centroid(self):
-        # check our native implementation of the shoelace algorithm
-        # against the results from shapely which we use for native
-        # checks of counter-clockwise 2D coordinate loops
-
-        # get some polygons without interiors
-        polygons = [g.Polygon(i) for i in g.data['nestable']]
-        # same data as (n, 2) float arrays
-        coords = [g.np.array(i) for i in g.data['nestable']]
-
-        for p, c in zip(polygons, coords):
-            # area will be signed with respect to counter-clockwise
-            ccw, area, centroid = g.trimesh.util.is_ccw(c, return_all=True)
-            assert g.np.allclose(
-                centroid, g.np.array(p.centroid.coords)[0])
-            assert g.np.isclose(abs(area), p.area)
+            # now check a rectangle with the corner
+            # at the origin to see if we've done something
+            # silly with regards to offsets
+            r = rectangle(bh)
+            r += r.min(axis=0)
+            a = second_moment(r)
+            t = truth_corner(bh)
+            # not sure what Ixy is analytically because
+            # wikipedia didn't list it for this case
+            assert g.np.allclose(a[:2], t)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_proximity.py b/tests/test_proximity.py
index 63be516a..d1015274 100644
--- a/tests/test_proximity.py
+++ b/tests/test_proximity.py
@@ -36,7 +36,7 @@ class NearestTest(g.unittest.TestCase):
     def test_helper(self):
         # just make sure the plumbing returns something
         for mesh in g.get_meshes(2):
-            points = (g.random((100, 3)) - .5) * 100
+            points = (g.np.random.random((100, 3)) - .5) * 100
 
             a = mesh.nearest.on_surface(points)
             assert a is not None
@@ -210,7 +210,7 @@ class NearestTest(g.unittest.TestCase):
 
     def test_candidates(self):
         mesh = g.trimesh.creation.random_soup(2000)
-        points = g.random((2000, 3))
+        points = g.np.random.random((2000, 3))
         g.trimesh.proximity.nearby_faces(
             mesh=mesh, points=points)
 
diff --git a/tests/test_ray.py b/tests/test_ray.py
index f3e6e77b..d94ea744 100644
--- a/tests/test_ray.py
+++ b/tests/test_ray.py
@@ -33,7 +33,7 @@ class RayTests(g.unittest.TestCase):
             sphere = g.get_mesh('unit_sphere.STL',
                                 use_embree=use_embree)
 
-            ray_origins = g.random(dimension)
+            ray_origins = g.np.random.random(dimension)
             ray_directions = g.np.tile([0, 0, 1], (dimension[0], 1))
             ray_origins[:, 2] = -5
 
@@ -66,7 +66,7 @@ class RayTests(g.unittest.TestCase):
             sphere = g.get_mesh('unit_sphere.STL',
                                 use_embree=use_embree)
             # should never hit the sphere
-            ray_origins = g.random(dimension)
+            ray_origins = g.np.random.random(dimension)
             ray_directions = g.np.tile([0, 1, 0], (dimension[0], 1))
             ray_origins[:, 2] = -5
 
@@ -94,7 +94,7 @@ class RayTests(g.unittest.TestCase):
             assert not test_out.any()
 
             points_way_out = (
-                g.random(
+                g.np.random.random(
                     (30, 3)) * 100) + 1.0 + mesh.bounds[1]
             test_way_out = mesh.ray.contains_points(points_way_out)
             assert not test_way_out.any()
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 10267ac0..4519c80d 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -13,11 +13,11 @@ class RegistrationTest(g.unittest.TestCase):
         opt = g.itertools.combinations([True, False] * 6, 6)
         for reflection, translation, scale, a_flip, a_scale, weight in opt:
             # create random points in space
-            points_a = (g.random((1000, 3)) - .5) * 1000
+            points_a = (g.np.random.random((1000, 3)) - .5) * 1000
             # create a random transform
             matrix = g.trimesh.transformations.random_rotation_matrix()
             # add a translation component to transform
-            matrix[:3, 3] = g.random(3) * 100
+            matrix[:3, 3] = g.np.random.random(3) * 100
             # apply a flip (reflection) to test data
             if a_flip:
                 matrix = g.np.dot(
@@ -34,7 +34,7 @@ class RegistrationTest(g.unittest.TestCase):
 
             # weight points or not
             if weight:
-                weights = (g.random(len(points_a)) + 9) / 10
+                weights = (g.np.random.random(len(points_a)) + 9) / 10
             else:
                 weights = None
 
@@ -114,7 +114,7 @@ class RegistrationTest(g.unittest.TestCase):
     def test_icp_points(self):
         # see if ICP alignment works with point clouds
         # create random points in space
-        points_a = (g.random((1000, 3)) - .5) * 1000
+        points_a = (g.np.random.random((1000, 3)) - .5) * 1000
         # create a random transform
         # matrix = g.trimesh.transformations.random_rotation_matrix()
         # create a small transform
@@ -150,7 +150,7 @@ class RegistrationTest(g.unittest.TestCase):
         mesh = mesh.permutate.noise(noise)
         # randomly rotation with translation
         transform = g.trimesh.transformations.random_rotation_matrix()
-        transform[:3, 3] = (g.random(3) - .5) * 1000
+        transform[:3, 3] = (g.np.random.random(3) - .5) * 1000
 
         mesh.apply_transform(transform)
 
diff --git a/tests/test_render.py b/tests/test_render.py
index 8cbac6db..2a56f752 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -50,13 +50,13 @@ class RenderTest(g.unittest.TestCase):
         assert len(args) == 6
         assert len(args_auto) == len(args)
 
-        P22 = g.random((100, 2))
+        P22 = g.np.random.random((100, 2))
         args = rendering.points_to_vertexlist(P22)
         args_auto = rendering.convert_to_vertexlist(P22)
         assert len(args) == 6
         assert len(args_auto) == len(args)
 
-        P31 = g.random((100, 3))
+        P31 = g.np.random.random((100, 3))
         args = rendering.points_to_vertexlist(P31)
         args_auto = rendering.convert_to_vertexlist(P31)
         assert len(args) == 6
diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py
index 7c3fcc4f..c34d7cb7 100644
--- a/tests/test_scenegraph.py
+++ b/tests/test_scenegraph.py
@@ -7,7 +7,7 @@ from trimesh.scene.transforms import EnforcedForest
 
 
 def random_chr():
-    return chr(ord('a') + int(round(g.random() * 25)))
+    return chr(ord('a') + int(round(g.np.random.random() * 25)))
 
 
 class GraphTests(g.unittest.TestCase):
@@ -82,7 +82,7 @@ class GraphTests(g.unittest.TestCase):
         assert g.np.allclose(f(), g.np.eye(4))
 
         # a passed matrix should return immediately
-        fix = g.random((4, 4))
+        fix = g.np.random.random((4, 4))
         assert g.np.allclose(f(matrix=fix), fix)
 
         quat = g.trimesh.unitize([1, 2, 3, 1])
@@ -205,13 +205,13 @@ class GraphTests(g.unittest.TestCase):
             n=1000, seed=0, create_using=g.nx.DiGraph)
         for e in tree.edges:
             data = {}
-            if g.random() > .5:
+            if g.np.random.random() > .5:
                 # if a matrix is omitted but an edge exists it is
                 # the same as passing an identity matrix
                 data['matrix'] = tf.random_rotation_matrix()
-            if g.random() > .4:
+            if g.np.random.random() > .4:
                 # a geometry is not required for a node
-                data['geometry'] = str(int(g.random() * 1e8))
+                data['geometry'] = str(int(g.np.random.random() * 1e8))
             edgelist[e] = data
 
         # now apply the random data to an EnforcedForest
diff --git a/tests/test_section.py b/tests/test_section.py
index 752821ed..d05f90bc 100644
--- a/tests/test_section.py
+++ b/tests/test_section.py
@@ -136,11 +136,11 @@ class PlaneLine(g.unittest.TestCase):
         z = g.np.linspace(-1, 1, count)
 
         plane_origins = g.np.column_stack((
-            g.random((count, 2)), z))
+            g.np.random.random((count, 2)), z))
         plane_normals = g.np.tile([0, 0, -1], (count, 1))
 
         line_origins = g.np.tile([0, 0, 0], (count, 1))
-        line_directions = g.random((count, 3))
+        line_directions = g.np.random.random((count, 3))
 
         i, valid = g.trimesh.intersections.planes_lines(
             plane_origins=plane_origins,
diff --git a/tests/test_segments.py b/tests/test_segments.py
index 3a127559..b48fc56f 100644
--- a/tests/test_segments.py
+++ b/tests/test_segments.py
@@ -12,7 +12,7 @@ class SegmentsTest(g.unittest.TestCase):
         # check 2D and 3D
         for dimension in [2, 3]:
             # a bunch of random line segments
-            s = g.random((100, 2, dimension))
+            s = g.np.random.random((100, 2, dimension))
             # convert segment to point on line closest to origin
             # as well as a vector and two distances along vector
             param = segments.segments_to_parameters(s)
diff --git a/tests/test_stl.py b/tests/test_stl.py
index 1e9e1a81..eb3855e3 100644
--- a/tests/test_stl.py
+++ b/tests/test_stl.py
@@ -36,7 +36,7 @@ class STLTests(g.unittest.TestCase):
 
         len_vertices = len(m.vertices)
         # assign some random vertex attributes
-        random = g.random(len(m.vertices))
+        random = g.np.random.random(len(m.vertices))
         m.vertex_attributes['random'] = random
         m.vertex_attributes['nah'] = 20
 
diff --git a/tests/test_transformations.py b/tests/test_transformations.py
index a32fd9e2..42dd60f5 100644
--- a/tests/test_transformations.py
+++ b/tests/test_transformations.py
@@ -65,11 +65,11 @@ class TransformTest(g.unittest.TestCase):
 
     def test_around(self):
         # check transform_around on 2D points
-        points = g.random((100, 2))
+        points = g.np.random.random((100, 2))
         for i, p in enumerate(points):
-            offset = g.random(2)
+            offset = g.np.random.random(2)
             matrix = g.trimesh.transformations.planar_matrix(
-                theta=g.random() + .1,
+                theta=g.np.random.random() + .1,
                 offset=offset,
                 point=p)
 
@@ -82,7 +82,7 @@ class TransformTest(g.unittest.TestCase):
             assert compare.all(axis=1).sum() == 1
 
         # check transform_around on 3D points
-        points = g.random((100, 3))
+        points = g.np.random.random((100, 3))
         for i, p in enumerate(points):
             matrix = g.trimesh.transformations.random_rotation_matrix()
             matrix = g.trimesh.transformations.transform_around(matrix, p)
@@ -106,9 +106,9 @@ class TransformTest(g.unittest.TestCase):
                                       [0, 0, 0, 1]),
                              [1, -1, 0, 1])
 
-        angle = (g.random() - 0.5) * (2 * g.np.pi)
-        direc = g.random(3) - 0.5
-        point = g.random(3) - 0.5
+        angle = (g.np.random.random() - 0.5) * (2 * g.np.pi)
+        direc = g.np.random.random(3) - 0.5
+        point = g.np.random.random(3) - 0.5
         R0 = rotation_matrix(angle, direc, point)
         R1 = rotation_matrix(angle - 2 * g.np.pi, direc, point)
         assert g.trimesh.transformations.is_same_transform(R0, R1)
diff --git a/tests/test_util.py b/tests/test_util.py
index cf8d17f6..6d720982 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -48,7 +48,7 @@ class UtilTests(unittest.TestCase):
     def test_bounds_tree(self):
         for attempt in range(3):
             for dimension in [2, 3]:
-                t = g.random((1000, 3, dimension))
+                t = g.np.random.random((1000, 3, dimension))
                 bounds = g.np.column_stack((t.min(axis=1), t.max(axis=1)))
                 tree = g.trimesh.util.bounds_tree(bounds)
                 self.assertTrue(0 in tree.intersection(bounds[0]))
@@ -57,7 +57,7 @@ class UtilTests(unittest.TestCase):
         # shortcut to the function
         f = g.trimesh.util.stack_3D
         # start with some random points
-        p = g.random((100, 2))
+        p = g.np.random.random((100, 2))
         stack = f(p)
         # shape should be 3D
         assert stack.shape == (100, 3)
@@ -234,7 +234,7 @@ class IOWrapTests(unittest.TestCase):
         util = g.trimesh.util
 
         # check wrap_as_stream
-        test_b = g.random(1).tobytes()
+        test_b = g.np.random.random(1).tobytes()
         test_s = 'this is a test yo'
         res_b = util.wrap_as_stream(test_b).read()
         res_s = util.wrap_as_stream(test_s).read()
diff --git a/tests/test_vector.py b/tests/test_vector.py
index 61cdb1f4..ff356adc 100644
--- a/tests/test_vector.py
+++ b/tests/test_vector.py
@@ -11,7 +11,7 @@ class SphericalTests(g.unittest.TestCase):
         Convert vectors to spherical coordinates
         """
         # random unit vectors
-        v = g.trimesh.unitize(g.random((1000, 3)) - .5)
+        v = g.trimesh.unitize(g.np.random.random((1000, 3)) - .5)
         # (n, 2) angles in radians
         spherical = g.trimesh.util.vector_to_spherical(v)
         # back to unit vectors
@@ -26,7 +26,7 @@ class HemisphereTests(g.unittest.TestCase):
         for dimension in [2, 3]:
             # random unit vectors
             v = g.trimesh.unitize(
-                g.random((10000, dimension)) - .5)
+                g.np.random.random((10000, dimension)) - .5)
 
             # add some on- axis points
             v[:dimension] = g.np.eye(dimension)