Chainerを使ってウォーリーを探せをしようと思ったけど、結果全然至らずな話

はじめまして、Shunterです。

当方、10月から機械学習/人工知能について勉強をしているおじさんです。

昨今の Deep Learning ブームに乗っかってアルゴリズムの勉強と平行してフレームワークで遊ぶことをしてきました。いろいろ試すなかで、挫折しながらもChainerが唯一「これならいける！」と思わせてくれたフレームワークです。

国産だし(Ruby好き、同じにおい感じる)、環境構築で全然つまづかなかったし、本家ドキュメントチュートリアルも親切で、とっつきやすかったです。

自分を奮い立たせるためにも、アドベントカレンダーに参加してなにか作ろうと思い、本記事を書いています。

結果から言うと**失敗しました**。その軌跡を共有できれば幸いです。

# 作りたかったもの
 
ウォーリーを探せを機械学習でやる！

画像を入力するとそのなかからウォーリーを探しだしてくれるものです。

## データ収集

### 正解データ

はじめに、ウォーリーを学ばせます。

理想を言えば本当の正解が欲しいところなんですが、
なかなか正解データがありません。

ウォーリーの顔といえばこちら。

`positive_images/1.png`
<img width="139" alt="1.png" src="https://qiita-image-store.s3.amazonaws.com/0/57586/1bb2d54a-1d9e-b5b1-0b5c-e3ea1bbcfa28.png">
`positive_images/2.png`
<img width="111" alt="2.png" src="https://qiita-image-store.s3.amazonaws.com/0/57586/74997f36-1705-9fd8-edef-5e1935b881b2.png">
`positive_images/3.png`
<img width="56" alt="3.png" src="https://qiita-image-store.s3.amazonaws.com/0/57586/4676d401-83a8-bf82-9b32-2946d57aaa0f.png">
`positive_images/4.png`
<img width="56" alt="4.png" src="https://qiita-image-store.s3.amazonaws.com/0/57586/d33cb06f-a86c-e3ba-fa23-6c83d41f6327.png">
`positive_images/5.png`
<img width="47" alt="5.png" src="https://qiita-image-store.s3.amazonaws.com/0/57586/16f0a887-91b0-8134-58c3-e4f482466834.png">

大量にデータが欲しいところなのですが、これらを使ってデータを生成しました。
データ生成は、openCVの　createsamples を使っています。

参考
http://www.pro-s.co.jp/engineerblog/opencv/post_6397.html

※（え、OpenCVでウォーリー探せばいいじゃん！というツッコミはなしで...） 

opencv_createsamples は画像を読み込んで、画像を歪ませて、.vecファイルにしてくれます。

```bash
echo 'generate sample from postive image'

for file in ./positive_images/* 
do
	sub=${file##*/}
	num=${sub%.*}
	opencv_createsamples -img $file -vec ./vectors/positive/$num.vec -maxxangle 0.2 -maxyangle 0.2 -maxzangle 0.2 -w 32 -h 32
done
```

vecファイルだと、Chainerで扱いづらそうなので（実態がよくわかってないので）vecを画像にして保存します。

また、歪みでも座標的にゆらぎがおこらないので、大きめに画像を作って、そこからマージンをランダムに設定して縦横でゆらぎが起きるようにしています。

```bash
echo 'convert positive vector to images'

for file in ./vectors/positive/* 
do
	python showvec.py -i $file -f positive 
done
```

