2_EDA Analysis

210824

저작권때문에 이미지 사진은 모두 삭제합니다.

Lesson 2 - Image Classification & EDA

  • 2강에서는 데이터를 분석하는 과정인 EDA(Exploratory Data Analysis)에 대해 진행되었습니다. 모델을 설계하는데 있어 데이터를 분석하는 작업은 중요합니다. 이 실습 자료에서는 마스크 데이터셋을 이용하여 간단한 분석 및 시각화를 해봅니다.

  • 마스크 데이터셋에는 다양한 정보가 존재합니다. 넓은 시야에서 모든 사람의 정보를 수집하여 성별과 연령에 대한 분포를 분석할 수도 있고 이미지 값의 분포를 파악할 수 있습니다. 혹은 개별 이미지를 시각화하여 어떠한 데이터가 있는지 탐색할 수도 있고 마스크의 유무에 따라 이미지가 어떻게 다른지 비교해볼 수도 있겠죠. 이 코드는 단순한 예시이며 이 보다 더 많은 분석을 자유롭게 할 수 있습니다!

0. Libraries & Configurations

import os
import sys
from glob import glob
import numpy as np
import pandas as pd
import cv2
from PIL import Image
from tqdm.notebook import tqdm
from time import time

import matplotlib.pyplot as plt
import seaborn as sns
import multiprocessing as mp
  • os : 운영 체제와 상호 작용하기 위한 수십 가지 함수들을 제공한다. 여기서는 주로 이미지와 폴더의 경로를 지정해주기 위한 도구로 사용한다.

  • sys : 파이썬 인터프리터가 제공하는 변수와 함수를 직접 제어할 수 있게 해주는 모듈이다. 여기서는 선언만 할 뿐 사용하지는 않았다.

  • glob : 특정한 패턴을 가지고 파일들의 리스트를 뽑을 때 사용한다. 이미지를 열기 위해 파일명을 구할 때 사용할 것이다.

    • 예를 들어 glob('*.exe') 를 입력하면 현재 디렉토리에서 .exe 파일에 해당하는 파일 이름들(['python.exe', 'pythonw.exe'])을 반환한다. 이러한 패턴은 정규표현식과 관련이 있다.

  • cv2 : Open Source Computer Vision Library의 약어로 오픈소스 컴퓨터 비전 라이브러리이다. 이미지를 조작(열기, 변환, 출력 등)할 수 있니다. 이번 EDA에서 이미지들을 열고 출력할 때 사용할 것이다.

    • aistages 환경에서는 다음과 같이 설치해야 사용할 수 있다

apt-get install libgl1-mesa-glx 
  • PIL : Python Image Library의 약자이다. 이미지 분석 및 처리를 쉽게 할 수 있는 라이브러리이다.

  • tqdm : 작업진행률을 시각적으로 표시하기 위한 라이브러리이다.

    • tqdm.tqdm 은 지저분해서 tqdm.notebook.tqdm 또는 tqdm.auto.tqdm 을 많이 사용한다.

    • aistages 환경에서는 다음과 같이...

pip install ipywidgets
  • time : 컴퓨터에서 시간을 측정하기 위한 모듈로 사용된다.

  • seaborn : matplotlib에서 구현할 수 있는 기능을 편리하게 사용할 수 있도록 구현된 시각화 라이브러리이다.

    • aistages 환경에서는 ...

pip install seaborn

class cfg:
    data_dir = './input/data/train'
    img_dir = f'{data_dir}/images'
    df_path = f'{data_dir}/train.csv'

데이터들의 경로를 모아두는 주머니로 클래스를 사용했다. 내가 지금까지 봤던 방법은 대문자 변수로 (like DATR_DIR ) 선언하는 것이었는데, 이렇게도 할 수 있는 것을 알았다.

여기서는 데이터가 들어있는 경로를 기본 경로로 먼저 선언하고 여기서 이미지 데이터와 csv파일에 대한 경로를 기본 경로에 붙여서 선언했다.

