さくらVPS+django+scikit-learnで指定した顔の類似画像（エロ動画）検索できるサイトを作るまで

#**さくらのVPSとDjangoとscikit-learnを使って、気になる顔画像と類似したAV女優が出てるAVの類似画像検索できるウェブサイトを作った。**

サイト名は[そっくりナビ](http://sokkurinavi.com/)

今までアプリの開発をしていたのでunityとかopen-GLとかObjective-Cは使ったことあったのだけど、pythonや機械学習の勉強と実益もかねて、1からウェブサービスを作った履歴を記録します。

初心者がはまるポイントは全部はまっていると思うので初心者の人たちには参考になると思います。

ちなみにSIFTとかCNNとか使わなかったのは、BOVW的なやつの精度が低かったり、次元が大きくなって計算量が増えちゃったからです。CNNやるなら動画から画像抽出とかしないとサンプル数足りない感じになっちゃって貧弱なマシンだったので諦めました。

元ネタ：[AV画像認識技術とその周辺](http://sssslide.com/www.slideshare.net/parosky0/av-ut-startup-gym) 　**ありがとうございます!**

##**目次**

------------ローカル側作業----------------


- 【１】DMMからAV女優の顔画像をスクレイピングする。
- 【２】opencvで顔画像抽出、顔画像のヒストグラムを抽出する
- 【３】pcaとkmeans次元圧縮して、与えた顔画像を分類するモデルを作る
- 【４】djangoを使ってサイトを作る
- 【５】クラウドワークスでhtmlとデザイン作ってもらう
- 【６】CropperJSを使って、顔画像を切り取るスクリプトを作る

------------サーバー側作業----------------


- 【７】さくらvpsにopencv、virtualenv、mod_wsgi、scikit-learn,django等々の環境構築する
- 【８】mod_wsgiの設定をして、さくらVPSにdjangoをデプロイする
- 【９】さくらVPSでドメインを取得して、バーチャルホスト設定する


##【１】DMMからAV女優の顔画像をスクレイピングする。

クロールに使うスクリプトはあげないのが世の常っぽいのでコードは晒さないけど、
pythonとBeautifulSoup4を使った。

[PythonとBeautiful Soupでスクレイピング](http://qiita.com/itkr/items/513318a9b5b92bd56185)を見れば、代々解決するので見てください。

##【２】opencvで顔画像抽出、顔画像のヒストグラムを抽出する

opencv自体はビルドすると大変なのだけど、ローカルでやMacとかでやる分にはanacondaインストールして、

`pip install anaconda`
`conda install -c https://conda.binstar.org/jjhelmus opencv`

の二つのコマンドを打つだけで利用可能。簡単。

この辺が参考になる
[Python(Anaconda)とOpenCVを使って動画から顔画像を抽出してみる](https://hogehuga.com/post-241/)
[OpenCVで顔認識を行い、顔の部分だけトリミングして保存する【Python】](http://famirror.hateblo.jp/entry/2015/12/19/180000)

```py
# -*- coding:utf-8 -*-
import cv2
import sys
import os
import shutil

image_path = "image.png"#任意の画像パス

#カスケード分類器の特徴量を取得する
face_cascade = cv2.CascadeClassifier('[yourdir]/opencv/data/haarcascades/haarcascade_frontalface_default.xml')

#ファイル読み込み
image = cv2.imread(image_path)#任意の画像パス

#グレースケール変換
image_gray = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)


#物体認識（顔認識）の実行
faces = cascade.detectMultiScale(image_gray, scaleFactor=1.2, minNeighbors=2, minSize=(10, 10))


#ディレクトリの作成
if len(faces) > 0:
	path = os.path.splitext(image_path)
	dir_path = path[0] + '_face'
	if os.path.isdir(dir_path):
		shutil.rmtree(dir_path)
	os.mkdir(dir_path)

i = 0;
for rect in faces:
	#顔だけ切り出して保存
	x = rect[0]
	y = rect[1]
	width = rect[2]
	height = rect[3]
	dst = image[y:y+height, x:x+width]
	new_image_path = dir_path + '/' + str(i) + path[1];
	cv2.imwrite(new_image_path, dst)#ここで切り取った画像を保存するパスを入れる
	i += 1
```

顔画像を抽出したら、続いてHOG特徴量(Histograms of Oriented Gradients)を抽出する

 HOG特徴量について参考：
[画像からHOG特徴量の抽出](http://qiita.com/kooh-z/items/3e3f85e93d894b4645f7)

めっちゃシンプルで

```py
hog=cv2.HOGDescriptor()
img=cv2.imread('test.jpg')
res=hog.compute(img)
```

とするだけ。これで画像の分散表現が取れる。

##【３】pcaとkmeans次元圧縮して、与えた顔画像を分類するモデルを作る

ただ、HOG特徴量の問題点として、次元の呪いがある。
普通に３０万枚近い画像とウェブサイトで類似度計算して返そうとするなら計算量的に無理なので、次元圧縮(PCA)とPCAした結果をクラスタリング(kmeans）しておく必要がある。

まず全画像について分散表現を得る必要があるので、
下のクラスを作って、画像をフォルダ単位で一括で読み込むようにした。

```py
from PIL import Image
import os
import cv2
from sklearn.cluster import KMeans
import numpy as np
import random
import pickle
import re
import sklearn
import dill



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

	def fileread(self, filepath):
		self.path = filepath
		try:
			temp = cv2.imread(filepath)
			self.src = cv2.resize(temp,(64,64))
			self.shape = temp.shape
			
		except cv2.error as e:
			pass

	def readArray(self, array):
		self.srcGrey = array


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

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

	def readAllFiles(self, folderpath, isResize=False, height=256, width=256):

		for path in self.readAllFilePath(folderpath):
			p = path.replace(folderpath, "")
			p = p.split("/")
			m= m+1

			image = Imageob()
			image.fileread(path)

			if image.shape[0] > 50: #小さすぎる画像を削除
				pi = ProcessImage()
				image = pi.resize(image, 64, 64)
				self.addImage(image)
				n = n+1


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

#ここからHOG特徴量を抽出する

images = Images()
print("画像の読み込みを開始します")

images.readAllFiles("/image_path")#顔画像が入っているフォルダを指定

data = []
label = []
num = 0

for image in images.images:
	hog = cv2.HOGDescriptor((64, 64), (16, 16), (8, 8), (8, 8), 9)
	img_ = cv2.imread(image.path)
	img_ = cv2.resize(img_,(64,64))
	try:
		hist = hog.compute(img_)
		k = [] #numpyにするようにリスト表現にする。
		if hist is not None:
			for i in hist:
				k.append(i[0])
		else:
			print("hist is NONE")

		data.append(k)

	except cv2.error as e:
		pass


npdata = np.array(data) # scikit-learnで使えるようにnp.array形式にする

pca = sklearn.decomposition.PCA(100)
pca.fit(npdata)

X_pca= pca.transform(npdata)

a =[] # 上記で作られたリスト形式でヒストグラムを入れる

for x in X_pca:
   a.append(x)

kmeans_model  = KMeans(n_clusters=10, random_state=10).fit(a)
labels = kmeans_model.labels_
```

これでPCAのモデルとKmeansのモデルができた。
僕みたいにきったないクラスを作ったりして、子要素を持つクラスを作ると、
Pythonのpickleでserializeできない問題が起こるので、
dillで保存すると重くもないし、永続化されるのでオススメ。

[Pythonのpickleでserializeできない問題の回避策](http://nishidy.hatenablog.com/entry/2016/02/01/225750)

あとは以下のように書けば、画像パスを与えて、COS類似度を計算できる


```py
def cos_sim(v1, v2):
	return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

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

	def hog(self, path):
		hog = cv2.HOGDescriptor((64, 64), (16, 16), (8, 8), (8, 8), 9)
		img = cv2.imread(path)
		img = cv2.resize(img , (64, 64))

		t = []
		try:
			hist = hog.compute(img)
			k = []
			if hist is not None:
				for i in hist:
					k.append(i[0])
			else:
				print("hist is NONE")
			t.append(k)

		except cv2.error as e:
			print('cv2_error')
		return t


```

画像を与えたらHOG特徴量取って、主成分分析モデルを使って次元圧縮して、それを元にKmeasnのモデルを使って所属するクラスタを推定して、そのクラスタ内の全画像と類似度を計算すれば良いです。

参考にしたサイトは下記。(こちらの方がの方がソースコードは確実に綺麗です。)

[アニメの顔写真をBag of Keywordsで分類してみた](http://nonbiri-tereka.hatenablog.com/entry/2014/09/29/101030)
※注：SIFTは現在明るく書いても使えないので、KAZE,AKAZEをお使いください。

KAZEとかの使い方
[OpenCV3.0.0-dev+Python2.7.6で特徴量(ORB/AKAZE/KAZE/FAST/BRISK)の計測](http://tamanobi.hatenablog.com/entry/2015/07/13/093440)


##【４】djangoを使ってサイトを作る

ここまでくればあとはチュートリアルみたいな物
ここを見て作った。

[Python Django入門](http://qiita.com/kaki_k/items/511611cadac1d0c69c54)

##【５】クラウドワークスでhtmlとデザイン作ってもらう

正直デザインとマークアップが一番向いていない作業なので、外注した。
こんな感じ。ちなみに投資は全く回収できてない。回収の目処もない。

[男のロマンがつまった「そっくりナビ!!」 の画面のデザイン/コーディング ](https://crowdworks.jp/public/jobs/556788)

##【６】CropperJSを使って、顔画像を切り取るスクリプトを作る

JSは苦手だったらしくこちらで実装することに。

[Cropper.js](https://fengyuanchen.github.io/cropperjs/)

スマホもそのまま最適されるのですごい便利な画像をブラウザ上で抽出するライブラリでした。あっさりここに書いてある

[画像を切り抜くjqueryプラグイン「Cropper」を使ってみる](http://co.bsnws.net/article/50)

基本demoをいじればわかる感じ。

##ここからはサーバー側

##【７】さくらvpsにopencv、virtualenv、mod_wsgi、scikit-learn,django等々の環境構築する

正直ここが一番時間かかった。

- さくらVPSの初期設定は[ここ](http://qiita.com/aild_arch_bfmv/items/99dd58fc6be84f87a075)の通りにやる。


- pythonいれる[CentOS 6.5にPython 2.7をインストールしApacheで使用](http://a-records.info/2014/10/25/34/)の通りにやる

そもそもさくらVPSのCentOS6.5に入っているのがpython 2.6.6 でmod_wsgi とバージョンが違ってエラーがでてはまった。しかもdjangoのエラーを吐いてくれるわけでもなく、白い画面でリクエストが返ってこなくなるので本当に困る。

- virtualenvいれる
[ここ](http://qiita.com/H-A-L/items/5d5a2ef73be8d140bdf3)から。これがないと普通にパスがどこのライブラリを参照しているんだかわけわからんくなるので必須。

- CENTOS6.5にOpenCV入れる(全工程で一番大変かも）

以下を参考に見よう見真似でやりきる。多分一番大変だった。#正直覚えていない。
ちなみになんか依存関係があるファイルをバックアップせず明るく上書きしたらサーバー使えなくなったことは悪夢だった。

[AMIにOpenCV3.0をインストールしてみる](http://qiita.com/mitokazzy/items/df2d8a4fd93f46210f5f)
[OpenCV3.0とopencv_contribをubuntuに入れた作業メモ](http://qiita.com/iwata-n@github/items/6ae7e430e8008be29377)
[CENTOS6でopenCVを利用する時に必要なパッケージ](http://www.gimmickgeek.com/2014/10/04/centos6%E3%81%A7opencv%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E6%99%82%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8/)


ちなみに基本的な考え方はこのへんで学べる
[まっさらなVPS（CentOS6）からApacheを使ってDjangoを動かすまでのメモ](http://qiita.com/itkr/items/fef0978f808f36449f1f)


##【８】mod_wsgiの設定をして、さくらVPSにdjangoをデプロイする

djangoのデプロイ設定は

[django 本番環境構築からリリースまで(django1.8.7 + Apache + mod_wsgi)]
(http://marrsan.hateblo.jp/entry/2015/12/03/145235)

を見れば99%は大丈夫。

ただ、djnago と　scikit-learn　が　CENTOSで起こす問題を解消する必要があった。ここもつまった。
https://github.com/scikit-learn/scikit-learn/issues/3947

上で作ったwsgi.confファイルに

`LoadModule wsgi_module modules/mod_wsgi.so
WSGIPythonPath /home/hoge/ENV/lib/python2.7/site-packages
WSGIApplicationGroup %{GLOBAL}
`

としてあげれば解決する。


##【9】さくらVPSでドメインを取得して、バーチャルホスト設定する

あとはドメイン取得してバーチャルホスト設定する。
下記サイトとか参照

[さくらのVPSを使ってみる【8】-VirtualHost（バーチャルホスト）の設定をしてみる](http://pro-grammer.info/archives/894)

適度に******で埋めているので書き換えてください。

```apache:python.conf
<VirtualHost *:80>

ServerName sokkurinavi.com
ServerAlias www.sokkurinavi.com

WSGIScriptAlias / /var/www/cgi-bin/*******************/wsgi.py

#WSGIScriptAlias /sokuri /var/www/cgi-bin/*************
#Alias /static /home/hoge/ENV/lib/python2.7/site-packages/django/contrib/admin/static

Alias /static/ /var/www/cgi-bin/******************/static/

<Directory /var/www/cgi-bin/******************/static>
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>

<Directory /var/www/cgi-bin/***************>
Order deny,allow
Allow from all
</Directory>

</VirtualHost>
```

完成版が[こちらのサイト](http://sokkurinavi.com/)

#[そっくりナビ](http://sokkurinavi.com/)



**一つ最後に言いたいのは、エロサイト作れば、流入うはうはだと思ってたけど、いつまでたっても流入１とか２とかだから、エロサイトはオススメしない。**