Module pymskt.statistics.pca

Expand source code
from tracemalloc import start
import numpy as np
from scipy.linalg import svd
import vtk
from vtk.util.numpy_support import numpy_to_vtk
from pymskt.mesh.utils import GIF

def pca_svd(data):
    """
    Calculate eigenvalues & eigenvectors of `data` using Singular Value Decomposition (SVD)

    Parameters
    ----------
    data : numpy.ndarray
        MxN matrix 
        M = # of features / dimensions of data
        N = # of trials / participants in dataset

    Returns
    -------
    tuple (PC = numpy.ndarray, V = numpy.ndarray)
        PC - each volumn is a principal component (eigenvector)
        V - Mx1 matrix of variances (coinciding with each PC)

    Notes
    -----
    Adapted from:
    "A Tutorial on Principal Component Analysis by Jonathon Shlens"
    https://arxiv.org/abs/1404.1100
    Inputs
    data = MxN matrix (M dimensions, N trials)
    Returns
    PC - each column is a PC
    V - Mx1 matrix of variances
    """
    M, N = data.shape
    mn = np.mean(data, axis=1)
    data = data - mn[:, None]  # produce centered data. If already centered this shouldnt be harmful.

    Y = data.T / np.sqrt(N - 1)

    U, S, V = svd(Y, full_matrices=False)
    PC = V.T  # V are the principle components (PC)
    V = S ** 2  # The squared singular values are the variances (V)

    return PC, V

def get_ssm_deformation(PCs, Vs, mean_coords, pc=0, n_sds=3):
    """
    Function to Statistical Shape Model (SSM) deformed along given Principal Component.

    Parameters
    ----------
    PCs : numpy.ndarray
        NxM ndarray; N = number of points on surface, M = number of principal components in model
        Each column is a principal component.
    Vs : numpy.ndarray
        M ndarray; M = number of principal components in model
        Each entry is the variance for the coinciding principal component in PCs
    mean_coords : numpy.ndarray
        3xN ndarray; N = number of points on surface. 
    pc : int, optional
        The principal component of the SSM to deform, by default 0
    n_sds : int, optional
        The number of standard deviations (sd) to deform the SSM. 
        This can be positive or negative to scale the model in either direction. , by default 3

    Returns
    -------
    numpy.ndarray
        3xN ndarray; N=number of points on mesh surface. 
        This includes the x/y/z position of each surface node after deformation using the SSM and
        the specified characteristics (pc, n_sds)
    """

    pc_vector = PCs[:, pc]
    pc_vector_scale = np.sqrt(Vs[pc]) * n_sds # convert Variances to SDs & multiply by n_sds (negative/positive important)
    coords_deformation = pc_vector * pc_vector_scale
    deformed_coords = (mean_coords.flatten() + coords_deformation).reshape(mean_coords.shape)
    return deformed_coords

def get_rand_bone_shape(PCs, Vs, mean_coords, n_pcs=100, n_samples=1, mean_=0., sd_=1.0):
    """
    Function to get random bones using a Statistical Shape Model (SSM).

    Parameters
    ----------
    PCs : numpy.ndarray
        NxM ndarray; N = number of points on surface, M = number of principal components in model
        Each column is a principal component.
    Vs : numpy.ndarray
        M ndarray; M = number of principal components in model
        Each entry is the variance for the coinciding principal component in PCs
    mean_coords : numpy.ndarray
        3xN ndarray; N = number of points on surface.
    n_pcs : int, optional
        Number of PCs to randomly sample from (sequentially), by default 100
    n_samples : int, optional
        number of bones to create, by default 1
    mean_ : float, optional
        Mean of the normal distribution to sample PCs from, by default 0.
    sd_ : float, optional
        Standard deviation of the normal distribution to sample PCs from, by default 1.0

    Returns
    -------
    numpy.ndarray
        nx(3xN) ndarray; N=number of points on mesh surface, n=number of new meshes
        This includes the x/y/z position of each surface node(N) for the random bones(n).
    """

    rand_pc_scores = np.random.normal(mean_, sd_, size=[n_samples, n_pcs])
    rand_pc_weights = rand_pc_scores * np.sqrt(Vs[:n_pcs])
    rand_data = rand_pc_weights @ PCs[:, :n_pcs].T
    rand_data = mean_coords.flatten() + rand_data
    
    return rand_data   

