diff --git a/.travis.yml b/.travis.yml index e88e4e990..062f645a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ before_deploy: - python docs/build.py # extract the current trimesh version from the version file -- export TMVERSION=`python -c "f = open('trimesh/version.py', 'r'); exec(f.read()); print(__version__)"` +- export TMVERSION=`python -c "exec(open('trimesh/version.py','r').read()); print(__version__)"` # tag the release and if it's already tagged chill - git tag $TMVERSION || true @@ -75,9 +75,12 @@ install: script: +# try simple tests with only 3 minimal deps - python -c "import trimesh" -- conda install scikit-image rtree shapely +- pytest tests/test_inertia.py +# install most deps here +- conda install scikit-image rtree shapely # eat exit codes for these two packages # pyembree and python-fcl not available everywhere - conda install pyembree || true @@ -85,7 +88,7 @@ script: - pip install .[easy] - pip install triangle xxhash -# run tests +# run main tests - pytest --cov=trimesh tests/ # downgrade networkx from 2.x to 1.x to make sure our diff --git a/docs/examples.rst b/docs/examples.rst index ce38faf80..4a747973c 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -11,3 +11,9 @@ How to slice a mesh using trimesh: How do do signed distance and proximity queries: `Nearest point queries `__ + +Find a path from one mesh vertex to another, travelling along edges of the mesh: + `Edge graph traversal `__ + +Do simple ray- mesh queries, including finding the indexes of triangles hit by rays, the locations of points hit on the mesh surface, etc. Ray queries have the same API with two available backends, one implemented in pure numpy and one that requires pyembree but is 50x faster. + `Ray-mesh queries `__ diff --git a/examples/ray.ipynb b/examples/ray.ipynb new file mode 100644 index 000000000..74a3d5ae7 --- /dev/null +++ b/examples/ray.ipynb @@ -0,0 +1,398 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "ray.ipynb\n", + "----------------\n", + "\n", + "Demonstrate simple ray- mesh queries\n", + "\"\"\"\n", + "\n", + "import trimesh\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# test on a sphere primitive\n", + "mesh = trimesh.primitives.Sphere()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# create some rays\n", + "ray_origins = np.array([[0, 0, -5],\n", + " [2, 2, -10]])\n", + "ray_directions = np.array([[0, 0, 1],\n", + " [0, 0, 1]])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " Return the location of where a ray hits a surface.\n", + "\n", + " Parameters\n", + " ----------\n", + " ray_origins: (n,3) float, origins of rays\n", + " ray_directions: (n,3) float, direction (vector) of rays\n", + "\n", + "\n", + " Returns\n", + " ---------\n", + " locations: (n) sequence of (m,3) intersection points\n", + " index_ray: (n,) int, list of ray index\n", + " index_tri: (n,) int, list of triangle (face) indexes\n", + " \n" + ] + } + ], + "source": [ + "# check out the docstring for intersects_location queries\n", + "print(mesh.ray.intersects_location.__doc__)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# run the mesh- ray query\n", + "locations, index_ray, index_tri = mesh.ray.intersects_location(\n", + " ray_origins=ray_origins,\n", + " ray_directions=ray_directions)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rays hit the mesh at coordinates:\n", + " [[ 0. 0. -1.]\n", + " [ 0. 0. 1.]]\n" + ] + } + ], + "source": [ + "print('The rays hit the mesh at coordinates:\\n', locations)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rays with index: [0 0] hit the triangles stored at mesh.faces[[1205 1018]]\n" + ] + } + ], + "source": [ + "print('The rays with index: {} hit the triangles stored at mesh.faces[{}]'.format(index_ray, index_tri))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# stack rays into line segments for visualization as Path3D\n", + "ray_visualize = trimesh.load_path(np.hstack((ray_origins,\n", + " ray_origins + ray_directions*15.0)).reshape(-1, 2, 3))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# make mesh transparent- ish\n", + "mesh.visual.face_colors = [100, 100, 100, 200]" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# create a visualization scene with rays, hits, and mesh\n", + "scene = trimesh.Scene([mesh,\n", + " ray_visualize])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show the visualization\n", + "scene.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/shortest.ipynb b/examples/shortest.ipynb new file mode 100644 index 000000000..c80b21ba1 --- /dev/null +++ b/examples/shortest.ipynb @@ -0,0 +1,354 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "shortest.ipynb\n", + "----------------\n", + "\n", + "Given a mesh and two vertex indices find the shortest path\n", + "between the two vertices while only traveling along edges\n", + "of the mesh.\n", + "\"\"\"\n", + "\n", + "import trimesh\n", + "import numpy as np\n", + "import networkx as nx" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# test on a sphere mesh\n", + "mesh = trimesh.primitives.Sphere()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# edges without duplication\n", + "edges = mesh.edges_unique" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# the actual length of each unique edge\n", + "length = mesh.edges_unique_length" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# create the graph with edge attributes for length\n", + "g = nx.Graph()\n", + "for edge, L in zip(edges, length):\n", + " g.add_edge(*edge, length=L)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# arbitrary indices of mesh.vertices to test with\n", + "start = 0\n", + "end = int(len(mesh.vertices) / 2.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# run the shortest path query using length for edge weight\n", + "path = nx.shortest_path(g,\n", + " source=start,\n", + " target=end,\n", + " weight='length')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# VISUALIZE RESULT\n", + "# make the sphere white\n", + "mesh.visual.face_colors = [255, 255, 255, 255]\n", + "# Path3D with the path between the points\n", + "path_visual = trimesh.load_path(mesh.vertices[path])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# create a scene with the mesh, path, and points\n", + "scene = trimesh.Scene([path_visual, mesh])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scene.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/shortest.py b/examples/shortest.py index f88168124..1737fcb24 100644 --- a/examples/shortest.py +++ b/examples/shortest.py @@ -21,9 +21,7 @@ edges = mesh.edges_unique # the actual length of each unique edge - length = np.linalg.norm(mesh.vertices[edges[:, 0]] - - mesh.vertices[edges[:, 1]], - axis=1) + length = mesh.edges_unique_length # create the graph with edge attributes for length g = nx.Graph() @@ -49,10 +47,15 @@ # VISUALIZE RESULT # make the sphere transparent-ish mesh.visual.face_colors = [100, 100, 100, 100] - # make a scene with the two points, the path between them - # and the original mesh we were working on + # Path3D with the path between the points + path_visual = trimesh.load_path(mesh.vertices[path]) + # visualizable two points + points_visual = trimesh.points.PointCloud(mesh.vertices[[start, end]]) + + # create a scene with the mesh, path, and points scene = trimesh.Scene([ - trimesh.points.PointCloud(mesh.vertices[[start, end]]), - mesh, - trimesh.load_path(mesh.vertices[path])]) - scene.show() + points_visual, + path_visual, + mesh]) + + scene.show(smooth=False) diff --git a/tests/generic.py b/tests/generic.py index 95e1a44c2..74564bf06 100644 --- a/tests/generic.py +++ b/tests/generic.py @@ -16,7 +16,11 @@ import subprocess import numpy as np -import sympy as sp + +try: + import sympy as sp +except ImportError: + pass import trimesh import collections @@ -79,14 +83,7 @@ break """ - -def io_wrap(item): - if isinstance(item, str): - return StringIO(item) - if _PY3 and isinstance(item, bytes): - return BytesIO(item) - return item - +io_wrap = trimesh.util.wrap_as_stream def _load_data(): data = {} diff --git a/tests/test_export.py b/tests/test_export.py index b43d2396e..727417dd1 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -29,7 +29,14 @@ def test_export(self): g.log.info('Export/import testing on %s', mesh.metadata['file_name']) - loaded = g.trimesh.load(file_obj=g.io_wrap(export), + + # if export is string or bytes wrap as pseudo file object + if isinstance(export, str) or isinstance(export, bytes): + file_obj = g.io_wrap(export) + else: + file_obj = export + + loaded = g.trimesh.load(file_obj=file_obj, file_type=file_type) if (not g.trimesh.util.is_shape(loaded._data['faces'], (-1, 3)) or diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 3dac231ab..61350e22f 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -18,14 +18,14 @@ def test_meshes(self): for mesh in meshes: g.log.info('Testing %s', mesh.metadata['file_name']) - self.assertTrue(len(mesh.faces) > 0) - self.assertTrue(len(mesh.vertices) > 0) + assert len(mesh.faces) > 0 + assert len(mesh.vertices) > 0 - self.assertTrue(len(mesh.edges) > 0) - self.assertTrue(len(mesh.edges_unique) > 0) - self.assertTrue(len(mesh.edges_sorted) > 0) - self.assertTrue(len(mesh.edges_face) > 0) - self.assertTrue(isinstance(mesh.euler_number, int)) + assert len(mesh.edges) > 0 + assert len(mesh.edges_unique) > 0 + assert len(mesh.edges_sorted) > 0 + assert len(mesh.edges_face) > 0 + assert isinstance(mesh.euler_number, int) mesh.process() @@ -59,10 +59,10 @@ def test_meshes(self): # make sure vertex kdtree and triangles rtree exist t = mesh.kdtree - self.assertTrue(hasattr(t, 'query')) + assert hasattr(t, 'query') g.log.info('Creating triangles tree') r = mesh.triangles_tree - self.assertTrue(hasattr(r, 'intersection')) + assert hasattr(r, 'intersection') g.log.info('Triangles tree ok') # some memory issues only show up when you copy the mesh a bunch @@ -88,12 +88,12 @@ def test_meshes(self): def test_vertex_neighbors(self): m = g.trimesh.primitives.Box() neighbors = m.vertex_neighbors - self.assertTrue(len(neighbors) == len(m.vertices)) + assert len(neighbors) == len(m.vertices) elist = m.edges_unique.tolist() for v_i, neighs in enumerate(neighbors): for n in neighs: - self.assertTrue(([v_i, n] in elist or [n, v_i] in elist)) + assert ([v_i, n] in elist or [n, v_i] in elist) if __name__ == '__main__': diff --git a/trimesh/base.py b/trimesh/base.py index c3acfd936..47fbf8c21 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -898,6 +898,20 @@ def edges_unique(self): self._cache['edges_unique_inv'] = inverse return edges_unique + @caching.cache_decorator + def edges_unique_length(self): + """ + How long is each unique edge. + + Returns + ---------- + length : (len(self.edges_unique), ) float + Length of each unique edge + """ + vector = np.subtract(*self.vertices[self.edges_unique.T]) + length = np.linalg.norm(vector, axis=1) + return length + @caching.cache_decorator def edges_sorted(self): """ @@ -914,14 +928,16 @@ def edges_sorted(self): @caching.cache_decorator def edges_sparse(self): """ - Edges in sparse COO graph format. + Edges in sparse bool COO graph format where connected + vertices are True. Returns ---------- sparse: (len(self.vertices), len(self.vertices)) bool Sparse graph in COO format """ - sparse = graph.edges_to_coo(self.edges) + sparse = graph.edges_to_coo(self.edges, + count=len(self.vertices)) return sparse @caching.cache_decorator @@ -1622,9 +1638,8 @@ def facets_on_hull(self): # a facet plane is on the convex hull if every vertex # of the convex hull is behind that plane # which we are checking with dot products - on_hull[i] = (np.dot( - normal, - (convex - origin).T) < tol.merge).all() + dot = np.dot(normal, (convex - origin).T) + on_hull[i] = (dot < tol.merge).all() return on_hull diff --git a/trimesh/graph.py b/trimesh/graph.py index 0e3ed9a91..936a2b915 100644 --- a/trimesh/graph.py +++ b/trimesh/graph.py @@ -683,35 +683,45 @@ def traversals(edges, mode='bfs'): return traversals -def edges_to_coo(edges, count=None): +def edges_to_coo(edges, count=None, data=None): """ Given an edge list, return a boolean scipy.sparse.coo_matrix representing the edges in matrix form. Parameters ------------ - edges: (n,2) int, edges of a graph - count: int, the number of nodes. - if None: count = edges.max() + 1 + edges : (n,2) int + Edges of a graph + count : int + The total number of nodes in the graph + if None: count = edges.max() + 1 + data : (n,) any + Assign data to each edge, if None will + be bool True for each specified edge Returns ------------ - matrix: (count, count) bool, scipy.sparse.coo_matrix + matrix: (count, count) scipy.sparse.coo_matrix + Sparse COO """ edges = np.asanyarray(edges, dtype=np.int64) if not (len(edges) == 0 or util.is_shape(edges, (-1, 2))): raise ValueError('edges must be (n,2)!') + # if count isn't specified just set it to largest + # value referenced in edges if count is None: count = edges.max() + 1 - else: - count = int(count) + count = int(count) + + # if no data is specified set every specified edge + # to True + if data is None: + data = np.ones(len(edges), dtype=np.bool) - matrix = coo_matrix((np.ones(len(edges), - dtype=np.bool), - (edges[:, 0], edges[:, 1])), - dtype=np.bool, + matrix = coo_matrix((data, edges.T), + dtype=data.dtype, shape=(count, count)) return matrix diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index ffbd9ded1..5e13e9d6f 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -81,6 +81,7 @@ def add_geometry(self, if geometry is None: return + # PointCloud objects will look like a sequence elif util.is_sequence(geometry): # if passed a sequence add all elements return [self.add_geometry(i) for i in geometry] diff --git a/trimesh/util.py b/trimesh/util.py index 479977669..0e58f705a 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -203,6 +203,9 @@ def is_sequence(obj): set, basestring)) + # PointCloud objects can look like an array but are not + seq = seq and type(obj).__name__ not in ['PointCloud'] + # numpy sometimes returns objects that are single float64 values # but sure look like sequences, so we check the shape if hasattr(obj, 'shape'): @@ -1454,11 +1457,12 @@ def bounds_tree(bounds): def wrap_as_stream(item): """ - Wrap a string or bytes object as a file object + Wrap a string or bytes object as a file object. Parameters ---------- - item: str or bytes: item to be wrapped + item: str or bytes + Item to be wrapped Returns --------- @@ -1470,7 +1474,7 @@ def wrap_as_stream(item): return StringIO(item) elif isinstance(item, bytes): return BytesIO(item) - raise ValueError('Not a wrappable item!') + raise ValueError('{} is not wrappable!'.format(type(item).__name__)) def sigfig_round(values, sigfig=1): diff --git a/trimesh/version.py b/trimesh/version.py index 0a6859068..26d09810d 100644 --- a/trimesh/version.py +++ b/trimesh/version.py @@ -1 +1 @@ -__version__ = '2.33.25' +__version__ = '2.33.26'