Coverage for trimesh/comparison.py: 98%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2comparison.py
3----------------
5Provide methods for quickly hashing and comparing meshes.
6"""
8from hashlib import sha256
10import numpy as np
12from . import util
13from .constants import tol
15# how many significant figures to use for each
16# field of the identifier based on hand-tuning
17id_sigfig = np.array(
18 [
19 5, # area
20 10, # euler number
21 5, # area/volume ratio
22 2, # convex/mesh area ratio
23 2, # convex area/volume ratio
24 3, # max radius squared / area
25 1,
26 ]
27) # sign of triangle count for mirrored
30def identifier_simple(mesh):
31 """
32 Return a basic identifier for a mesh consisting of
33 properties that have been hand tuned to be somewhat
34 robust to rigid transformations and different
35 tessellations.
37 Parameters
38 ------------
39 mesh : trimesh.Trimesh
40 Source geometry
42 Returns
43 ----------
44 identifier : (7,) float
45 Identifying values of the mesh
46 """
47 # verify the cache once
48 mesh._cache.verify()
50 # don't check hashes during identifier as we aren't
51 # changing any data values of the mesh inside block
52 # if we did change values in cache block things would break
53 with mesh._cache:
54 # pre-allocate identifier so indexes of values can't move around
55 # like they might if we used hstack or something else
56 identifier = np.zeros(7, dtype=np.float64)
57 # avoid thrashing the cache unnecessarily
58 mesh_area = mesh.area
59 # start with properties that are valid regardless of watertightness
60 # note that we're going to try to make all parameters relative
61 # to area so other values don't get blown up at weird scales
62 identifier[0] = mesh_area
63 # avoid divide-by-zero later
64 if mesh_area < tol.merge:
65 mesh_area = 1.0
66 # topological constant and the only thing we can really
67 # trust in this fallen world
68 identifier[1] = mesh.euler_number
70 # if we have a watertight mesh include volume and inertia
71 if mesh.is_volume:
72 # side length of a cube ratio
73 # 1.0 for cubes, different values for other things
74 identifier[2] = ((mesh_area / 6.0) ** (1.0 / 2.0)) / (
75 mesh.volume ** (1.0 / 3.0)
76 )
77 else:
78 # if we don't have a watertight mesh add information about the
79 # convex hull which is slow to compute and unreliable
80 try:
81 # get the hull area and volume
82 hull = mesh.convex_hull
83 hull_area = hull.area
84 hull_volume = hull.volume
85 except BaseException:
86 # in-plane or single point geometry has no hull
87 hull_area = 6.0
88 hull_volume = 1.0
89 # just what we're looking for in a hash but hey
90 identifier[3] = mesh_area / hull_area
91 # cube side length ratio for the hull
92 if hull_volume > 1e-12:
93 identifier[4] = ((hull_area / 6.0) ** (1.0 / 2.0)) / (
94 hull_volume ** (1.0 / 3.0)
95 )
96 # calculate maximum mesh radius
97 vertices = mesh.vertices - mesh.centroid
98 # add in max radius^2 to area ratio
99 R2 = np.dot((vertices**2), [1, 1, 1]).max()
100 identifier[5] = R2 / mesh_area
102 # mirrored meshes will look identical in terms of
103 # area, volume, etc: use a count of relative edge
104 # lengths to differentiate identical but mirrored meshes
105 # this doesn't work well on meshes with a small number of faces
106 if len(mesh.faces) > 50:
107 # does this mesh have edges that differ substantially in length
108 # if not this method for detecting reflection will not work
109 # and the result will definitely be garbage
110 edges_length = mesh.edges_unique_length
111 variance = edges_length.std() / edges_length.mean()
112 if variance > 0.25:
113 # the length of each edge in faces
114 norms = edges_length[mesh.edges_unique_inverse].reshape((-1, 3))
115 # stack edge length and get the relative difference
116 stack = np.diff(np.column_stack((norms, norms[:, 0])), axis=1)
117 pick_idx = np.abs(stack).argmin(axis=1)
118 # get the edge length diff
119 pick = stack.reshape(-1)[pick_idx + (np.arange(len(pick_idx)) * 3)]
120 # reduce to the bare minimum that tests stable
121 identifier[6] = np.sign(pick.sum())
122 return identifier
125def identifier_hash(identifier):
126 """
127 Hash an identifier array in a way that is hand-tuned to be
128 somewhat robust to likely changes.
130 Parameters
131 ------------
132 identifier : (n,) float
133 Vector of properties
135 Returns
136 ----------
137 hash : (64,) str
138 A SHA256 of the identifier vector at hand-tuned precision.
139 """
141 # convert identifier to integers and order of magnitude
142 as_int, multiplier = util.sigfig_int(identifier, id_sigfig)
144 # make all scales positive
145 if (multiplier < 0).any():
146 multiplier += np.abs(multiplier.min())
147 data = (as_int * (10**multiplier)).astype(np.int64)
148 return sha256(data.tobytes()).hexdigest()