def create_vtk_mesh_from_deformed_points(mean_mesh, new_points):
    """
    Create new vtk mesh (polydata) from a set of points (ndarray) deformed using the SSM. 

    Parameters
    ----------
    mean_mesh : vtk.PolyData
        vtk polydata of the mean mesh
    new_points : numpy.ndarray
        3xN ndarray; N=number of points on mesh surface (same as number of points on mean_mesh).
        This includes the x/y/z position of each surface node should be deformed to.

    Returns
    -------
    vtk.PolyData
        vtk polydata of the deformed mesh
    """

    new_mesh = vtk.vtkPolyData()
    new_mesh.DeepCopy(mean_mesh)
    new_mesh.GetPoints().SetData(numpy_to_vtk(new_points))
    
    return new_mesh

def save_gif(
    path_save,
    PCs,
    Vs,
    mean_coords,  # mean_coords could be extracted from mean mesh...?
    mean_mesh,
    pc=0,
    min_sd=-3.,
    max_sd=3.,
    step=0.25,
    color='orange', 
    show_edges=True, 
    edge_color='black',
    camera_position='xz',
    window_size=[3000, 4000],
    background_color='white',
    verbose=False,
):
    """
    Function to save a gif of the SSM deformation.

    Parameters
    ----------
    path_save : str
        Path to save the gif to.
    PCs : numpy.ndarray
        SSM Principal Components.
    Vs : numpy.ndarray
        SSM Variances.
    mean_coords : numpy.ndarray
        NxM ndarray; N = number of meshes, M = number of points x n_dimensions
    mean_mesh : vtk.PolyData
        vtk polydata of the mean mesh
    pc : int, optional
        The principal component of the SSM to deform, by default 0
    min_sd : float, optional
        The lower bound (minimum) standard deviations (sd) to deform the SSM from
        This can be positive or negative to scale the model in either direction. , by default -3.
    max_sd : float, optional
        The upper bound (maximum) standard deviations (sd) to deform the SSM from
        This can be positive or negative to scale the model in either direction. , by default 3.
    step : float, optional
        The step size (sd) to deform the SSM by, by default 0.25
    color : str, optional
        The color of the SSM surface during rendering, by default 'orange'
    show_edges : bool, optional
        Whether to show the edges of the SSM surface during rendering, by default True
    edge_color : str, optional
        The color of the edges of the SSM surface during rendering, by default 'black'
    camera_position : str, optional
        The camera position to use during rendering, by default 'xz'
    window_size : list, optional
        The window size to use during rendering, by default [3000, 4000]
    background_color : str, optional
        The background color to use during rendering, by default 'white'
    verbose : bool, optional
        Whether to print progress to console, by default False


    """
    # ALTERNATIVELY... could pass a bunch of the above parameters as kwargs..?? but thats less clear
    gif = GIF(
        path_save=path_save,
        color=color, 
        show_edges=show_edges, 
        edge_color=edge_color,
        camera_position=camera_position,
        window_size=window_size,
        background_color=background_color,
    )

    for idx, sd in enumerate(np.arange(min_sd, max_sd + step, step)):
        if verbose is True:
            print(f'Deforming SSM with idx={idx} sd={sd}')
        pts = get_ssm_deformation(PCs, Vs, mean_coords, pc=pc, n_sds=sd)
        
        if type(mean_mesh) == dict:
            mesh = []
            start_idx = 0
            for mesh_name, mesh_params in mean_mesh.items():
                mesh.append(
                    create_vtk_mesh_from_deformed_points(
                        mesh_params['mesh'], 
                        pts[start_idx:start_idx+mesh_params['n_points'], :],
                    )
                )
                start_idx += mesh_params['n_points']
        if type(mean_mesh) in (list, tuple):
            mesh = []
            start_idx = 0
            for mesh_ in mean_mesh:
                n_pts = mesh_.GetNumberOfPoints()
                mesh.append(
                    create_vtk_mesh_from_deformed_points(
                        mesh_, 
                        pts[start_idx:start_idx+n_pts, :],
                    )
                )
                start_idx += n_pts
        
        else:
            mesh = create_vtk_mesh_from_deformed_points(mean_mesh, pts)
        
        gif.add_mesh_frame(mesh)

    gif.done()

Functions

def create_vtk_mesh_from_deformed_points(mean_mesh, new_points)

Create new vtk mesh (polydata) from a set of points (ndarray) deformed using the SSM.

