Helper Module for Deep Learning.
Source code for pynet.models.spherical.sampling
# -*- coding: utf-8 -*-
##########################################################################
# NSAp - Copyright (C) CEA, 2021
# Distributed under the terms of the CeCILL-B license, as published by
# the CEA-CNRS-INRIA. Refer to the LICENSE file or to
# http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
# for details.
##########################################################################
"""
Module that provides spherical sampling & associated utilities.
"""
# Imports
import collections
import numpy as np
from math import sqrt, degrees
from sklearn.neighbors import BallTree
import networkx as nx
[docs]def interpolate(vertices, target_vertices, target_triangles):
""" Interpolate missing data.
Parameters
----------
vertices: array (n_samples, n_dim)
points of data set.
target_vertices: array (n_query, n_dim)
points to find interpolated texture for.
target_triangles: array (n_query, 3)
the mesh geometry definition.
Returns
-------
interp_textures: array (n_query, n_feats)
the interplatedd textures.
"""
interp_textures = collections.OrderedDict()
graph = vertex_adjacency_graph(target_vertices, target_triangles)
common_vertices = downsample(target_vertices, vertices)
missing_vertices = set(range(len(target_vertices))) - set(common_vertices)
for node in sorted(graph.nodes):
if node in common_vertices:
interp_textures[node] = [node] * 2
else:
node_neighs = [idx for idx in graph.neighbors(node)
if idx in common_vertices]
node_weights = np.linalg.norm(
target_vertices[node_neighs] - target_vertices[node], axis=1)
interp_textures[node] = node_neighs
return interp_textures
[docs]def neighbors(vertices, triangles, depth=1, direct_neighbor=False):
""" Build mesh vertices neighbors.
Parameters
----------
vertices: array (N, 3)
the icosahedron vertices.
triangles: array (N, 3)
the icosahedron triangles.
depth: int, default 1
depth to stop the neighbors search, only paths of length <= depth are
returned.
direct_neighbor: bool, default False
each spherical surface is composed of two types of vertices: 1) 12
vertices with each having only 5 direct neighbors; and 2) the
remaining vertices with each having 6 direct neighbors. For those
vertices with 6 neighbors, DiNe assigns the index 1 to the center
vertex and the indices 2–7 to its neighbors sequentially according
to the angle between the vector of center vertex to neighboring vertex
and the x-axis in the tangent plane. For the 12 vertices with only
5 neighbors, DiNe assigns the indices both 1 and 2 to the center
vertex, and indices 3–7 to the neighbors in the same way as those
vertices with 6 neighbors.
Returns
--------
neighs: dict
a dictionary with vertices row index as keys and a dictionary of
neighbors vertices row indexes organized by rungs as values.
"""
graph = vertex_adjacency_graph(vertices, triangles)
neighs = collections.OrderedDict()
for node in sorted(graph.nodes):
node_neighs = {}
# node_neighs = [idx for idx in graph.neighbors(node)]
for neigh, ring in nx.single_source_shortest_path_length(
graph, node, cutoff=depth).items():
if ring == 0:
continue
node_neighs.setdefault(ring, []).append(neigh)
if direct_neighbor:
_node_neighs = []
if depth == 1:
delta = np.pi / 4
elif depth == 2:
delta = np.pi / 8
else:
raise ValueError("Direct neighbors implemented only for "
"depth <= 2.")
for ring, ring_neighs in node_neighs.items():
angles = np.asarray([
get_angle_with_xaxis(vertices[node], vertices[node], vec)
for vec in vertices[ring_neighs]])
angles += delta
angles = np.degrees(np.mod(angles, 2 * np.pi))
ring_neighs = [x for _, x in sorted(
zip(angles, ring_neighs), key=lambda pair: pair[0])]
if depth == 1 and ring == 1:
if len(ring_neighs) == 5:
ring_neighs.append(node)
elif len(ring_neighs) != 6:
raise ValueError("Mesh is not an icosahedron.")
if depth == 2 and ring == 2:
ring_neighs = ring_neighs[1::2]
if len(_node_neighs) + len(ring_neighs) == 10:
ring_neighs.extend([node] * 2)
elif len(_node_neighs) + len(ring_neighs) == 11:
ring_neighs.append(node)
elif len(_node_neighs) + len(ring_neighs) != 12:
raise ValueError("Mesh is not an icosahedron.")
_node_neighs.extend(ring_neighs)
_node_neighs.append(node)
node_neighs = _node_neighs
neighs[node] = node_neighs
return neighs
[docs]def vertex_adjacency_graph(vertices, triangles):
""" Build a networkx graph representation of the vertices and
their connections in the mesh.
Parameters
----------
vertices: array (N, 3)
the icosahedron vertices.
triangles: array (N, 3)
the icosahedron triangles.
Returns
-------
graph: networkx.Graph
Graph representing vertices and edges between
them where vertices are nodes and edges are edges
Examples
----------
This is useful for getting nearby vertices for a given vertex,
potentially for some simple smoothing techniques.
>>> graph = mesh.vertex_adjacency_graph
>>> graph.neighbors(0)
> [1, 3, 4]
"""
graph = nx.Graph()
graph.add_nodes_from(range(len(vertices)))
edges, edges_triangle = triangles_to_edges(triangles)
edges_cache = []
for idx1, idx2 in edges:
smaller_index = min(idx1, idx2)
greater_index = max(idx1, idx2)
key = "{0}-{1}".format(smaller_index, greater_index)
if key in edges_cache:
continue
edges_cache.append(key)
graph.add_edge(smaller_index, greater_index)
return graph
[docs]def get_angle_with_xaxis(center, normal, point):
""" Project a point to the sphere tangent plane and compute the angle
with the x-axis.
Parameters
----------
center: array (3, )
a point in the plane.
normal: array (3, )
the normal to the plane.
points: array (3, )
the points to be projected.
"""
# Assert is array
center = np.asarray(center)
normal = np.asarray(normal)
point = np.asarray(point)
# Project points to plane
vector = point - center
dist = np.dot(vector, normal)
projection = point - normal * dist
# Compute normal of the new projected x-axis and y-axis
if center[0] != 0 or center[1] != 0:
nx = np.cross(np.array([0, 0, 1]), center)
ny = np.cross(center, nx)
else:
nx = np.array([1, 0, 0])
ny = np.array([0, 1, 0])
# Compute the angle between projected points and the x-axis
vector = projection - center
unit_vector = vector
if np.linalg.norm(vector) != 0:
unit_vector = unit_vector / np.linalg.norm(vector)
unit_nx = nx / np.linalg.norm(nx)
cos_theta = np.dot(unit_vector, unit_nx)
if cos_theta > 1.:
cos_theta = 1.
elif cos_theta < -1.:
cos_theta = -1.
angle = np.arccos(cos_theta)
if np.dot(unit_vector, ny) < 0:
angle = 2 * np.pi - angle
return angle
[docs]def triangles_to_edges(triangles, return_index=False):
""" Given a list of triangles, return a list of edges.
Parameters
----------
triangles: array int (N, 3)
Vertex indices representing triangles.
Returns
-------
edges: array int (N * 3, 2)
Vertex indices representing edges.
triangles_index: array (N * 3, )
Triangle indexes.
"""
# Each triangles has three edges
edges = triangles[:, [0, 1, 1, 2, 2, 0]].reshape((-1, 2))
# Edges are in order of triangles due to reshape
triangles_index = np.tile(
np.arange(len(triangles)), (3, 1)).T.reshape(-1)
return edges, triangles_index
[docs]def downsample(vertices, target_vertices):
""" Downsample by finding nearest neighbors.
Parameters
----------
vertices: array (n_samples, n_dim)
points of data set.
target_vertices: array (n_query, n_dim)
points to find nearest neighbors for.
Returns
-------
nearest_idx: array (n_query, )
index of nearest neighbor in target_vertices for every point in
vertices.
"""
if vertices.size == 0 or target_vertices.size == 0:
return np.array([], int), np.array([])
tree = BallTree(vertices, leaf_size=2)
distances, nearest_idx = tree.query(
target_vertices, return_distance=True, k=1)
n_duplicates = len(nearest_idx) - len(np.unique(nearest_idx))
if n_duplicates:
raise RuntimeError("Could not downsample proprely, '{0}' duplicates "
"were found. Are you using an icosahedron "
"mesh?".format(n_duplicates))
return nearest_idx.squeeze()
[docs]def neighbors_rec(vertices, triangles, size=5, zoom=5):
""" Build rectangular grid neighbors and weights.
Parameters
----------
vertices: array (N, 3)
the icosahedron vertices.
triangles: array (N, 3)
the icosahedron triangles.
size: int, default 5
the rectangular grid size.
zoom: int, default 5
scale factor applied on the unit sphere to control the neighborhood
density.
Returns
--------
neighs: array (N, size**2, 3)
grid samples neighbors for each vertex.
weights: array (N, size**2, 3)
grid samples weights with neighbors for each vertex.
grid_in_sphere: array (N, size**2, 3)
zoomed rectangular grid on the sphere vertices.
"""
grid_in_sphere = np.zeros((len(vertices), size**2, 3), dtype=float)
neighs = np.zeros((len(vertices), size**2, 3), dtype=int)
weights = np.zeros((len(vertices), size**2, 3), dtype=float)
for idx1, node in enumerate(vertices):
grid_in_sphere[idx1], _ = get_rectangular_projection(
node, size=size, zoom=zoom)
for idx2, point in enumerate(grid_in_sphere[idx1]):
dist = np.linalg.norm(vertices - point, axis=1)
ordered_neighs = np.argsort(dist)
neighs[idx1, idx2] = ordered_neighs[:3]
weights[idx1, idx2] = dist[neighs[idx1, idx2]]
return neighs, weights, grid_in_sphere
[docs]def get_rectangular_projection(node, size=5, zoom=5):
""" Project rectangular grid in 2D sapce into 3D spherical space.
Parameters
----------
node: array (3, )
a point in the sphere.
size: int, default 5
the rectangular grid size.
zoom: int, default 5
scale factor applied on the unit sphere to control the neighborhood
density.
Returns
-------
grid_in_sphere: array (size**2, 3)
zoomed rectangular grid on the sphere.
grid_in_tplane: array (size**2, 3)
zoomed rectangular grid in the tangent space.
"""
# Check kernel size
if (size % 2) == 0:
raise ValueError("An odd kernel size is expected.")
midsize = size // 2
# Compute normal of the new projected x-axis and y-axis
node = node.copy() * zoom
if node[0] != 0 or node[1] != 0:
nx = np.cross(np.array([0, 0, 1]), node)
ny = np.cross(node, nx)
else:
nx = np.array([1, 0, 0])
ny = np.array([0, 1, 0])
nx = nx / np.linalg.norm(nx)
ny = ny / np.linalg.norm(ny)
# Caculate the grid coordinate in tangent plane and project back on sphere
grid_in_tplane = np.zeros((size ** 2, 3))
grid_in_sphere = np.zeros((size ** 2, 3))
corner = node - midsize * nx + midsize * ny
for row in range(size):
for column in range(size):
point = corner - row * ny + column * nx
grid_in_tplane[row * size + column, :] = point
grid_in_sphere[row * size + column, :] = (
point / np.linalg.norm(point) * zoom)
return grid_in_sphere, grid_in_tplane
[docs]def icosahedron(order=3):
""" Define an icosahedron mesh of any order.
Parameters
----------
order: int, default 3
the icosahedron order.
Returns
-------
vertices: array (N, 3)
the icosahedron vertices.
triangles: array (N, 3)
the icosahedron triangles.
"""
middle_point_cache = {}
r = (1 + np.sqrt(5)) / 2
vertices = [
normalize([-1, r, 0]),
normalize([1, r, 0]),
normalize([-1, -r, 0]),
normalize([1, -r, 0]),
normalize([0, -1, r]),
normalize([0, 1, r]),
normalize([0, -1, -r]),
normalize([0, 1, -r]),
normalize([r, 0, -1]),
normalize([r, 0, 1]),
normalize([-r, 0, -1]),
normalize([-r, 0, 1])]
triangles = [
[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1]]
for idx in range(order):
subdiv = []
for tri in triangles:
v1 = middle_point(tri[0], tri[1], vertices, middle_point_cache)
v2 = middle_point(tri[1], tri[2], vertices, middle_point_cache)
v3 = middle_point(tri[2], tri[0], vertices, middle_point_cache)
subdiv.append([tri[0], v1, v3])
subdiv.append([tri[1], v2, v1])
subdiv.append([tri[2], v3, v2])
subdiv.append([v1, v2, v3])
triangles = subdiv
return np.asarray(vertices), np.asarray(triangles)
[docs]def normalize(vertex):
""" Return vertex coordinates fixed to the unit sphere.
"""
x, y, z = vertex
length = sqrt(x**2 + y**2 + z**2)
return [idx / length for idx in (x, y, z)]
[docs]def middle_point(point_1, point_2, vertices, middle_point_cache):
""" Find a middle point and project to the unit sphere.
"""
# We check if we have already cut this edge first to avoid duplicated verts
smaller_index = min(point_1, point_2)
greater_index = max(point_1, point_2)
key = "{0}-{1}".format(smaller_index, greater_index)
if key in middle_point_cache:
return middle_point_cache[key]
# If it's not in cache, then we can cut it
vert_1 = vertices[point_1]
vert_2 = vertices[point_2]
middle = [sum(elems) / 2. for elems in zip(vert_1, vert_2)]
vertices.append(normalize(middle))
index = len(vertices) - 1
middle_point_cache[key] = index
return index
[docs]def number_of_ico_vertices(order=3):
""" Get the number of vertices of an icosahedron of specific order.
Parameters
----------
order: int, default 3
the icosahedron order.
Returns
-------
vertices: array (N, 3)
the icosahedron vertices.
triangles: array (N, 3)
the icosahedron triangles.
"""
return 10 * 4 ** order + 2
if __name__ == "__main__":
from pprint import pprint
for order in range(5):
print("=" * 5)
print("ico", order)
vertices, triangles = icosahedron(order=order)
print("verts", vertices.shape, "tris", triangles.shape)
print("n_verts", number_of_ico_vertices(order=order))
print("=" * 5)
vertices, triangles = icosahedron(order=1)
print("verts", vertices.shape, "tris", triangles.shape)
neighs = neighbors(vertices, triangles, depth=1, direct_neighbor=True)
print("n_neighs", len(neighs))
print("size_neighs", [len(elem) for elem in neighs.values()])
print("neighs", neighs)
print("=" * 5)
target_vertices, _ = icosahedron(order=0)
down_indexes = downsample(vertices, target_vertices)
print("down 1 -> 0", down_indexes.shape)
print("=" * 5)
interp = interpolate(target_vertices, vertices, triangles)
print("interp 0 -> 1")
pprint(interp)
print("=" * 5)
neighs, weights, grid_in_sphere = neighbors_rec(
vertices, triangles, size=5, zoom=5)
print("rec_neighs", neighs.shape, "rec_weights", weights.shape,
"rec_grid_shpere", grid_in_sphere.shape)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
vertices, triangles = icosahedron(order=2)
neighs = neighbors(vertices, triangles, depth=2, direct_neighbor=True)
colors = ["red", "green", "blue", "orange", "purple", "brown", "pink",
"gray", "olive", "cyan", "yellow", "tan", "salmon", "violet",
"steelblue", "lime", "navy"] * 5
x, y, z = vertices[:, 0], vertices[:, 1], vertices[:, 2]
fig, ax = plt.subplots(1, 1, subplot_kw={
"projection": "3d", "aspect": "equal"}, figsize=(10, 10))
ax.plot_trisurf(x, y, z, triangles=triangles, alpha=0., edgecolor="black",
facecolors="None", linewidth=0.1)
for vidx in (0, 4): # 13, 42, 0, 4
for cnt, idx in enumerate(neighs[vidx]):
point = vertices[idx]
ax.scatter(point[0], point[1], point[2], marker="o", c=colors[cnt],
s=100)
zoom = 5
fig, ax = plt.subplots(1, 1, subplot_kw={
"projection": "3d", "aspect": "equal"}, figsize=(10, 10))
ax.plot_trisurf(x * zoom, y * zoom, z * zoom, triangles=triangles,
alpha=0., edgecolor="black", facecolors="None",
linewidth=0.2)
for idx in (13, 40):
node = vertices[idx]
node_rec_neighs, node_tplane_neighs = get_rectangular_projection(
node, size=3, zoom=zoom)
ax.scatter(node[0] * zoom, node[1] * zoom, node[2] * zoom, marker="^",
c=colors[0], s=100)
for cnt, point in enumerate(node_tplane_neighs):
ax.scatter(point[0], point[1], point[2], marker="o",
c=colors[cnt + 1], s=5)
for cnt, point in enumerate(node_rec_neighs):
ax.scatter(point[0], point[1], point[2], marker="o",
c=colors[cnt + 1], s=20)
plt.show()
Follow us
© 2019, pynet developers .
Inspired by AZMIND template.
Inspired by AZMIND template.