내가 알기로는 문자열로 경로를 직접적으로 선언하면 문제가 있을 수도 있다. 운영체제간에 경로 표기법이 /\ 로 나뉘기 때문. 그래서 기본적으로 해당 파일(.ipynb 또는 .py)이 있는 경로는 os.path.dirname(os.path.abspath(__file__)) 로 선언하고 os.path.join 으로 하위 디렉토리나 파일의 경로를 선언한다.

num2class = ['incorrect_mask', 'mask1', 'mask2', 'mask3',
             'mask4', 'mask5', 'normal']
class2num = {k: v for v, k in enumerate(num2class)}

df = pd.read_csv(cfg.df_path)
df.head()

id

gender

race

age

path

0

000001

female

Asian

45

000001_female_Asian_45

1

000002

female

Asian

52

000002_female_Asian_52

2

000004

male

Asian

54

000004_male_Asian_54

3

000005

female

Asian

58

000005_female_Asian_58

4

000006

female

Asian

59

000006_female_Asian_59

한 사람당 7장의 사진을 가지고 있다. 미착용 1장, 완벽히 미착용(턱스크) 1장, 완벽히 착용(코스크) 5장. 이러한 범주형 데이터를 이를 0번부터 6번까지 전처리 해주는 모습이다.

python에서는 csv파일을 pandas의 dataframe으로 불러올 수 있다. 이를 위해 pd.read_csv 를 사용해서 이전에 클래스로 선언한 train.csv 의 경로를 불러왔다.

df.head() 는 dateframe의 데이터를 처음부터 보여주며 인자를 입력하지 않으면 deafult로 5개를 보여준다. 끝에서부터 보여주는 방법으로는 df.tail() 이 있다.

1. 이미지 RGB정보, 사이즈

input이 될 이미지에 대한 분석으로 이미지의 각 채널별 정보, 사이즈, 객체 위치등을 이용하여 이미지의 특성들을 알아봅시다.

1.1 Dataset Statistics

  • 여기에선 전체 이미지에 대해서 이미지의 개수와 크기, R, G, B 값의 평균과 표준편차를 계산합니다.

def get_ext(img_dir, img_id):
    """
    학습 데이터셋 이미지 폴더에는 여러 하위폴더로 구성되고, 이 하위폴더들에는 각 사람의 사진들이 들어가있습니다. 하위폴더에 속한 이미지의 확장자를 구하는 함수입니다.
    
    Args:
        img_dir: 학습 데이터셋 이미지 폴더 경로 
        img_id: 학습 데이터셋 하위폴더 이름

    Returns:
        ext: 이미지의 확장자
    """
    filename = os.listdir(os.path.join(img_dir, img_id))[0]
    ext = os.path.splitext(filename)[-1].lower()
    return ext

get_ext 라는 함수를 선언한다. 이름부터 확장자를 얻을 수 있다는 느낌이 물씬 온다. ext는 EXTended file system 의 줄임말이다.

  • 인자로 폴더 경로와 폴더 이름을 받는다.

    • img_dir : cfg.img_dir 를 주로 받게 될 것이다

      • 이전에 img_dir = f'{data_dir}/images' 로 설정했다.

    • img_id : 001131_female_Asian_22 와 같은 텍스트를 받는다.

  • os.listdir : 주어진 주소에 있는 파일명을 리스트로 반환한다. 이전에 설명한 glob.glob 과 비슷한 기능이다. 전체 주소를 얻는 것이 아니라 파일명만 얻는다. 그래서 추후에 os.path.join 을 사용한다.

  • os.path.join : 경로를 이어주는 역할을 한다

    • ex) os.path.join('user', 'documents') == 'user/documnets

  • os.path.splitext : 주어진 파일의 이름을 /. 을 기준으로 나눈다.

그래서, filename은 7개의 이미지 파일명을 얻게 될 것이고 이 중 첫번째 파일의 확장자를 반환할 것이다.