Parameters

mean_mesh : vtk.PolyData
vtk polydata of the mean mesh
new_points : numpy.ndarray
3xN ndarray; N=number of points on mesh surface (same as number of points on mean_mesh). This includes the x/y/z position of each surface node should be deformed to.

Returns

vtk.PolyData
vtk polydata of the deformed mesh
Expand source code
def create_vtk_mesh_from_deformed_points(mean_mesh, new_points):
    """
    Create new vtk mesh (polydata) from a set of points (ndarray) deformed using the SSM. 

    Parameters
    ----------
    mean_mesh : vtk.PolyData
        vtk polydata of the mean mesh
    new_points : numpy.ndarray
        3xN ndarray; N=number of points on mesh surface (same as number of points on mean_mesh).
        This includes the x/y/z position of each surface node should be deformed to.

    Returns
    -------
    vtk.PolyData
        vtk polydata of the deformed mesh
    """

    new_mesh = vtk.vtkPolyData()
    new_mesh.DeepCopy(mean_mesh)
    new_mesh.GetPoints().SetData(numpy_to_vtk(new_points))
    
    return new_mesh
def get_rand_bone_shape(PCs, Vs, mean_coords, n_pcs=100, n_samples=1, mean_=0.0, sd_=1.0)

Function to get random bones using a Statistical Shape Model (SSM).

Parameters

PCs : numpy.ndarray
NxM ndarray; N = number of points on surface, M = number of principal components in model Each column is a principal component.
Vs : numpy.ndarray
M ndarray; M = number of principal components in model Each entry is the variance for the coinciding principal component in PCs
mean_coords : numpy.ndarray
3xN ndarray; N = number of points on surface.
n_pcs : int, optional
Number of PCs to randomly sample from (sequentially), by default 100
n_samples : int, optional
number of bones to create, by default 1
mean_ : float, optional
Mean of the normal distribution to sample PCs from, by default 0.
sd_ : float, optional
Standard deviation of the normal distribution to sample PCs from, by default 1.0

Returns

numpy.ndarray
nx(3xN) ndarray; N=number of points on mesh surface, n=number of new meshes This includes the x/y/z position of each surface node(N) for the random bones(n).
Expand source code
def get_rand_bone_shape(PCs, Vs, mean_coords, n_pcs=100, n_samples=1, mean_=0., sd_=1.0):
    """
    Function to get random bones using a Statistical Shape Model (SSM).

    Parameters
    ----------
    PCs : numpy.ndarray
        NxM ndarray; N = number of points on surface, M = number of principal components in model
        Each column is a principal component.
    Vs : numpy.ndarray
        M ndarray; M = number of principal components in model
        Each entry is the variance for the coinciding principal component in PCs
    mean_coords : numpy.ndarray
        3xN ndarray; N = number of points on surface.
    n_pcs : int, optional
        Number of PCs to randomly sample from (sequentially), by default 100
    n_samples : int, optional
        number of bones to create, by default 1
    mean_ : float, optional
        Mean of the normal distribution to sample PCs from, by default 0.
    sd_ : float, optional
        Standard deviation of the normal distribution to sample PCs from, by default 1.0

    Returns
    -------
    numpy.ndarray
        nx(3xN) ndarray; N=number of points on mesh surface, n=number of new meshes
        This includes the x/y/z position of each surface node(N) for the random bones(n).
    """

    rand_pc_scores = np.random.normal(mean_, sd_, size=[n_samples, n_pcs])
    rand_pc_weights = rand_pc_scores * np.sqrt(Vs[:n_pcs])
    rand_data = rand_pc_weights @ PCs[:, :n_pcs].T
    rand_data = mean_coords.flatten() + rand_data
    
    return rand_data   
def get_ssm_deformation(PCs, Vs, mean_coords, pc=0, n_sds=3)

Function to Statistical Shape Model (SSM) deformed along given Principal Component.

Parameters

PCs : numpy.ndarray
NxM ndarray; N = number of points on surface, M = number of principal components in model Each column is a principal component.
Vs : numpy.ndarray
M ndarray; M = number of principal components in model Each entry is the variance for the coinciding principal component in PCs
mean_coords : numpy.ndarray
3xN ndarray; N = number of points on surface.
pc : int, optional
The principal component of the SSM to deform, by default 0
n_sds : int, optional
The number of standard deviations (sd) to deform the SSM. This can be positive or negative to scale the model in either direction. , by default 3

