複数画像をKerasのVGG16で特徴抽出してk-means++でクラスタリング

VGG16, VGG19, ResNet50, InceptionV3など、 ImageNetで学習済みのモデルがKerasで使える。 物体認識だけでなく特徴抽出にも使えるので、 複数画像をVGG16で特徴抽出して、これをk-means++でクラスタリングしてみた。 なお複数画像は、ハワイで撮影したフラダンスの動画をフレーム分割して用意した。 以下に説明するコードは、ここ に置いておく。

ディレクトリー構成

ディレクトリー構成は以下の通り。 src/image_clustering.py が実行ファイルとなる。 data/video/クラスタリングしたい動画を置く。 data/images/targrt/ に動画をフレーム分割した画像が保存される。 data/images/clustered/ に分類済みの画像が保存される。 なお、動画でなくても、クラスタリングしたい画像を data/images/targrt/ に置くこともできる。 なお、ここ には src/image_clustering.py しかないので、 data/video/data/images/ フォルダーを追加して使ってください。

image_clustering
├─data
│  ├─images
│  │  ├─clustered
│  │  └─target
│  └─video
└─src
    └─image_clustering.py

初期設定

グローバル変数は固定。 __init__()の引数に、動画の場合は video_file にファイル名を指定する。 画像の場合は、input_video をFalseに変更する。

DATA_DIR = '../data/'
VIDEOS_DIR = '../data/video/'                        # The place to put the video
TARGET_IMAGES_DIR = '../data/images/target/'         # The place to put the images which you want to execute clustering
CLUSTERED_IMAGES_DIR = '../data/images/clustered/'   # The place to put the images which are clustered
IMAGE_LABEL_FILE ='image_label.csv'                  # Image name and its label

class Image_Clustering:
    def __init__(self, n_clusters=50, video_file='IMG_2140.MOV', image_file_temp='img_%s.png', input_video=True):
        self.n_clusters = n_clusters            # The number of cluster
        self.video_file = video_file            # Input video file name
        self.image_file_temp = image_file_temp  # Image file name template
        self.input_video = input_video          # If input data is a video

メイン関数

以下の順で処理を行う。1.は動画の場合のみ。

  1. 動画のフレーム分割
  2. 画像のラベリング(特徴抽出とクラスタリング
  3. 画像の分類(ラベルごとにディレクトリーにまとめる)
def main(self):
    if self.input_video == True:
        self.video_2_frames()
    self.label_images()
    self.classify_images()

動画のフレーム分割

OpenCVでフレーム分割して、data/images/targrt/ に保存する。

def video_2_frames(self):
    print('Video to frames...')
    cap = cv2.VideoCapture(VIDEOS_DIR+self.video_file)

    # Remove and make a directory.
    if os.path.exists(TARGET_IMAGES_DIR):
        shutil.rmtree(TARGET_IMAGES_DIR)  # Delete an entire directory tree
    if not os.path.exists(TARGET_IMAGES_DIR):
        os.makedirs(TARGET_IMAGES_DIR)  # Make a directory

    i = 0
    while(cap.isOpened()):
        flag, frame = cap.read()  # Capture frame-by-frame
        if flag == False:
            break  # A frame is not left
        cv2.imwrite(TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6), frame)  # Save a frame
        i += 1
        print('Save', TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6))

    cap.release()  # When everything done, release the capture
    print('')

実行すると、次々と画像が保存されていく。 フラダンスの動画は17秒だが、フレーム数は530枚となっている。

> python .\image_clustering.py
Video to frames...
Save ../data/images/target/img_000001.png
Save ../data/images/target/img_000002.png
Save ../data/images/target/img_000003.png
...
Save ../data/images/target/img_000530.png

画像のラベリング(特徴抽出とクラスタリング

モデルにはVGG16を使用。 include_top=Falseとすることで、特徴抽出が可能になる。 動画を分割したフレームはpngで保存しているが、画像はjpgも対象。 jpegとかも対象にしたい場合は、この辺を書き換えてください。 後述する__feature_extraction()にVGG16のモデルと画像を渡して特徴を取得する。 全画像の特徴をk-means++でクラスタリングを行い、ラベルを取得して画像とセットにして、pandasでCSVに保存する。

def label_images(self):
    print('Label images...')

    # Load a model
    model = VGG16(weights='imagenet', include_top=False)

    # Get images
    images = [f for f in os.listdir(TARGET_IMAGES_DIR) if f[-4:] in ['.png', '.jpg']]
    assert(len(images)>0)

    X = []
    pb = ProgressBar(max_value=len(images))
    for i in range(len(images)):
        # Extract image features
        feat = self.__feature_extraction(model, TARGET_IMAGES_DIR+images[i])
        X.append(feat)
        pb.update(i)  # Update progressbar

    # Clutering images by k-means++
    X = np.array(X)
    kmeans = KMeans(n_clusters=self.n_clusters, random_state=0).fit(X)
    print('')
    print('labels:')
    print(kmeans.labels_)
    print('')

    # Merge images and labels
    df = pd.DataFrame({'image': images, 'label': kmeans.labels_})
    df.to_csv(DATA_DIR+IMAGE_LABEL_FILE, index=False)

__feature_extraction() の中身は、 Applications - Keras Documentation に書かれてある通り。 リサイズして、4次元データにして、zero-centeringしてから特徴を取得。 ただし、特徴は (1, 7, 7, 512) の4次元で返ってくるので、 flatten()(25088,) の1次元に変換する。

def __feature_extraction(self, model, img_path):
    img = image.load_img(img_path, target_size=(224, 224))  # resize
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)  # add a dimention of samples
    x = preprocess_input(x)  # RGB 2 BGR and zero-centering by mean pixel based on the position of channels

    feat = model.predict(x)  # Get image features
    feat = feat.flatten()  # Convert 3-dimentional matrix to (1, n) array

    return feat

