# BEGIN OF LICENSE NOTE
# This file is part of Pyoints.
# Copyright (c) 2018, Sebastian Lamprecht, Trier University,
# lamprecht@uni-trier.de
#
# Pyoints is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyoints is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pyoints. If not, see <https://www.gnu.org/licenses/>.
# END OF LICENSE NOTE
"""Finds roto-translation matrices of multiple point sets.
"""
import numpy as np
from .. import (
assertion,
transformation,
nptools,
)
from ..misc import print_rounded
[docs]def find_rototranslations(coords_dict, pairs_dict, weights=None):
"""Finds the optimal roto-translation matrices between multiple
point sets using pairs of points. The algorithm assumes infinitesimal
rotations between the point sets.
Parameters
----------
coords_dict: dict of array_like(int, shape=(n, k))
Dictionary of point sets with `k` dimensions.
pairs_dict : dict of array_like(int, shape=(m, 2))
Dictionary of point pairs.
weights : optional, dict or list or int.
Tries to keep the original location and orientation by weighting. Each
point set can be weighted by a list of values. The first `k` values
represent the weighting factors for location. The last values
represent the weighting factors for orientation (angles).
The weights can be provided for each point set individially in form
of a dictionary. If not provided, weights are set to zero.
Returns
-------
T_dict : dict of np.ndarray(Number, shape=(k+1, k+1))
Dictionary of desired roto-translation matrices.
Notes
-----
Algorithm idea taken from [1].
References
----------
[1] University of Genoa (2017):
URL http://geomatica.como.polimi.it/corsi/def\_monitoring/roto-translationsb.pdf.
Examples
--------
2D coordinates.
>>> coordsA = [(-1, -2), (-1, 2), (1, 2), (1, -2), (5, 10)]
>>> T = transformation.matrix(
... t=[10, 4],
... r=0.03,
... )
>>> coordsB = transformation.transform(coordsA, T)
>>> coords_dict = {'A': coordsA, 'B': coordsB}
>>> pairs_dict = { 'A': { 'B': [(0, 0), (1, 1), (2, 2)] } }
>>> weights = {'A': [1, 1, 1], 'B': [0, 0, 0]}
>>> res = find_rototranslations(coords_dict, pairs_dict, weights=weights)
>>> print(sorted(res.keys()))
['A', 'B']
>>> tA = res['A'].to_local(coords_dict['A'])
>>> print_rounded(tA)
[[ -1. -2.]
[ -1. 2.]
[ 1. 2.]
[ 1. -2.]
[ 5. 10.]]
>>> tB = res['B'].to_local(coords_dict['B'])
>>> print_rounded(tB)
[[ -1. -2.]
[ -1. 2.]
[ 1. 2.]
[ 1. -2.]
[ 5. 10.]]
3D coordinates.
>>> coordsA = [(-10, -20, 3), (-1, 2, 4), (1, 10, 5), (1, -2, 60)]
>>> TB = transformation.matrix(
... t=[2, 5, 10], r=[-0.01, 0.02, 0.03], order='trs')
>>> coordsB = transformation.transform(coordsA, TB)
>>> TC= transformation.matrix(
... t=[-2, 3, -6], r=[-0.03, 0.01, -0.02], order='trs')
>>> coordsC = transformation.transform(coordsA, TC)
>>> coords_dict = {'A': coordsA, 'B': coordsB, 'C': coordsC}
>>> pairs_dict = {
... 'A': { 'B': [(0, 0), (1, 1), (3, 3)], 'C': [(0, 0), (3, 3)] },
... 'B': { 'A': [(0, 0), (1, 1), (3, 3)], 'C': [(1, 1), (2, 2)] },
... 'C': { 'A': [(0, 0), (1, 1), (3, 3)], 'B': [(1, 1), (2, 2)] },
... }
>>> weights = {'A': [1, 1, 1, 1, 1, 1], 'B': [0, 0, 0, 0, 0, 0]}
>>> res = find_rototranslations(coords_dict, pairs_dict, weights=weights)
>>> print(sorted(res.keys()))
['A', 'B', 'C']
>>> tA = res['A'].to_local(coords_dict['A'])
>>> print_rounded(tA, 1)
[[-10. -20. 3.]
[ -1. 2. 4.]
[ 1. 10. 5.]
[ 1. -2. 60.]]
>>> tB = res['B'].to_local(coords_dict['B'])
>>> print_rounded(tB, 1)
[[-10. -20. 3.]
[ -1. 2. 4.]
[ 1. 10. 5.]
[ 1. -2. 60.]]
>>> tC = res['C'].to_local(coords_dict['C'])
>>> print_rounded(tC, 1)
[[-10. -20. 3.]
[ -1. 2. 4.]
[ 1. 10. 5.]
[ 1. -2. 60.]]
"""
# prepare input
dim, center, ccoords, centers, pairs, w = _prepare_input(
coords_dict,
pairs_dict,
weights
)
# get equations
rA, rB = _build_rototranslation_equations(ccoords, pairs, w)
oA, oB = _build_location_orientation_equations(center, centers, w, len(rA))
if not len(rB) + len(oB) > 0:
raise ValueError("At least one equation is needed")
# solve linear equation system
mA = np.vstack(rA + oA)
mB = np.hstack(rB + oB)
# set rcond for backwards compatibility
rcond = np.finfo(np.float64).eps * max(mA.shape)
M = np.linalg.lstsq(mA, mB, rcond=rcond)[0]
# Extract roto-transformation matrices
T_dict = _extract_transformations(M, centers, center)
return T_dict
def _unknowns(dim):
if dim == 2:
return 3
elif dim == 3:
return 6
else:
raise ValueError("%i dimensions not supported" % dim)
def _equations(coords):
N, dim = coords.shape
cols = _unknowns(dim)
if dim == 2:
Mx = np.zeros((N, cols))
Mx[:, 0] = 1 # t_x
Mx[:, 2] = coords[:, 1] # r
My = np.zeros((N, cols))
My[:, 1] = 1 # t_y
My[:, 2] = -coords[:, 0] # -r
return np.vstack((Mx, My))
elif dim == 3:
Mx = np.zeros((N, cols))
Mx[:, 0] = 1 # t_x
Mx[:, 4] = -coords[:, 2] # -z
Mx[:, 5] = coords[:, 1] # y
My = np.zeros((N, cols))
My[:, 1] = 1 # t_y
My[:, 3] = coords[:, 2] # z
My[:, 5] = -coords[:, 0] # -x
Mz = np.zeros((N, cols))
Mz[:, 2] = 1 # t_z
Mz[:, 3] = -coords[:, 1] # -y
Mz[:, 4] = coords[:, 0] # x
return np.vstack((Mx, My, Mz))
else:
raise ValueError("%i dimensions are not supported yet" % dim)
def _build_rototranslation_equations(ccoords, wpairs, weights):
# build linear equation system mA = mB * M
dim = ccoords[list(ccoords.keys())[0]].shape[1]
unknowns = _unknowns(dim)
k = len(ccoords)
mA = []
mB = []
for iA, keyA in enumerate(ccoords):
if keyA in wpairs:
for iB, keyB in enumerate(ccoords):
if keyB in wpairs[keyA]:
# get pairs of points
p, pw = wpairs[keyA][keyB]
A = ccoords[keyA][p[:, 0], :]
B = ccoords[keyB][p[:, 1], :]
# set equations
equations_A = _equations(-A)
equations_B = _equations(-B)
a = np.zeros((A.shape[0] * dim, k * unknowns))
a[:, iA * unknowns:(iA + 1) * unknowns] = equations_A
a[:, iB * unknowns:(iB + 1) * unknowns] = -equations_B
b = B.T.flatten() - A.T.flatten()
# weighting
w = np.tile(pw, dim)
a = (a.T * w).T
b = b * w
mA.extend(a)
mB.extend(b)
return mA, mB
def _build_location_orientation_equations(center, centers, weights, n):
# try to keep the original locations and orientations
k = len(centers)
dim = len(center)
cols = _unknowns(dim)
mA = []
mB = []
for i, key in enumerate(centers):
if key in weights:
a = np.eye(cols, k * cols, k=i * cols)
b = np.zeros(cols)
w = weights[key] * n # **2# * 1000
a = (a.T * w).T
b = b * w
mA.extend(a)
mB.extend(b)
return mA, mB
def _extract_transformations(M, centers, center):
dim = len(center)
cols = _unknowns(dim)
res = {}
t_dict = {key: M[i * cols: i * cols + dim]
for i, key in enumerate(centers)}
r_dict = {key: M[i * cols + dim: (i + 1) * cols]
for i, key in enumerate(centers)}
TC = transformation.t_matrix(center)
for i, key in enumerate(centers):
R = transformation.t_matrix(t_dict[key])
T = transformation.r_matrix(r_dict[key], order='xyz')
M = R @ T
res[key] = TC @ M @ TC.inv
return res
def _prepare_input(coords_dict, pairs_dict, weights):
# type validation
if not isinstance(coords_dict, dict):
raise TypeError("'coords_dict' of type 'dict' required")
if not isinstance(pairs_dict, dict):
raise TypeError("'pairs_dict' of type 'dict' required")
for key in pairs_dict:
if key not in coords_dict:
m = "key '%s' of 'pairs_dict' not found in 'coords_dict'" % key
raise ValueError(m)
# number of point clouds
k = len(coords_dict)
if k < 2:
raise ValueError("at least 2 point sets required")
# derive centered coordinates
dim = None
centers_dict = {}
ccoords_dict = {}
for key in coords_dict:
if dim is None:
coords = assertion.ensure_coords(coords_dict[key])
dim = coords.shape[1]
else:
coords = assertion.ensure_coords(coords_dict[key], dim=dim)
centers_dict[key] = coords.mean(0)
coords_dict[key] = coords
unknowns = _unknowns(dim)
# common mean centering
center = np.mean(list(centers_dict.values()), axis=0)
ccoords_dict = {key: coords_dict[key] - center for key in coords_dict}
# pairs
wpairs_dict = {}
for keyA in pairs_dict:
wpairs_dict[keyA] = {}
for keyB in pairs_dict[keyA]:
pairs = pairs_dict[keyA][keyB]
if isinstance(pairs, tuple):
pairs, w = pairs
else:
w = np.ones(len(pairs))
pairs = assertion.ensure_numarray(pairs)
if len(pairs) > 0:
if not nptools.isnumeric(pairs, dtypes=[np.int32, np.int64]):
raise ValueError("'pairs' needs to have integer values")
if not (len(pairs.shape) == 2 and pairs.shape[1] == 2):
m = "malformed shape of 'pairs' (got '%s')"
raise ValueError(m % str(pairs.shape))
w = assertion.ensure_numvector(w, length=pairs.shape[0])
wpairs_dict[keyA][keyB] = (pairs, w.astype(float))
# try to keep the original location and orientation
weights_dict = {}
if weights is None:
weights = {}
if isinstance(weights, dict):
for key in coords_dict:
if key in weights:
weights_dict[key] = assertion.ensure_numvector(
weights[key],
length=unknowns
).astype(float)
else:
weights_dict[key] = np.zeros(unknowns)
else:
if assertion.isnumeric(weights):
weights = np.repeat(weights, unknowns)
if nptools.isarray(weights):
weights = assertion.ensure_numvector(weights, length=unknowns)
for key in ccoords_dict.keys():
weights_dict[key] = weights
else:
m = "type '%' of 'weights' not supported" % type(weights)
raise ValueError(m)
return dim, center, ccoords_dict, centers_dict, wpairs_dict, weights_dict