def get_img_stats(img_dir, img_ids):
    """
    데이터셋에 있는 이미지들의 크기와 RGB 평균 및 표준편차를 수집하는 함수입니다.
    
    Args:
        img_dir: 학습 데이터셋 이미지 폴더 경로 
        img_ids: 학습 데이터셋 하위폴더 이름들

    Returns:
        img_info: 이미지들의 정보 (크기, 평균, 표준편차)
    """
    img_info = dict(heights=[], widths=[], means=[], stds=[])
    for img_id in tqdm(img_ids):
        for path in glob(os.path.join(img_dir, img_id, '*')):
            img = np.array(Image.open(path))
            h, w, _ = img.shape
            img_info['heights'].append(h)
            img_info['widths'].append(w)
            img_info['means'].append(img.mean(axis=(0,1)))
            img_info['stds'].append(img.std(axis=(0,1)))
    return img_info

이미지의 통계적인(=statistics) 데이터들을 얻는 다는 뜻으로 함수 이름을 get_img_stats 로 지은 듯 하다. get_ext 처럼 폴더 경로와 폴더 이름을 인자로 가지는데, 여기서 폴더 이름은 여러개인 것이 차이.

  • img_info 는 이미지의 높이, 너비, 평균, 표준편차를 딕셔너리 정보로 가진다.

  • tqdm(img_ids) : for문이 도는 것을 progressbar로 시각적으로 표현하려고 한다. 이 때, 다음을 입력해주고 커널을 재시작해야 정상적으로 출력된다.

    • 그리고 apt-get nodejs 를 설치해줘야 한다.

    • 하지만, 그래도 난 잘 안된다. 똑같이 해도 팀원은 되던데 뭐가 문제일까..

# jupyter nbextension enable --py widgetsnbextension

Enabling notebook extension jupyter-js-widgets/extension...
      - Validating: OK
  • np.array(Image.open(path)) : 파이썬 이미지 라이브러리 PIL의 함수 Image.open을 통해 이미지를 연다. 이미지를 픽셀 단위로 분석하기 위해서 이를 numpy array로 변경한다.

  • img.shape : 이미지의 높이, 너비, 채널 수가 순서대로 반환된다.

numpy array 이기 때문에 meanstd라는 numpy 함수를 사용하여 평균과 표준편차를 구한다. 이 때 axis = (0, 1)로 명시해주는데, 전체 픽셀을 가로축과 세로축 모두 한번에 종합하라는 의미이다. axis 를 사용하지 않아도 동일한 결과가 반환된다.

>>> a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> a.mean(axis=0)
array([4., 5., 6.])
>>> a.mean(axis=1)
array([2., 5., 8.])
>>> a.mean(axis=(0, 1))
5.0
>>> a.mean()
5.0

img_info = get_img_stats(cfg.img_dir, df.path.values[:100])

print(f'Total number of people is {len(df)}')
print(f'Total number of images is {len(df) * 7}')

print(f'Minimum height for dataset is {np.min(img_info["heights"])}')
print(f'Maximum height for dataset is {np.max(img_info["heights"])}')
print(f'Average height for dataset is {int(np.mean(img_info["heights"]))}')
print(f'Minimum width for dataset is {np.min(img_info["widths"])}')
print(f'Maximum width for dataset is {np.max(img_info["widths"])}')
print(f'Average width for dataset is {int(np.mean(img_info["widths"]))}')

print(f'RGB Mean: {np.mean(img_info["means"], axis=0) / 255.}')
print(f'RGB Standard Deviation: {np.mean(img_info["stds"], axis=0) / 255.}')
Total number of people is 2700
Total number of images is 18900
Minimum height for dataset is 512
Maximum height for dataset is 512
Average height for dataset is 512
Minimum width for dataset is 384
Maximum width for dataset is 384
Average width for dataset is 384
RGB Mean: [0.55800916 0.51224077 0.47767341]
RGB Standard Deviation: [0.21817792 0.23804603 0.25183411]
  • df.path.values[:100] : 잊었다면 다시, dftrain.csvpd.read_csv 로 읽어들인 객체이다. 이 csv파일에는 path 라는 컬럼을 가지고 있다. 이 컬럼의 값들을 100개까지만 가져온 것

    • dataframe의 column 접근법에는 df.pathdf['path'] 가 있다. 둘이 동일하지만, column name에 공백이 있다면 후자로만 접근할 수 있다.