実行した結果、画像530枚でも、VGG16による特徴抽出は24秒で終わった。なお、GPUGeForce 1080 x1。 k-means++で50クラスタに分類しているが、だいたい連続するフレームが同じラベルになっていることが分かる。 しかし、ラベル7などフレームが飛んでいても同じラベルになっているものもある。

Label images...
 99% (527 of 530) |################################## | Elapsed Time: 0:00:24 ETA: 0:00:00
labels:
[28 28 28 28 28 28 28 28 28  3  3  3  3  3  3  3  3  3  3  3  3  3  3  3 16
 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 26 26 26 26 26 26 26 26 26
 26 26 26 26 26 26 26 26 26 11 11 11 11 11 11 11 11 11 32 32 32 32 32 32 32
 32 32 32 32 27 27 27 27  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9
  9  9  9  9  9 37 37 37 37 37 37 37 37 37 37 37 37 37 37 21 21 21 21 21 21
 21 21 21 21 21 21 21 21 21 21 21 29 29 29 29 29 29 29 29 29 29 29 29 29  1
  1  1  1  1 42 42 42 42 42 42 45 45 45 45 45 45 45 45 45 45 45  4  4  4  4
  4  4  4  4  4  4  4  4  4  4  4  4  4 33 33 33 33 33 33 33 33 33 33 33 33
 33 33 33 35 35 35 35 35 35 35 35 35 35 35 35 19 19 19 19 19 19 41 41 41 41
 41 41 41 49 49 49 49 49 49 49 13 13 13 13 13 13 44 44 44 44 44 44 44 44 44
 44 44  5  5 17 17 17 17 17 17 17 17 17 17 13 13  5  5 34 34 34 34 34 34 34
 34 34 34  5  5  5  5  5  5 25 25 25 25 25 25 25 25 25 15 15 15 15 15 15 31
 31 31 31 31 48 48 48 48 48 48 40 40 40 40 40 40 40 20 20 20 20 20 20 20 39
 39 39 39 39 39 39 39  2  2  2  2  2  2  2  2  2  2  2  2  2  2 18 18 18 18
 18 18 18 18 18 18 18 18 18 46 46 46 46 46 46 46 46 46 47 47 47 47 47 47 47
 47 47 22 22 22 22 22 22  6  6  6  6  6  6  6  6  6  6 43 43 43 43 43 43 43
 43 43 43 30 30 30 30 30 30 30 30 30 30  7  7  7  7  7  7  7  7  7  7  7  7
  7  7  7 38 38 38 38 38 38 38 38 38 38 38 38 38 38 38  7  7  7 10 10 10 10
 10 10 10 10 10 24 24 24 24 24 24 24 24 24 14 14  0  0  0  0  0  0  0  0 14
 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 36 36 36 36 36 36 36
 36 36 36 36 36 12 12 12 12 12 12 12 12  8  8  8  8  8  8  8  8 23 23 23 23
 23 23 23 23 23]

CSVに保存したデータは以下の通り。

image,label
img_000000.png,28
img_000001.png,28
img_000002.png,28
...
img_000009.png,3
img_000010.png,3
img_000011.png,3
...
img_000024.png,16
img_000025.png,16
img_000026.png,16
...

画像の分類(ラベルごとにディレクトリーにまとめる)

CSVファイルを読み込んで、ラベルの数だけ data/images/clustered/ にラベル名のディレクトリーを作成し、 data/images/target/ から画像をラベルごとにコピペする。

def classify_images(self):
    print('Classify images...')

    # Get labels and images
    df = pd.read_csv(DATA_DIR+IMAGE_LABEL_FILE)
    labels = list(set(df['label'].values))

    # Delete images which were clustered before
    if os.path.exists(CLUSTERED_IMAGES_DIR):
        shutil.rmtree(CLUSTERED_IMAGES_DIR)

    for label in labels:
        print('Copy and paste label %s images.' % label)

        # Make directories named each label
        new_dir = CLUSTERED_IMAGES_DIR + str(label) + '/'
        if not os.path.exists(new_dir):
            os.makedirs(new_dir)

        # Copy images to the directories
        clustered_images = df[df['label']==label]['image'].values
        for ci in clustered_images:
            src = TARGET_IMAGES_DIR + ci
            dst = CLUSTERED_IMAGES_DIR + str(label) + '/' + ci
            shutil.copyfile(src, dst)

以下が実行ログ。

Classify images...
Copy and paste label 0 images.
Copy and paste label 1 images.
Copy and paste label 2 images.
...
Copy and paste label 49 images.

分類結果

先ほどのラベル7の画像を見てみると、左手を頭より上に、右手を胸の高さに上げている画像がまとめられている。 画像のindexは427から443に富んでいるが、どちらも同じポーズをしている。

f:id:Shoto:20170722193736p:plain

またラベル45の画像を見てみると、両手を胸の高さに上げて、海の方を向いている画像がまとめられている。

f:id:Shoto:20170722193749p:plain

感想

今回、動画からフレーム分割した画像ということもあって、特徴がほとんど変わらない画像が対象となってはいるが、 上手くクラスタリングできている。今後はなるべく似ていない画像で試してみようと思う。 あとできれば、k-means++を使わずにCNNに閉じた方法があれば試してみたい。

参考文献