のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

アニメの顔写真をBag of Keywordsで分類してみた

Sponsored Links

皆さんこんにちは
お元気ですか。私は眠いです。書いている時間が深夜なんて言えない。

さて、今日はアニメの顔写真を分類してみます。
画像認識の分野において結構メジャーな手法であるBag of Keywordsを使います。

Bag of Keywordsの概要

Bag of keypoints, Bag of featureとも呼ばれる手法
元来は、文書分類の手法であるBag of wordである。画像の集合を局所特徴量の集合とみなし、利用する手法のことである。

局所特徴量

特徴量抽出法により、濃淡の変化が大きい部分を
特徴ベクトルとし、特徴点周りを微分したりしたもの。

要は最も特徴のある点を算出し、何らかの形でベクトルにしたものである。

今回は有名な手法であるSIFTを利用する。

SIFTの簡単な説明

特徴点と特徴量を記述するアルゴリズムだそうな。
詳しいアルゴリズムの内容はよくわかりませんが、エッジに対して不変なものを持っています。

Python Opencv2を今回は利用しました。

具体的なアルゴリズムについて

今回扱う手法についてのフローチャートを生成しました。

f:id:tereka:20140929014858p:plain

1.特徴量抽出
画像の特徴的なポイントを取得します。128次元のベクトルです。
今回はSIFTを利用して抽出しました。輝度変化が大きなところです。

2.クラスタリング(Bag of Keywords)
1で取得した特徴的なポイントをクラスタリングします。
ベクトルのままだと比較しずらいのでクラスタのインデックスをかわりに置こうということです。
これをすることにより不必要な些細な違いを無視することができます。

今回は時間がかかると面倒なのでK平均法を使ってみました。どんなことをやっているかイメージとしては以下の図を参照ください。
似ているものは同じ表現として置き換えるイメージの発想です。クラスタリングをどんなのを使うかとか議論はありますが、簡便化しました。

f:id:tereka:20140929014726p:plain

3.ヒストグラムの生成
クラスタリングのインデックスに基づいてヒストグラムを生成。ようはどんな特徴がどれだけあるかをヒストグラムにします。類似度を図るなら、ヒストグラムを比較するアルゴリズムを使えば良いと思いますが、今回はこれをクラスタリングにかけます。

各画像につき一つのヒストグラムを生成し、横軸はクラスタのインデックスとなります。

4.再度クラスタリング(画像の分類)
3で得たヒストグラムをクラスタリングし、画像にしています。
これも同じくK平均法を使っています。要はヒストグラムを元に勝手に分類ですね。

Dataset

animeface-charactor-dataset様を利用させて頂きました。
animeface-character-dataset

Result

画像貼るので目視で誰か評価してください。(画像が多いので、続きを読むから)

Cluster0
f:id:tereka:20140926230816p:plain

Cluster1
f:id:tereka:20140926230837p:plain

Cluster2
f:id:tereka:20140926230903p:plain

Cluster3
f:id:tereka:20140926230915p:plain

Cluster4
f:id:tereka:20140926230933p:plain

Cluster5
f:id:tereka:20140926230945p:plain

Cluster6
f:id:tereka:20140926231003p:plain

Cluster7
f:id:tereka:20140926231020p:plain

Cluster8
f:id:tereka:20140926231031p:plain

Cluster9
f:id:tereka:20140926231048p:plain


いや近い部分は本当に近いのが集まっていると思うのですよ。
評価の方法なくて困っているけどさ。

ソースコード

#coding:utf-8
import cv2
from sklearn.cluster import KMeans
import numpy as np
import os
import random

def random_choice_unique(lists, num=4):
    if not lists:
        return []

    if len(lists) <= num:
        return lists

    copy_lists = lists[:]
    results = []
    for i in range(num):
        t = random.choice(copy_lists)
        results.append(t)
        copy_lists.remove(t)

    return results