1.2 객체의 위치들 확인해보기

  • 조금 특수한 분석을 해봅시다. 이 부분은 강의 내용을 벗어나는 코드가 포함되어 있으므로 생략하셔도 괜찮습니다.

  • 사람 얼굴을 찾는데 딥러닝이 사용되기 이전에, Haar Cascade라는 방법이 많이 사용되었습니다. 이 방법을 이용하여 간단하게 결과를 시각화 해봅시다.

  • 코드가 제대로 실행이 되지않는다면, opencv에서 제공하는 haarcascade_frontalface_default.xml파일을 현재 경로에 받아야합니다.

face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

이 부분은 오류가 나는데, 다음과 같이 작성하면 오류가 나지 않는다. 나만 나는건가?

face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

face_cascade가 무엇일까? 이것은 cascade라 하는 간단한 특징을 가지고 object detection을 하는 방법이다. 현재의 머신러닝이 여기에 기반을 두어서 시작했다고 한다.

face_cascade는 위와 같은 커널을 가지고 이미지의 특징을 뽑아낸다. 흰색 부분의 픽셀값을 빼고 검은색 부분의 픽셀값을 더해서 feature를 만든다. 이러한 feature는 24x24 윈도우로 무려 16만개의 특징이 생성된다고 한다. (윈도우가 정확히는 무엇인지 파악하지 못했지만 커널의 크기로 짐작된다) 딥러닝은 이미지의 피처맵을 알아서 뽑아낸다. 그에 반해 딥러닝이 있기전의 머신러닝은 사람이 직접 이미지의 피처를 정한다. 위의 커널들처럼. 자세한 설명은 여기를 참조하면 된다.

우리가 사용하려는 face_cascade는 최적의 threshold값을 학습하고 이미제 얼굴이 있는지 없는지에 대해 판단한다고 한다. 아무래도 edge detection을 거치면 주로 사람의 얼굴을 세로로 타원형의 모양을 가지고 있고 사람의 눈코입 윤곽이 드러날테니 이러한 원리로 학습하는 듯 싶다. 총 6000개가 넘는 특징을 38개의 단계에 거쳐 학습한 이 face_cascade를 사용하려고 한다.

  • threshold는 문턱값이라는 뜻으로, 결정할 수 있는 지점이라고 생각하면 된다. 국어 시험 점수가 60점 이상이어야만 재시험에서 면제된다면 여기서 threshold는 60점이다. edge detection을 하면 이미지에서 윤곽선만 대체로 잡히는데, 이러한 윤곽이 각 선마다 뚜렷하기도, 희미하기도 하기 때문에 어디까지 edge로 볼 것인가에 대해서 정하는 것이 threshold이다. face_cascade는 다량의 이미지와 많은 특징으로 각 stage마다 최적의 threshold를 학습한 것

imgs = []
img_id = df.iloc[500].path
ext = get_ext(cfg.img_dir, img_id)
for class_id in num2class:
    img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, class_id+ext)))
    imgs.append(img)
imgs = np.array(imgs)

500번째 데이터의 경로를 가져와서 그 경로안에 있는 7장의 이미지를 가져오는 코드

  • os.path.join의 인자를 여러개로 사용할 수 있는지 처음 알았다

fig, axes = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(12, 6))
axes[0].imshow(imgs[0])
axes[1].imshow(imgs[1])
axes[2].imshow(imgs[-1])
plt.show()

클래스를 다음과 같이 선언했으므로, 위와 같은 출력 결과를 얻는다.

num2class = ['incorrect_mask', 'mask1', 'mask2', 'mask3', 'mask4', 'mask5', 'normal']

2. target값 y에 대한 분석

저희가 맞춰야하는 정보들이 어떤 것인지 확인해보고 어떤 분포를 갖고 있는지 확인해봅시다.

  • 여기에선 train.csv에 저장되어있는 메타 데이터를 분석합니다. seaborn 시각화 라이브러리를 통해 성별의 분포와 연령 분포를 확인해봅시다.

2.1 y값 독립적 분포 확인

plt.figure(figsize=(6, 4.5)) 
ax = sns.countplot(x = 'gender', data = df, palette=["#55967e", "#263959"])