Returns

numpy.ndarray
3xN ndarray; N=number of points on mesh surface. This includes the x/y/z position of each surface node after deformation using the SSM and the specified characteristics (pc, n_sds)
Expand source code
def get_ssm_deformation(PCs, Vs, mean_coords, pc=0, n_sds=3):
    """
    Function to Statistical Shape Model (SSM) deformed along given Principal Component.

    Parameters
    ----------
    PCs : numpy.ndarray
        NxM ndarray; N = number of points on surface, M = number of principal components in model
        Each column is a principal component.
    Vs : numpy.ndarray
        M ndarray; M = number of principal components in model
        Each entry is the variance for the coinciding principal component in PCs
    mean_coords : numpy.ndarray
        3xN ndarray; N = number of points on surface. 
    pc : int, optional
        The principal component of the SSM to deform, by default 0
    n_sds : int, optional
        The number of standard deviations (sd) to deform the SSM. 
        This can be positive or negative to scale the model in either direction. , by default 3

    Returns
    -------
    numpy.ndarray
        3xN ndarray; N=number of points on mesh surface. 
        This includes the x/y/z position of each surface node after deformation using the SSM and
        the specified characteristics (pc, n_sds)
    """

    pc_vector = PCs[:, pc]
    pc_vector_scale = np.sqrt(Vs[pc]) * n_sds # convert Variances to SDs & multiply by n_sds (negative/positive important)
    coords_deformation = pc_vector * pc_vector_scale
    deformed_coords = (mean_coords.flatten() + coords_deformation).reshape(mean_coords.shape)
    return deformed_coords
def pca_svd(data)

Calculate eigenvalues & eigenvectors of data using Singular Value Decomposition (SVD)

Parameters

data : numpy.ndarray
MxN matrix M = # of features / dimensions of data N = # of trials / participants in dataset

Returns

tuple (PC = numpy.ndarray, V = numpy.ndarray)
PC - each volumn is a principal component (eigenvector) V - Mx1 matrix of variances (coinciding with each PC)

Notes

Adapted from: "A Tutorial on Principal Component Analysis by Jonathon Shlens" https://arxiv.org/abs/1404.1100 Inputs data = MxN matrix (M dimensions, N trials) Returns PC - each column is a PC V - Mx1 matrix of variances

Expand source code
def pca_svd(data):
    """
    Calculate eigenvalues & eigenvectors of `data` using Singular Value Decomposition (SVD)

    Parameters
    ----------
    data : numpy.ndarray
        MxN matrix 
        M = # of features / dimensions of data
        N = # of trials / participants in dataset

    Returns
    -------
    tuple (PC = numpy.ndarray, V = numpy.ndarray)
        PC - each volumn is a principal component (eigenvector)
        V - Mx1 matrix of variances (coinciding with each PC)

    Notes
    -----
    Adapted from:
    "A Tutorial on Principal Component Analysis by Jonathon Shlens"
    https://arxiv.org/abs/1404.1100
    Inputs
    data = MxN matrix (M dimensions, N trials)
    Returns
    PC - each column is a PC
    V - Mx1 matrix of variances
    """
    M, N = data.shape
    mn = np.mean(data, axis=1)
    data = data - mn[:, None]  # produce centered data. If already centered this shouldnt be harmful.

    Y = data.T / np.sqrt(N - 1)

    U, S, V = svd(Y, full_matrices=False)
    PC = V.T  # V are the principle components (PC)
    V = S ** 2  # The squared singular values are the variances (V)

    return PC, V
def save_gif(path_save, PCs, Vs, mean_coords, mean_mesh, pc=0, min_sd=-3.0, max_sd=3.0, step=0.25, color='orange', show_edges=True, edge_color='black', camera_position='xz', window_size=[3000, 4000], background_color='white', verbose=False)

Function to save a gif of the SSM deformation.

Parameters