class Image(object):
	def __init__(self):
		pass

	def fileread(self,filepath):
		#print filepath
		self.src = cv2.imread(filepath)
		self.srcGrey = cv2.cvtColor(self.src, cv2.COLOR_BGR2GRAY)
		self.height,self.width = self.srcGrey.shape

	def readArray(self,array):
		self.srcGrey = array
		#self.height,self.width = self.srcGrey.shape

	def sift(self):
		sift = cv2.SIFT()
		kp, des = sift.detectAndCompute(self.srcGrey,None)
		return np.array(des)

	def addhist(self,histgram):
		self.hist = histgram

class Images(object):
	def __init__(self):
		self.images = []

	def addImage(self,image):
		self.images.append(image)

	def readAllFiles(self,folderpath,isResize=False,height=0,width=0):
		for path in self.readAllFilePath(folderpath):
			image = Image()
			image.fileread(path)
			if isResize:
				pi = ProcessImage()
				image = pi.resize(image, height, width)
			self.addImage(image)

	def readAllFilePath(self,folderpath):
		for root, dirs, files in os.walk(folderpath):
			for file in files:
				if not file.startswith(".") and file.endswith(".png"):
					yield os.path.join(root, file)

	def extractAllSift(self):
		sift_feature = self.images[0].sift()
		flag = False
		for image in self.images:
			if flag:
				sift_feature = np.vstack((sift_feature,image.sift()))
			flag = True
		return sift_feature

	def tile(self,height,width,filename):
		""" サイズを構成 """
		board_height = 1120
		board_width = 1120
		board = np.zeros((board_height,board_width,3),dtype='uint8')

		pi = ProcessImage()

		height_index_num = board_height / height
		width_index_num = board_width / width

		image_list = random_choice_unique(self.images, width_index_num * height_index_num)

		for height_index in xrange(height_index_num):
			for width_index in xrange(width_index_num):
				try:
					index  = height_index * (board_width / width) + width_index
					resizeImage = pi.resize(image_list[index], height, width)
					board[height_index * height:height_index * height + height,width_index * width:width_index * width + width] = resizeImage.srcGrey
				except:
					break
		cv2.imwrite(filename,board)

class ProcessImage(object):
	def __init__(self):
		pass

	def resize(self,image,height,width):
		resizeImage = cv2.resize(image.src,(width,height))
		resizeImgObject = Image()
		resizeImgObject.readArray(resizeImage)

		return resizeImgObject

if __name__ == '__main__':
	images = Images()

	print "画像の読み込みを開始します"
	images.readAllFiles("/Users/Tereka/Downloads/animeface-character-dataset/thumb")
	print "画像の読みこみを終了しました"

	print "画像からSIFT特徴量を抽出します。"
	sift_feature = images.extractAllSift()
	print "画像からSIFT特徴量の抽出を完了しました。"

	print "Bag of keywordsの構築"
	cluster_number = 200
	kmeans = KMeans(n_clusters=cluster_number)
	kmeans.fit(sift_feature)

	print "Bag of keywordsの学習終了"

	print "ヒストグラムの構築を開始"
	hist_list = []
	for image in images.images:
		image_sift = image.sift()
		predict_index = kmeans.predict(image_sift)
		histgram= np.zeros((cluster_number))

		for index in predict_index:
			histgram[index] += 1

		histgram = histgram / sum(histgram)
		image.addhist(histgram)
		hist_list.append(histgram)

	print "ヒストグラムの構築を完了"

	print "画像分類を開始"
	cluster_number = 10
	kmeans = KMeans(n_clusters=cluster_number)
	kmeans.fit(np.array(hist_list))

	print "画像分類を完了"

	print "画像分類したものを画像に"
	TileList = []
	for i in xrange(cluster_number):
		tile_images = Images()
		TileList.append(tile_images)

	for image in images.images:
		index = kmeans.predict(np.array([image.hist]))
		TileList[index[0]].addImage(image)

	num = 0
	for tile in TileList:
		tile.tile(80,80,"./cluster_%d.png" % num)
		num += 1

	print "全てのプログラムを完了しました"