plt.xticks( np.arange(2), ['female', 'male'] )
plt.title('Sex Ratio',fontsize= 14)
plt.xlabel('')
plt.ylabel('Number of images')

counts = df['gender'].value_counts()
counts_pct = [f'{elem * 100:.2f}%' for elem in counts / counts.sum()]
for i, v in enumerate(counts_pct):
    ax.text(i, 0, v, horizontalalignment = 'center', size = 14, color = 'w', fontweight = 'bold')
    
plt.show()

성별간 수치를 막대 그래프로 표현했다.

  • Line 2

    • gender 라는 컬럼의 값들을 x축, 그에 해당하는 값을 y축으로 설정하고 색을 지정해주었다.

  • Line 4-7

    • 제목과 x축, y축의 이름을 설정했다

  • Line 9-10

    • gender 컬럼을 기준으로 데이터의 개수를 세고 퍼센테이지로 표현하기 위해 전체 데이터의 개수로 나누었다.

  • Line 11

    • 가로축 정렬을 가운데정렬로, 색은 하얀색인 텍스트를 (i, 0) 위치에 v라고 입력한다.

sns.displot(df, x="age", stat="density")
plt.show()

seaborn에는 distplotdisplot 이 둘 다 있으므로 헷갈리지 말자. displot 은 분포를 막대그래프로 그려주는 함수이다. x축은 age 가 y축은 age의 count로 설정되 stat 은 이 y축을 어떤 단위로 표현할지 결정한다. 여기서는 density 라는 밀도로 표현했으며 기본값은 count 이다.

2.2 y값들 간의 관계 분포

  • 나이와 성별에 따른 분포는 어떻게 구성되었는지 알아봅시다.

sns.displot(df, x="age", hue="gender", stat="density")
plt.show()

여기서 hue 인자를 추가하게 되면 새로운 데이터 변수를 추가해서 색상으로 구분되는 그래프를 그릴 수 있다. 여기서는 남성과 여성이라는 새로운 변수를 추가했다.

df['age'].describe()
count    2700.000000
mean       37.708148
std        16.985904
min        18.000000
25%        20.000000
50%        36.000000
75%        55.000000
max        60.000000
Name: age, dtype: float64

dataframe은 describe 를 통해서 수치형 데이터의 각종 통계량을를 파악할 수 있다.

sns.boxplot(x='gender', y='age', data=df)
plt.show()
  • 남성과 여성의 나이의 범위는 같지만 경향성은 다른 것을 확인할 수 있습니다.

  • 데이터들의 불균형(data imbalance)가 심해보이네요 이를 위해서는 어떤 분석방법을 사용해야할까요??

3. X, y 관계확인

X인 이미지와 y의 관계는 어떤 것이 있을까요??

  • 분석하고자 하는 이미지와 y의 관계를 알게된다면 전처리, data augmentation 혹은 CNN의 구조를 풀고자하는 문제에 적합하게 적용해볼 수 있습니다.

3.1 이미지 사이즈와 y값의 관계

  • image size는 모두 같은 사이즈라 y값과 관계가 없습니다.

3.2 이미지 RGB 통계값과 y 특성의 관계

img_id = df.iloc[500].path
ext = get_ext(cfg.img_dir, img_id)

여기서는 이미지의 통계량을 확인하기 위해 임의의 이미지, 여기서는 500번째 이미지를 선택했다.

plt.figure()
plt.subplot(111)

for class_id in num2class:
    img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, class_id+ext)).convert('L'))
    histogram, bin_edges = np.histogram(img, bins=256, range=(0, 255))
    sns.lineplot(data=histogram)

plt.legend(num2class)
plt.title('Class Grayscale Histogram Plot', fontsize=15)
plt.show()
  • 마스크의 종류가 5개라 plot이 산만한 것같으니 마스크는 평균을 취해서 확인해봅시다.

Image 객체는 convert 라는 기능이 있는데, 이미지의 타입을 바꾼다. 여기서는 L 이라는 것으로 바꾸었고 이것은 GrayScale(흑백) 타입으로 바꾸는 것이다. 흑백으로 바꾸면 이미지는 0에서 255까지의 값(명도) 만 가지게 된다.

  • 또 다른 인자로는 bilevel(검은색 또는 흰색)의 1 과 투명도정보까지 포함되는 P 가 있다.