```showvec.py
import struct,array
import os
import cv2
import numpy as np
import argparse
import random

parser = argparse.ArgumentParser(
    description='A Converter from Vector to Images')
parser.add_argument('--img', '-i', default='', help='path of input image')
parser.add_argument('--folder', '-f', default='', help='name of parent folder')
args = parser.parse_args()

def showvec(fn, width=32, height=32, resize=1.0):
  f = open(fn,'rb')
  HEADERTYP = '<iihh' # img count, img size, min, max

  # read header
  imgcount,imgsize,_,_ = struct.unpack(HEADERTYP, f.read(12))

  for i in range(imgcount):
    img  = np.zeros((height,width),np.uint8)

    f.read(1) # read gap byte
    data = array.array('h')
    data.fromfile(f,imgsize)
    for r in range(height):
      for c in range(width):
        img[r,c] = data[r * width + c]

    img = cv2.resize(img, (0,0), fx=resize, fy=resize, interpolation=cv2.INTER_LINEAR)
    rand_x = random.randint(0, (32-28))
    rand_y = random.randint(0, (32-28))
    clopped_img = img[rand_x:rand_x+28, rand_y:rand_y+28]
    filename = "./data/" + args.folder + "/" + fn.split("/")[-1].replace(".vec","") + "_" + str(i) + ".png"
    cv2.imwrite(filename, clopped_img)
    
showvec(args.img)
```

すると以下の様な画像が5000枚生成されます。

