Skip to content

Commit

Permalink
Merge pull request #697 from mikedh/fixes/dedupe
Browse files Browse the repository at this point in the history
Add deduplicated method to scenes
  • Loading branch information
mikedh committed Jan 22, 2020
2 parents a5d71ee + 2b4ae63 commit ead8e5a
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 35 deletions.
20 changes: 19 additions & 1 deletion tests/test_scene.py
Expand Up @@ -204,9 +204,27 @@ def test_dupe(self):
assert len(u.duplicate_nodes) == 1
assert len(u.duplicate_nodes[0]) == 1

def test_dedupe(self):
# create a scene with two identical meshes
a = g.trimesh.creation.box()
b = g.trimesh.creation.box().apply_translation([2, 2, 2])
s = g.trimesh.Scene([a, b])

# should have 2 geometries
assert len(s.geometry) == 2
assert len(s.graph.nodes_geometry) == 2

# get a de-duplicated scene
d = s.deduplicated()
# should not have mutated original
assert len(s.geometry) == 2
assert len(s.graph.nodes_geometry) == 2
# should only have one geometry
assert len(d.geometry) == 1
assert len(d.graph.nodes_geometry) == 1

def test_3DXML(self):
s = g.get_mesh('rod.3DXML')

assert len(s.geometry) == 3
assert len(s.graph.nodes_geometry) == 29

Expand Down
31 changes: 16 additions & 15 deletions trimesh/base.py
Expand Up @@ -6,10 +6,6 @@
Library for importing, exporting and doing simple operations on triangular meshes.
"""

import numpy as np

import copy

from . import ray
from . import util
from . import units
Expand Down Expand Up @@ -45,6 +41,9 @@
from .scene import Scene
from .parent import Geometry

import copy
import numpy as np


class Trimesh(Geometry):

Expand Down Expand Up @@ -542,7 +541,7 @@ def extents(self):
if self.bounds is None:
return None
extents = self.bounds.ptp(axis=0)
extents.flags.writeable = False

return extents

@caching.cache_decorator
Expand All @@ -566,8 +565,8 @@ def scale(self):
@caching.cache_decorator
def centroid(self):
"""
The point in space which is the average of the triangle centroids
weighted by the area of each triangle.
The point in space which is the average of the triangle
centroids weighted by the area of each triangle.
This will be valid even for non-watertight meshes,
unlike self.center_mass
Expand All @@ -580,19 +579,22 @@ def centroid(self):

# use the centroid of each triangle weighted by
# the area of the triangle to find the overall centroid
centroid = np.average(self.triangles_center,
axis=0,
weights=self.area_faces)
centroid.flags.writeable = False
try:
centroid = np.average(self.triangles_center,
weights=self.area_faces,
axis=0)
except BaseException:
# if all triangles are zero-area weights will not work
centroid = self.triangles_center.mean(axis=0)
return centroid

@property
def center_mass(self):
"""
The point in space which is the center of mass/volume.
If the current mesh is not watertight, this is meaningless garbage
unless it was explicitly set.
If the current mesh is not watertight this is meaningless
garbage unless it was explicitly set.
Returns
-----------
Expand Down Expand Up @@ -793,8 +795,7 @@ def triangles(self):
# trigger a change flag which means the MD5 will have to be
# recomputed. We can escape this check by viewing the array.
triangles = self.vertices.view(np.ndarray)[self.faces]
# make triangles (which are derived from faces/vertices) not writeable
triangles.flags.writeable = False

return triangles

@caching.cache_decorator
Expand Down
33 changes: 22 additions & 11 deletions trimesh/comparison.py
Expand Up @@ -52,7 +52,9 @@ def identifier_simple(mesh):
# note that we're going to try to make all parameters relative
# to area so other values don't get blown up at weird scales
identifier[0] = mesh_area

# avoid divide-by-zero later
if mesh_area < tol.merge:
mesh_area = 1.0
# topological constant and the only thing we can really
# trust in this fallen world
identifier[1] = mesh.euler_number
Expand All @@ -75,30 +77,39 @@ def identifier_simple(mesh):
if mesh.symmetry == 'radial':
# cylinder height
h = np.dot(vertices, mesh.symmetry_axis).ptp()
# section radius
R2 = (np.dot(vertices, mesh.symmetry_section.T)
** 2).sum(axis=1).max()
# section radius summed per row then overall max
R2 = np.dot((np.dot(vertices, mesh.symmetry_section.T)
** 2), [1, 1]).max()
# area of a cylinder primitive
area = (2 * np.pi * (R2**.5) * h) + (2 * np.pi * R2)
# replace area in this case with area ratio
identifier[0] = mesh_area / area
elif mesh.symmetry == 'spherical':
# handle a spherically symmetric mesh
R2 = (vertices ** 2).sum(axis=1).max()
R2 = np.dot((vertices ** 2), [1, 1, 1]).max()
area = 4 * np.pi * R2
identifier[0] = mesh_area / area
else:
# if we don't have a watertight mesh add information about the
# convex hull, which is slow to compute and unreliable
# convex hull which is slow to compute and unreliable
try:
# get the hull area and volume
hull = mesh.convex_hull
hull_area = hull.area
hull_volume = hull.volume
except BaseException:
# in-plane or single point geometry has no hull
hull_area = 6.0
hull_volume = 1.0
# just what we're looking for in a hash but hey
identifier[3] = mesh_area / mesh.convex_hull.area
identifier[3] = mesh_area / hull_area
# cube side length ratio for the hull
identifier[4] = (((mesh.convex_hull.area / 6.0) ** (1.0 / 2.0)) /
(mesh.convex_hull.volume ** (1.0 / 3.0)))
identifier[4] = (((hull_area / 6.0) ** (1.0 / 2.0)) /
(hull_volume ** (1.0 / 3.0)))
# calculate maximum mesh radius
vertices = mesh.vertices - mesh.centroid

# add in max radius^2 to area ratio
R2 = (vertices ** 2).sum(axis=1).max()
R2 = np.dot((vertices ** 2), [1, 1, 1]).max()
identifier[5] = R2 / mesh_area

return identifier
Expand Down
6 changes: 6 additions & 0 deletions trimesh/exchange/gltf.py
Expand Up @@ -491,6 +491,11 @@ def _append_mesh(mesh,
mat_hashes : dict
Which materials have already been added
"""
# return early from empty meshes to avoid crashing later
if len(mesh.faces) == 0:
log.warning('skipping empty mesh!')
return

