Coverage for trimesh/comparison.py: 98%

45 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2comparison.py 

3---------------- 

4 

5Provide methods for quickly hashing and comparing meshes. 

6""" 

7 

8from hashlib import sha256 

9 

10import numpy as np 

11 

12from . import util 

13from .constants import tol 

14 

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 

28 

29 

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. 

36 

37 Parameters 

38 ------------ 

39 mesh : trimesh.Trimesh 

40 Source geometry 

41 

42 Returns 

43 ---------- 

44 identifier : (7,) float 

45 Identifying values of the mesh 

46 """ 

47 # verify the cache once 

48 mesh._cache.verify() 

49 

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 

69 

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 

101 

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 

123 

124 

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. 

129 

130 Parameters 

131 ------------ 

132 identifier : (n,) float 

133 Vector of properties 

134 

135 Returns 

136 ---------- 

137 hash : (64,) str 

138 A SHA256 of the identifier vector at hand-tuned precision. 

139 """ 

140 

141 # convert identifier to integers and order of magnitude 

142 as_int, multiplier = util.sigfig_int(identifier, id_sigfig) 

143 

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()