코드 전문https://github.com/Arat5724/Eigenface/blob/main/eigenface.ipynb

https://youtu.be/7DAUnT1Xrqs (유튜브: 이상화의 선형대수와 확률이론) 강의를 실습한 내용입니다.

SVD(특잇값분해)를 얼굴 사진에 사용해 나오는 Eigenvectors(고유벡터)들을 Eigenfaces(고유얼굴)라고 부르고, 그것들을 결합해 원래 얼굴을 복구하는 과정을 파이썬으로 구현해보려고 한다.

0. 필요한 모듈 불러오기

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

1. 사진 불러오기&가공하기

http://vis-www.cs.umass.edu/lfw/dml 의 데이터셋에서 100개를 랜덤으로 선택해 사용했다.

images = [np.asarray(Image.open(f'images/{i}.jpg'))
          for i in range(100)]

원본 이미지는 250x250인데 이미지가 너무 크면 SVD가 오래 걸릴 수 있어,가로 14\frac{1}{4} 세로 13\frac{1}{3}로 잘라주었다.

# crop images
h, w, _ = images[0].shape
for i, image in enumerate(images):
    images[i] = image[int(h * 0.25):int(h * 0.75),
                      int(w * 0.33):int(w * 0.66)]
h, w, _ = images[0].shape

마찬가지로 차원을 줄여주기 위해 흑백 사진으로 변환했다.

# gray-scale images
images = [image.mean(axis=2) for image in images]
images[0].shape #output: (125, 83)

2. 데이터 행렬 AA 만들기

이미지들을 열백터 혹은 행백터로 만들어 하나의 행렬로 합쳐줘야 한다.

이미지를 행렬 AA의 열백터로 만들든, 행백터로 만들든 SVD를 통해 Eigenfaces를 구할 수 있는 것은 동일하다.

하지만 SVD과정에서 ATAA^{T}A의 Eigenvalues와 Eigenvectors를 구할 때, ATAA^{T}A의 크기가 작은 것이 좋으므로, 이미지의 개수보다 이미지의 차원이 크다면 이미지를 열백터로 만드는 것이 좋고, 그렇지 않다면 행백터로 만드는 것이 좋다.

현재 이미지의 차원은 10375이고, 이미지의 개수는 100이므로 각 이미지들을 행렬 AA의 열백터로 만들어줬다.

# horizontal stack images
A = np.hstack([image.reshape(-1, 1) for image in images])
A.shape #output: (10375, 100)

행렬 AA의 열 공간은 덧셈에 닫혀있어야 하는데, 흑백 이미지는 각 픽셀에 0~255까지밖에 담지 못한다.
두 이미지를 더하거나 할 때 범위를 벗어나는 경우가 생길 수 있다.
완벽하진 않지만 좀 더 백터 공간에 가깝게 구성하기 위해 모든 백터들에서 평균 백터를 빼주자.

# mean vector of images
mean_vector = A.mean(axis=1).reshape(-1, 1)

# subtract mean vector from images
A -= mean_vector

3. SVD 적용하기

SVD를 적용했다!
구해진 Eigenvectors가 Eigenfaces이다.

U, S, Vh = np.linalg.svd(A, full_matrices=False)

100개의 Eigenfaces가 구해졌는데, 50개만 시각화해보자.

# visualize eigenfaces
number_principal_components = 50
eigenfaces = U[:, :number_principal_components]
for i, eigenface in enumerate(eigenfaces.T):
    plt.subplot(5, 10, i + 1)
    plt.imshow(eigenface.reshape(h, w), cmap='gray')
    plt.axis('off')
plt.show()

사람들의 얼굴을 잘 표현하는 Eigenface일수록 앞에 있다.
뒤로 갈수록 특정 사람의 얼굴만 표현하는 Eigenface이다.
강의에서는 0~255으로 스케일링하지만, 나는 저장하거나 하지 않고 보기만 할 거라서 알아서 흑백으로 띄워주는 cmap='gray'을 사용했다.

강의 내용대로 변환하려면 이렇게 해야 한다.

    eigenface = eigenface.reshape(h, w, 1)
    eigenface = (eigenface - eigenface.min()) / \
        (eigenface.max() - eigenface.min()) * 255
    eigenface = eigenface.astype(np.uint8)
    eigenface = np.broadcast_to(eigenface, (h, w, 3))

4. 사진 복구하기

복구된 사진과 비교하기 위해 먼저 원래 이미지를 띄워놓는다.

# show original images
for i in range(10):
    plt.subplot(6, 10, i + 1)
    plt.imshow(images[i], cmap='gray')
    plt.axis('off')

복구하려는 사진의 벡터를 ff라고 하면, fmf - m은 Eigenfaces의 선형결합이다. (mm은 2단계에서 각 이미지 백터들에서 빼줬던 평균 벡터)

fm=c1e1+c2e2+...+cnenf - m = c_{1}e_{1} + c_{2}e_{2} + ... + c_{n}e_{n}

양변에 ene_{n}을 내적해 ene_{n}의 계수 cnc_{n}를 구할 수 있다.

(fm)en=c1e1en+c2e2en+...+cnenen(f - m)·e_{n} = c_{1}e_{1}·e_{n} + c_{2}e_{2}·e_{n} + ... + c_{n}e_{n}·e_{n}

Eigenfaces는 orthonormal이므로

(fm)en=cn(f - m)·e_{n} = c_{n}

가 된다. 이 식을 확장해 ATA^{T}와 Eigenfaces를 곱하면 원본 이미지들에 대한 계수들이 담긴 행렬 CC를 얻는다.

다시 Eigenfaces와 행렬 CTC^{T} 곱하면 복구된 이미지 행렬 FF의 각 열에 mm을 뺀 행렬이 나오게 된다.

행렬 FF에 다시 mm을 더하면 이미지 복구 완료!

한번에 계수들을 구하고 이미지를 복구하는 건 강의에 없긴 하지만, 당연한 거라 설명 생략

SVD를 했을 때 Eigenfaces가 100개 나왔는데,
Eigenfaces를 20, 40, ..., 100개씩 선택했을 때 복구된 이미지를 원본 이미지와 함께 살펴보자.

# show reconstructed images
for i in range(1, 6):
    number_principal_components = (6 - i) * 20
    eigenfaces = U[:, :number_principal_components]

    # 4. Find the coefficients
    coefficients = np.dot(A.T, eigenfaces)

    # 5. Generate face images using eigenfaces
    reconstructed_images = np.dot(eigenfaces, coefficients.T) + mean_vector

    for j in range(10):
        plt.subplot(6, 10, j + 1 + i * 10)
        plt.imshow(reconstructed_images.T[j].reshape(h, w), cmap='gray')
        plt.axis('off')
plt.show()

평균에 가까운 얼굴일수록 주성분을 많이 가지고 있어 Eigenfaces를 조금만 선택했을 때도 복구가 잘 된다.
Eigenfaces를 20개만 선택했을 때, 첫 번째 사진처럼 얼굴이 사각형에 알맞게 들어가있지 않은 얼굴은 형체를 알아볼 수 없게 복구되었지만, 다른 사진들은 그래도 형체는 알아볼 수 있다.

profile
Jeongble

0개의 댓글

Powered by GraphCDN, the GraphQL CDN