def crop_image(img, center, scale, res, base=384): h = base * scale t = Translation( [ res[0] * (-center[0] / h + .5), res[1] * (-center[1] / h + .5) ]) \ .compose_after( Scale( (res[0] / h, res[1] / h) )).pseudoinverse() # Upper left point of original image ul = np.floor(t.apply([0, 0])) # Bottom right point of original image br = np.ceil(t.apply(res).astype(np.int)) # crop and rescale cimg, trans = img.warp_to_shape(br - ul, Translation(-(br - ul) / 2 + (br + ul) / 2), return_transform=True) c_scale = np.min(cimg.shape) / np.mean(res) new_img = cimg.rescale(1 / c_scale).resize(res) return new_img, trans, c_scale
def noisy_alignment_similarity_transform(source, target, noise_type='uniform', noise_percentage=0.1, allow_alignment_rotation=False): r""" Constructs and perturbs the optimal similarity transform between the source and target shapes by adding noise to its parameters. Parameters ---------- source : `menpo.shape.PointCloud` The source pointcloud instance used in the alignment target : `menpo.shape.PointCloud` The target pointcloud instance used in the alignment noise_type : ``{'uniform', 'gaussian'}``, optional The type of noise to be added. noise_percentage : `float` in ``(0, 1)`` or `list` of `len` `3`, optional The standard percentage of noise to be added. If `float`, then the same amount of noise is applied to the scale, rotation and translation parameters of the optimal similarity transform. If `list` of `float` it must have length 3, where the first, second and third elements denote the amount of noise to be applied to the scale, rotation and translation parameters, respectively. allow_alignment_rotation : `bool`, optional If ``False``, then the rotation is not considered when computing the optimal similarity transform between source and target. Returns ------- noisy_alignment_similarity_transform : `menpo.transform.Similarity` The noisy Similarity Transform between source and target. """ if isinstance(noise_percentage, float): noise_percentage = [noise_percentage] * 3 elif len(noise_percentage) == 1: noise_percentage *= 3 similarity = AlignmentSimilarity(source, target, rotation=allow_alignment_rotation) if noise_type is 'gaussian': s = noise_percentage[0] * (0.5 / 3) * np.asscalar(np.random.randn(1)) r = noise_percentage[1] * (180 / 3) * np.asscalar(np.random.randn(1)) t = noise_percentage[2] * (target.range() / 3) * np.random.randn(2) s = scale_about_centre(target, 1 + s) r = rotate_ccw_about_centre(target, r) t = Translation(t, source.n_dims) elif noise_type is 'uniform': s = noise_percentage[0] * 0.5 * (2 * np.asscalar(np.random.randn(1)) - 1) r = noise_percentage[1] * 180 * (2 * np.asscalar(np.random.rand(1)) - 1) t = noise_percentage[2] * target.range() * (2 * np.random.rand(2) - 1) s = scale_about_centre(target, 1. + s) r = rotate_ccw_about_centre(target, r) t = Translation(t, source.n_dims) else: raise ValueError('Unexpected noise type. ' 'Supported values are {gaussian, uniform}') return similarity.compose_after(t.compose_after(s.compose_after(r)))
def _recursive_procrustes(self): r""" Recursively calculates a procrustes alignment. """ from menpo.shape import mean_pointcloud from menpo.transform import Similarity if self.n_iterations > self.max_iterations: return False new_tgt = mean_pointcloud([t.aligned_source.points for t in self.transforms]) # rescale the new_target to be the same size as the original about # it's centre rescale = Similarity.identity(new_tgt.n_dims) s = UniformScale(self.initial_target_scale / new_tgt.norm(), self.n_dims, skip_checks=True) t = Translation(-new_tgt.centre, skip_checks=True) rescale.compose_before_inplace(t) rescale.compose_before_inplace(s) rescale.compose_before_inplace(t.pseudoinverse) rescale.apply_inplace(new_tgt) # check to see if we have converged yet delta_target = np.linalg.norm(self.target.points - new_tgt.points) if delta_target < 1e-6: return True else: self.n_iterations += 1 for t in self.transforms: t.set_target(new_tgt) self.target = new_tgt return self._recursive_procrustes()
def _recursive_procrustes(self): r""" Recursively calculates a procrustes alignment. """ from menpo.shape import mean_pointcloud from menpo.transform import Similarity if self.n_iterations > self.max_iterations: return False new_tgt = mean_pointcloud( [t.aligned_source.points for t in self.transforms]) # rescale the new_target to be the same size as the original about # it's centre rescale = Similarity.identity(new_tgt.n_dims) s = UniformScale(self.initial_target_scale / new_tgt.norm(), self.n_dims, skip_checks=True) t = Translation(-new_tgt.centre, skip_checks=True) rescale.compose_before_inplace(t) rescale.compose_before_inplace(s) rescale.compose_before_inplace(t.pseudoinverse) rescale.apply_inplace(new_tgt) # check to see if we have converged yet delta_target = np.linalg.norm(self.target.points - new_tgt.points) if delta_target < 1e-6: return True else: self.n_iterations += 1 for t in self.transforms: t.set_target(new_tgt) self.target = new_tgt return self._recursive_procrustes()
def skew_shape(pointcloud, theta, phi): r""" Method that skews the provided pointcloud. Parameters ---------- pointcloud : `menpo.shape.PointCloud` The shape to distort. theta : `float` The skew angle over x axis (tan(theta)). phi : `float` The skew angle over y axis (tan(phi)). Returns ------- skewed_shape : `menpo.shape.PointCloud` The skewed (distorted) pointcloud. """ rotate_ccw = Similarity.init_identity(pointcloud.n_dims) # Create skew matrix h_matrix = np.ones((3, 3)) h_matrix[0, 1] = np.tan(theta * np.pi / 180.) h_matrix[1, 0] = np.tan(phi * np.pi / 180.) h_matrix[:2, 2] = 0. h_matrix[2, :2] = 0. r = Affine(h_matrix) t = Translation(-pointcloud.centre(), skip_checks=True) # Translate to origin, rotate counter-clockwise, then translate back rotate_ccw.compose_before_inplace(t) rotate_ccw.compose_before_inplace(r) rotate_ccw.compose_before_inplace(t.pseudoinverse()) return rotate_ccw.apply(pointcloud)
def lm_centres_correction(centres): r""" Construct a transform that will correct landmarks for a window iterating feature calculation Parameters ---------- centres : `ndarray` (H, W, 2) The location of the window centres in the features Returns ------- :map:`Affine` An affine transform that performs the correction. Should be applied to the landmarks on the target image. """ t = Translation(-centres.min(axis=0).min(axis=0), skip_checks=True) step_v = centres[0, 0, 0] if centres.shape[0] > 1: step_v = centres[1, 0, 0] - centres[0, 0, 0] step_h = centres[0, 0, 1] if centres.shape[1] > 1: step_h = centres[0, 1, 1] - centres[0, 0, 1] s = NonUniformScale((1. / step_v, 1. / step_h), skip_checks=True) return t.compose_before(s)
def optimal_cylindrical_unwrap(points): r""" Returns a :map:`TransformChain` of [:map:`Translation`, :map:`CylindricalUnwrap`] which optimally cylindrically unwraps the points provided. This is done by: #. Find an optimal :map:`Translation` to centre the points in ``x-z`` plane #. Use :map:`circle_fit` to find the optimal radius for fitting the points #. Calculate a :map:`CylindricalUnwrap` using the optimal radius #. Return a composition of the two. Parameters ---------- points : :map:`PointCloud` The 3D points that will be used to find the optimum unwrapping position Returns ------- transform: :map:`TransformChain` A :map:`TransformChain` which performs the optimal translation and unwrapping. """ # find the optimum centre to unwrap xy = points.points[:, [0, 2]] # just in the x-z plane centre, radius = radial_fit(xy) # convert the 2D circle data into the 3D space translation = np.array([centre[0], 0, centre[1]]) centring_transform = Translation(-translation) unwrap = CylindricalUnwrap(radius) return centring_transform.compose_before(unwrap)
def _recursive_procrustes(self): """ Recursively calculates a Procrustes alignment """ if self.n_iterations > self.max_iterations: return False av_aligned_source = sum(t.aligned_source.points for t in self.transforms) / self.n_sources new_target = PointCloud(av_aligned_source) # rescale the new_target to be the same size as the original about # it's centre rescale = UniformScale(self.initial_target_scale / new_target.norm(), self.n_dims) centre = Translation(-new_target.centre) rescale_about_centre = centre.compose_before(rescale).compose_before( centre.pseudoinverse) rescale_about_centre.apply_inplace(new_target) # check to see if we have converged yet delta_target = np.linalg.norm(self.target.points - new_target.points) if delta_target < 1e-6: return True else: self.n_iterations += 1 for t in self.transforms: t.set_target(new_target) self.target = new_target return self._recursive_procrustes()
def _recursive_procrustes(self): r""" Recursively calculates a procrustes alignment. """ from menpo.shape import PointCloud if self.n_iterations > self.max_iterations: return False av_aligned_source = sum(t.aligned_source.points for t in self.transforms) / self.n_sources new_target = PointCloud(av_aligned_source) # rescale the new_target to be the same size as the original about # it's centre rescale = UniformScale(self.initial_target_scale / new_target.norm(), self.n_dims) centre = Translation(-new_target.centre) rescale_about_centre = centre.compose_before(rescale).compose_before(centre.pseudoinverse) rescale_about_centre.apply_inplace(new_target) # check to see if we have converged yet delta_target = np.linalg.norm(self.target.points - new_target.points) if delta_target < 1e-6: return True else: self.n_iterations += 1 for t in self.transforms: t.set_target(new_target) self.target = new_target return self._recursive_procrustes()
def lm_centres_correction(centres): r""" Construct a transform that will correct landmarks for a window iterating feature calculation Parameters ---------- centres : `ndarray` (H, W, 2) The location of the window centres in the features Returns ------- :map:`Affine` An affine transform that performs the correction. Should be applied to the landmarks on the target image. """ t = Translation(-centres.min(axis=0).min(axis=0), skip_checks=True) step_v = centres[0, 0, 0] if centres.shape[0] > 1: step_v = centres[1, 0, 0] - centres[0, 0, 0] step_h = centres[0, 0, 1] if centres.shape[1] > 1: step_h = centres[0, 1, 1] - centres[0, 0, 1] s = NonUniformScale((1./step_v, 1./step_h), skip_checks=True) return t.compose_before(s)
def test_translation_compose_after_homog(): # can't do this inplace - so should just give transform chain homog = Homogeneous(np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]])) t = Translation([3, 4]) res = t.compose_after(homog) assert(type(res) == Homogeneous)
def test_align_2d_translation_set_h_matrix_raises_notimplemented_error(): t_vec = np.array([1, 2]) translation = Translation(t_vec) source = PointCloud(np.array([[0, 1], [1, 1], [-1, -5], [3, -5]])) target = translation.apply(source) # estimate the transform from source to source.. estimate = AlignmentTranslation(source, source) # and change the target. estimate.set_h_matrix(translation.h_matrix)
def test_align_2d_translation(): t_vec = np.array([1, 2]) translation = Translation(t_vec) source = PointCloud(np.array([[0, 1], [1, 1], [-1, -5], [3, -5]])) target = translation.apply(source) # estimate the transform from source and target estimate = AlignmentTranslation(source, target) # check the estimates is correct assert_allclose(translation.h_matrix, estimate.h_matrix)
def crop(self, min_indices, max_indices, constrain_to_boundary=True): r""" Crops this image using the given minimum and maximum indices. Landmarks are correctly adjusted so they maintain their position relative to the newly cropped image. Parameters ----------- min_indices: (n_dims, ) ndarray The minimum index over each dimension max_indices: (n_dims, ) ndarray The maximum index over each dimension constrain_to_boundary: boolean, optional If True the crop will be snapped to not go beyond this images boundary. If False, an ImageBoundaryError will be raised if an attempt is made to go beyond the edge of the image. Default: True Returns ------- cropped_image : :class:`type(self)` This image, but cropped. Raises ------ ValueError min_indices and max_indices both have to be of length n_dims. All max_indices must be greater than min_indices. ImageBoundaryError Raised if constrain_to_boundary is False, and an attempt is made to crop the image in a way that violates the image bounds. """ min_indices = np.floor(min_indices) max_indices = np.ceil(max_indices) if not (min_indices.size == max_indices.size == self.n_dims): raise ValueError( "Both min and max indices should be 1D numpy arrays of" " length n_dims ({})".format(self.n_dims) ) elif not np.all(max_indices > min_indices): raise ValueError("All max indices must be greater that the min " "indices") min_bounded = self.constrain_points_to_bounds(min_indices) max_bounded = self.constrain_points_to_bounds(max_indices) if not constrain_to_boundary and not (np.all(min_bounded == min_indices) or np.all(max_bounded == max_indices)): # points have been constrained and the user didn't want this - raise ImageBoundaryError(min_indices, max_indices, min_bounded, max_bounded) slices = [slice(int(min_i), int(max_i)) for min_i, max_i in zip(list(min_bounded), list(max_bounded))] self.pixels = self.pixels[slices].copy() # update all our landmarks lm_translation = Translation(-min_bounded) lm_translation.apply_inplace(self.landmarks) return self
def _build_reference_frame(landmarks, boundary=3, group='source'): # translate landmarks to the origin minimum = landmarks.bounds(boundary=boundary)[0] landmarks = Translation(-minimum).apply(landmarks) resolution = landmarks.range(boundary=boundary) reference_frame = MaskedImage.blank(resolution) reference_frame.landmarks[group] = landmarks return reference_frame
def test_align_2d_translation_from_vector_inplace(): t_vec = np.array([1, 2]) translation = Translation(t_vec) source = PointCloud(np.array([[0, 1], [1, 1], [-1, -5], [3, -5]])) target = translation.apply(source) # estimate the transform from source to source.. estimate = AlignmentTranslation(source, source) # and update from_vector estimate._from_vector_inplace(t_vec) # check the estimates is correct assert_allclose(target.points, estimate.target.points)
def init_from_pointcloud(cls, pointcloud, group=None, boundary=0, constrain=True, fill=True): r""" Create an Image that is big enough to contain the given pointcloud. The pointcloud will be translated to the origin and then translated according to its bounds in order to fit inside the new image. An optional boundary can be provided in order to increase the space around the boundary of the pointcloud. The boundary will be added to *all sides of the image* and so a boundary of 5 provides 10 pixels of boundary total for each dimension. By default, the mask will be constrained to the convex hull of the provided pointcloud. Parameters ---------- pointcloud : :map:`PointCloud` Pointcloud to place inside the newly created image. group : `str`, optional If ``None``, the pointcloud will only be used to create the image. If a `str` then the pointcloud will be attached as a landmark group to the image, with the given string as key. boundary : `float` A optional padding distance that is added to the pointcloud bounds. Default is ``0``, meaning the max/min of tightest possible containing image is returned. fill : `int`, optional The value to fill all pixels with. constrain : `bool`, optional If ``True``, the ``True`` values will be image will be constrained to the convex hull of the provided pointcloud. If ``False``, the mask will be the value of ``fill``. Returns ------- image : :map:`MaskedImage` A new image with the same size as the given pointcloud, optionally with the pointcloud attached as landmarks and the mask constrained to the convex hull of the pointcloud. """ # Translate pointcloud to the origin minimum = pointcloud.bounds(boundary=boundary)[0] origin_pc = Translation(-minimum).apply(pointcloud) image_shape = origin_pc.range(boundary=boundary) new_image = cls.init_blank(image_shape, fill=fill) if constrain: new_image = new_image.constrain_to_pointcloud(origin_pc) if group is not None: new_image.landmarks[group] = origin_pc return new_image
def test_align_2d_translation_from_vector_inplace(): t_vec = np.array([1, 2]) translation = Translation(t_vec) source = PointCloud(np.array([[0, 1], [1, 1], [-1, -5], [3, -5]])) target = translation.apply(source) # estimate the transform from source to source.. estimate = AlignmentTranslation(source, source) # and update from_vector estimate.from_vector_inplace(t_vec) # check the estimates is correct assert_allclose(target.points, estimate.target.points)
def mask_pc(pc): t = Translation(-pc.centre()) p = t.apply(pc) (y1, x1), (y2, x2) = p.bounds() a, b = np.meshgrid(np.arange(np.floor(y1), np.ceil(y2)), np.arange(np.floor(x1), np.ceil(x2))) mask = np.vstack([a.flatten(), b.flatten()]).T return PointCloud(t.pseudoinverse().apply(mask[matpath( p.points).contains_points(mask)]).astype(int))
def chain_compose_after_inplace_chain_test(): a = PointCloud(np.random.random([10, 2])) b = PointCloud(np.random.random([10, 2])) t = Translation([3, 4]) s = Scale([4, 2]) chain_1 = TransformChain([t, s]) chain_2 = TransformChain([s.pseudoinverse(), t.pseudoinverse()]) chain_1.compose_before_inplace(chain_2) points = PointCloud(np.random.random([10, 2])) chain_res = chain_1.apply(points) assert(np.allclose(points.points, chain_res.points))
def test_align_2d_translation_from_vector(): t_vec = np.array([1, 2]) translation = Translation(t_vec) source = PointCloud(np.array([[0, 1], [1, 1], [-1, -5], [3, -5]])) target = translation.apply(source) # estimate the transform from source to source.. estimate = AlignmentTranslation(source, source) # and update from_vector new_est = estimate.from_vector(t_vec) # check the original is unchanged assert_allclose(estimate.source.points, source.points) assert_allclose(estimate.target.points, source.points) # check the new estimate has the source and target correct assert_allclose(new_est.source.points, source.points) assert_allclose(new_est.target.points, target.points)
def test_chain_compose_after_inplace_tps(): a = PointCloud(np.random.random([10, 2])) b = PointCloud(np.random.random([10, 2])) tps = ThinPlateSplines(a, b) t = Translation([3, 4]) s = Scale([4, 2]) chain = TransformChain([t, s]) chain.compose_after_inplace(tps) points = PointCloud(np.random.random([10, 2])) manual_res = s.apply(t.apply(tps.apply(points))) chain_res = chain.apply(points) assert (np.all(manual_res.points == chain_res.points))
def chain_compose_before_tps_test(): a = PointCloud(np.random.random([10, 2])) b = PointCloud(np.random.random([10, 2])) tps = ThinPlateSplines(a, b) t = Translation([3, 4]) s = Scale([4, 2]) chain = TransformChain([t, s]) chain_mod = chain.compose_before(tps) points = PointCloud(np.random.random([10, 2])) manual_res = tps.apply(s.apply(t.apply(points))) chain_res = chain_mod.apply(points) assert(np.all(manual_res.points == chain_res.points))
def __init__(self, batch_size=1): super().__init__(batch_size) from menpo.transform import Translation, scale_about_centre import menpo3d.io as m3dio self.name = 'LFPW' self.batch_size = batch_size self.root = Path('/vol/atlas/databases/lfpw/trainset') template = m3dio.import_mesh( '/vol/construct3dmm/regression/src/template.obj') template = Translation(-template.centre()).apply(template) self.template = scale_about_centre(template, 1. / 1000.).apply(template) self.image_extension = '.png' self.lms_extension = '.pts'
def clip_to_image_transform(width, height): r""" Affine transform that converts 3D clip space coordinates into 2D image space coordinates. Note that the z axis of the clip space coordinates is ignored. Parameters ---------- width: int The width of the image height: int The height of the image Returns ------- :map`Homogeneous` A homogeneous transform that moves clip space coordinates into image space. """ # 1. Remove the z axis from the clip space rem_z = dims_3to2() # 2. invert the y direction (up becomes down) invert_y = Scale([1, -1]) # 3. [-1, 1] [-1, 1] -> [0, 2] [0, 2] t = Translation([1, 1]) # 4. [0, 2] [0, 2] -> [0, 1] [0, 1] unit_scale = Scale(0.5, n_dims=2) # 5. [0, 1] [0, 1] -> [0, w - 1] [0, h - 1] im_scale = Scale([width - 1, height - 1]) # 6. [0, w] [0, h] -> [0, h] [0, w] xy_yx = Homogeneous( np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], dtype=np.float)) # reduce the full transform chain to a single affine matrix transforms = [rem_z, invert_y, t, unit_scale, im_scale, xy_yx] return reduce(lambda a, b: a.compose_before(b), transforms)
def retrieve_view_projection_transforms(image, mesh, group=None): rows = image.shape[0] cols = image.shape[1] max_d = max(rows, cols) camera_matrix = np.array([[max_d, 0, cols / 2.0], [0, max_d, rows / 2.0], [0, 0, 1.0]]) distortion_coeffs = np.zeros(4) converged, r_vec, t_vec = cv2.solvePnP( mesh.landmarks[group].lms.points, image.landmarks[group].lms.points[:, ::-1], camera_matrix, distortion_coeffs) rotation_matrix = cv2.Rodrigues(r_vec)[0] h_camera_matrix = np.eye(4) h_camera_matrix[:3, :3] = camera_matrix c = Homogeneous(h_camera_matrix) t = Translation(t_vec.ravel()) r = Rotation(rotation_matrix) view_t_flipped = r.compose_before(t) view_t = view_t_flipped.compose_before(axes_flip_t) proj_t = Homogeneous( weak_projection_matrix(image.width, image.height, view_t_flipped.apply(mesh))) return view_t, proj_t, r
def build_shape_model(shapes, max_components=None): r""" Builds a shape model given a set of shapes. Parameters ---------- shapes: list of :map:`PointCloud` The set of shapes from which to build the model. max_components: None or int or float Specifies the number of components of the trained shape model. If int, it specifies the exact number of components to be retained. If float, it specifies the percentage of variance to be retained. If None, all the available components are kept (100% of variance). Returns ------- shape_model: :class:`menpo.model.pca` The PCA shape model. """ # centralize shapes centered_shapes = [Translation(-s.centre()).apply(s) for s in shapes] # align centralized shape using Procrustes Analysis gpa = GeneralizedProcrustesAnalysis(centered_shapes) aligned_shapes = [s.aligned_source() for s in gpa.transforms] # build shape model shape_model = PCAModel(aligned_shapes) if max_components is not None: # trim shape model if required shape_model.trim_components(max_components) return shape_model
def compose_tps_after_translation_test(): a = PointCloud(np.random.random([10, 2])) b = PointCloud(np.random.random([10, 2])) t = Translation([3, 4]) tps = ThinPlateSplines(a, b) chain = tps.compose_after(t) assert(isinstance(chain, TransformChain))
def test_translation_2d_from_vector(): params = np.array([1, 2]) h**o = np.array([[1, 0, params[0]], [0, 1, params[1]], [0, 0, 1]]) tr = Translation.init_identity(2).from_vector(params) assert_equal(tr.h_matrix, h**o)
def __init__(self, batch_size=1): super().__init__(batch_size) from menpo.transform import Translation, scale_about_centre import menpo3d.io as m3dio self.name = 'FDDB' self.root = Path('/vol/atlas/databases/fddb_ibug') template = m3dio.import_mesh( '/vol/construct3dmm/regression/src/template.obj') template = Translation(-template.centre()).apply(template) self.template = scale_about_centre(template, 1. / 1000.).apply(template) pca_path = '/homes/gt108/Projects/ibugface/pose_settings/pca_params.pkl' self.eigenvectors, self.eigenvalues, self.h_mean, self.h_max = mio.import_pickle( pca_path) self.image_extension = '.jpg' self.lms_extension = '.ljson'
def test_translation_3d_from_vector(): params = np.array([1, 2, 3]) h**o = np.array([[1, 0, 0, params[0]], [0, 1, 0, params[1]], [0, 0, 1, params[2]], [0, 0, 0, 1]]) tr = Translation.identity(3).from_vector(params) assert_almost_equal(tr.h_matrix, h**o)
def rescale(mesh, center, var): """ Rescale mesh according to given variances. Parameters: mesh (menpo.shape.mesh.base.TriMesh): mesh to be rescaled center (numpy.array of 3 floats): center on 3 dimensions var (numpy.array of 3 floats): variance on 3 dimensions Returns: mesh (menpo.shape.mesh.base.TriMesh): rescaled source """ tr = Translation(center) sc = UniformScale(np.mean(var / mesh.range()), 3) prepare = tr.compose_after(sc) return prepare.apply(mesh)
def model_to_clip_transform(points, xy_scale=0.9, z_scale=0.3): r""" Produces an Affine Transform which centres and scales 3D points to fit into the OpenGL clipping space ([-1, 1], [-1, 1], [1, 1-]). This can be used to construct an appropriate projection matrix for use in an orthographic Rasterizer. Note that the z-axis is flipped as is default in OpenGL - as a result this transform converts the right handed coordinate input into a left hand one. Parameters ---------- points: :map:`PointCloud` The points that should be adjusted. xy_scale: `float` 0-1, optional Amount by which the boundary is relaxed so the points are not right against the edge. A value of 1 means the extremities of the point cloud will be mapped onto [-1, 1] [-1, 1] exactly (no boarder) A value of 0.5 means the points will be mapped into the range [-0.5, 0.5]. Default: 0.9 (map to [-0.9, 0.9]) z_scale: float 0-1, optional Scale factor by which the z-dimension is squeezed. A value of 1 means the z-range of the points will be mapped to exactly fit in [1, -1]. A scale of 0.1 means the z-range is compressed to fit in the range [0.1, -0.1]. Returns ------- :map:`Affine` The affine transform that creates this mapping """ # 1. Centre the points on the origin center = Translation(points.centre_of_bounds()).pseudoinverse() # 2. Scale the points to exactly fit the boundaries scale = Scale(points.range() / 2.0) # 3. Apply the relaxations requested - note the flip in the z axis!! # This is because OpenGL by default evaluates depth as bigger number == # further away. Thus not only do we need to get to clip space [-1, 1] in # all dims) but we must invert the z axis so depth buffering is correctly # applied. b_scale = NonUniformScale([xy_scale, xy_scale, -z_scale]) return center.compose_before(scale.pseudoinverse()).compose_before(b_scale)
def align_shapes(shapes): r""" """ # centralize shapes centered_shapes = [Translation(-s.centre()).apply(s) for s in shapes] # align centralized shape using Procrustes Analysis gpa = GeneralizedProcrustesAnalysis(centered_shapes) return [s.aligned_source() for s in gpa.transforms]
def prepare_template_reference_space(template): r"""Return a copy of the template centred at the origin and with max radial distance from centre of 1. This means the template is: 1. fully contained by a bounding sphere of radius 1 at the origin 2. centred at the origin. This isn't necessary, but it's nice to have a meaningful reference space for our models. """ max_radial = np.sqrt( ((template.points - template.centre())**2).sum(axis=1)).max() translation = Translation(-template.centre()) scale = Scale(1 / max_radial, n_dims=3) adjustment = translation.compose_before(scale) adjustment.apply(template) return adjustment.apply(template)
def perspective_camera_for_template(img_shape, focal_length_mult=1.1, pose_angle_deg=0): f = np.array(img_shape).max() * focal_length_mult rot_z = rotation_from_3d_ccw_angle_around_z(180) rot_y = rotation_from_3d_ccw_angle_around_y(180 + pose_angle_deg) rotation = rot_z.compose_before(rot_y) translation = Translation([0, 0, +3]) projection = PerspectiveProjection(f, img_shape) return PerspectiveCamera(rotation, translation, projection)
def test_translation(): t_vec = np.array([1, 2, 3]) starting_vector = np.random.rand(10, 3) transform = Translation(t_vec) transformed = transform.apply(starting_vector) assert_allclose(starting_vector + t_vec, transformed)
def normalize(gt): from menpo.transform import Translation, NonUniformScale t = Translation(gt.centre()).pseudoinverse() s = NonUniformScale(gt.range()).pseudoinverse() return t.compose_before(s)
def test_translation_set_h_matrix_raises_notimplementederror(): t = Translation([3, 4]) t.set_h_matrix(t.h_matrix)
def non_rigid_icp_generator(source, target, eps=1e-3, stiffness_weights=None, data_weights=None, landmark_group=None, landmark_weights=None, v_i_update_func=None, verbose=False): r""" Deforms the source trimesh to align with to optimally the target. """ # If landmarks are provided, we should always start with a simple # AlignmentSimilarity between the landmarks to initialize optimally. if landmark_group is not None: if verbose: print("'{}' landmarks will be used as " "a landmark constraint.".format(landmark_group)) print("performing similarity alignment using landmarks") lm_align = AlignmentSimilarity(source.landmarks[landmark_group], target.landmarks[landmark_group]).as_non_alignment() source = lm_align.apply(source) # Scale factors completely change the behavior of the algorithm - always # rescale the source down to a sensible size (so it fits inside box of # diagonal 1) and is centred on the origin. We'll undo this after the fit # so the user can use whatever scale they prefer. tr = Translation(-1 * source.centre()) sc = UniformScale(1.0 / np.sqrt(np.sum(source.range() ** 2)), 3) prepare = tr.compose_before(sc) source = prepare.apply(source) target = prepare.apply(target) # store how to undo the similarity transform restore = prepare.pseudoinverse() n_dims = source.n_dims # Homogeneous dimension (1 extra for translation effects) h_dims = n_dims + 1 points, trilist = source.points, source.trilist n = points.shape[0] # record number of points edge_tris = source.boundary_tri_index() M_s, unique_edge_pairs = node_arc_incidence_matrix(source) # weight matrix G = np.identity(n_dims + 1) M_kron_G_s = sp.kron(M_s, G) # build octree for finding closest points on target. target_vtk = trimesh_to_vtk(target) closest_points_on_target = VTKClosestPointLocator(target_vtk) # save out the target normals. We need them for the weight matrix. target_tri_normals = target.tri_normals() # init transformation X_prev = np.tile(np.zeros((n_dims, h_dims)), n).T v_i = points if stiffness_weights is not None: if verbose: print('using user-defined stiffness_weights') validate_weights('stiffness_weights', stiffness_weights, source.n_points, verbose=verbose) else: # these values have been empirically found to perform well for well # rigidly aligned facial meshes stiffness_weights = [50, 20, 5, 2, 0.8, 0.5, 0.35, 0.2] if verbose: print('using default ' 'stiffness_weights: {}'.format(stiffness_weights)) n_iterations = len(stiffness_weights) if landmark_weights is not None: if verbose: print('using user defined ' 'landmark_weights: {}'.format(landmark_weights)) elif landmark_group is not None: # these values have been empirically found to perform well for well # rigidly aligned facial meshes landmark_weights = [5, 2, .5, 0, 0, 0, 0, 0] if verbose: print('using default ' 'landmark_weights: {}'.format(landmark_weights)) else: # no landmark_weights provided - no landmark_group in use. We still # need a landmark group for the iterator landmark_weights = [None] * n_iterations # We should definitely have some landmark weights set now - check the # number is correct. # Note we say verbose=False, as we have done custom reporting above, and # per-vertex landmarks are not supported. validate_weights('landmark_weights', landmark_weights, source.n_points, n_iterations=n_iterations, verbose=False) if data_weights is not None: if verbose: print('using user-defined data_weights') validate_weights('data_weights', data_weights, source.n_points, n_iterations=n_iterations, verbose=verbose) else: data_weights = [None] * n_iterations if verbose: print('Not customising data_weights') # we need to prepare some indices for efficient construction of the D # sparse matrix. row = np.hstack((np.repeat(np.arange(n)[:, None], n_dims, axis=1).ravel(), np.arange(n))) x = np.arange(n * h_dims).reshape((n, h_dims)) col = np.hstack((x[:, :n_dims].ravel(), x[:, n_dims])) o = np.ones(n) if landmark_group is not None: source_lm_index = source.distance_to( source.landmarks[landmark_group]).argmin(axis=0) target_lms = target.landmarks[landmark_group] U_L = target_lms.points n_landmarks = target_lms.n_points lm_mask = np.in1d(row, source_lm_index) col_lm = col[lm_mask] # pull out the rows for the lms - but the values are # all wrong! need to map them back to the order of the landmarks row_lm_to_fix = row[lm_mask] source_lm_index_l = list(source_lm_index) row_lm = np.array([source_lm_index_l.index(r) for r in row_lm_to_fix]) for i, (alpha, beta, gamma) in enumerate(zip(stiffness_weights, landmark_weights, data_weights), 1): alpha_is_per_vertex = isinstance(alpha, np.ndarray) if alpha_is_per_vertex: # stiffness is provided per-vertex if alpha.shape[0] != source.n_points: raise ValueError() alpha_per_edge = alpha[unique_edge_pairs].mean(axis=1) alpha_M_s = sp.diags(alpha_per_edge).dot(M_s) alpha_M_kron_G_s = sp.kron(alpha_M_s, G) else: # stiffness is global - just a scalar multiply. Note that here # we don't have to recalculate M_kron_G_s alpha_M_kron_G_s = alpha * M_kron_G_s if verbose: a_str = (alpha if not alpha_is_per_vertex else 'min: {:.2f}, max: {:.2f}'.format(alpha.min(), alpha.max())) i_str = '{}/{}: stiffness: {}'.format(i, len(stiffness_weights), a_str) if landmark_group is not None: i_str += ' lm_weight: {}'.format(beta) print(i_str) j = 0 while True: # iterate until convergence j += 1 # track the iterations for this alpha/landmark weight # find nearest neighbour and the normals U, tri_indices = closest_points_on_target(v_i) # ---- WEIGHTS ---- # 1. Edges # Are any of the corresponding tris on the edge of the target? # Where they are we return a false weight (we *don't* want to # include these points in the solve) w_i_e = np.in1d(tri_indices, edge_tris, invert=True) # 2. Normals # Calculate the normals of the current v_i v_i_tm = TriMesh(v_i, trilist=trilist) v_i_n = v_i_tm.vertex_normals() # Extract the corresponding normals from the target u_i_n = target_tri_normals[tri_indices] # If the dot of the normals is lt 0.9 don't contrib to deformation w_i_n = (u_i_n * v_i_n).sum(axis=1) > 0.9 # 3. Self-intersection # This adds approximately 12% to the running cost and doesn't seem # to be very critical in helping mesh fitting performance so for # now it's removed. Revisit later. # # Build an intersector for the current deformed target # intersect = build_intersector(to_vtk(v_i_tm)) # # budge the source points 1% closer to the target # source = v_i + ((U - v_i) * 0.5) # # if the vector from source to target intersects the deformed # # template we don't want to include it in the optimisation. # problematic = [i for i, (s, t) in enumerate(zip(source, U)) # if len(intersect(s, t)[0]) > 0] # print(len(problematic) * 1.0 / n) # w_i_i = np.ones(v_i_tm.n_points, dtype=np.bool) # w_i_i[problematic] = False # Form the overall w_i from the normals, edge case # for now disable the edge constraint (it was noisy anyway) w_i = w_i_n # w_i = np.logical_and(w_i_n, w_i_e).astype(np.float) # we could add self intersection at a later date too... # w_i = np.logical_and(np.logical_and(w_i_n, # w_i_e, # w_i_i).astype(np.float) prop_w_i = (n - w_i.sum() * 1.0) / n prop_w_i_n = (n - w_i_n.sum() * 1.0) / n prop_w_i_e = (n - w_i_e.sum() * 1.0) / n if data_weights is not None: w_i = w_i * gamma # Build the sparse diagonal weight matrix W_s = sp.diags(w_i.astype(np.float)[None, :], [0]) data = np.hstack((v_i.ravel(), o)) D_s = sp.coo_matrix((data, (row, col))) to_stack_A = [alpha_M_kron_G_s, W_s.dot(D_s)] to_stack_B = [np.zeros((alpha_M_kron_G_s.shape[0], n_dims)), U * w_i[:, None]] # nullify nearest points by w_i if landmark_group is not None: D_L = sp.coo_matrix((data[lm_mask], (row_lm, col_lm)), shape=(n_landmarks, D_s.shape[1])) to_stack_A.append(beta * D_L) to_stack_B.append(beta * U_L) A_s = sp.vstack(to_stack_A).tocsr() B_s = sp.vstack(to_stack_B).tocsr() X = spsolve(A_s, B_s) # deform template v_i_prev = v_i v_i = D_s.dot(X) delta_v_i = v_i - v_i_prev if v_i_update_func: # custom logic is provided to update the current template # deformation. This is typically used by Active NICP. # take the v_i points matrix and convert back to a TriMesh in # the original space def_template = restore.apply(source.from_vector(v_i.ravel())) # perform the update updated_def_template = v_i_update_func(def_template) # convert back to points in the NICP space v_i = prepare.apply(updated_def_template.points) err = np.linalg.norm(X_prev - X, ord='fro') stop_criterion = err / np.sqrt(np.size(X_prev)) if landmark_group is not None: src_lms = v_i[source_lm_index] lm_err = np.sqrt((src_lms - U_L) ** 2).sum(axis=1).mean() if verbose: v_str = (' - {} stop crit: {:.3f} ' 'total: {:.0%} norms: {:.0%} ' 'edges: {:.0%}'.format(j, stop_criterion, prop_w_i, prop_w_i_n, prop_w_i_e)) if landmark_group is not None: v_str += ' lm_err: {:.4f}'.format(lm_err) print(v_str) X_prev = X # track the progress of the algorithm per-iteration info_dict = { 'alpha': alpha, 'iteration': j, 'prop_omitted': prop_w_i, 'prop_omitted_norms': prop_w_i_n, 'prop_omitted_edges': prop_w_i_e, 'delta': err, 'mask_normals': w_i_n, 'mask_edges': w_i_e, 'mask_all': w_i, 'nearest_points': restore.apply(U), 'deformation_per_step': delta_v_i } current_instance = source.copy() current_instance.points = v_i.copy() if landmark_group: info_dict['beta'] = beta info_dict['lm_err'] = lm_err current_instance.landmarks[landmark_group] = PointCloud(src_lms) yield restore.apply(current_instance), info_dict if stop_criterion < eps: break
def manual_no_op_chain_test(): points = PointCloud(np.random.random([10, 2])) t = Translation([3, 4]) chain = TransformChain([t, t.pseudoinverse()]) points_applied = chain.apply(points) assert(np.allclose(points_applied.points, points.points))
def test_translation_identity_2d(): assert_allclose(Translation.identity(2).h_matrix, np.eye(3))
def test_init_from_pointcloud_return_transform(): correct_tr = Translation([5, 5]) pc = correct_tr.apply(PointCloud.init_2d_grid((10, 10))) im, tr = Image.init_from_pointcloud(pc, return_transform=True) assert im.shape == (9, 9) assert_allclose(tr.as_vector(), -correct_tr.as_vector())
def non_rigid_icp(source, target, eps=1e-3, stiffness_values=None, verbose=False, landmarks=None, lm_weight=None): r""" Deforms the source trimesh to align with to optimally the target. """ # Scale factors completely change the behavior of the algorithm - always # rescale the source down to a sensible size (so it fits inside box of # diagonal 1) and is centred on the origin. We'll undo this after the fit # so the user can use whatever scale they prefer. tr = Translation(-1 * source.centre()) sc = UniformScale(1.0 / np.sqrt(np.sum(source.range() ** 2)), 3) prepare = tr.compose_before(sc) source = prepare.apply(source) target = prepare.apply(target) # store how to undo the similarity transform restore = prepare.pseudoinverse() n_dims = source.n_dims # Homogeneous dimension (1 extra for translation effects) h_dims = n_dims + 1 points, trilist = source.points, source.trilist n = points.shape[0] # record number of points edge_tris = source.boundary_tri_index() M_s = node_arc_incidence_matrix(source) # weight matrix G = np.identity(n_dims + 1) M_kron_G_s = sp.kron(M_s, G) # build octree for finding closest points on target. target_vtk = trimesh_to_vtk(target) closest_points_on_target = VTKClosestPointLocator(target_vtk) # save out the target normals. We need them for the weight matrix. target_tri_normals = target.tri_normals() # init transformation X_prev = np.tile(np.zeros((n_dims, h_dims)), n).T v_i = points if stiffness_values is not None: stiffness = stiffness_values if verbose: print('using user defined stiffness values: {}'.format(stiffness)) else: # these values have been empirically found to perform well for well # rigidly aligned facial meshes stiffness = [50, 20, 5, 2, 0.8, 0.5, 0.35, 0.2] if verbose: print('using default stiffness values: {}'.format(stiffness)) if lm_weight is not None: lm_weight = lm_weight if verbose: print('using user defined lm_weight values: {}'.format(lm_weight)) else: # these values have been empirically found to perform well for well # rigidly aligned facial meshes lm_weight = [5, 2, .5, 0, 0, 0, 0, 0] if verbose: print('using default lm_weight values: {}'.format(lm_weight)) # to store per iteration information info = [] # we need to prepare some indices for efficient construction of the D # sparse matrix. row = np.hstack((np.repeat(np.arange(n)[:, None], n_dims, axis=1).ravel(), np.arange(n))) x = np.arange(n * h_dims).reshape((n, h_dims)) col = np.hstack((x[:, :n_dims].ravel(), x[:, n_dims])) if landmarks is not None: if verbose: print("'{}' landmarks will be used as a landmark constraint.".format(landmarks)) source_lm_index = source.distance_to( source.landmarks[landmarks].lms).argmin(axis=0) target_lms = target.landmarks[landmarks].lms U_L = target_lms.points n_landmarks = target_lms.n_points lm_mask = np.in1d(row, source_lm_index) col_lm = col[lm_mask] # pull out the rows for the lms - but the values are # all wrong! need to map them back to the order of the landmarks row_lm_to_fix = row[lm_mask] source_lm_index_l = list(source_lm_index) row_lm = np.array([source_lm_index_l.index(r) for r in row_lm_to_fix]) o = np.ones(n) for alpha, beta in zip(stiffness, lm_weight): alpha_M_kron_G_s = alpha * M_kron_G_s # get the term for stiffness j = 0 while True: # iterate until convergence # find nearest neighbour and the normals U, tri_indices = closest_points_on_target(v_i) # ---- WEIGHTS ---- # 1. Edges # Are any of the corresponding tris on the edge of the target? # Where they are we return a false weight (we *don't* want to # include these points in the solve) w_i_e = np.in1d(tri_indices, edge_tris, invert=True) # 2. Normals # Calculate the normals of the current v_i v_i_tm = TriMesh(v_i, trilist=trilist, copy=False) v_i_n = v_i_tm.vertex_normals() # Extract the corresponding normals from the target u_i_n = target_tri_normals[tri_indices] # If the dot of the normals is lt 0.9 don't contrib to deformation w_i_n = (u_i_n * v_i_n).sum(axis=1) > 0.9 # 3. Self-intersection # This adds approximately 12% to the running cost and doesn't seem # to be very critical in helping mesh fitting performance so for # now it's removed. Revisit later. # # Build an intersector for the current deformed target # intersect = build_intersector(to_vtk(v_i_tm)) # # budge the source points 1% closer to the target # source = v_i + ((U - v_i) * 0.5) # # if the vector from source to target intersects the deformed # # template we don't want to include it in the optimisation. # problematic = [i for i, (s, t) in enumerate(zip(source, U)) # if len(intersect(s, t)[0]) > 0] # print(len(problematic) * 1.0 / n) # w_i_i = np.ones(v_i_tm.n_points, dtype=np.bool) # w_i_i[problematic] = False # Form the overall w_i from the normals, edge case w_i = np.logical_and(w_i_n, w_i_e) # we could add self intersection at a later date too... # w_i = np.logical_and(np.logical_and(w_i_n, w_i_e), w_i_i) prop_w_i = (n - w_i.sum() * 1.0) / n prop_w_i_n = (n - w_i_n.sum() * 1.0) / n prop_w_i_e = (n - w_i_e.sum() * 1.0) / n j = j + 1 # Build the sparse diagonal weight matrix W_s = sp.diags(w_i.astype(np.float)[None, :], [0]) data = np.hstack((v_i.ravel(), o)) D_s = sp.coo_matrix((data, (row, col))) # nullify the masked U values U[~w_i] = 0 to_stack_A = [alpha_M_kron_G_s, W_s.dot(D_s)] to_stack_B = [np.zeros((alpha_M_kron_G_s.shape[0], n_dims)), U] if landmarks: D_L = sp.coo_matrix((data[lm_mask], (row_lm, col_lm)), shape=(n_landmarks, D_s.shape[1])) to_stack_A.append(beta * D_L) to_stack_B.append(beta * U_L) A_s = sp.vstack(to_stack_A).tocsr() B_s = sp.vstack(to_stack_B).tocsr() X = spsolve(A_s, B_s) # deform template v_i = D_s.dot(X) err = np.linalg.norm(X_prev - X, ord='fro') if landmarks is not None: src_lms = v_i[source_lm_index] lm_err = np.sqrt((src_lms - U_L) ** 2).sum(axis=1).mean() if verbose: v_str = ('a: {}, ({}) - total : {:.0%} norms: {:.0%} ' 'edges: {:.0%}'.format(alpha, j, prop_w_i, prop_w_i_n, prop_w_i_e)) if landmarks is not None: v_str += ' beta: {}, lm_err: {:.5f}'.format(beta, lm_err) print(v_str) info_dict = { 'alpha': alpha, 'iteration': j + 1, 'prop_omitted': prop_w_i, 'prop_omitted_norms': prop_w_i_n, 'prop_omitted_edges': prop_w_i_e, 'delta': err } if landmarks: info_dict['beta'] = beta info_dict['lm_err'] = lm_err info.append(info_dict) X_prev = X if err / np.sqrt(np.size(X_prev)) < eps: break # final result if we choose closest points point_corr = closest_points_on_target(v_i)[0] result = { 'deformed_source': restore.apply(v_i), 'matched_target': restore.apply(point_corr), 'matched_tri_indices': tri_indices, 'info': info } if landmarks is not None: result['source_lm_index'] = source_lm_index return result
def test_translation_compose_after_uniformscale(): t = Translation([3, 4]) s = UniformScale(2, 2) res = t.compose_after(s) assert(type(res) == Similarity)
def test_translation_identity_3d(): assert_allclose(Translation.init_identity(3).h_matrix, np.eye(4))
def test_translation_decompose_optional(): t = Translation.init_identity(2) d = t.decompose() assert np.all(d[0].h_matrix == t.h_matrix)
def init_from_image_shape_and_vector(cls, image_shape, vector): r = Rotation.init_identity(n_dims=3) t = Translation.init_identity(n_dims=3) p = PerspectiveProjection(focal_length=1, image_shape=image_shape) return cls(r, t, p).from_vector(vector)