<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Playing with Eigen Faces
In this tutorial article, we want to learn how to combine landmark points together in order to create new faces. This process is used in developing Active Shape Model (ASM), which is a technique for detecting landmarks (key facial points) on a face image. This is an example of landmark points on a face image
Step1:  After downloading the dataset, unzip the landmarks, and use a file named muct76.csv.
Step2:  The df contains both $x$ and $y$ coordinates of landmark points. There are 3755 images in the dataset, but the reverse (mirrored) images are also considered here, so total we have 7510 rows. Next, let us split the $x$, $y$ coordinates into separate numpy arrays for convinience.
Step3:  Now, let us visualize it to get more familar with the data. We can plot the landmarks for one image using matplotlib. Landmarks identify eyebrows, eyes, face boundary, nose, upper and lower lips. These facial points are exteremly useful for a lot of computer vision applications.
Step4:  Step 1
Step5:  Scaling faces
For scaling, we need to normalize the faces to a fixed value. One idea is to scale based on the interpupillary distance (IPD), the distance between the center of pupils, to a fixed value, while keeping the same aspect ratio in the original face. The center of pupils are marked as landmark indexed at 31 and 36.
Step6:  Compute the mean face
Now that the faces are aligned using scaling and linear translation, we compute the mean face.
(By the way, mean face here refers to the average face, not mean=angry! 
Step7:  Compute the eigne faces
We want to develop a model that by chaning its parameters, we can generate new faces. But, we can arbitralily apply some distortions to faces. 
In order to generate new faces, we can compute some vectors that have the highest variations among our current faces. Then, we can apply change the mean face along those faces which gives us a new face. This can be done by computing the eigen vevtors of the covariance matrix of these faces, also we call it "Eigen Faces".
Covariance matrix of all faces
Step8:  Eigen vectors and eigen values of covariance matrix
Step9:  Finally, everything is ready to generate new faces. Applying a distortion along those eigen faces generates a new face according to the following formula
Step10:  For clarity, we generate distorion vectors of all zero values except one non-zero element. That non-zero element is a multiplication of the corresponding eigen-value times a small factor.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline
import pandas
Explanation: Playing with Eigen Faces
In this tutorial article, we want to learn how to combine landmark points together in order to create new faces. This process is used in developing Active Shape Model (ASM), which is a technique for detecting landmarks (key facial points) on a face image. This is an example of landmark points on a face image:
<img src="figs/asm-example.jpg"></img>
In ASM, first you start training a model using a set of pre-annotated faces, where a fixed number of landmarks are given, and the goal is to automatically identify landmarks on a new face image. 
The goal of this tutorial is to walk you through the first step of ASM training. We have a dataset of images and their landmark points. We will 1) align all the landmarks, 2) compute the mean face 3) create a model for generating new faces using eigen faces.
For this tutorial, we use MUCT database: <a href="www.milbo.org/muct">www.milbo.org/muct</a> which contains 3755 faces with manually landmarked 76 points for each face.
End of explanation
df = pandas.read_csv('muct76.csv', header=0, usecols=np.arange(2,154), dtype=float)
df.head()
Explanation: After downloading the dataset, unzip the landmarks, and use a file named muct76.csv.
End of explanation
X = df.iloc[:, ::2].values
Y = df.iloc[:, 1::2].values
print(X.shape, Y.shape)
Explanation: The df contains both $x$ and $y$ coordinates of landmark points. There are 3755 images in the dataset, but the reverse (mirrored) images are also considered here, so total we have 7510 rows. Next, let us split the $x$, $y$ coordinates into separate numpy arrays for convinience.
End of explanation
plt.plot(X[0,:], Y[0,:])
plt.show()
Explanation: Now, let us visualize it to get more familar with the data. We can plot the landmarks for one image using matplotlib. Landmarks identify eyebrows, eyes, face boundary, nose, upper and lower lips. These facial points are exteremly useful for a lot of computer vision applications.
End of explanation
xmeans = np.mean(X, axis=1)
ymeans = np.mean(Y, axis=1)
xmeans.shape
## mean-centering each image
X = (X.T - xmeans).T
Y = (Y.T - ymeans).T
Explanation: Step 1: Computing the mean face
In order to compute the mean-face, we need to align all faces together. Typically for alignment we need to do three forms of transformation: linear translation, scaling and rotation. Assuming that all the images have straight pose, we can skip the rotation. The general formula for transformation is the following
$$\mathrm T\displaystyle \left(\begin{array}{c}x\y\end{array}\right) = s \left[ \begin{array}{cc} \cos \theta & \sin \theta \ -\sin \theta & \cos \theta \end{array}\right]  \left( \begin{array}{c}x \ y \end{array}\right) + \left(\begin{array}{c} x_{translate} \ y_{translate}  \end{array} \right) $$
Where, $s$ is the scaling factor, $\theta$ is the rotation angle, and $x_{translate}$ and $y_{translate}$ are the translations in $x, y$ coordinates.
We want to do two forms of transformation: linear translation, and scaling.
Mean centering all faces
For mean centering, first, we need to find the center of each face, i.e.  the mean of all $x$ and $y$ coordinates. Then, we can apply a linear translation so that the center of each face is located at coordinate $(0, 0)$. This way, we can overlap all the images.
End of explanation
for i, (fx, fy) in enumerate(zip(X, Y)):
    interpupils_dist = np.abs(fx[31] - fx[36])
    
    X[i,:] = X[i,:] * 100 / interpupils_dist
    Y[i,:] = Y[i,:] * 100 / interpupils_dist
