Module sverchok.utils.alpha_shape

Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#  
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE

import numpy as np
from collections import defaultdict

from sverchok.utils.sv_mesh_utils import mask_vertices
from sverchok.utils.sv_bmesh_utils import recalc_normals
from sverchok.dependencies import scipy

if scipy is not None:
    from scipy.spatial import Delaunay

def alpha_shape(verts, alpha, fix_normals=True, volume_threshold = 1e-4):
    """
    Compute the alpha shape (concave hull) of a set of 3D points.
    Parameters:
        verts - np.array of shape (n, 3) points.
        alpha - alpha value.
    return
        outer surface edge indices and triangle indices
    """

    def calc_volume(tetra_verts):
        a = tetra_verts[:,0,:]
        b = tetra_verts[:,1,:]
        c = tetra_verts[:,2,:]
        d = tetra_verts[:,3,:]

        e1 = b - a
        e2 = c - a
        e3 = d - a

        e1 /= np.linalg.norm(e1, axis=1, keepdims=True)
        e2 /= np.linalg.norm(e2, axis=1, keepdims=True)
        e3 /= np.linalg.norm(e3, axis=1, keepdims=True)

        volume = np.cross(e1, e2)
        volume = (volume * e3).sum(axis=1) / 6
        return abs(volume)

    alpha2 = alpha**2
    tetra = Delaunay(verts)
    # Find radius of the circumsphere.
    # By definition, radius of the sphere fitting inside the tetrahedral needs 
    # to be smaller than alpha value
    # http://mathworld.wolfram.com/Circumsphere.html
    tetra_verts = np.take(verts, tetra.simplices, axis=0) # (n_simplices, 4, 3)
    normsq = np.sum(tetra_verts**2, axis=2)[:, :, None]
    ones = np.ones((tetra_verts.shape[0], tetra_verts.shape[1], 1))
    a = np.linalg.det(np.concatenate((tetra_verts, ones), axis=2))
    Dx = np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [1, 2]], ones), axis=2))
    Dy = -np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [0, 2]], ones), axis=2))
    Dz = np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [0, 1]], ones), axis=2))
    c = np.linalg.det(np.concatenate((normsq, tetra_verts), axis=2))
    r2 = (Dx**2 + Dy**2 + Dz**2 - 4*a*c)/(4*np.abs(a)**2)

    small_radius = r2 < alpha2
    volumes = calc_volume(tetra_verts)
    non_planar = volumes >= volume_threshold
    good = np.logical_and(small_radius, non_planar)

    # Find tetrahedrals
    tetras = tetra.simplices[good, :]
    # triangles
    triangle_combinations = np.array([(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)])
    triangles = tetras[:, triangle_combinations].reshape(-1, 3)
    triangles = np.sort(triangles, axis=1)
    # Remove triangles that occurs twice, because they are within shapes
    triangles_dict = defaultdict(int)
    for tri in triangles:
        triangles_dict[tuple(tri)] += 1
    triangles = np.array([tri for tri in triangles_dict if triangles_dict[tri] ==1])
    #edges
    edges_comb = np.array([(0, 1), (0, 2), (1, 2)])
    edges = triangles[:, edges_comb].reshape(-1, 2)
    edges = np.sort(edges, axis=1)
    edges = np.unique(edges, axis=0)

    edges = edges.tolist()
    triangles = triangles.tolist()

    used_vert_idxs = set()
    for triangle in triangles:
        used_vert_idxs.update(set(triangle))
    verts_mask = [i in used_vert_idxs for i in range(len(verts))]

    verts, edges, faces = mask_vertices(verts, edges, triangles, verts_mask)
    if fix_normals:
        verts, edges, faces = recalc_normals(verts, edges, faces)

    return verts, edges, faces

Functions

def alpha_shape(verts, alpha, fix_normals=True, volume_threshold=0.0001)

Compute the alpha shape (concave hull) of a set of 3D points.

Parameters

verts - np.array of shape (n, 3) points. alpha - alpha value. return outer surface edge indices and triangle indices

Expand source code
def alpha_shape(verts, alpha, fix_normals=True, volume_threshold = 1e-4):
    """
    Compute the alpha shape (concave hull) of a set of 3D points.
    Parameters:
        verts - np.array of shape (n, 3) points.
        alpha - alpha value.
    return
        outer surface edge indices and triangle indices
    """

    def calc_volume(tetra_verts):
        a = tetra_verts[:,0,:]
        b = tetra_verts[:,1,:]
        c = tetra_verts[:,2,:]
        d = tetra_verts[:,3,:]

        e1 = b - a
        e2 = c - a
        e3 = d - a

        e1 /= np.linalg.norm(e1, axis=1, keepdims=True)
        e2 /= np.linalg.norm(e2, axis=1, keepdims=True)
        e3 /= np.linalg.norm(e3, axis=1, keepdims=True)

        volume = np.cross(e1, e2)
        volume = (volume * e3).sum(axis=1) / 6
        return abs(volume)

    alpha2 = alpha**2
    tetra = Delaunay(verts)
    # Find radius of the circumsphere.
    # By definition, radius of the sphere fitting inside the tetrahedral needs 
    # to be smaller than alpha value
    # http://mathworld.wolfram.com/Circumsphere.html
    tetra_verts = np.take(verts, tetra.simplices, axis=0) # (n_simplices, 4, 3)
    normsq = np.sum(tetra_verts**2, axis=2)[:, :, None]
    ones = np.ones((tetra_verts.shape[0], tetra_verts.shape[1], 1))
    a = np.linalg.det(np.concatenate((tetra_verts, ones), axis=2))
    Dx = np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [1, 2]], ones), axis=2))
    Dy = -np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [0, 2]], ones), axis=2))
    Dz = np.linalg.det(np.concatenate((normsq, tetra_verts[:, :, [0, 1]], ones), axis=2))
    c = np.linalg.det(np.concatenate((normsq, tetra_verts), axis=2))
    r2 = (Dx**2 + Dy**2 + Dz**2 - 4*a*c)/(4*np.abs(a)**2)

    small_radius = r2 < alpha2
    volumes = calc_volume(tetra_verts)
    non_planar = volumes >= volume_threshold
    good = np.logical_and(small_radius, non_planar)

    # Find tetrahedrals
    tetras = tetra.simplices[good, :]
    # triangles
    triangle_combinations = np.array([(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)])
    triangles = tetras[:, triangle_combinations].reshape(-1, 3)
    triangles = np.sort(triangles, axis=1)
    # Remove triangles that occurs twice, because they are within shapes
    triangles_dict = defaultdict(int)
    for tri in triangles:
        triangles_dict[tuple(tri)] += 1
    triangles = np.array([tri for tri in triangles_dict if triangles_dict[tri] ==1])
    #edges
    edges_comb = np.array([(0, 1), (0, 2), (1, 2)])
    edges = triangles[:, edges_comb].reshape(-1, 2)
    edges = np.sort(edges, axis=1)
    edges = np.unique(edges, axis=0)

    edges = edges.tolist()
    triangles = triangles.tolist()

    used_vert_idxs = set()
    for triangle in triangles:
        used_vert_idxs.update(set(triangle))
    verts_mask = [i in used_vert_idxs for i in range(len(verts))]

    verts, edges, faces = mask_vertices(verts, edges, triangles, verts_mask)
    if fix_normals:
        verts, edges, faces = recalc_normals(verts, edges, faces)

    return verts, edges, faces