Source code for procrustes

'''
Procrustes-related functions.
'''

from sklearn.base import BaseEstimator, ClassifierMixin
import numpy as np
from scipy.spatial import procrustes
from typing import List

[docs]class ShapeClassifier(BaseEstimator, ClassifierMixin): """ A classifier that uses Procrustes analysis to classify 3D shapes based on their similarity to a set of medoids. :param medoids: A list of medoid shapes to compare against. :type medoids: List[np.ndarray] """ def __init__(self, medoids): self.medoids = medoids
[docs] def fit(self, X): """ Fit the classifier to a set of training shapes. :param X: The set of training shapes to fit the classifier to. :type X: List[np.ndarray] :return: The fitted classifier object. :rtype: ShapeClassifier """ self.medoids = X return self
[docs] def predict(self, X: List[np.ndarray]) -> np.ndarray: """ Predict the class labels of a set of test shapes. :param X: The set of test shapes to predict class labels for. :type X: List[np.ndarray] :return: An array of predicted class labels. :rtype: np.ndarray """ return np.array([ShapeClassifier.predict_class(self.medoids, shape) for shape in X])
[docs] def predict_proba(self, X: List[np.ndarray], use_weighted: bool = False) -> np.ndarray: """ Predict the class probabilities of a set of test shapes. :param X: The set of test shapes to predict class probabilities for. :type X: List[np.ndarray] :param use_weighted: Whether to use weighted probabilities based on distance, defaults to False. :type use_weighted: bool :return: An array of predicted class probabilities. :rtype: np.ndarray """ return np.array([ShapeClassifier.predict_class(self.medoids, shape, soft=True, use_weighted=use_weighted) for shape in X])
[docs] @staticmethod def procrustes_disparity(shape_1: np.ndarray, shape_2: np.ndarray) -> float: """ Calculate the Procrustes disparity between two shapes. :param shape_1: The first shape. :type shape_1: np.ndarray :param shape_2: The second shape. :type shape_2: np.ndarray :return: The Procrustes disparity between the two shapes. :rtype: float """ return procrustes(shape_1, shape_2)[2]
[docs] def predict_class(medoids: np.ndarray, shape: np.ndarray, soft: bool = False, use_weighted: bool = False) -> np.ndarray: """ Predicts the class of a given shape by comparing it to a set of medoids using the Procrustes distance. :param medoids: Array of shape medoids used to compare the input shape. :type medoids: np.ndarray :param shape: Input shape to classify. :type shape: np.ndarray :param soft: If True, returns a soft classification, where the output is an array of probabilities of belonging to each class. If False, returns a hard classification, where the output is the index of the closest medoid. :type soft: bool, optional :param use_weighted: If True and soft=True, uses weighted distances instead of the exponential function to calculate the probabilities. :type use_weighted: bool, optional :return: If soft=True, returns an array of probabilities of belonging to each class. If soft=False, returns the index of the closest medoid. :rtype: np.ndarray """ dists = np.array([ShapeClassifier.procrustes_disparity(medoid, shape) for medoid in medoids]) if soft: min_dist = np.min(dists) if min_dist == 0: # If the minimum distance is zero, return a one-hot encoding with a 1 at the minimum distance index proba = np.zeros(len(medoids)) proba[np.argmin(dists)] = 1 else: # Otherwise, calculate the probabilities based on the distances and normalize them if use_weighted: proba = 1.0 / dists else: proba = np.exp(-dists / min_dist) proba /= np.sum(proba) return proba else: # Return the index of the closest medoid return np.argmin(dists)