196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
|
"""
|
||
|
comparison.py
|
||
|
----------------
|
||
|
|
||
|
Provide methods for quickly hashing and comparing meshes.
|
||
|
"""
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
from . import util
|
||
|
|
||
|
from .constants import tol
|
||
|
|
||
|
# how many significant figures to use for each field of the identifier
|
||
|
id_sigfig = np.array([5, # area
|
||
|
10, # euler number
|
||
|
5, # area/volume ratio
|
||
|
2, # convex/mesh area ratio
|
||
|
2, # convex area/volume ratio
|
||
|
3]) # max radius squared / area
|
||
|
|
||
|
|
||
|
def identifier_simple(mesh):
|
||
|
"""
|
||
|
Return a basic identifier for a mesh, consisting of properties
|
||
|
that have been hand tuned to be somewhat robust to rigid
|
||
|
transformations and different tesselations.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
mesh : trimesh.Trimesh
|
||
|
Source geometry
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
identifier : (6,) float
|
||
|
Identifying values of the mesh
|
||
|
"""
|
||
|
# verify the cache once
|
||
|
mesh._cache.verify()
|
||
|
|
||
|
# don't check hashes during identifier as we aren't
|
||
|
# changing any data values of the mesh inside block
|
||
|
# if we did change values in cache block things would break
|
||
|
with mesh._cache:
|
||
|
# pre-allocate identifier so indexes of values can't move around
|
||
|
# like they might if we used hstack or something else
|
||
|
identifier = np.zeros(6, dtype=np.float64)
|
||
|
# avoid thrashing the cache unnecessarily
|
||
|
mesh_area = mesh.area
|
||
|
# start with properties that are valid regardless of watertightness
|
||
|
# 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
|
||
|
|
||
|
# if we have a watertight mesh include volume and inertia
|
||
|
if mesh.is_volume:
|
||
|
# side length of a cube ratio
|
||
|
# 1.0 for cubes, different values for other things
|
||
|
identifier[2] = (((mesh_area / 6.0) ** (1.0 / 2.0)) /
|
||
|
(mesh.volume ** (1.0 / 3.0)))
|
||
|
# save vertices for radius calculation
|
||
|
vertices = mesh.vertices - mesh.center_mass
|
||
|
# we are going to special case radially symmetric meshes
|
||
|
# to replace their surface area with ratio of their
|
||
|
# surface area to a primitive sphere or cylinder surface area
|
||
|
# this is because tessellated curved surfaces are really rough
|
||
|
# to reliably hash as they are very sensitive to floating point
|
||
|
# and tessellation error. By making area proportionate to a fit
|
||
|
# primitive area we are able to reliably hash at more sigfigs
|
||
|
if mesh.symmetry == 'radial':
|
||
|
# cylinder height
|
||
|
h = np.dot(vertices, mesh.symmetry_axis).ptp()
|
||
|
# 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 = 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
|
||
|
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 / hull_area
|
||
|
# cube side length ratio for the hull
|
||
|
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 = np.dot((vertices ** 2), [1, 1, 1]).max()
|
||
|
identifier[5] = R2 / mesh_area
|
||
|
|
||
|
return identifier
|
||
|
|
||
|
|
||
|
def identifier_hash(identifier, sigfig=None):
|
||
|
"""
|
||
|
Hash an identifier array to a specified number of
|
||
|
significant figures.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
identifier : (n,) float
|
||
|
Vector of properties
|
||
|
sigfig : (n,) int
|
||
|
Number of sigfigs per property
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
md5 : str
|
||
|
MD5 hash of identifier
|
||
|
"""
|
||
|
if sigfig is None:
|
||
|
sigfig = id_sigfig
|
||
|
|
||
|
# convert identifier to integers and order of magnitude
|
||
|
as_int, multiplier = util.sigfig_int(identifier, sigfig)
|
||
|
# make all scales positive
|
||
|
if (multiplier < 0).any():
|
||
|
multiplier += np.abs(multiplier.min())
|
||
|
hashable = (as_int * (10 ** multiplier)).astype(np.int64)
|
||
|
md5 = util.md5_object(hashable)
|
||
|
return md5
|
||
|
|
||
|
|
||
|
def face_ordering(mesh):
|
||
|
"""
|
||
|
Return the size-order of every face in the input mesh.
|
||
|
|
||
|
Triangles can be considered by the length order:
|
||
|
[small edge, medium edge, large edge] (SML)
|
||
|
[small edge, large edge, medium edge] (SLM)
|
||
|
|
||
|
This function returns [-1, 0, 1], depending on whether
|
||
|
the triangle is SML or SLM, and 0 if M == L.
|
||
|
|
||
|
The reason this is useful as it as a rare property that is
|
||
|
invariant to translation and rotation but changes when a
|
||
|
mesh is reflected or inverted. It is NOT invariant to
|
||
|
different tesselations of the same surface.
|
||
|
|
||
|
Parameters
|
||
|
-------------
|
||
|
mesh : trimesh.Trimesh
|
||
|
Source geometry to calculate ordering on
|
||
|
|
||
|
Returns
|
||
|
--------------
|
||
|
order : (len(mesh.faces), ) int
|
||
|
Is each face SML (-1), SLM (+1), or M==L (0)
|
||
|
"""
|
||
|
|
||
|
# the length of each edge in faces
|
||
|
norms = mesh.edges_unique_length[
|
||
|
mesh.edges_unique_inverse].reshape((-1, 3))
|
||
|
|
||
|
# the per- row index of the shortest edge
|
||
|
small = norms.argmin(axis=1)
|
||
|
|
||
|
# the ordered index for the medium and large edge norm
|
||
|
# arranged to reference flattened norms for indexing
|
||
|
MLidx = np.column_stack((small + 1, small + 2)) % 3
|
||
|
MLidx += (np.arange(len(small)) * 3).reshape((-1, 1))
|
||
|
|
||
|
# subtract the two largest edge lengths from each other
|
||
|
diff = np.subtract(*norms.reshape(-1)[MLidx.T])
|
||
|
|
||
|
# mark by sign but keep zero values zero
|
||
|
order = np.zeros(len(norms), dtype=np.int64)
|
||
|
order[diff < tol.merge] = -1
|
||
|
order[diff > tol.merge] = 1
|
||
|
|
||
|
return order
|