path_save : str
Path to save the gif to.
PCs : numpy.ndarray
SSM Principal Components.
Vs : numpy.ndarray
SSM Variances.
mean_coords : numpy.ndarray
NxM ndarray; N = number of meshes, M = number of points x n_dimensions
mean_mesh : vtk.PolyData
vtk polydata of the mean mesh
pc : int, optional
The principal component of the SSM to deform, by default 0
min_sd : float, optional
The lower bound (minimum) standard deviations (sd) to deform the SSM from This can be positive or negative to scale the model in either direction. , by default -3.
max_sd : float, optional
The upper bound (maximum) standard deviations (sd) to deform the SSM from This can be positive or negative to scale the model in either direction. , by default 3.
step : float, optional
The step size (sd) to deform the SSM by, by default 0.25
color : str, optional
The color of the SSM surface during rendering, by default 'orange'
show_edges : bool, optional
Whether to show the edges of the SSM surface during rendering, by default True
edge_color : str, optional
The color of the edges of the SSM surface during rendering, by default 'black'
camera_position : str, optional
The camera position to use during rendering, by default 'xz'
window_size : list, optional
The window size to use during rendering, by default [3000, 4000]
background_color : str, optional
The background color to use during rendering, by default 'white'
verbose : bool, optional
Whether to print progress to console, by default False
Expand source code
def save_gif(
    path_save,
    PCs,
    Vs,
    mean_coords,  # mean_coords could be extracted from mean mesh...?
    mean_mesh,
    pc=0,
    min_sd=-3.,
    max_sd=3.,
    step=0.25,
    color='orange', 
    show_edges=True, 
    edge_color='black',
    camera_position='xz',
    window_size=[3000, 4000],
    background_color='white',
    verbose=False,
):
    """
    Function to save a gif of the SSM deformation.

    Parameters
    ----------
    path_save : str
        Path to save the gif to.
    PCs : numpy.ndarray
        SSM Principal Components.
    Vs : numpy.ndarray
        SSM Variances.
    mean_coords : numpy.ndarray
        NxM ndarray; N = number of meshes, M = number of points x n_dimensions
    mean_mesh : vtk.PolyData
        vtk polydata of the mean mesh
    pc : int, optional
        The principal component of the SSM to deform, by default 0
    min_sd : float, optional
        The lower bound (minimum) standard deviations (sd) to deform the SSM from
        This can be positive or negative to scale the model in either direction. , by default -3.
    max_sd : float, optional
        The upper bound (maximum) standard deviations (sd) to deform the SSM from
        This can be positive or negative to scale the model in either direction. , by default 3.
    step : float, optional
        The step size (sd) to deform the SSM by, by default 0.25
    color : str, optional
        The color of the SSM surface during rendering, by default 'orange'
    show_edges : bool, optional
        Whether to show the edges of the SSM surface during rendering, by default True
    edge_color : str, optional
        The color of the edges of the SSM surface during rendering, by default 'black'
    camera_position : str, optional
        The camera position to use during rendering, by default 'xz'
    window_size : list, optional
        The window size to use during rendering, by default [3000, 4000]
    background_color : str, optional
        The background color to use during rendering, by default 'white'
    verbose : bool, optional
        Whether to print progress to console, by default False


    """
    # ALTERNATIVELY... could pass a bunch of the above parameters as kwargs..?? but thats less clear
    gif = GIF(
        path_save=path_save,
        color=color, 
        show_edges=show_edges, 
        edge_color=edge_color,
        camera_position=camera_position,
        window_size=window_size,
        background_color=background_color,
    )

    for idx, sd in enumerate(np.arange(min_sd, max_sd + step, step)):
        if verbose is True:
            print(f'Deforming SSM with idx={idx} sd={sd}')
        pts = get_ssm_deformation(PCs, Vs, mean_coords, pc=pc, n_sds=sd)
        
        if type(mean_mesh) == dict:
            mesh = []
            start_idx = 0
            for mesh_name, mesh_params in mean_mesh.items():
                mesh.append(
                    create_vtk_mesh_from_deformed_points(
                        mesh_params['mesh'], 
                        pts[start_idx:start_idx+mesh_params['n_points'], :],
                    )
                )
                start_idx += mesh_params['n_points']
        if type(mean_mesh) in (list, tuple):
            mesh = []
            start_idx = 0
            for mesh_ in mean_mesh:
                n_pts = mesh_.GetNumberOfPoints()
                mesh.append(
                    create_vtk_mesh_from_deformed_points(
                        mesh_, 
                        pts[start_idx:start_idx+n_pts, :],
                    )
                )
                start_idx += n_pts
        
        else:
            mesh = create_vtk_mesh_from_deformed_points(mean_mesh, pts)
        
        gif.add_mesh_frame(mesh)

    gif.done()