`data/positive/*.png`
![1_3.png](https://qiita-image-store.s3.amazonaws.com/0/57586/667653ec-30cd-dd65-56b9-bf4d6e6dd9b3.png)
![1_41.png](https://qiita-image-store.s3.amazonaws.com/0/57586/f17eabaf-789a-97c2-30a9-40c4501faa97.png)
![1_77.png](https://qiita-image-store.s3.amazonaws.com/0/57586/5d9e46a3-f9ad-fde5-a1e7-1880c7ad147d.png)
![1_99.png](https://qiita-image-store.s3.amazonaws.com/0/57586/6e7356e8-c12a-0a14-7081-4534c15bff7f.png)
![1_115.png](https://qiita-image-store.s3.amazonaws.com/0/57586/15e72a3d-1ad8-fda0-5a1f-9af14ad94f3f.png)
...

### 失敗データ

ここでは、ウォーリーでない画像が欲しいので、適当な画像を読み込んで
、ランダムにグレースケールをして28x28の領域を選んで画像として切り出しています。

``` generate_negative.py
import cv
import cv2
import random 
import numpy as np

img = cv2.imread('/path/to/image/you_want_to_crop')
width, height, channels = img.shape
img = cv2.cvtColor(img, cv2.cv.CV_BGR2GRAY)

wsize = 28
gennum = 16000

np.asarray(img)

for i in range(gennum):
  x = random.randint(0,width - wsize)
  y = random.randint(0,height - wsize)
  cropped_img = img[x:x+wsize, y:y+wsize]
  filename = "./data/negative/"+str(i)+".png"
  cv2.imwrite(filename, cropped_img)

```

16000枚のランダムな画像が生成されます。

`data/negative/*.png`
![1.png](https://qiita-image-store.s3.amazonaws.com/0/57586/45577760-a425-4624-e990-caf69ecae1ab.png)
![167.png](https://qiita-image-store.s3.amazonaws.com/0/57586/26a2578c-523d-fbc5-b13f-9694e8667059.png)
![172.png](https://qiita-image-store.s3.amazonaws.com/0/57586/388643e7-cd08-c9a2-378d-e7dcf73c9eab.png)
![187.png](https://qiita-image-store.s3.amazonaws.com/0/57586/6c5a3a9e-172b-66ca-f340-aa4f7e6e49ee.png)
![191.png](https://qiita-image-store.s3.amazonaws.com/0/57586/c4dcb56f-cfb9-486b-da0e-93af334827d2.png)
...

## 画像をPythonのオブジェクトに... 

基本的にはPythonの変数からいろいろゴニョゴニョするので、
そうできるように、用意した画像をnumpyオブジェクト化し、
正解データと一緒にPythonのオブジェクトに格納し、pickle化しています。

```generate_pickle.py
import cv
import cv2
import numpy as np
import os
import glob
from six.moves import cPickle

stack = []

data_dictionary = {"data":[], "label":[]}

def pack_image_into_data(dir, label, stack):

	path = './data/' + dir + '/*.png'
	images = glob.glob(path)
	for img_name in images : 
		img = cv2.imread(img_name)
		resized_img = cv2.resize(img, (28, 28))
		image_gray = cv2.cvtColor(resized_img, cv2.cv.CV_BGR2GRAY)
		npimage = np.asarray(image_gray).reshape(1,784)[0]
		npimage = npimage/255.
		stack.append((npimage, label))
	return stack

stack = pack_image_into_data("negative", 0, stack)
stack = pack_image_into_data("positive", 1, stack)
stack = np.asarray(stack)

np.random.shuffle(stack)
stack = stack.tolist()
for t in stack :
	data_dictionary["data"].append(t[0])  	
	data_dictionary["label"].append(t[1])  	

with open('data.pkl', 'wb') as output:
  cPickle.dump(data_dictionary, output, -1)
  print data_dictionary["data"][0].shape
  print data_dictionary["label"][0]
  print "Saved ", len(data_dictionary["data"]), " images with ", len(data_dictionary["label"]), " labels."
```

## モデルの学習

**Chainerの出番です！**

画像認識についてはCNNを組むほうが良いというのが一般的ですが、
12月段階で勉強がおっついておらず、MNISTの文字認識と同じように
28x28の入力からすべて全結合層の４層ニューラルネットワークを組んでしましました。

実際のChainerのコード

``` train.py
from six.moves import cPickle
import numpy as np
import argparse
from chainer import cuda, Variable, FunctionSet, optimizers
import chainer.functions  as F
import matplotlib.pyplot as plt
import pdb
import six


def unpickle(file):
    fo = open(file, 'rb')
    dict = cPickle.load(fo)
    fo.close()
    return dict

all_data = unpickle("./data.pkl")
x_all = np.asarray(all_data["data"]).astype(np.float32)
y_all = np.asarray(all_data["label"]).astype(np.int32)

x_train, x_test = np.split(x_all, [18000])
y_train, y_test = np.split(y_all, [18000])

## Build Model
model = FunctionSet( 
  l1 = F.Linear(784, 200),
  l2 = F.Linear(200, 80),
  l3 = F.Linear(80, 20),
  l4 = F.Linear(20, 2)
)

optimizer = optimizers.SGD()
optimizer.setup(model)

def forward(x_data, y_data):
  x = Variable(x_data)
  t = Variable(y_data)
  h1 = F.relu(model.l1(x))
  h2 = F.relu(model.l2(h1))
  h3 = F.relu(model.l3(h2))
  y = model.l4(h3)
  return F.softmax_cross_entropy(y, t), F.accuracy(y, t)

accuracy_data = []

batchsize = 1000
datasize = 18000  
for epoch in range(40):
  print('epoch %d' % epoch)
  indexes = np.random.permutation(datasize)
  for i in range(0, datasize, batchsize):
    x_batch = x_train[indexes[i : i + batchsize]]
    y_batch = y_train[indexes[i : i + batchsize]]
    optimizer.zero_grads()
    loss, accuracy = forward(x_batch, y_batch)
    loss.backward()
    accuracy_data.append(accuracy.data)
    optimizer.update()

sum_loss, sum_accuracy = 0, 0

plt.plot(accuracy_data, 'k--')
plt.show()
plt.savefig("accuracy.png")

for i in range(0, 3000, batchsize):
  x_batch = x_test[i : i + batchsize]
  y_batch = y_test[i : i + batchsize]
  loss, accuracy = forward(x_batch, y_batch)
  sum_loss      += loss.data * batchsize
  sum_accuracy  += accuracy.data * batchsize

mean_loss     = sum_loss / 3000
mean_accuracy = sum_accuracy / 3000

print('mean_loss %.2f' % mean_loss)
print('mean_accuracy %d' % (mean_accuracy * 100))

if (mean_accuracy * 100) > 90:
  with open('trained_model.pkl', 'wb') as output:
    six.moves.cPickle.dump(model, output, -1)
    print "model has saved, it has enough quality as trained model :)"
```

学習したモデルは、pickleを使って、`trained_model.pkl`としてシリアライズしています。

## 検出

本を買いました！

![IMG_3287.JPG](https://qiita-image-store.s3.amazonaws.com/0/57586/e71efb1b-9c72-0964-d930-63e7d794d46e.jpeg)

画像をスキャンして ... 

![3.jpg](https://dl.dropboxusercontent.com/u/5738192/img/3.jpg)

検出のプログラムを書いてみました。

```detect.py
import cv
import cv2
import numpy as np
from chainer import cuda, Function, FunctionSet, gradient_check, Variable, optimizers, utils
import chainer.functions as F
import six
import argparse
import sliding_window as sw
from PIL import Image
from PIL import ImageOps

parser = argparse.ArgumentParser(
    description='A Neural Algorithm of Artistic Style')
parser.add_argument('--img', '-i', default='',
                    help='path of input image')
args = parser.parse_args()

with open('trained_model.pkl', 'rb') as model_pickle:
  model = six.moves.cPickle.load(model_pickle)

ratio = 0.7

def forward(x_data):
  x = Variable(x_data)
  h1 = F.relu(model.l1(x))
  h2 = F.relu(model.l2(h1))
  h3 = F.relu(model.l3(h2))
  y = F.softmax(model.l4(h3))
  return y

img = Image.open(args.img)
(iw, ih) = img.size
print iw
print ih

new_width = int(iw*ratio) 
new_height = int(ih*ratio)
print new_width
print new_height

original_img = cv2.imread(args.img)

original_img = cv2.resize(original_img, (new_width, new_height))

img = cv2.cvtColor(original_img, cv2.cv.CV_BGR2GRAY)
img = np.asarray(img).astype(np.float32) / 255.

def judge(array, r, c, wsize):

  xd = np.asarray(array).reshape((1,784)).astype(np.float32)
  yd = forward(xd).data[0]
  prob = yd[1]/(yd[0]+yd[1])
  threshold = 0.8
  if(prob > threshold):
    print "------ detected (" + str(c) + "," + str(r) + ")  P(" + str(prob) + " )-------------"
    cv2.rectangle(original_img, (c, r), (c+wsize, r+wsize), (0,255,0), 3) 

sw.slide_window(img, 28, 14, judge)

cv2.imwrite('./result.png',original_img)
cv2.imshow('img',original_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

```

やっていることは...
1. 学習させたモデルの読み込み
2. 画像を28x28が顔の大きさになるように縮小（ratioでハードコーディング) 
3. Sliding Windowで28x28の窓を走らせ、ウォーリーだったら四角を元画像に書きこむ
4. 元画像 + 四角 を画像として書き出し

結果は以下のとおりです。

![result.jpg](https://dl.dropboxusercontent.com/u/5738192/img/result.png)

黄緑色の四角が検出した場所です。

**失敗!!** 

## なぜ失敗？ 自分なりに理由を考えた

時間的に余裕がなかったので、理想的にどうするべきかを踏まえていくつか。

- 本当は１冊自力でウォーリーを探して、それを正解データに含めるべきだった。
  - 正解データが全部真正面で汎化性能落ちてるはず
  - しましまに反応しているのは、ウォーリーの正解データの2/5が背景ボーダだったからかなと思う
- 本当は、正解データを増やすプログラムも自分で書くべきだった
  - 色データ残したかった
  - ホワイトノイズ加えたかった
  - Z軸の回転による歪み（opencv_createsamplesがやってくれる）を加えられたら、顔の向きに対する汎化性能あがった気がする
- モデルはやっぱりCNNにするべきだった
  - FCNNは過学習が起きやすいらしいじゃないですか
  - ChainerのConvoltional2D の引数の関係性が理解できなかったから、もっと勉強しなきゃ
- 検出プログラムにイメージハイラルキーを実装したほうがよかった
  - 窓の大きさがヒューリスティックに決まっていたのがちょっと。 

## 最後に

もっと時間が確保できたら、失敗した理由をきちんと拭って、ちゃんとウォーリー検出器つくりたいです。

現状のコードは、[Github](https://github.com/shunter1112/wally_detector)にアップしています。

良かったらコメントいただけると幸いです。