Mesh morphing with pyssam

This example visualises how pyssam can be used to visualise SSM modes of variance as a surface.

On this dataset, and definitely more real-world datasets, the results of the morphing will be highly dependent on the quality of the template. If the results are not satisfactory, simply use a template which is more similar to the target.

[1]:
import pyssam
[2]:
from copy import copy
import matplotlib.pyplot as plt
import numpy as np
import vedo as v

First, we source landmark data to use in our shape model

[3]:
N_SAMPLES = 50
torus = pyssam.datasets.Torus()
torus_data = torus.make_dataset(N_SAMPLES)
landmark_coordinates = np.array([torus_i.points() for torus_i in torus_data])

Initialising the model

Here we convert to landmark coordinates into a parameterised shape model. We first initialise the class, which handles all pre-processing. Then, we can compute the shape model components and mean population shape

[4]:
# if USE_SCALED_DATA = True, we should have 1 mode. If False, we should have 2 modes.
USE_SCALED_DATA = True

ssm_obj = pyssam.SSM(landmark_coordinates)
if USE_SCALED_DATA:
    ssm_obj.create_pca_model(ssm_obj.landmarks_columns_scale)
else:
    ssm_obj.create_pca_model(landmark_coordinates.reshape(N_SAMPLES, -1))
mean_shape_columnvector = ssm_obj.compute_dataset_mean(ssm_obj.landmarks_columns_scale)
mean_shape = mean_shape_columnvector.reshape(-1, 3)
shape_model_components = ssm_obj.pca_model_components
/Users/josh.williams/gitrepos/pyssam-loadmodel/pyssam/statistical_model_base.py:269: UserWarning: Dataset mean should be 0, is equal to [-2.40707919e-07  1.30819524e-08  1.12504786e-07 -8.63408829e-08
 -6.27933687e-08 -1.54367029e-07 -2.72104614e-07  1.20353960e-07
 -2.93035725e-07 -2.82570170e-07  4.70950283e-08  1.54367029e-07
  8.89572718e-08 -4.00307727e-07  3.13966844e-08 -6.27933687e-08
 -8.11081051e-08 -1.17737571e-07  1.17737571e-07  1.36052307e-07
  1.98845669e-07  1.56983422e-08  4.44786359e-08 -4.97114172e-08
  1.85763724e-07 -9.15736678e-08  1.98845669e-07  5.75605910e-08
  4.44786359e-08 -2.72104614e-07  4.68333894e-07  1.04655618e-08
  2.09311235e-08  2.48557086e-07 -3.13966865e-07  6.01769798e-08
  2.09311235e-08  0.00000000e+00 -4.18622470e-08  5.75605910e-08
  1.20353960e-07 -8.11081051e-08 -3.45363532e-07  1.22970349e-07
  4.44786359e-08 -4.55251950e-07  1.54367029e-07  2.98268503e-07
  6.54097576e-08  1.33435918e-07]
  warn("Dataset mean should be 0, " f"is equal to {dataset.mean(axis=1)}")

Plotting and analysis

[5]:
# Define some plotting functions

def plot_cumulative_variance(explained_variance, target_variance=-1):
    number_of_components = np.arange(0, len(explained_variance))+1
    fig, ax = plt.subplots(1,1)
    color = "blue"
    ax.plot(number_of_components, explained_variance*100.0, marker="o", ms=2, color=color, mec=color, mfc=color)
    if target_variance > 0.0:
        ax.axhline(target_variance*100.0)

    ax.set_ylabel("Variance [%]")
    ax.set_xlabel("Number of components")
    ax.grid(axis="x")
    plt.show()

def plot_shape_modes(
  mean_shape_columnvector,
  mean_shape,
  original_shape_parameter_vector,
  shape_model_components,
  mode_to_plot,
):
  weights = [-2, 0, 2]
  fig, ax = plt.subplots(1, 3, figsize=(10, 4))
  x_min, x_max, y_min, y_max = np.inf, -np.inf, np.inf, -np.inf
  mode_outputs = []
  for j, weights_i in enumerate(weights):
    shape_parameter_vector = copy(original_shape_parameter_vector)
    shape_parameter_vector[mode_to_plot] = weights_i
    mode_i_coords = ssm_obj.morph_model(
        shape_parameter_vector
    ).reshape(-1, 3)
    print(mode_i_coords.min(axis=0), mode_i_coords.max(axis=0))

    offset_dist = pyssam.utils.euclidean_distance(
      mean_shape,
      mode_i_coords
    )
    # colour points blue if closer to point cloud centre than mean shape
    mean_shape_dist_from_centre = pyssam.utils.euclidean_distance(
      mean_shape,
      np.zeros(3),
    )
    mode_i_dist_from_centre = pyssam.utils.euclidean_distance(
      mode_i_coords,
      np.zeros(3),
    )
    offset_dist = np.where(
        mode_i_dist_from_centre<mean_shape_dist_from_centre,
        offset_dist*-1,
        offset_dist,
    )
    if weights_i == 0:
      ax[j].scatter(
        mode_i_coords[:, 0],
        mode_i_coords[:, 1],
        c="gray",
        s=1,
      )
      ax[j].set_title("mean shape")
    else:
      ax[j].scatter(
        mode_i_coords[:, 0],
        mode_i_coords[:, 1],
        c=offset_dist,
        cmap="seismic",
        vmin=-1,
        vmax=1,
        s=1,
      )
      ax[j].set_title(f"mode {mode_to_plot} \nweight {weights_i}")
    mode_outputs.append(mode_i_coords)
    if mode_i_coords[:,0].min() < x_min:
        x_min = mode_i_coords[:,0].min()
    if mode_i_coords[:,1].min() < y_min:
        y_min = mode_i_coords[:,1].min()
    if mode_i_coords[:,0].max() > x_max:
        x_max = mode_i_coords[:,0].max()
    if mode_i_coords[:,1].max() > y_max:
        y_max = mode_i_coords[:,1].max()
    ax[j].axis('off')
    ax[j].margins(0,0)
    ax[j].xaxis.set_major_locator(plt.NullLocator())
    ax[j].yaxis.set_major_locator(plt.NullLocator())

  for j, weights_i in enumerate(weights):
    ax[j].set_xlim([x_min, x_max])
    ax[j].set_ylim([y_min, y_max])
  plt.show()
  return mode_outputs