np.histogram 은 array를 인자로 받는다. bins 는 히스토그램의 구간을 몇개로 설정할것인지, range는 범위를 의미한다. 반환값으로는 적용된 histogram과 len(hist)+1의 값인 bin_edges가 반환된다.

plt.figure()
plt.subplot(111)

img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, 'incorrect_mask'+ext)).convert('L'))
histogram, bin_edges = np.histogram(img, bins=256, range=(0, 255))
sns.lineplot(data=histogram)

img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, 'normal'+ext)).convert('L'))
histogram, bin_edges = np.histogram(img, bins=256, range=(0, 255))
sns.lineplot(data=histogram, color='hotpink')

histograms = []
for i in range(1, 6):
    img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, num2class[i]+ext)).convert('L'))
    histogram, bin_edges = np.histogram(img, bins=256, range=(0, 255))
    histograms.append(histogram)
sns.lineplot(data=np.mean(histograms, axis=0))

plt.legend(['incorrect_mask', 'normal', 'mask average'])
plt.title('Class Grayscale Histogram Plot', fontsize=15)
plt.show()

여기서는 마스크 이미지를 모두 np.mean 으로 평균내어서 선 그래프로 그려지게된다.

  • 마스크를 쓰지않은 사진의 RGB 분포도 살펴볼까요?

plt.figure()
plt.subplot(111)

img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, 'normal'+ext)))
colormap = ['red', 'green', 'blue']
for i in range(3):
    histogram, bin_edges = np.histogram(img[..., i], bins=256, range=(0, 255))
    sns.lineplot(data=histogram, color=colormap[i])

plt.legend()
plt.title('RGB Histogram Plot - Normal', fontsize=15)
plt.show()

3.3 객체의 위치와 y의 관계

객체의 위치와 y의 관계를 찾는 방법은 직접 다 확인하는 방법이 있을 수도 있지만 위에서 사용한 face detection을 이용하여 box의 위치들의 통계값들을 이용하여 찾을 수 있을 것 같습니다.

  • 이미지 별로 통계값을 뽑아내는 것은 캠퍼님들이 직접 해보시면 좋을 것 같습니다.

  • 아래 코드는 어떤 label이 얼굴을 잘찾지 못하는지 확인해봅시다.

face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
imgs = []
bboxes = []
not_found_idx = []
img_id = df.iloc[504].path
ext = get_ext(cfg.img_dir, img_id)
for i, class_id in enumerate(num2class):
    img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, class_id+ext)))
    bbox = face_cascade.detectMultiScale(img)
    imgs.append(img)
    if len(bbox) != 0:
        bboxes.append(bbox.max(axis=0))
    else:
        not_found_idx.append(i)
        print(f'{class_id} not found face')
imgs = np.array(imgs)
bboxes = np.array(bboxes)
incorrect_mask not found face
mask1 not found face
mask5 not found face

이번에는 504번째 이미지를 선택해보자. 이때 bboxface_cascade.detectMultiScale 로 얻게되는데, 이는 bounding box를 의미하며, bounding box의 좌표값을 얻게된다.

여기서는 마스크를 쓴 사진 6장(정상은 5장) 중 3장에서 얼굴이 검출되지 못했다.

fig, axes = plt.subplots(1, len(not_found_idx), sharex=True, sharey=True, figsize=(12, 6))
for i, j in enumerate(range(len(not_found_idx))):
    axes[i].imshow(imgs[j])
    axes[i].set_title(f'{num2class[j]}')
plt.show()
  • 대부분의 이미지들은 인물들이 정중앙에 있는 것으로 확인

  • mask5는 대부분 bbox를 찾지 못함

  • 가끔 mask1도 찾지 못함

3.4 데이터 노이즈 확인

  • 사람마다 총 7장의 사진이 존재합니다. (마스크 정상 착용 5장, 미착용 1장, 이상하게 착용 1장).

  • 이 파트에서는 이미지를 직접 시각화하여 눈으로 관찰하여 label에 문제가 없는지 확인해봅시다.