plt.figure(figsize=(10,10))
plt.subplot(2, 2, 1)
plt.plot(X[0,:], Y[0,:])
plt.subplot(2, 2, 2)
plt.plot(X[100,:], Y[100,:])
plt.subplot(2, 2, 3)
plt.plot(X[200,:], Y[200,:])
plt.subplot(2, 2, 4)
plt.plot(X[300,:], Y[300,:])
plt.show()
Explanation: Scaling faces
For scaling, we need to normalize the faces to a fixed value. One idea is to scale based on the interpupillary distance (IPD), the distance between the center of pupils, to a fixed value, while keeping the same aspect ratio in the original face. The center of pupils are marked as landmark indexed at 31 and 36.
End of explanation
mean_face_x = np.mean(X, axis=0)
mean_face_y = np.mean(Y, axis=0)
plt.plot(mean_face_x, mean_face_y)
plt.show()
Explanation: Compute the mean face
Now that the faces are aligned using scaling and linear translation, we compute the mean face.
(By the way, mean face here refers to the average face, not mean=angry! :))
End of explanation
D = np.concatenate((X, Y), axis=1)
D.shape
cov_mat = np.cov(D.T)
cov_mat.shape
Explanation: Compute the eigne faces
We want to develop a model that by chaning its parameters, we can generate new faces. But, we can arbitralily apply some distortions to faces. 
In order to generate new faces, we can compute some vectors that have the highest variations among our current faces. Then, we can apply change the mean face along those faces which gives us a new face. This can be done by computing the eigen vevtors of the covariance matrix of these faces, also we call it "Eigen Faces".
Covariance matrix of all faces
End of explanation
eig_values, eig_vectors = np.linalg.eig(cov_mat)
print(eig_values.shape, eig_vectors.shape)
plt.plot(eig_values)
num_eigs = 20
Phi_matrix = eig_vectors[:,:num_eigs]
Phi_matrix.shape
Explanation: Eigen vectors and eigen values of covariance matrix
End of explanation
def construct_newface(meanface, Phi_matrix, b):
    face = meanface + np.dot(Phi_matrix, b )
    return (face[:76], face[76:])
Explanation: Finally, everything is ready to generate new faces. Applying a distortion along those eigen faces generates a new face according to the following formula:
$$\hat{r} = \bar{r} + \phi b$$
where $\hat{r}$ is the new face, $\bar{r}$ is the mean face, $\phi$ is the matrix of eigen values, and $b$ is the distortion parameter along each eigen vector.
End of explanation
meanface = np.concatenate((mean_face_x, mean_face_y))
plt.figure(figsize=(10,10))
for i in range(4):
    plt.subplot(2, 2, i+1)
    b = np.zeros(shape=num_eigs)
    for j in (-0.025, -0.02, -0.015, -0.01, 0, 0.01, 0.015, 0.02, 0.025):
        b[i] = j*eig_values[i]
        xnew, ynew = construct_newface(meanface, Phi_matrix, b=b) 
                                  
        plt.plot(xnew, ynew)
    
plt.show()
Explanation: For clarity, we generate distorion vectors of all zero values except one non-zero element. That non-zero element is a multiplication of the corresponding eigen-value times a small factor.
End of explanation
<END_TASK>