To check how different the shapes we are dealing with are, we first visualise the modes as point cloud from pyssam.

[6]:
mode_to_plot = 0
print(f"explained variance is {ssm_obj.pca_object.explained_variance_ratio_[mode_to_plot]}")

mode_outputs = plot_shape_modes(
    mean_shape_columnvector,
    mean_shape,
    ssm_obj.model_parameters,
    ssm_obj.pca_model_components,
    mode_to_plot,
)
explained variance is 0.9926544427871704
[-2.47113479 -2.48272705 -1.16445051] [2.47113479 2.45901084 1.16445051]
[-2.29824519 -2.31162167 -0.79496741] [2.29824519 2.28437424 0.79496741]
[-2.12535559 -2.14051628 -0.42548432] [2.12535559 2.10974027 0.42548432]
../_images/tutorial_mesh_morphing_example_12_1.png

If the variable USE_SCALED_DATA above is true, the following should show essentially no variation.

[7]:
mode_to_plot = 1
print(f"explained variance is {ssm_obj.pca_object.explained_variance_ratio_[mode_to_plot]}")

plot_shape_modes(
    mean_shape_columnvector,
    mean_shape,
    ssm_obj.model_parameters,
    ssm_obj.pca_model_components,
    mode_to_plot,
)
explained variance is 0.00734558142721653
[-2.34566274 -2.35931855 -0.81101496] [2.34566274 2.33150221 0.81101496]
[-2.29824519 -2.31162167 -0.79496741] [2.29824519 2.28437424 0.79496741]
[-2.25082764 -2.26392479 -0.77891987] [2.25082764 2.23725036 0.77891987]
../_images/tutorial_mesh_morphing_example_14_1.png
[7]:
[array([[-3.58806753e-05,  2.33149812e+00, -1.57952403e-09],
        [ 3.55803679e-10,  2.31845573e+00,  1.44958303e-01],
        [ 4.08452689e-10,  2.27973549e+00,  2.85257430e-01],
        ...,
        [ 3.30136920e-10,  2.27973549e+00, -2.85257434e-01],
        [ 3.30128386e-10,  2.31845573e+00, -1.44958293e-01],
        [ 3.30127967e-10,  2.33150221e+00, -1.58371696e-09]]),
 array([[ 3.23460236e-09,  2.28437424e+00,  6.36053044e-10],
        [ 3.23460236e-09,  2.27158594e+00,  1.42090023e-01],
        [ 3.23460236e-09,  2.23363185e+00,  2.79613048e-01],
        ...,
        [ 3.23456595e-09,  2.23363185e+00, -2.79613048e-01],
        [ 3.23456506e-09,  2.27158594e+00, -1.42090008e-01],
        [ 3.23456439e-09,  2.28437424e+00,  6.36052711e-10]]),
 array([[ 3.58871445e-05,  2.23725036e+00,  2.85163012e-09],
        [ 6.11340105e-09,  2.22471615e+00,  1.39221743e-01],
        [ 6.06075204e-09,  2.18752821e+00,  2.73968666e-01],
        ...,
        [ 6.13899498e-09,  2.18752821e+00, -2.73968662e-01],
        [ 6.13900173e-09,  2.22471615e+00, -1.39221723e-01],
        [ 6.13900082e-09,  2.23724626e+00,  2.85582238e-09]])]

The above plots are useful, but it is nicer to visualise the output as a surface.

Mesh morphing

First, lets look at the template mesh and template landmarks (blue). We also show the target landmarks as black for comparison.

[8]:
TEMPLATE_CASE = 0
landmark_target = mode_outputs[0]
mesh_template = torus_data[TEMPLATE_CASE]
landmark_template = landmark_coordinates[TEMPLATE_CASE]
v.show(mesh_template.alpha(0.2), v.Points(landmark_template, r=5, c="blue"), v.Points(landmark_target, r=5))
../_images/tutorial_mesh_morphing_example_18_0.png

Now we will mesh the template to the target. We then will visualise the target mesh and landmarks.

[9]:
mesh_target_i = pyssam.morph_mesh.MorphTemplateMesh(landmark_target, landmark_coordinates[TEMPLATE_CASE], torus_data[TEMPLATE_CASE]).mesh_target
v.show(mesh_target_i.alpha(0.2), v.Points(landmark_target, r=5))
../_images/tutorial_mesh_morphing_example_20_0.png