sra-trajectory-code / MID /utils /transform.py
po03087's picture
SRA: MID/LED/MoFlow code + RUNNING.md instructions (code only, no data/ckpts)
d4cbafd verified
Raw
History Blame Contribute Delete
9.54 kB
import torch
import numpy as np
import math
import random
import numbers
import random
from itertools import repeat
class Center(object):
r"""Centers node positions around the origin."""
def __init__(self, attr):
self.attr = attr
def __call__(self, data):
for key in self.attr:
data[key] = data[key] - data[key].mean(dim=-2, keepdim=True)
return data
def __repr__(self):
return '{}()'.format(self.__class__.__name__)
class NormalizeScale(object):
r"""Centers and normalizes node positions to the interval :math:`(-1, 1)`.
"""
def __init__(self, attr):
self.center = Center(attr=attr)
self.attr = attr
def __call__(self, data):
data = self.center(data)
for key in self.attr:
scale = (1 / data[key].abs().max()) * 0.999999
data[key] = data[key] * scale
return data
class FixedPoints(object):
r"""Samples a fixed number of :obj:`num` points and features from a point
cloud.
Args:
num (int): The number of points to sample.
replace (bool, optional): If set to :obj:`False`, samples fixed
points without replacement. In case :obj:`num` is greater than
the number of points, duplicated points are kept to a
minimum. (default: :obj:`True`)
"""
def __init__(self, num, replace=True):
self.num = num
self.replace = replace
# warnings.warn('FixedPoints is not deterministic')
def __call__(self, data):
num_nodes = data['pos'].size(0)
data['dense'] = data['pos']
if self.replace:
choice = np.random.choice(num_nodes, self.num, replace=True)
else:
choice = torch.cat([
torch.randperm(num_nodes)
for _ in range(math.ceil(self.num / num_nodes))
], dim=0)[:self.num]
for key, item in data.items():
if torch.is_tensor(item) and item.size(0) == num_nodes and key != 'dense':
data[key] = item[choice]
return data
def __repr__(self):
return '{}({}, replace={})'.format(self.__class__.__name__, self.num,
self.replace)
class LinearTransformation(object):
r"""Transforms node positions with a square transformation matrix computed
offline.
Args:
matrix (Tensor): tensor with shape :math:`[D, D]` where :math:`D`
corresponds to the dimensionality of node positions.
"""
def __init__(self, matrix, attr):
assert matrix.dim() == 2, (
'Transformation matrix should be two-dimensional.')
assert matrix.size(0) == matrix.size(1), (
'Transformation matrix should be square. Got [{} x {}] rectangular'
'matrix.'.format(*matrix.size()))
self.matrix = matrix
self.attr = attr
def __call__(self, data):
for key in self.attr:
pos = data[key].view(-1, 1) if data[key].dim() == 1 else data[key]
assert pos.size(-1) == self.matrix.size(-2), (
'Node position matrix and transformation matrix have incompatible '
'shape.')
data[key] = torch.matmul(pos, self.matrix.to(pos.dtype).to(pos.device))
return data
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.matrix.tolist())
class RandomRotate(object):
r"""Rotates node positions around a specific axis by a randomly sampled
factor within a given interval.
Args:
degrees (tuple or float): Rotation interval from which the rotation
angle is sampled. If :obj:`degrees` is a number instead of a
tuple, the interval is given by :math:`[-\mathrm{degrees},
\mathrm{degrees}]`.
axis (int, optional): The rotation axis. (default: :obj:`0`)
"""
def __init__(self, degrees, attr, axis=0):
if isinstance(degrees, numbers.Number):
degrees = (-abs(degrees), abs(degrees))
assert isinstance(degrees, (tuple, list)) and len(degrees) == 2
self.degrees = degrees
self.axis = axis
self.attr = attr
def __call__(self, data):
degree = math.pi * random.uniform(*self.degrees) / 180.0
sin, cos = math.sin(degree), math.cos(degree)
if self.axis == 0:
matrix = [[1, 0, 0], [0, cos, sin], [0, -sin, cos]]
elif self.axis == 1:
matrix = [[cos, 0, -sin], [0, 1, 0], [sin, 0, cos]]
else:
matrix = [[cos, sin, 0], [-sin, cos, 0], [0, 0, 1]]
return LinearTransformation(torch.tensor(matrix), attr=self.attr)(data)
def __repr__(self):
return '{}({}, axis={})'.format(self.__class__.__name__, self.degrees,
self.axis)
class AddNoise(object):
def __init__(self, std=0.01, noiseless_item_key='clean'):
self.std = std
self.key = noiseless_item_key
def __call__(self, data):
data[self.key] = data['pos']
data['pos'] = data['pos'] + torch.normal(mean=0, std=self.std, size=data['pos'].size())
return data
class AddRandomNoise(object):
def __init__(self, std_range=[0, 0.10], noiseless_item_key='clean'):
self.std_range = std_range
self.key = noiseless_item_key
def __call__(self, data):
noise_std = random.uniform(*self.std_range)
data[self.key] = data['pos']
data['pos'] = data['pos'] + torch.normal(mean=0, std=noise_std, size=data['pos'].size())
return data
class AddNoiseForEval(object):
def __init__(self, stds=[0.0, 0.01, 0.02, 0.03, 0.05, 0.10, 0.15]):
self.stds = stds
self.keys = ['noisy_%.2f' % s for s in stds]
def __call__(self, data):
data['clean'] = data['pos']
for noise_std in self.stds:
data['noisy_%.2f' % noise_std] = data['pos'] + torch.normal(mean=0, std=noise_std, size=data['pos'].size())
return data
class IdentityTransform(object):
def __call__(self, data):
return data
class RandomScale(object):
r"""Scales node positions by a randomly sampled factor :math:`s` within a
given interval, *e.g.*, resulting in the transformation matrix
.. math::
\begin{bmatrix}
s & 0 & 0 \\
0 & s & 0 \\
0 & 0 & s \\
\end{bmatrix}
for three-dimensional positions.
Args:
scales (tuple): scaling factor interval, e.g. :obj:`(a, b)`, then scale
is randomly sampled from the range
:math:`a \leq \mathrm{scale} \leq b`.
"""
def __init__(self, scales, attr):
assert isinstance(scales, (tuple, list)) and len(scales) == 2
self.scales = scales
self.attr = attr
def __call__(self, data):
scale = random.uniform(*self.scales)
for key in self.attr:
data[key] = data[key] * scale
return data
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.scales)
class RandomTranslate(object):
r"""Translates node positions by randomly sampled translation values
within a given interval. In contrast to other random transformations,
translation is applied separately at each position.
Args:
translate (sequence or float or int): Maximum translation in each
dimension, defining the range
:math:`(-\mathrm{translate}, +\mathrm{translate})` to sample from.
If :obj:`translate` is a number instead of a sequence, the same
range is used for each dimension.
"""
def __init__(self, translate, attr):
self.translate = translate
self.attr = attr
def __call__(self, data):
(n, dim), t = data['pos'].size(), self.translate
if isinstance(t, numbers.Number):
t = list(repeat(t, times=dim))
assert len(t) == dim
ts = []
for d in range(dim):
ts.append(data['pos'].new_empty(n).uniform_(-abs(t[d]), abs(t[d])))
for key in self.attr:
data[key] = data[key] + torch.stack(ts, dim=-1)
return data
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.translate)
class Rotate(object):
r"""Rotates node positions around a specific axis by a randomly sampled
factor within a given interval.
Args:
degrees (tuple or float): Rotation interval from which the rotation
angle is sampled. If :obj:`degrees` is a number instead of a
tuple, the interval is given by :math:`[-\mathrm{degrees},
\mathrm{degrees}]`.
axis (int, optional): The rotation axis. (default: :obj:`0`)
"""
def __init__(self, degree, attr, axis=0):
self.degree = degree
self.axis = axis
self.attr = attr
def __call__(self, data):
degree = math.pi * self.degree / 180.0
sin, cos = math.sin(degree), math.cos(degree)
if self.axis == 0:
matrix = [[1, 0, 0], [0, cos, sin], [0, -sin, cos]]
elif self.axis == 1:
matrix = [[cos, 0, -sin], [0, 1, 0], [sin, 0, cos]]
else:
matrix = [[cos, sin, 0], [-sin, cos, 0], [0, 0, 1]]
return LinearTransformation(torch.tensor(matrix), attr=self.attr)(data)
def __repr__(self):
return '{}({}, axis={})'.format(self.__class__.__name__, self.degrees,
self.axis)