한 사람의 데이터를 시각화해봅시다.

def plot_raw_images(img_dir, img_id):
    """
    마스크 미착용 이미지를 시각화하는 함수입니다.
    
    Args:
        img_dir: 학습 데이터셋 이미지 폴더 경로 
        img_id: 학습 데이터셋 하위폴더 이름
    """
    ext = get_ext(img_dir, img_id)
    img = np.array(Image.open(os.path.join(img_dir, img_id, 'normal' + ext)))
    
    plt.figure(figsize=(6,6))
    plt.imshow(img)
def show_from_id(idx):
    img_id = df.iloc[idx].path
    gen = df.iloc[idx].gender
    age = df.iloc[idx].age
    plot_raw_images(cfg.img_dir, img_id)
    plt.title(f'{gen} {age}')
    plt.show()
  • 남성으로 보이지만 여성으로 표시되어 있는경우

show_from_id(2399)
show_from_id(2400)
  • 여성으로 보이지만 남성으로 표시되어 있는 경우

show_from_id(1912)
show_from_id(764)

꾀 많은 경우로 데이터의 경향성을 방해하는 데이터가 있는 것으로 확인됩니다.

이를 위해서는 어떤 방법을 이용하는 것이 좋을까요???

  • id 별로 마스크 착용 상태를 확인해봅시다.