# meshes reference accessor indexes
# mode 4 is GL_TRIANGLES
tree["meshes"].append({
Expand Down Expand Up @@ -857,6 +862,7 @@ def _read_buffers(header, buffers, mesh_kwargs, resolver=None):
# if we don't have a triangular mesh continue
# if not specified assume it is a mesh
if "mode" in p and p["mode"] != 4:
log.warning('skipping primitive with mode {}!'.format(p['mode']))
continue

# store those units
Expand Down
35 changes: 28 additions & 7 deletions trimesh/scene/scene.py
@@ -1,3 +1,4 @@
import os
import numpy as np
import collections

Expand Down Expand Up @@ -439,22 +440,41 @@ def duplicate_nodes(self):
# geometry name : md5 of mesh
mesh_hash = {k: int(m.identifier_md5, 16)
for k, m in self.geometry.items()}

# the name of nodes in the scene graph with geometry
node_names = np.array(self.graph.nodes_geometry)
# the geometry names for each node in the same order
node_geom = np.array([self.graph[i][1] for i in node_names])

# the mesh md5 for each node in the same order
node_hash = np.array([mesh_hash[v] for v in node_geom])

# indexes of identical hashes
node_groups = grouping.group(node_hash)

# sequence of node names, where each sublist has identical geometry
duplicates = [np.sort(node_names[g]).tolist() for g in node_groups]
# sequence of node names where each
# sublist has identical geometry
duplicates = [np.sort(node_names[g]).tolist()
for g in node_groups]
return duplicates

def deduplicated(self):
"""
Return a new scene where each unique geometry is only
included once and transforms are discarded.
Returns
-------------
dedupe : Scene
One copy of each unique geometry from scene
"""
# collect geometry
geometry = {}
# loop through groups of identical nodes
for group in self.duplicate_nodes:
# get the name of the geometry
name = self.graph[group[0]][1]
# collect our unique collection of geometry
geometry[name] = self.geometry[name]

return Scene(geometry)

def set_camera(self,
angles=None,
distance=None,
Expand Down Expand Up @@ -735,7 +755,8 @@ def export(self, file_obj=None, file_type=None, **kwargs):
file_obj.write(data)
elif util.is_string(file_obj):
# assume strings are file paths
with open(file_obj, 'wb') as f:
file_path = os.path.expanduser(os.path.abspath(file_obj))
with open(file_path, 'wb') as f:
f.write(data)
else:
# no writeable file object so return data
Expand Down
6 changes: 5 additions & 1 deletion trimesh/viewer/windowed.py
Expand Up @@ -482,13 +482,17 @@ def update_flags(self):
from ..path.creation import grid
bounds = self.scene.bounds
center = bounds.mean(axis=0)
center[2] = bounds[0][2]
# 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)
# choose the side length by maximum XY length
side = bounds.ptp(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
args = rendering.convert_to_vertexlist(grid_mesh)
# create ordered args for a vertex list
self._grid = self.batch.add_indexed(*args)
Expand Down

0 comments on commit ead8e5a

Please sign in to comment.