diff --git a/Dockerfile b/Dockerfile index fe40e20c0..cb9cc3684 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,13 @@ USER user # install trimesh into .local # then delete any included test directories # and remove Cython after all the building is complete + + +# TODO +# remove mapbox-earcut fork when this is merged: +# https://github.com/skogler/mapbox_earcut_python/pull/15 RUN pip install --user /home/user[easy] && \ + pip install --user --force-reinstall git+https://github.com/mikedh/mapbox_earcut_python.git && \ find /home/user/.local -type d -name tests -prune -exec rm -rf {} \; #################################### @@ -67,7 +73,9 @@ RUN trimesh-setup --install=test,gmsh,gltf_validator,llvmpipe,binvox USER user # install things like pytest -RUN pip install -e .[all] +# install prerelease for tests and make sure we're on Numpy 2.X +RUN pip install --pre --upgrade .[all] && \ + python -c "import numpy as n; assert(n.__version__.startswith('1'))" # check for lint problems RUN ruff check trimesh diff --git a/docker/trimesh-setup b/docker/trimesh-setup index e0d8fa171..c8eb461ea 100755 --- a/docker/trimesh-setup +++ b/docker/trimesh-setup @@ -27,7 +27,8 @@ config_json = """ "build": [ "build-essential", "g++", - "make" + "make", + "git" ], "docs": [ "make", diff --git a/docs/requirements.txt b/docs/requirements.txt index 53e871e52..8362dfcaa 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,11 +3,11 @@ recommonmark==0.7.1 jupyter==1.0.0 # get sphinx version range from furo install -furo==2024.1.29 -myst-parser==2.0.0 +furo==2024.5.6 +myst-parser==3.0.1 pyopenssl==24.1.0 autodocsumm==0.2.12 -jinja2==3.1.3 -matplotlib==3.8.3 -nbconvert==7.16.2 +jinja2==3.1.4 +matplotlib==3.8.4 +nbconvert==7.16.4 diff --git a/examples/raytrace.py b/examples/raytrace.py index 582429561..c8a23f704 100644 --- a/examples/raytrace.py +++ b/examples/raytrace.py @@ -45,7 +45,7 @@ a = np.zeros(scene.camera.resolution, dtype=np.uint8) # scale depth against range (0.0 - 1.0) - depth_float = (depth - depth.min()) / depth.ptp() + depth_float = (depth - depth.min()) / np.ptp(depth) # convert depth into 0 - 255 uint8 depth_int = (depth_float * 255).round().astype(np.uint8) diff --git a/models/Mesh_PrimitiveMode_04.bin b/models/Mesh_PrimitiveMode_04.bin new file mode 100644 index 000000000..a2e5d56a2 Binary files /dev/null and b/models/Mesh_PrimitiveMode_04.bin differ diff --git a/models/Mesh_PrimitiveMode_04.gltf b/models/Mesh_PrimitiveMode_04.gltf new file mode 100644 index 000000000..7a1b67ddf --- /dev/null +++ b/models/Mesh_PrimitiveMode_04.gltf @@ -0,0 +1,71 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 4, + "type": "VEC3", + "max": [ + 0.5, + 0.5, + 0.0 + ], + "min": [ + -0.5, + -0.5, + 0.0 + ], + "name": "Positions Accessor" + } + ], + "asset": { + "generator": "glTF Asset Generator", + "version": "2.0" + }, + "buffers": [ + { + "uri": "Mesh_PrimitiveMode_04.bin", + "byteLength": 48 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 48, + "name": "Positions" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0.0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 0 + }, + "material": 0, + "mode": 5 + } + ] + } + ], + "nodes": [ + { + "mesh": 0 + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ] +} \ No newline at end of file diff --git a/models/no_indices_3storybuilding.glb b/models/no_indices_3storybuilding.glb new file mode 100644 index 000000000..058dceb97 Binary files /dev/null and b/models/no_indices_3storybuilding.glb differ diff --git a/pyproject.toml b/pyproject.toml index a219cf6fd..45bc85bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"] [project] name = "trimesh" requires-python = ">=3.7" -version = "4.3.2" +version = "4.4.0" authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}] license = {file = "LICENSE.md"} description = "Import, export, process, analyze and view triangular meshes." @@ -102,18 +102,20 @@ test = [ "coveralls", "pyright", "ezdxf", - "gmsh>=4.12.1", "pytest", - "pymeshlab", + #"pymeshlab", "pyinstrument", "matplotlib", "ruff", "pytest-beartype; python_version>='3.10'" ] +# interfaces.gmsh will be dropped Jan 2025 +deprecated = ["gmsh==4.12.2"] + # requires pip >= 21.2 # https://hynek.me/articles/python-recursive-optional-dependencies/ -all = ["trimesh[easy,recommend,test]"] +all = ["trimesh[easy,recommend,test,deprecated]"] [tool.ruff] target-version = "py37" diff --git a/tests/generic.py b/tests/generic.py index 12e74b454..6ebddd3cb 100644 --- a/tests/generic.py +++ b/tests/generic.py @@ -74,7 +74,7 @@ def output_text(*args, **kwargs): include_rendering = "INCLUDE_RENDERING" in argv if all_dependencies and not trimesh.ray.has_embree: - raise ValueError("missing embree!") + import embreex try: import sympy as sp @@ -512,7 +512,7 @@ def check_fuze(fuze): # UV coordinates should be unmerged correctly assert len(fuze.visual.uv) == 664 # UV coordinates shouldn't be all zero- ish - assert fuze.visual.uv[:, :2].ptp(axis=0).min() > 0.1 + assert np.ptp(fuze.visual.uv[:, :2], axis=0).min() > 0.1 # UV coordinates should be between zero and 1 assert fuze.visual.uv.min() > -tol.merge assert fuze.visual.uv.max() < 1 + tol.merge @@ -538,7 +538,7 @@ def check_fuze(fuze): viz = fuze.visual.to_color() assert viz.kind == "vertex" # should be actual colors defined - assert viz.vertex_colors.ptp(axis=0).ptp() != 0 + assert np.ptp(np.ptp(viz.vertex_colors, axis=0)) != 0 # shouldn't crash fuze.visual.copy() fuze.visual.material.copy() diff --git a/tests/test_bounds.py b/tests/test_bounds.py index b33d94b53..9b6d050a9 100644 --- a/tests/test_bounds.py +++ b/tests/test_bounds.py @@ -117,7 +117,7 @@ def test_2D(self): points = g.random((10, 2)) * [5, 1] # save the basic AABB of the points before rotation - rectangle_pre = points.ptp(axis=0) + rectangle_pre = g.np.ptp(points, axis=0) # rotate them by an increment TR = g.trimesh.transformations.planar_matrix(theta=theta) @@ -129,10 +129,10 @@ def test_2D(self): # apply the calculated OBB oriented = g.trimesh.transform_points(points, T) - origin = oriented.min(axis=0) + oriented.ptp(axis=0) / 2.0 + origin = oriented.min(axis=0) + g.np.ptp(oriented, axis=0) / 2.0 # check to make sure the returned rectangle size is right - assert g.np.allclose(oriented.ptp(axis=0), rectangle) + assert g.np.allclose(g.np.ptp(oriented, axis=0), rectangle) # check to make sure the OBB consistently returns the # long axis in the same direction assert rectangle[0] > rectangle[1] @@ -199,7 +199,7 @@ def test_bounding_egg(self): r = p.bounding_cylinder # transformed height should match source mesh - assert g.np.isclose(i.vertices[:, 2].ptp(), r.primitive.height, rtol=1e-6) + assert g.np.isclose(g.np.ptp(i.vertices[:, 2]), r.primitive.height, rtol=1e-6) # slightly inflated cylinder should contain all # vertices of the source mesh assert r.buffer(0.01).contains(p.vertices).all() diff --git a/tests/test_camera.py b/tests/test_camera.py index bafec8738..98ea3ecb6 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -93,7 +93,7 @@ def test_lookat(self): T = g.trimesh.scene.cameras.look_at(points + offset, fov) # check using trig - check = (points.ptp(axis=0)[:2] / 2.0) / g.np.tan(np.radians(fov / 2)) + check = (np.ptp(points, axis=0)[:2] / 2.0) / g.np.tan(np.radians(fov / 2)) check += points[:, 2].mean() # Z should be the same as maximum trig option diff --git a/tests/test_color.py b/tests/test_color.py index d19f26e7f..c05ffe008 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -26,7 +26,7 @@ def test_concatenate(self): a.visual.face_colors = [255, 0, 0] r = a + b - assert any(r.visual.face_colors.ptp(axis=0) > 1) + assert any(g.np.ptp(r.visual.face_colors, axis=0) > 1) def test_concatenate_empty_mesh(self): box = g.get_mesh("box.STL") @@ -153,23 +153,23 @@ def test_smooth(self): # will put smoothed mesh into visuals cache s = m.smooth_shaded # every color should be default color - assert s.visual.face_colors.ptp(axis=0).max() == 0 + assert g.np.ptp(s.visual.face_colors, axis=0).max() == 0 # set one face to a different color m.visual.face_colors[0] = [255, 0, 0, 255] # cache should be dumped yo s1 = m.smooth_shaded - assert s1.visual.face_colors.ptp(axis=0).max() != 0 + assert g.np.ptp(s1.visual.face_colors, axis=0).max() != 0 # do the same check on vertex color m = g.get_mesh("featuretype.STL") s = m.smooth_shaded # every color should be default color - assert s.visual.vertex_colors.ptp(axis=0).max() == 0 + assert g.np.ptp(s.visual.vertex_colors, axis=0).max() == 0 m.visual.vertex_colors[g.np.arange(10)] = [255, 0, 0, 255] s1 = m.smooth_shaded - assert s1.visual.face_colors.ptp(axis=0).max() != 0 + assert g.np.ptp(s1.visual.face_colors, axis=0).max() != 0 def test_vertex(self): m = g.get_mesh("torus.STL") diff --git a/tests/test_dae.py b/tests/test_dae.py index 36b7fe110..7da5ec60d 100644 --- a/tests/test_dae.py +++ b/tests/test_dae.py @@ -63,7 +63,7 @@ def test_obj_roundtrip(self): s.export(path) # bring it back from outer space rec = g.trimesh.load(path, force="mesh") - assert rec.visual.uv.ptp(axis=0).ptp() > 1e-5 + assert g.np.ptp(g.np.ptp(rec.visual.uv, axis=0)) > 1e-5 assert s.visual.material.baseColorTexture.size == rec.visual.material.image.size conv = s.convert_units("inch") diff --git a/tests/test_dxf.py b/tests/test_dxf.py index a9866380d..33b5f4339 100644 --- a/tests/test_dxf.py +++ b/tests/test_dxf.py @@ -143,7 +143,7 @@ def test_versions(self): # count the number of entities in the path # this should be the same for every version E = g.np.array([len(paths[i].entities) for i in group], dtype=g.np.int64) - assert E.ptp() == 0 + assert g.np.ptp(E) == 0 def test_bulge(self): """ diff --git a/tests/test_gltf.py b/tests/test_gltf.py index 95aec4634..91eb608e4 100644 --- a/tests/test_gltf.py +++ b/tests/test_gltf.py @@ -771,7 +771,7 @@ def test_vertex_colors(self): ) # original mesh should have vertex colors assert m.visual.kind == "face" - assert m.visual.vertex_colors.ptp(axis=0).ptp() > 0 + assert g.np.ptp(g.np.ptp(m.visual.vertex_colors, axis=0)) > 0 # vertex colors should have survived import-export assert g.np.allclose(m.visual.vertex_colors, r.visual.vertex_colors) @@ -1058,6 +1058,16 @@ def test_unitize_normals_null_values(self): # Check that the normals are still null assert g.np.allclose(reimported_mesh.vertex_normals[0], [0, 0, 0]) + def test_no_indices(self): + # test mesh with no indices (faces should be generated correctly) + mesh = g.get_mesh("no_indices_3storybuilding.glb") + assert len(mesh.triangles) == 72 + + # the mesh is actually mode 5 with 4 vertices + # which as triangle strips would be 2 faces + mesh = g.get_mesh("Mesh_PrimitiveMode_04.gltf") + assert len(mesh.triangles) == 2 + if __name__ == "__main__": g.trimesh.util.attach_to_log() diff --git a/tests/test_grouping.py b/tests/test_grouping.py index 52a75d160..515df67b7 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -215,11 +215,11 @@ def test_group_rows(self): gr = g.trimesh.grouping.group_rows(c) assert g.np.shape(gr) == (100, 2) - assert g.np.allclose(c[gr].ptp(axis=1), 0.0) + assert g.np.allclose(g.np.ptp(c[gr], axis=1), 0.0) gr = g.trimesh.grouping.group_rows(c, require_count=2) assert gr.shape == (100, 2) - assert g.np.allclose(c[gr].ptp(axis=1), 0.0) + assert g.np.allclose(g.np.ptp(c[gr], axis=1), 0.0) c = g.np.vstack((c, [1, 2, 3])) gr = g.trimesh.grouping.group_rows(c, require_count=2) @@ -229,7 +229,7 @@ def test_group_rows(self): # should get the single element correctly assert len(grd) == 101 assert sum(1 for i in grd if len(i) == 2) == 100 - assert g.np.allclose(c[gr].ptp(axis=1), 0.0) + assert g.np.allclose(g.np.ptp(c[gr], axis=1), 0.0) def test_group_vector(self): x = g.np.linspace(-100, 100, 100) diff --git a/tests/test_medial.py b/tests/test_medial.py index 6aa957ff8..9621f67c1 100644 --- a/tests/test_medial.py +++ b/tests/test_medial.py @@ -27,7 +27,7 @@ def test_medial(self): # with midpoint at origin assert len(med.vertices) == 2 assert len(med.entities) == 1 - assert float(med.vertices.mean(axis=0).ptp()) < 1e-8 + assert float(g.np.ptp(med.vertices.mean(axis=0))) < 1e-8 if __name__ == "__main__": diff --git a/tests/test_minimal.py b/tests/test_minimal.py index 9075f31b6..9856dbd50 100644 --- a/tests/test_minimal.py +++ b/tests/test_minimal.py @@ -35,7 +35,7 @@ def test_path_exc(self): bounds, inserted = packing.rectangles_single([[1, 1], [2, 2]], size=[2, 4]) assert inserted.all() - extents = bounds.reshape((-1, 2)).ptp(axis=0) + extents = np.ptp(bounds.reshape((-1, 2)), axis=0) assert np.allclose(extents, [2, 3]) assert bounds.shape == (2, 2, 2) density = 5.0 / np.prod(extents) diff --git a/tests/test_obj.py b/tests/test_obj.py index 226781f09..c241e4363 100644 --- a/tests/test_obj.py +++ b/tests/test_obj.py @@ -24,7 +24,7 @@ def test_no_img(self): assert m.visual.uv.max() < (1 + 1e-5) assert m.visual.uv.min() > -1e-5 # check to make sure it's not all zeros - assert m.visual.uv.ptp() > 0.5 + assert g.np.ptp(m.visual.uv) > 0.5 rec = g.roundtrip(m.export(file_type="obj"), file_type="obj") assert g.np.isclose(m.area, rec.area) @@ -48,7 +48,7 @@ def test_obj_groups(self): # assert len(mesh.metadata['face_groups']) == len(mesh.faces) # check to make sure there is signal not just zeros - # assert mesh.metadata['face_groups'].ptp() > 0 + # assert g.np.ptp(mesh.metadata['face_groups']) > 0 def test_obj_negative_indices(self): # a wavefront file with negative indices diff --git a/tests/test_paths.py b/tests/test_paths.py index 61c2a44dd..9aa8ff04b 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -298,6 +298,26 @@ def test_section(self): # should be a valid Path2D g.check_path2D(planar) + def test_multiplane(self): + # check to make sure we're applying `tol_path.merge_digits` for line segments + vertices = g.np.array( + [ + [19.402250931139097, -14.88787016674277, 64.0], + [20.03318396099334, -14.02738654377374, 64.0], + [19.402250931139097, -14.88787016674277, 0.0], + [20.03318396099334, -14.02738654377374, 0.0], + [21, -16.5, 32], + ] + ) + faces = g.np.array( + [[1, 3, 0], [0, 3, 2], [1, 0, 4], [0, 2, 4], [3, 1, 4], [2, 3, 4]] + ) + z_layer_centers = g.np.array([3, 3.0283334255218506]) + m = g.trimesh.Trimesh(vertices, faces) + r = m.section_multiplane([0, 0, 0], [0, 0, 1], z_layer_centers) + assert len(r) == 2 + assert all(i.is_closed for i in r) + if __name__ == "__main__": g.trimesh.util.attach_to_log() diff --git a/tests/test_ply.py b/tests/test_ply.py index a55f543fd..4cdcf0f7f 100644 --- a/tests/test_ply.py +++ b/tests/test_ply.py @@ -16,7 +16,7 @@ def test_ply(self): m = g.get_mesh("machinist.XAML") assert m.visual.kind == "face" - assert m.visual.face_colors.ptp(axis=0).max() > 0 + assert g.np.ptp(m.visual.face_colors, axis=0).max() > 0 export = m.export(file_type="ply") reconstructed = g.roundtrip(export, file_type="ply") @@ -28,7 +28,7 @@ def test_ply(self): m = g.get_mesh("reference.ply") assert m.visual.kind == "vertex" - assert m.visual.vertex_colors.ptp(axis=0).max() > 0 + assert g.np.ptp(m.visual.vertex_colors, axis=0).max() > 0 export = m.export(file_type="ply") reconstructed = g.roundtrip(export, file_type="ply") diff --git a/tests/test_points.py b/tests/test_points.py index 1b3724eaf..68e8370cb 100644 --- a/tests/test_points.py +++ b/tests/test_points.py @@ -47,7 +47,9 @@ def test_pointcloud(self): assert hash(cloud) != initial_hash # AABB volume should be same as points - assert g.np.isclose(cloud.bounding_box.volume, g.np.prod(points.ptp(axis=0))) + assert g.np.isclose( + cloud.bounding_box.volume, g.np.prod(g.np.ptp(points, axis=0)) + ) # will populate all bounding primitives assert cloud.bounding_primitive.volume > 0.0 @@ -153,7 +155,7 @@ def test_kmeans(self, cluster_count=5, points_per_cluster=100): centroids, klabel = g.trimesh.points.k_means(points=clustered, k=cluster_count) # reshape to make sure all groups have the same index - variance = klabel.reshape((cluster_count, points_per_cluster)).ptp(axis=1) + variance = g.np.ptp(klabel.reshape((cluster_count, points_per_cluster)), axis=1) assert len(centroids) == cluster_count assert (variance == 0).all() diff --git a/tests/test_polygons.py b/tests/test_polygons.py index 005923da8..68c08e5bc 100644 --- a/tests/test_polygons.py +++ b/tests/test_polygons.py @@ -73,7 +73,7 @@ def test_sample(self): # try getting OBB of samples _T, extents = g.trimesh.path.polygons.polygon_obb(s) # OBB of samples should be less than diameter of circle - diameter = g.np.reshape(p.bounds, (2, 2)).ptp(axis=0).max() + diameter = g.np.ptp(g.np.reshape(p.bounds, (2, 2)), axis=0).max() assert (extents <= diameter).all() # test sampling with multiple bodies diff --git a/tests/test_ray.py b/tests/test_ray.py index 37493e955..e2c9ead5f 100644 --- a/tests/test_ray.py +++ b/tests/test_ray.py @@ -21,7 +21,7 @@ def test_rays(self): hit_any = g.np.array(hit_any, dtype=g.np.int64) for i in g.trimesh.grouping.group(g.np.unique(names, return_inverse=True)[1]): - broken = hit_any[i].astype(g.np.int64).ptp(axis=0).sum() + broken = g.np.ptp(hit_any[i].astype(g.np.int64), axis=0).sum() assert broken == 0 def test_rps(self): diff --git a/tests/test_remesh.py b/tests/test_remesh.py index fe6cedda5..2988a8276 100644 --- a/tests/test_remesh.py +++ b/tests/test_remesh.py @@ -44,7 +44,7 @@ def test_subdivide(self): assert bary.max() < (1 + epsilon) assert bary.min() > -epsilon # make sure it's not all zeros - assert bary.ptp() > epsilon + assert g.np.ptp(bary) > epsilon v, f = g.trimesh.remesh.subdivide(vertices=m.vertices, faces=m.faces) @@ -74,7 +74,7 @@ def test_subdivide(self): assert bary.max() < (1 + epsilon) assert bary.min() > -epsilon # make sure it's not all zeros - assert bary.ptp() > epsilon + assert g.np.ptp(bary) > epsilon check = m.subdivide_to_size( max_edge=m.extents.sum(), max_iter=1, return_index=False @@ -201,8 +201,8 @@ def test_uv(self): sv = s.visual.uv[s.faces] # both subdivided and original should have faces # that don't vary wildly - assert ov.ptp(axis=1).mean(axis=0).max() < 0.1 - assert sv.ptp(axis=1).mean(axis=0).max() < 0.1 + assert g.np.ptp(ov, axis=1).mean(axis=0).max() < 0.1 + assert g.np.ptp(sv, axis=1).mean(axis=0).max() < 0.1 max_edge = m.scale / 50 s = m.subdivide_to_size(max_edge=max_edge) @@ -220,8 +220,8 @@ def test_uv(self): sv = s.visual.uv[s.faces] # both subdivided and original should have faces # that don't vary wildly - assert ov.ptp(axis=1).mean(axis=0).max() < 0.1 - assert sv.ptp(axis=1).mean(axis=0).max() < 0.1 + assert g.np.ptp(ov, axis=1).mean(axis=0).max() < 0.1 + assert g.np.ptp(sv, axis=1).mean(axis=0).max() < 0.1 def test_max_iter(self): m = g.trimesh.creation.box() diff --git a/tests/test_scene.py b/tests/test_scene.py index 03473e6d7..dd6f56356 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,3 +1,5 @@ +import numpy as np + try: from . import generic as g except BaseException: @@ -102,6 +104,31 @@ def test_scene(self): # make sure explode doesn't crash s.explode() + def test_cam_gltf(self): + # Test that the camera is stored and loaded successfully into a Scene from a gltf. + cam = g.trimesh.scene.cameras.Camera(fov=[60, 90], name="cam1") + box = g.trimesh.creation.box(extents=[1, 2, 3]) + scene = g.trimesh.Scene( + geometry=[box], + camera=cam, + camera_transform=np.array( + [[0, 1, 0, -1], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] + ), + ) + with g.TemporaryDirectory() as d: + # exports by path allow files to be written + path = g.os.path.join(d, "tmp.glb") + scene.export(path) + r = g.trimesh.load(path, force="scene") + + # ensure no added nodes + assert set(r.graph.nodes) == {"world", "geometry_0", "cam1"} + # ensure same camera parameters and extrinsics + assert (r.camera_transform == scene.camera_transform).all() + assert r.camera.name == cam.name + assert (r.camera.fov == cam.fov).all() + assert r.camera.z_near == cam.z_near + def test_scaling(self): # Test the scaling of scenes including unit conversion. diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py index b5556a316..03dc9ec37 100644 --- a/tests/test_scenegraph.py +++ b/tests/test_scenegraph.py @@ -232,7 +232,7 @@ def test_shortest_path(self): # generate a lot of random queries queries = g.np.random.choice(list(forest.nodes), 10000).reshape((-1, 2)) # filter out any self-queries as networkx doesn't handle them - queries = queries[queries.ptp(axis=1) > 0] + queries = queries[g.np.ptp(queries, axis=1) > 0] # now run our shortest path algorithm in a profiler with g.Profiler() as P: diff --git a/tests/test_section.py b/tests/test_section.py index 20c6cb040..aad45ef2c 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -99,8 +99,8 @@ def test_section(self): back_3D.apply_transform(base_inv) # make sure all vertices have constant Z - assert back_3D.vertices[:, 2].ptp() < 1e-8 - assert sections_3D[index].vertices[:, 2].ptp() < 1e-8 + assert g.np.ptp(back_3D.vertices[:, 2]) < 1e-8 + assert g.np.ptp(sections_3D[index].vertices[:, 2]) < 1e-8 # make sure reconstructed 3D section is at right height assert g.np.isclose( diff --git a/tests/test_segments.py b/tests/test_segments.py index 86f44e596..c210d7681 100644 --- a/tests/test_segments.py +++ b/tests/test_segments.py @@ -68,7 +68,7 @@ def test_colinear(self): for pair in n: val = seg[pair] close = g.np.append( - (val[0] - val[1]).ptp(axis=1), (val[0] - val[1][::-1]).ptp(axis=1) + g.np.ptp(val[0] - val[1], axis=1), g.np.ptp(val[0] - val[1][::-1], axis=1) ).min() assert close < epsilon diff --git a/tests/test_sweep.py b/tests/test_sweep.py index 402f8a2d6..05c18f333 100644 --- a/tests/test_sweep.py +++ b/tests/test_sweep.py @@ -47,7 +47,7 @@ def test_simple_extrude(height=10): b = sweep_polygon(circle, path) # should be a straight extrude along Z - expected = np.append(np.reshape(circle.bounds, (2, 2)).ptp(axis=0), height) + expected = np.append(np.ptp(np.reshape(circle.bounds, (2, 2)), axis=0), height) assert np.allclose(expected, b.extents) # should be well constructed diff --git a/tests/test_voxel.py b/tests/test_voxel.py index fe172f39d..db3893633 100644 --- a/tests/test_voxel.py +++ b/tests/test_voxel.py @@ -90,7 +90,7 @@ def test_marching_points(self): # get some points on the surface of an icosahedron points = g.trimesh.creation.icosahedron().sample(1000) # make the pitch proportional to scale - pitch = points.ptp(axis=0).min() / 10 + pitch = g.np.ptp(points, axis=0).min() / 10 # run marching cubes mesh = g.trimesh.voxel.ops.points_to_marching_cubes(points=points, pitch=pitch) diff --git a/trimesh/base.py b/trimesh/base.py index dd7cb4ed1..0126267af 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -412,7 +412,7 @@ def face_normals(self, values: Optional[ArrayLike]) -> None: return # check if any values are larger than tol.merge # don't set the normals if they are all zero - ptp = values.ptp() + ptp = np.ptp(values) if not np.isfinite(ptp): log.debug("face_normals contain NaN, ignoring!") return @@ -503,7 +503,7 @@ def vertex_normals(self, values: ArrayLike) -> None: values = np.asanyarray(values, order="C", dtype=float64) if values.shape == self.vertices.shape: # check to see if they assigned all zeros - if values.ptp() < tol.merge: + if np.ptp(values) < tol.merge: log.debug("vertex_normals are all zero!") self._cache["vertex_normals"] = values @@ -560,7 +560,7 @@ def extents(self) -> Optional[NDArray[float64]]: # if mesh is empty return None if self.bounds is None: return None - extents = self.bounds.ptp(axis=0) + extents = np.ptp(self.bounds, axis=0) return extents diff --git a/trimesh/bounds.py b/trimesh/bounds.py index 9dfd38e46..b86a9c249 100644 --- a/trimesh/bounds.py +++ b/trimesh/bounds.py @@ -272,7 +272,7 @@ def oriented_bounds_coplanar(points): area = ((x.max(axis=1) - x.min(axis=1)) * (y.max(axis=1) - y.min(axis=1))).min() # the volume is 2D area plus the projected height - volume = area * projected[:, 2].ptp() + volume = area * np.ptp(projected[:, 2]) # store this transform if it's better than one we've seen if volume < min_volume: @@ -283,7 +283,7 @@ def oriented_bounds_coplanar(points): # part so now we need to do the bookkeeping to find the box vert_ones = np.column_stack((vertices, np.ones(len(vertices)))).T projected = np.dot(min_2D, vert_ones).T[:, :3] - height = projected[:, 2].ptp() + height = np.ptp(projected[:, 2]) rotation_2D, box = oriented_bounds_2D(projected[:, :2]) min_extents = np.append(box, height) rotation_2D[:2, 2] = 0.0 @@ -294,7 +294,7 @@ def oriented_bounds_coplanar(points): # transform points using our matrix to find the translation transformed = transformations.transform_points(vertices, to_origin) - box_center = transformed.min(axis=0) + transformed.ptp(axis=0) * 0.5 + box_center = transformed.min(axis=0) + np.ptp(transformed, axis=0) * 0.5 to_origin[:3, 3] = -box_center # return ordered 3D extents @@ -374,7 +374,7 @@ def volume_from_angles(spherical, return_data=False): """ to_2D = transformations.spherical_matrix(*spherical, axes="rxyz") projected = transformations.transform_points(hull, matrix=to_2D) - height = projected[:, 2].ptp() + height = np.ptp(projected[:, 2]) try: center_2D, radius = nsphere.minimum_nsphere(projected[:, :2]) @@ -405,7 +405,7 @@ def volume_from_angles(spherical, return_data=False): # transform vertices to plane to check on_plane = transformations.transform_points(obj.vertices, to_2D) # cylinder height is overall Z span - height = on_plane[:, 2].ptp() + height = np.ptp(on_plane[:, 2]) # center mass is correct on plane, but position # along symmetry axis may be wrong so slide it slide = transformations.translation_matrix( @@ -481,7 +481,7 @@ def to_extents(bounds): if bounds.shape != (2, 3): raise ValueError("bounds must be (2, 3)") - extents = bounds.ptp(axis=0) + extents = np.ptp(bounds, axis=0) transform = np.eye(4) transform[:3, 3] = bounds.mean(axis=0) diff --git a/trimesh/caching.py b/trimesh/caching.py index 05aea98fc..36c6fc5dd 100644 --- a/trimesh/caching.py +++ b/trimesh/caching.py @@ -186,13 +186,13 @@ def __array_finalize__(self, obj): if isinstance(obj, type(self)): obj._dirty_hash = True - def __array_wrap__(self, out_arr, context=None): + def __array_wrap__(self, out_arr, context=None, *args, **kwargs): """ Return a numpy scalar if array is 0d. See https://github.com/numpy/numpy/issues/5819 """ if out_arr.ndim: - return np.ndarray.__array_wrap__(self, out_arr, context) + return np.ndarray.__array_wrap__(self, out_arr, context, *args, **kwargs) # Match numpy's behavior and return a numpy dtype scalar return out_arr[()] diff --git a/trimesh/constants.py b/trimesh/constants.py index 28d4cdcf1..3c79982f2 100644 --- a/trimesh/constants.py +++ b/trimesh/constants.py @@ -2,7 +2,7 @@ import numpy as np -from .util import log, now +from .util import decimal_to_digits, log, now @dataclass @@ -88,6 +88,7 @@ class TolerancePath: zero: float = 1e-12 merge: float = 1e-5 + planar: float = 1e-5 seg_frac: float = 0.125 seg_angle: float = float(np.radians(50)) @@ -98,9 +99,12 @@ class TolerancePath: radius_min: float = 1e-4 radius_max: float = 50.0 tangent: float = float(np.radians(20)) - strict: bool = False + @property + def merge_digits(self) -> int: + return decimal_to_digits(self.merge) + @dataclass class ResolutionPath: diff --git a/trimesh/creation.py b/trimesh/creation.py index 0ed4a7930..21b8fa70f 100644 --- a/trimesh/creation.py +++ b/trimesh/creation.py @@ -714,7 +714,7 @@ def box( bounds = np.array(bounds, dtype=np.float64) if bounds.shape != (2, 3): raise ValueError("`bounds` must be (2, 3) float!") - extents = bounds.ptp(axis=0) + extents = np.ptp(bounds, axis=0) vertices *= extents vertices += bounds[0] elif extents is not None: diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py index e48b0696e..7ba1cf331 100644 --- a/trimesh/exchange/gltf.py +++ b/trimesh/exchange/gltf.py @@ -15,8 +15,10 @@ from .. import rendering, resources, transformations, util, visual from ..caching import hash_fast from ..constants import log, tol -from ..typed import NDArray, Optional -from ..util import unique_name +from ..resolvers import Resolver, ZipResolver +from ..scene.cameras import Camera +from ..typed import Mapping, NDArray, Optional, Stream, Union +from ..util import triangle_strips_to_faces, unique_name from ..visual.gloss import specular_to_pbr # magic numbers which have meaning in GLTF @@ -48,6 +50,9 @@ } } +# we can accept dict resolvers +ResolverLike = Union[Resolver, Mapping] + # GL geometry modes _GL_LINES = 1 _GL_POINTS = 0 @@ -261,11 +266,11 @@ def export_glb( def load_gltf( - file_obj=None, - resolver=None, - ignore_broken=False, - merge_primitives=False, - skip_materials=False, + file_obj: Optional[Stream] = None, + resolver: Optional[ResolverLike] = None, + ignore_broken: bool = False, + merge_primitives: bool = False, + skip_materials: bool = False, **mesh_kwargs, ): """ @@ -336,11 +341,11 @@ def load_gltf( def load_glb( - file_obj, - resolver=None, - ignore_broken=False, - merge_primitives=False, - skip_materials=False, + file_obj: Stream, + resolver: Optional[ResolverLike] = None, + ignore_broken: bool = False, + merge_primitives: bool = False, + skip_materials: bool = False, **mesh_kwargs, ): """ @@ -444,6 +449,7 @@ def load_glb( merge_primitives=merge_primitives, skip_materials=skip_materials, mesh_kwargs=mesh_kwargs, + resolver=resolver, ) return kwargs @@ -1350,10 +1356,10 @@ def _read_buffers( header, buffers, mesh_kwargs, - ignore_broken=False, - merge_primitives=False, - skip_materials=False, - resolver=None, + resolver: Optional[ResolverLike], + ignore_broken: bool = False, + merge_primitives: bool = False, + skip_materials: bool = False, ): """ Given binary data and a layout return the @@ -1511,15 +1517,21 @@ def _read_buffers( if mode == _GL_STRIP: # this is triangle strips flat = access[p["indices"]].reshape(-1) - kwargs["faces"] = util.triangle_strips_to_faces([flat]) + kwargs["faces"] = triangle_strips_to_faces([flat]) else: kwargs["faces"] = access[p["indices"]].reshape((-1, 3)) else: # indices are apparently optional and we are supposed to # do the same thing as webGL drawArrays? - kwargs["faces"] = np.arange( - len(kwargs["vertices"]) * 3, dtype=np.int64 - ).reshape((-1, 3)) + if mode == _GL_STRIP: + kwargs["faces"] = triangle_strips_to_faces( + np.array([np.arange(len(kwargs["vertices"]))]) + ) + else: + # GL_TRIANGLES + kwargs["faces"] = np.arange( + len(kwargs["vertices"]), dtype=np.int64 + ).reshape((-1, 3)) if "NORMAL" in attr: # vertex normals are specified @@ -1664,6 +1676,10 @@ def _read_buffers( # unvisited, pairs of node indexes queue = deque() + # camera(s), if they exist + camera = None + camera_transform = None + if "scene" in header: # specify the index of scenes if specified scene_index = header["scene"] @@ -1735,6 +1751,20 @@ def _read_buffers( kwargs["matrix"], np.diag(np.concatenate((child["scale"], [1.0]))) ) + # If a camera exists, create the camera and dont add the node to the graph + # TODO only process the first camera, ignore the rest + # TODO assumes the camera node is child of the world frame + # TODO will only read perspective camera + if "camera" in child and camera is None: + cam_idx = child["camera"] + try: + camera = _cam_from_gltf(header["cameras"][cam_idx]) + except KeyError: + log.debug("GLTF camera is not fully-defined") + if camera: + camera_transform = kwargs["matrix"] + continue + # treat node metadata similarly to mesh metadata if isinstance(child.get("extras"), dict): kwargs["metadata"] = child["extras"] @@ -1780,6 +1810,8 @@ def _read_buffers( "geometry": meshes, "graph": graph, "base_frame": base_frame, + "camera": camera, + "camera_transform": camera_transform, } try: # load any scene extras into scene.metadata @@ -1799,6 +1831,38 @@ def _read_buffers( return result +def _cam_from_gltf(cam): + """ + Convert a gltf perspective camera to trimesh. + + The retrieved camera will have default resolution, since the gltf specification + does not contain it. + + If the camera is not perspective will return None. + If the camera is perspective but is missing fields, will raise `KeyError` + + Parameters + ------------ + cam : dict + Camera represented as a dictionary according to glTF + + Returns + ------------- + camera : trimesh.scene.cameras.Camera + Trimesh camera object + """ + if "perspective" not in cam: + return + name = cam.get("name") + znear = cam["perspective"]["znear"] + aspect_ratio = cam["perspective"]["aspectRatio"] + yfov = np.degrees(cam["perspective"]["yfov"]) + + fov = (aspect_ratio * yfov, yfov) + + return Camera(name=name, fov=fov, z_near=znear) + + def _convert_camera(camera): """ Convert a trimesh camera to a GLTF camera. @@ -2049,7 +2113,6 @@ def get_schema(): """ # replace references # get zip resolver to access referenced assets - from ..resolvers import ZipResolver from ..schemas import resolve # get a blob of a zip file including the GLTF 2.0 schema diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py index 2781485ac..d2b2bdce3 100644 --- a/trimesh/exchange/load.py +++ b/trimesh/exchange/load.py @@ -413,6 +413,12 @@ def handle_scene(): else: scene = Scene(geometry) + # camera, if it exists + camera = kwargs.get("camera") + if camera: + scene.camera = camera + scene.camera_transform = kwargs.get("camera_transform") + if "base_frame" in kwargs: scene.graph.base_frame = kwargs["base_frame"] metadata = kwargs.get("metadata") diff --git a/trimesh/graph.py b/trimesh/graph.py index f3f670df2..9072ab306 100644 --- a/trimesh/graph.py +++ b/trimesh/graph.py @@ -281,16 +281,18 @@ def shared_edges(faces_a, faces_b): return shared -def facets(mesh, engine=None): +def facets(mesh, engine=None, facet_threshold: Optional[Number] = None): """ Find the list of parallel adjacent faces. Parameters ----------- - mesh : trimesh.Trimesh + mesh : trimesh.Trimesh engine : str - Which graph engine to use: - ('scipy', 'networkx') + Which graph engine to use: + ('scipy', 'networkx') + facet_threshold : float + Threshold for two facets to be considered coplanar Returns --------- @@ -298,6 +300,8 @@ def facets(mesh, engine=None): Groups of face indexes of parallel adjacent faces. """ + if facet_threshold is None: + facet_threshold = tol.facet_threshold # what is the radius of a circle that passes through the perpendicular # projection of the vector between the two non- shared vertices # onto the shared edge, with the face normal from the two adjacent faces @@ -314,7 +318,7 @@ def facets(mesh, engine=None): # if span is zero we know faces are small/parallel nonzero = np.abs(span) > tol.zero # faces with a radii/span ratio larger than a threshold pass - parallel[nonzero] = (radii[nonzero] / span[nonzero]) ** 2 > tol.facet_threshold + parallel[nonzero] = (radii[nonzero] / span[nonzero]) ** 2 > facet_threshold # run connected components on the parallel faces to group them components = connected_components( @@ -525,7 +529,7 @@ def split_traversal(traversal, edges, edges_hash=None): # hash each edge so we can compare to edge set trav_hash = grouping.hashable_rows(np.sort(trav_edge, axis=1)) # check if each edge is contained in edge set - contained = np.in1d(trav_hash, edges_hash) + contained = np.isin(trav_hash, edges_hash) # exit early if every edge of traversal exists if contained.all(): @@ -547,7 +551,7 @@ def split_traversal(traversal, edges, edges_hash=None): continue # make sure it's not already closed edge = np.sort([t[0], t[-1]]) - if edge.ptp() == 0: + if np.ptp(edge) == 0: continue close = grouping.hashable_rows(edge.reshape((1, 2)))[0] # if we need the edge add it diff --git a/trimesh/grouping.py b/trimesh/grouping.py index 84c8934be..24d9bc081 100644 --- a/trimesh/grouping.py +++ b/trimesh/grouping.py @@ -204,7 +204,7 @@ def hashable_rows(data: ArrayLike, digits=None) -> NDArray: hashable = np.zeros(len(as_int), dtype=np.uint64) # offset to the middle of the unsigned integer range # this array should contain only positive values - bitbang = (as_int + threshold).astype(np.uint64).T + bitbang = as_int.astype(np.uint64).T + threshold # loop through each column and bitwise xor to combine # make sure as_int is int64 otherwise bit offset won't work for offset, column in enumerate(bitbang): diff --git a/trimesh/interval.py b/trimesh/interval.py index 1dbc98362..c8dcfdb9a 100644 --- a/trimesh/interval.py +++ b/trimesh/interval.py @@ -27,7 +27,7 @@ def intersection(a: ArrayLike, b: NDArray[float64]) -> NDArray[float64]: -------------- inter : (2, ) or (2, 2) float The unioned range from the two inputs, - if not `inter.ptp(axis=1)` will be zero. + if not np.ptp(`inter, axis=1)` will be zero. """ a = np.array(a, dtype=np.float64) b = np.array(b, dtype=np.float64) diff --git a/trimesh/nsphere.py b/trimesh/nsphere.py index a23de86cc..a2371b7a6 100644 --- a/trimesh/nsphere.py +++ b/trimesh/nsphere.py @@ -67,7 +67,7 @@ def minimum_nsphere(obj): # this used to pass qhull_options 'QbB' to Voronoi however this had a bug somewhere # to avoid this we scale to a unit cube ourselves inside this function points_origin = points.min(axis=0) - points_scale = points.ptp(axis=0).min() + points_scale = np.ptp(points, axis=0).min() points = (points - points_origin) / points_scale # if all of the points are on an n-sphere already the voronoi @@ -169,7 +169,7 @@ def residuals(center): radii = util.row_norm(points - center_result) radius = radii.mean() - error = radii.ptp() + error = np.ptp(radii) return center_result, radius, error diff --git a/trimesh/path/entities.py b/trimesh/path/entities.py index c9ad6f8cf..b09b25062 100644 --- a/trimesh/path/entities.py +++ b/trimesh/path/entities.py @@ -379,7 +379,7 @@ def origin(self): @origin.setter def origin(self, value): value = int(value) - if not hasattr(self, "points") or self.points.ptp() == 0: + if not hasattr(self, "points") or np.ptp(self.points) == 0: self.points = np.ones(3, dtype=np.int64) * value else: self.points[0] = value diff --git a/trimesh/path/exchange/misc.py b/trimesh/path/exchange/misc.py index e99d56429..2ae3423ed 100644 --- a/trimesh/path/exchange/misc.py +++ b/trimesh/path/exchange/misc.py @@ -1,6 +1,7 @@ import numpy as np from ... import graph, grouping, util +from ...constants import tol_path from ..entities import Arc, Line @@ -62,7 +63,7 @@ def lines_to_path(lines): # convert lines to even number of (n, dimension) points lines = lines.reshape((-1, dimension)) # merge duplicate vertices - unique, inverse = grouping.unique_rows(lines) + unique, inverse = grouping.unique_rows(lines, digits=tol_path.merge_digits) # use scipy edges_to_path to skip creating # a bajillion individual line entities which # will be super slow vs. fewer polyline entities diff --git a/trimesh/path/exchange/svg_io.py b/trimesh/path/exchange/svg_io.py index 0e207a8ab..b3d189581 100644 --- a/trimesh/path/exchange/svg_io.py +++ b/trimesh/path/exchange/svg_io.py @@ -341,7 +341,7 @@ def __init__(self, lines): ) # all arcs share the same center radius and rotation closed = False - if verts[:, 4:].ptp(axis=0).mean() < 1e-3: + if np.ptp(verts[:, 4:], axis=0).mean() < 1e-3: start, end = verts[:, :2], verts[:, 2:4] # if every end point matches the start point of a new # arc that means this is really a closed circle made diff --git a/trimesh/path/packing.py b/trimesh/path/packing.py index 716a3a43a..8af416387 100644 --- a/trimesh/path/packing.py +++ b/trimesh/path/packing.py @@ -210,7 +210,7 @@ def rectangles_single(extents, size=None, shuffle=False, rotate=True, random=Non # if no bounds are passed start it with the size of a large # rectangle exactly which will require re-rooting for # subsequent insertions - root_bounds = [[0.0] * dimension, extents[extents.ptp(axis=1).argmax()]] + root_bounds = [[0.0] * dimension, extents[np.ptp(extents, axis=1).argmax()]] else: # restrict the bounds to passed size and disallow re-rooting root_bounds = [[0.0] * dimension, size] @@ -230,7 +230,7 @@ def rectangles_single(extents, size=None, shuffle=False, rotate=True, random=Non # get the size of the current root node bounds = root.bounds # current extents - current = bounds.ptp(axis=0) + current = np.ptp(bounds, axis=0) # pick the direction which has the least hyper-volume. best = np.inf @@ -280,7 +280,7 @@ def rectangles_single(extents, size=None, shuffle=False, rotate=True, random=Non new_root.child = [RectangleBin(bounds_ori), RectangleBin(bounds_ins)] # insert the original sheet into the new tree - root_offset = new_root.child[0].insert(bounds.ptp(axis=0), rotate=rotate) + root_offset = new_root.child[0].insert(np.ptp(bounds, axis=0), rotate=rotate) # we sized the cells so original tree would fit assert root_offset is not None @@ -477,7 +477,7 @@ def rectangles( ) count = insert.sum() - extents_all = bounds.reshape((-1, dim)).ptp(axis=0) + extents_all = np.ptp(bounds.reshape((-1, dim)), axis=0) if quanta is not None: # compute the density using an upsized quanta @@ -560,7 +560,7 @@ def images( assert insert.all() # re-index bounds back to original indexes bounds = bounds[inverse] - assert np.allclose(bounds.ptp(axis=1), [i.size for i in images]) + assert np.allclose(np.ptp(bounds, axis=1), [i.size for i in images]) else: # use the number of pixels as the rectangle size bounds, insert = rectangles( @@ -580,7 +580,7 @@ def images( # offsets should be integer multiple of pizels offset = bounds[:, 0].round().astype(int) - extents = bounds.reshape((-1, 2)).ptp(axis=0) + (spacing * 2) + extents = np.ptp(bounds.reshape((-1, 2)), axis=0) + (spacing * 2) size = extents.round().astype(int) if power_resize: # round up all dimensions to powers of 2 @@ -719,7 +719,7 @@ def roll_transform(bounds, extents): return [] # find the size of the AABB of the passed bounds - passed = bounds.ptp(axis=1) + passed = np.ptp(bounds, axis=1) # zeroth index is 2D, `1` is 3D dimension = passed.shape[1] @@ -759,7 +759,7 @@ def roll_transform(bounds, extents): rolled = np.roll(extents, roll, axis=1) # check to see if the rolled original extents # match the requested bounding box - ok = (passed - rolled).ptp(axis=1) < _TOL_ZERO + ok = np.ptp((passed - rolled), axis=1) < _TOL_ZERO if not ok.any(): continue diff --git a/trimesh/path/path.py b/trimesh/path/path.py index 50ce9702f..8d5cc8b4d 100644 --- a/trimesh/path/path.py +++ b/trimesh/path/path.py @@ -328,7 +328,7 @@ def extents(self): extents : (dimension,) float Edge length of AABB """ - return self.bounds.ptp(axis=0) + return np.ptp(self.bounds, axis=0) def convert_units(self, desired: str, guess: bool = False): """ @@ -833,7 +833,7 @@ def to_planar(self, to_2D=None, normal=None, check=True): # Z values of vertices which are referenced heights = flat[referenced][:, 2] # points are not on a plane because Z varies - if heights.ptp() > tol.planar: + if np.ptp(heights) > tol.planar: # since Z is inconsistent set height to zero height = 0.0 if check: diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py index 3edf453fb..887908a3b 100644 --- a/trimesh/path/polygons.py +++ b/trimesh/path/polygons.py @@ -394,7 +394,7 @@ def medial_axis(polygon: Polygon, resolution: Optional[Number] = None, clip=None # a circle will have a single point medial axis if len(polygon.interiors) == 0: # what is the approximate scale of the polygon - scale = np.reshape(polygon.bounds, (2, 2)).ptp(axis=0).max() + scale = np.ptp(np.reshape(polygon.bounds, (2, 2)), axis=0).max() # a (center, radius, error) tuple fit = fit_circle_check(polygon.exterior.coords, scale=scale) # is this polygon in fact a circle @@ -413,7 +413,7 @@ def medial_axis(polygon: Polygon, resolution: Optional[Number] = None, clip=None from shapely import vectorized if resolution is None: - resolution = np.reshape(polygon.bounds, (2, 2)).ptp(axis=0).max() / 100 + resolution = np.ptp(np.reshape(polygon.bounds, (2, 2)), axis=0).max() / 100 # get evenly spaced points on the polygons boundaries samples = resample_boundaries(polygon=polygon, resolution=resolution, clip=clip) @@ -442,7 +442,7 @@ def medial_axis(polygon: Polygon, resolution: Optional[Number] = None, clip=None if tol.strict: # make sure we didn't screw up indexes - assert (vertices[edges_final] - voronoi.vertices[edges]).ptp() < 1e-5 + assert np.ptp(vertices[edges_final] - voronoi.vertices[edges]) < 1e-5 return edges_final, vertices @@ -519,7 +519,7 @@ def polygon_scale(polygon): scale : float Length of AABB diagonal """ - extents = np.reshape(polygon.bounds, (2, 2)).ptp(axis=0) + extents = np.ptp(np.reshape(polygon.bounds, (2, 2)), axis=0) scale = (extents**2).sum() ** 0.5 return scale @@ -589,7 +589,7 @@ def sample(polygon, count, factor=1.5, max_iter=10): # get size of bounding box bounds = np.reshape(polygon.bounds, (2, 2)) - extents = bounds.ptp(axis=0) + extents = np.ptp(bounds, axis=0) # how many points to check per loop iteration per_loop = int(count * factor) @@ -663,7 +663,7 @@ def repair_invalid(polygon, scale=None, rtol=0.5): return basic if scale is None: - distance = 0.002 * np.reshape(polygon.bounds, (2, 2)).ptp(axis=0).mean() + distance = 0.002 * np.ptp(np.reshape(polygon.bounds, (2, 2)), axis=0).mean() else: distance = 0.002 * scale @@ -834,7 +834,7 @@ def projected( padding += float(apad) if rpad is not None: # get the 2D scale as the longest side of the AABB - scale = vertices_2D.ptp(axis=0).max() + scale = np.ptp(vertices_2D, axis=0).max() # apply the scale-relative padding padding += float(rpad) * scale diff --git a/trimesh/path/raster.py b/trimesh/path/raster.py index 1fc386f07..bc6a67fef 100644 --- a/trimesh/path/raster.py +++ b/trimesh/path/raster.py @@ -62,7 +62,7 @@ def rasterize(path, pitch=None, origin=None, resolution=None, fill=True, width=N # if resolution is None make it larger than path if resolution is None: - span = np.vstack((path.bounds, origin)).ptp(axis=0) + span = np.ptp(np.vstack((path.bounds, origin)), axis=0) resolution = np.ceil(span / pitch) + 2 # get resolution as a (2,) int tuple resolution = np.asanyarray(resolution, dtype=np.int64) diff --git a/trimesh/path/simplify.py b/trimesh/path/simplify.py index 04c5b6091..df5859ddb 100644 --- a/trimesh/path/simplify.py +++ b/trimesh/path/simplify.py @@ -130,7 +130,7 @@ def is_circle(points, scale, verbose=False): if np.linalg.norm(points[0] - points[-1]) > tol.merge: return None - box = points.ptp(axis=0) + box = np.ptp(points, axis=0) # the bounding box size of the points # check aspect ratio as an early exit if the path is not a circle aspect = np.divide(*box) diff --git a/trimesh/points.py b/trimesh/points.py index 7ff60ec25..d409e3a3a 100644 --- a/trimesh/points.py +++ b/trimesh/points.py @@ -558,7 +558,7 @@ def extents(self): extents : (3,) float Edge length of axis aligned bounding box """ - return self.bounds.ptp(axis=0) + return np.ptp(self.bounds, axis=0) @property def centroid(self): diff --git a/trimesh/primitives.py b/trimesh/primitives.py index 7718d9c7f..7fcbe8875 100644 --- a/trimesh/primitives.py +++ b/trimesh/primitives.py @@ -724,7 +724,7 @@ def __init__(self, extents=None, transform=None, bounds=None, mutable=True): if bounds.shape != (2, 3): raise ValueError("`bounds` must be (2, 3) float") # create extents from AABB - extents = bounds.ptp(axis=0) + extents = np.ptp(bounds, axis=0) # translate to the center of the box transform = np.eye(4) transform[:3, 3] = bounds[0] + extents / 2.0 diff --git a/trimesh/proximity.py b/trimesh/proximity.py index e7a423162..3909bdf87 100644 --- a/trimesh/proximity.py +++ b/trimesh/proximity.py @@ -184,7 +184,7 @@ def closest_point(mesh, points): # however: same closest point on two different faces # find the best one and correct triangle ids if necessary - check_distance = two_dists.ptp(axis=1) < tol.merge + check_distance = np.ptp(two_dists, axis=1) < tol.merge check_magnitude = np.all(np.abs(two_dists) > tol.merge, axis=1) # mask results where corrections may be apply diff --git a/trimesh/scene/cameras.py b/trimesh/scene/cameras.py index 283d63712..2b83efd36 100644 --- a/trimesh/scene/cameras.py +++ b/trimesh/scene/cameras.py @@ -318,7 +318,7 @@ def look_at(points, fov, rotation=None, distance=None, center=None, pad=None): if center is None: # Find the center of the points' AABB in camera frame - center_c = points_c.min(axis=0) + 0.5 * points_c.ptp(axis=0) + center_c = points_c.min(axis=0) + 0.5 * np.ptp(points_c, axis=0) else: # Transform center to camera frame center_c = rinv.dot(center) diff --git a/trimesh/transformations.py b/trimesh/transformations.py index 2a5708fad..ff179a336 100644 --- a/trimesh/transformations.py +++ b/trimesh/transformations.py @@ -209,7 +209,7 @@ def identity_matrix(): >>> I = identity_matrix() >>> np.allclose(I, np.dot(I, I)) True - >>> np.sum(I), np.trace(I) + >>> float(np.sum(I)), float(np.trace(I)) (4.0, 4.0) >>> np.allclose(I, np.identity(4)) True @@ -255,7 +255,7 @@ def translation_from_matrix(matrix): True """ - return np.array(matrix, copy=False)[:3, 3].copy() + return np.asarray(matrix)[:3, 3].copy() def reflection_matrix(point, normal): @@ -296,7 +296,7 @@ def reflection_from_matrix(matrix): True """ - M = np.array(matrix, dtype=np.float64, copy=False) + M = np.asarray(matrix, dtype=np.float64) # normal: unit eigenvector corresponding to eigenvalue -1 w, V = np.linalg.eig(M[:3, :3]) i = np.where(abs(np.real(w) + 1.0) < 1e-8)[0] @@ -384,7 +384,7 @@ def rotation_matrix(angle, direction, point=None): # if point is specified, rotation is not around origin if point is not None: - point = np.array(point[:3], dtype=np.float64, copy=False) + point = np.asarray(point[:3], dtype=np.float64) M[:3, 3] = point - np.dot(M[:3, :3], point) # return symbolic angles as sympy Matrix objects @@ -407,7 +407,7 @@ def rotation_from_matrix(matrix): True """ - R = np.array(matrix, dtype=np.float64, copy=False) + R = np.asarray(matrix, dtype=np.float64) R33 = R[:3, :3] # direction: unit eigenvector of R33 corresponding to eigenvalue of 1 w, W = np.linalg.eig(R33.T) @@ -486,7 +486,7 @@ def scale_from_matrix(matrix): True """ - M = np.array(matrix, dtype=np.float64, copy=False) + M = np.asarray(matrix, dtype=np.float64) M33 = M[:3, :3] factor = np.trace(M33) - 2.0 try: @@ -541,11 +541,11 @@ def projection_matrix(point, normal, direction=None, perspective=None, pseudo=Fa """ M = np.identity(4) - point = np.array(point[:3], dtype=np.float64, copy=False) + point = np.asarray(point[:3], dtype=np.float64) normal = unit_vector(normal[:3]) if perspective is not None: # perspective projection - perspective = np.array(perspective[:3], dtype=np.float64, copy=False) + perspective = np.asarray(perspective[:3], dtype=np.float64) M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal) M[:3, :3] -= np.outer(perspective, normal) if pseudo: @@ -558,7 +558,7 @@ def projection_matrix(point, normal, direction=None, perspective=None, pseudo=Fa M[3, 3] = np.dot(perspective, normal) elif direction is not None: # parallel projection - direction = np.array(direction[:3], dtype=np.float64, copy=False) + direction = np.asarray(direction[:3], dtype=np.float64) scale = np.dot(direction, normal) M[:3, :3] -= np.outer(direction, normal) / scale M[:3, 3] = direction * (np.dot(point, normal) / scale) @@ -601,7 +601,7 @@ def projection_from_matrix(matrix, pseudo=False): True """ - M = np.array(matrix, dtype=np.float64, copy=False) + M = np.asarray(matrix, dtype=np.float64) M33 = M[:3, :3] w, V = np.linalg.eig(M) i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0] @@ -746,7 +746,7 @@ def shear_from_matrix(matrix): True """ - M = np.array(matrix, dtype=np.float64, copy=False) + M = np.asarray(matrix, dtype=np.float64) M33 = M[:3, :3] # normal: cross independent eigenvectors corresponding to the eigenvalue 1 w, V = np.linalg.eig(M33) @@ -801,8 +801,8 @@ def decompose_matrix(matrix): True >>> S = scale_matrix(0.123) >>> scale, shear, angles, trans, persp = decompose_matrix(S) - >>> scale[0] - 0.123 + >>> bool(np.isclose(scale[0], 0.123)) + True >>> R0 = euler_matrix(1, 2, 3) >>> scale, shear, angles, trans, persp = decompose_matrix(R0) >>> R1 = euler_matrix(*angles) @@ -810,7 +810,7 @@ def decompose_matrix(matrix): True """ - M = np.array(matrix, dtype=np.float64, copy=True).T + M = np.array(matrix, dtype=np.float64).T if abs(M[3, 3]) < _EPS: raise ValueError("M[3, 3] is zero") M /= M[3, 3] @@ -982,8 +982,8 @@ def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True): More examples in superimposition_matrix() """ - v0 = np.array(v0, dtype=np.float64, copy=True) - v1 = np.array(v1, dtype=np.float64, copy=True) + v0 = np.array(v0, dtype=np.float64) + v1 = np.array(v1, dtype=np.float64) ndims = v0.shape[0] if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape: @@ -1097,8 +1097,8 @@ def superimposition_matrix(v0, v1, scale=False, usesvd=True): True """ - v0 = np.array(v0, dtype=np.float64, copy=False)[:3] - v1 = np.array(v1, dtype=np.float64, copy=False)[:3] + v0 = np.asarray(v0, dtype=np.float64)[:3] + v1 = np.asarray(v1, dtype=np.float64)[:3] return affine_matrix_from_points(v0, v1, shear=False, scale=scale, usesvd=usesvd) @@ -1203,7 +1203,7 @@ def euler_from_matrix(matrix, axes="sxyz"): j = _NEXT_AXIS[i + parity] k = _NEXT_AXIS[i - parity + 1] - M = np.array(matrix, dtype=np.float64, copy=False)[:3, :3] + M = np.asarray(matrix, dtype=np.float64)[:3, :3] if repetition: sy = np.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k]) if sy > _EPS: @@ -1335,7 +1335,7 @@ def quaternion_matrix(quaternion): """ - q = np.array(quaternion, dtype=np.float64, copy=True).reshape((-1, 4)) + q = np.array(quaternion, dtype=np.float64).reshape((-1, 4)) n = np.einsum("ij,ij->i", q, q) # how many entries do we have num_qs = len(n) @@ -1402,7 +1402,7 @@ def quaternion_from_matrix(matrix, isprecise=False): True """ - M = np.array(matrix, dtype=np.float64, copy=False)[:4, :4] + M = np.asarray(matrix, dtype=np.float64)[:4, :4] if isprecise: q = np.empty((4,)) t = np.trace(M) @@ -1482,7 +1482,7 @@ def quaternion_conjugate(quaternion): True """ - q = np.array(quaternion, dtype=np.float64, copy=True) + q = np.array(quaternion, dtype=np.float64) np.negative(q[1:], q[1:]) return q @@ -1496,7 +1496,7 @@ def quaternion_inverse(quaternion): True """ - q = np.array(quaternion, dtype=np.float64, copy=True) + q = np.array(quaternion, dtype=np.float64) np.negative(q[1:], q[1:]) return q / np.dot(q, q) @@ -1518,7 +1518,7 @@ def quaternion_imag(quaternion): array([0., 1., 2.]) """ - return np.array(quaternion[1:4], dtype=np.float64, copy=True) + return np.array(quaternion[1:4], dtype=np.float64) def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True): @@ -1743,8 +1743,8 @@ def arcball_map_to_sphere(point, center, radius): def arcball_constrain_to_axis(point, axis): """Return sphere point perpendicular to axis.""" - v = np.array(point, dtype=np.float64, copy=True) - a = np.array(axis, dtype=np.float64, copy=True) + v = np.array(point, dtype=np.float64) + a = np.array(axis, dtype=np.float64) v -= a * np.dot(a, v) # on plane n = vector_norm(v) if n > _EPS: @@ -1759,7 +1759,7 @@ def arcball_constrain_to_axis(point, axis): def arcball_nearest_axis(point, axes): """Return axis, which arc is nearest to point.""" - point = np.array(point, dtype=np.float64, copy=False) + point = np.asarray(point, dtype=np.float64) nearest = None mx = -1.0 for axis in axes: @@ -1826,13 +1826,13 @@ def vector_norm(data, axis=None, out=None): >>> vector_norm(v, axis=1, out=n) >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1))) True - >>> vector_norm([]) + >>> float(vector_norm([])) 0.0 - >>> vector_norm([1]) + >>> float(vector_norm([1])) 1.0 """ - data = np.array(data, dtype=np.float64, copy=True) + data = np.array(data, dtype=np.float64) if out is None: if data.ndim == 1: return np.sqrt(np.dot(data, data)) @@ -1868,18 +1868,18 @@ def unit_vector(data, axis=None, out=None): True >>> list(unit_vector([])) [] - >>> list(unit_vector([1])) + >>> [float(i) for i in unit_vector([1])] [1.0] """ if out is None: - data = np.array(data, dtype=np.float64, copy=True) + data = np.array(data, dtype=np.float64) if data.ndim == 1: data /= np.sqrt(np.dot(data, data)) return data else: if out is not data: - out[:] = np.array(data, copy=False) + out[:] = np.asarray(data) data = out length = np.atleast_1d(np.sum(data * data, axis)) np.sqrt(length, length) @@ -1894,11 +1894,11 @@ def random_vector(size): """Return array of random doubles in the half-open interval [0.0, 1.0). >>> v = random_vector(10000) - >>> np.all(v >= 0) and np.all(v < 1) + >>> bool(np.all(v >= 0) and np.all(v < 1)) True >>> v0 = random_vector(10) >>> v1 = random_vector(10) - >>> np.any(v0 == v1) + >>> bool(np.any(v0 == v1)) False """ @@ -1950,8 +1950,8 @@ def angle_between_vectors(v0, v1, directed=True, axis=0): True """ - v0 = np.array(v0, dtype=np.float64, copy=False) - v1 = np.array(v1, dtype=np.float64, copy=False) + v0 = np.asarray(v0, dtype=np.float64) + v1 = np.asarray(v1, dtype=np.float64) dot = np.sum(v0 * v1, axis=axis) dot /= vector_norm(v0, axis=axis) * vector_norm(v1, axis=axis) @@ -2001,9 +2001,9 @@ def is_same_transform(matrix0, matrix1): False """ - matrix0 = np.array(matrix0, dtype=np.float64, copy=True) + matrix0 = np.array(matrix0, dtype=np.float64) matrix0 /= matrix0[3, 3] - matrix1 = np.array(matrix1, dtype=np.float64, copy=True) + matrix1 = np.array(matrix1, dtype=np.float64) matrix1 /= matrix1[3, 3] return np.allclose(matrix0, matrix1) @@ -2251,13 +2251,13 @@ def is_rigid(matrix, epsilon=1e-8): return False # make sure last row has no scaling - if (matrix[-1] - [0, 0, 0, 1]).ptp() > epsilon: + if np.ptp(matrix[-1] - [0, 0, 0, 1]) > epsilon: return False # check dot product of rotation against transpose check = np.dot(matrix[:3, :3], matrix[:3, :3].T) - _IDENTITY[:3, :3] - return check.ptp() < epsilon + return np.ptp(check) < epsilon def scale_and_translate(scale=None, translate=None): diff --git a/trimesh/triangles.py b/trimesh/triangles.py index a65d8e32d..55e339635 100644 --- a/trimesh/triangles.py +++ b/trimesh/triangles.py @@ -335,9 +335,7 @@ def windings_aligned(triangles, normals_compare): """ triangles = np.asanyarray(triangles, dtype=np.float64) if not util.is_shape(triangles, (-1, 3, 3), allow_zeros=True): - raise ValueError( - f"triangles must have shape (n, 3, 3), got {triangles.shape!s}" - ) + raise ValueError(f"triangles must have shape (n, 3, 3), got {triangles.shape!s}") normals_compare = np.asanyarray(normals_compare, dtype=np.float64) calculated, valid = normals(triangles) diff --git a/trimesh/typed.py b/trimesh/typed.py index a95611934..cc9990bfb 100644 --- a/trimesh/typed.py +++ b/trimesh/typed.py @@ -5,6 +5,7 @@ IO, Any, BinaryIO, + Mapping, Optional, TextIO, Union, @@ -58,4 +59,5 @@ "Tuple", "float64", "int64", + "Mapping", ] diff --git a/trimesh/util.py b/trimesh/util.py index e53821b1a..309a77b71 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -2234,7 +2234,7 @@ def allclose(a, b, atol: float = 1e-8): bool indicating if all elements are within `atol`. """ # - return float((a - b).ptp()) < atol + return float(np.ptp(a - b)) < atol class FunctionRegistry(Mapping): diff --git a/trimesh/viewer/windowed.py b/trimesh/viewer/windowed.py index 32ca31230..085a442b3 100644 --- a/trimesh/viewer/windowed.py +++ b/trimesh/viewer/windowed.py @@ -562,9 +562,9 @@ def update_flags(self): center = bounds.mean(axis=0) # set the grid to the lowest Z position # also offset by the scale to avoid interference - center[2] = bounds[0][2] - (bounds[:, 2].ptp() / 100) + center[2] = bounds[0][2] - (np.ptp(bounds[:, 2]) / 100) # choose the side length by maximum XY length - side = bounds.ptp(axis=0)[:2].max() + side = np.ptp(bounds, axis=0)[:2].max() # create an axis marker sized relative to the scene grid_mesh = grid(side=side, count=4, transform=translation_matrix(center)) # convert the path to vertexlist args diff --git a/trimesh/visual/color.py b/trimesh/visual/color.py index 6b5ccae35..f78077f43 100644 --- a/trimesh/visual/color.py +++ b/trimesh/visual/color.py @@ -833,7 +833,7 @@ def interpolate(values, color_map=None, dtype=np.uint8): # make input always float values = np.asanyarray(values, dtype=np.float64).ravel() # scale values to 0.0 - 1.0 and get colors - colors = cmap((values - values.min()) / values.ptp()) + colors = cmap((values - values.min()) / np.ptp(values)) # convert to 0-255 RGBA rgba = to_rgba(colors, dtype=dtype) diff --git a/trimesh/voxel/base.py b/trimesh/voxel/base.py index 964d50166..6dc203f4d 100644 --- a/trimesh/voxel/base.py +++ b/trimesh/voxel/base.py @@ -68,9 +68,7 @@ def encoding(self, encoding): elif not isinstance(encoding, Encoding): raise ValueError(f"encoding must be an Encoding, got {encoding!s}") if len(encoding.shape) != 3: - raise ValueError( - f"encoding must be rank 3, got shape {encoding.shape!s}" - ) + raise ValueError(f"encoding must be rank 3, got shape {encoding.shape!s}") if encoding.dtype != bool: raise ValueError(f"encoding must be binary, got {encoding.dtype}") self._data["encoding"] = encoding diff --git a/trimesh/voxel/creation.py b/trimesh/voxel/creation.py index 7859a3de6..17f1af0c4 100644 --- a/trimesh/voxel/creation.py +++ b/trimesh/voxel/creation.py @@ -144,11 +144,8 @@ def local_voxelize(mesh, point, pitch, radius, fill=True, **kwargs): for i in range(1, n + 1) ] contains = mesh.contains(np.asarray(representatives) * pitch + local_origin) - where = np.where(contains)[0] + 1 - # use in1d vs isin for older numpy versions - internal = np.in1d(regions.flatten(), where).reshape(regions.shape) - + internal = np.isin(regions.flatten(), where).reshape(regions.shape) voxels = np.logical_or(voxels, internal) return base.VoxelGrid(voxels, tr.translation_matrix(local_origin)) diff --git a/trimesh/voxel/encoding.py b/trimesh/voxel/encoding.py index 544eb3ae1..cd02c28d1 100644 --- a/trimesh/voxel/encoding.py +++ b/trimesh/voxel/encoding.py @@ -815,9 +815,7 @@ class TransposedEncoding(LazyIndexMap): def __init__(self, base_encoding, perm): if not isinstance(base_encoding, Encoding): - raise ValueError( - f"base_encoding must be an Encoding, got {base_encoding!s}" - ) + raise ValueError(f"base_encoding must be an Encoding, got {base_encoding!s}") if len(base_encoding.shape) != len(perm): raise ValueError( "base_encoding has %d ndims - cannot transpose with perm %s" diff --git a/trimesh/voxel/morphology.py b/trimesh/voxel/morphology.py index 0f60ae279..dacdf1359 100644 --- a/trimesh/voxel/morphology.py +++ b/trimesh/voxel/morphology.py @@ -22,9 +22,7 @@ def _dense(encoding, rank=None): elif isinstance(encoding, enc.Encoding): dense = encoding.dense else: - raise ValueError( - f"encoding must be np.ndarray or Encoding, got {encoding!s}" - ) + raise ValueError(f"encoding must be np.ndarray or Encoding, got {encoding!s}") if rank: _assert_rank(dense, rank) return dense @@ -36,9 +34,7 @@ def _sparse_indices(encoding, rank=None): elif isinstance(encoding, enc.Encoding): sparse_indices = encoding.sparse_indices else: - raise ValueError( - f"encoding must be np.ndarray or Encoding, got {encoding!s}" - ) + raise ValueError(f"encoding must be np.ndarray or Encoding, got {encoding!s}") _assert_sparse_rank(sparse_indices, 3) return sparse_indices diff --git a/trimesh/voxel/ops.py b/trimesh/voxel/ops.py index f29b45ac0..8e1efed84 100644 --- a/trimesh/voxel/ops.py +++ b/trimesh/voxel/ops.py @@ -299,7 +299,7 @@ def boolean_sparse(a, b, operation=np.logical_and): # find the bounding box of both arrays extrema = np.array([a.min(axis=0), a.max(axis=0), b.min(axis=0), b.max(axis=0)]) origin = extrema.min(axis=0) - 1 - size = tuple(extrema.ptp(axis=0) + 2) + size = tuple(np.ptp(extrema, axis=0) + 2) # put nearby voxel arrays into same shape sparse array sp_a = sparse.COO((a - origin).T, data=np.ones(len(a), dtype=bool), shape=size) diff --git a/trimesh/voxel/runlength.py b/trimesh/voxel/runlength.py index 7b53d90b5..50d2c2f08 100644 --- a/trimesh/voxel/runlength.py +++ b/trimesh/voxel/runlength.py @@ -616,7 +616,7 @@ def rle_to_sparse(rle_data): try: while True: value = next(it) - counts = next(it) + counts = int(next(it)) end = index + counts if value: indices.append(np.arange(index, end, dtype=np.int64)) @@ -629,7 +629,7 @@ def rle_to_sparse(rle_data): return indices, values indices = np.concatenate(indices) - values = np.concatenate(values) + values = np.concatenate(values, dtype=rle_data.dtype) return indices, values