def plot_mask_images(img_dir, img_id):
    """
    마스크 정상착용 5장과 이상하게 착용한 1장을 2x3의 격자에 시각화하는 함수입니다.
    
    Args:
        img_dir: 학습 데이터셋 이미지 폴더 경로 
        img_id: 학습 데이터셋 하위폴더 이름
    """
    ext = get_ext(img_dir, img_id)
    imgs = [np.array(Image.open(os.path.join(img_dir, img_id, class_name + ext))) for class_name in num2class[:-1]]
    
    n_rows, n_cols = 2, 3
    fig, axes = plt.subplots(n_rows, n_cols, sharex=True, sharey=True, figsize=(15, 12))
    for i in range(n_rows*n_cols):
        axes[i//(n_rows+1)][i%n_cols].imshow(imgs[i])
        axes[i//(n_rows+1)][i%n_cols].set_title(f'{num2class[i]}', color='r')
    plt.tight_layout()
    plt.show()
idx = 500
img_id = df.iloc[idx].path
plot_mask_images(cfg.img_dir, img_id)

PCA 분석은 나의 지식이 부족해서, 시간상 설명하지 못한다. 이걸 공부하고 설명하면 라벨링을 못할 것 같다. 매우 안타깝지만 여기까지.

(Optional) PCA

  • 주성분 분석은 이미지 데이터 분포의 주성분을 구하는 방법입니다.

  • 300 장의 얼굴 이미지에 대한 주성분 벡터(eigenface)를 구하고 T-SNE를 통해 차원축소를 하여 각 클래스마다의 분포차이를 시각화해봅시다.

from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
n_imgs = 100

imgs = []
for img_id in df.path.values[:n_imgs]:
    for class_id in num2class:
        img = np.array(Image.open(os.path.join(cfg.img_dir, img_id, class_id+ext)).convert('L'))
        imgs.append(img)
imgs = np.array(imgs)
n_samples, h, w = imgs.shape

imgs = np.reshape(imgs, (n_samples, h*w))
n_components = 30

t0 = time()
pca = PCA(n_components=n_components, svd_solver='randomized',
          whiten=True).fit(imgs)
print(f"pca is fitted in {time() - t0:.0f}s")
print(f'Explained variation per principal component: \n{pca.explained_variance_ratio_}')

eigenfaces = pca.components_.reshape((n_components, h, w))
img_pca = pca.transform(imgs)
pca is fitted in 6s
Explained variation per principal component: 
[0.16400588 0.10582972 0.07423492 0.05696156 0.03344151 0.02725828
 0.02416499 0.02329284 0.02024689 0.01692791 0.01573021 0.013579
 0.01292885 0.01185079 0.01141357 0.00954064 0.00822195 0.0078436
 0.00709916 0.00670267 0.00646114 0.00626619 0.0059206  0.00564931
 0.00546337 0.00517184 0.00475507 0.00465659 0.00437207 0.00422053]
pca_df = pd.DataFrame(img_pca, columns=[str(col) for col in range(n_components)])
pca_df['class_id'] = [num2class[n % len(num2class)] for n in range(n_samples)]
pca_df['class_id'] = pca_df['class_id'].map(lambda x: x if x in ['incorrect_mask', 'normal'] else 'mask')
pca_df.head()

0

1

2

3

4

5

6

7

8

9

...

21

22

23

24

25

26

27

28

29

class_id

0

-0.780943

-0.301832

0.909870

0.561611

0.377147

0.319390

0.490564

-0.157337

0.226589

-0.055671

...

1.402771

0.698502

1.009226

1.024885

0.241752

-0.573497

0.032257

1.244118

-1.303907

incorrect_mask

1

-1.531008

0.054247

1.062841

0.448427

0.221644

0.127141

-0.152839

2.102684

2.525257

0.799864

...

-0.562831

-0.488063

-0.535817

-0.568799

1.178023

-0.692086

0.088736

0.438619

-0.489825

mask

2

-0.878984

-0.366578

0.961171

0.348036

0.147981

-0.001350

0.647854

-0.286916

0.415846

0.085153

...

0.990493

0.476577

0.431235

1.320901

-0.039927

-0.440994

0.039908

1.034641

-0.618757

mask

3

-0.354757

-0.443230

1.248900

0.964928

0.401074

0.732453

0.595657

-0.243793

0.063750

0.278082

...

1.053063

-0.556322

0.290911

0.385133

-0.800782

-0.706160

-0.874645

0.807353

-0.202597

mask

4

-0.526067

-0.342682

1.048815

0.778287

0.344716

0.535303

0.591965

-0.187899

0.033147

0.083656

...

1.541471

0.074838

1.002258

0.754855

-0.308133

-0.994480

-0.577034

1.347365

-1.466241

mask

5 rows × 31 columns

plt.figure(figsize=(8,6))
sns.scatterplot(
    x='0', y='1',
    hue="class_id",
    data=pca_df,
    legend="full",
    palette=sns.color_palette("Set2", 3),
    alpha=0.8
)
plt.show()
ax = plt.figure(figsize=(16,10)).gca(projection='3d')
simplified_num2class = ['incorrect_mask', 'mask', 'normal']
simplified_class2num = {k: v for v, k in enumerate(simplified_num2class)}
ax.scatter(
    xs=pca_df["0"], 
    ys=pca_df["1"], 
    zs=pca_df["2"], 
    c=pca_df['class_id'].map(lambda x: simplified_class2num[x]), 
)
ax.set_xlabel('pc1')
ax.set_ylabel('pc2')
ax.set_zlabel('pc3')

plt.legend(simplified_num2class)
plt.show()
time_start = time()
tsne = TSNE(n_components=2, verbose=1, perplexity=40, n_iter=300)
tsne_results = tsne.fit_transform(img_pca)
print('t-SNE done! Time elapsed: {} seconds'.format(time()-time_start))
[t-SNE] Computing 121 nearest neighbors...
[t-SNE] Indexed 700 samples in 0.000s...
[t-SNE] Computed neighbors for 700 samples in 0.036s...
[t-SNE] Computed conditional probabilities for sample 700 / 700
[t-SNE] Mean sigma: 2.323550
[t-SNE] KL divergence after 250 iterations with early exaggeration: 71.543358
[t-SNE] KL divergence after 300 iterations: 1.380570
t-SNE done! Time elapsed: 0.7664225101470947 seconds
pca_df['tsne-2d-one'] = tsne_results[:,0]
pca_df['tsne-2d-two'] = tsne_results[:,1]
plt.figure(figsize=(8,6))
sns.scatterplot(
    x="tsne-2d-one", y="tsne-2d-two",
    hue="class_id",
    palette=sns.color_palette("Set2", 3),
    data=pca_df,
    legend="full",
    alpha=0.8
)
plt.show()

4. Reference

Last updated

Was this helpful?