TensorFlow Object Detection APIを使ったロゴ検出

# はじめに
2017年7月に発表されたTensorFlow Object Detection APIを使ってロゴ検出をできるようにしてみます。
[以前に物体検出を試したときは](http://qiita.com/ozw/items/ac50ee4c0ad893ac3dd6)、用意されていた学習済みデータを使用しましたが、今回は教師データの作成からやってみます。

# 手順
1. 教師データの作成
2. 学習
3. 検出テスト

の順で進めていきます。

# 検出するロゴ
今回は僕が勤務するgifteeのロゴ１種類のみ検出させてみます。
![gifteelogo.png](https://qiita-image-store.s3.amazonaws.com/0/99106/d9d50a05-ca59-ce96-8644-c81eabee3742.png "gifteelogo.png")


# 教師データの作成
## イメージの収集
Google画像検索を使って、ロゴが含まれたイメージを収集します。
今回は40個ほどのイメージを集めました。

## 矩形選択とラベリング
TensorFlow Object Detection APIはイメージに含まれる物体を検出するので、
その物体を含む矩形とその物体のラベル教えるためのデータが必要です。
今回は[labelImg](https://github.com/tzutalin/labelImg)を使ってそのデータを作成します。

### labelImg
最初はmac上で動かそうと試してみましたが、公式の手順どおりにやってみてもなかなかうまくいかなかったので、Ubuntu上で動かしました。
かなり変則的ですが、

* mac上のvagrantでUbuntuを動かす
* Ubuntu上でlableImgをセットアップ、実行
* labelImgのGUIはvagrantのX11転送機能とXQuartzを使ってmac上で動かす

という手順を踏みました。

vagrantのX11転送はVagrantfileに

```
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ...
  config.ssh.forward_x11 = true
  ...
end
```
と書くと有効になります。

labelImgはイメージに格納されたディレクトリを指定して、ロゴデータを矩形選択して、
ラベルを付けていきます。

![labelImg_ss.png](https://qiita-image-store.s3.amazonaws.com/0/99106/4d188030-96ae-c3f1-3ed7-d5ddc21708a4.png "labelImg_ss.png")


Use Default labelにデフォルトのラベル名を入れておくと作業がだいぶ楽になります。

矩形選択とラベルの設定が終わったらsaveを押してXMLデータで保存しておきます。

## TFRecordへの変換
TensorFlowにデータを読み込ませるためにイメージとXMLをTFRecord形式に変換します。
今回は[チュートリアル](https://github.com/tensorflow/models/blob/master/object_detection/g3doc/running_pets.md)にある、[create_pet_tf_record.py](https://github.com/tensorflow/models/blob/master/object_detection/create_pet_tf_record.py)を修正して、変換スクリプトを作ります。

主な修正点は元のスクリプトではラベル名をファイル名から取得するようになっていたのを、決め打ちで「giftee」ラベルを返すようにしたところです。

```py
#class_name = get_class_name_from_filename(data['filename'])
class_name = 'giftee'
```
それ以外はほぼ修正なしで使用しています。

### labelmap.pbtxt作成
今回は１種類のみなのでitemをひとつだけ書いておきます。

```
item {
  id: 1
  name: 'giftee'
}
```
giftee_label_map.pbtxtとして保存しておきます。

### 変換スクリプト実行
変換スクリプトを実行します。

ここからの手順はほぼ、[チュートリアル](https://github.com/tensorflow/models/blob/master/object_detection/g3doc/running_pets.md)に沿って行っていきます。


```bash
$ python object_detection/create_giftee_tf_record.py --label_map_path=object_detection/data/giftee_label_map.pbtxt --data_dir=`pwd` --output_dir=`pwd`
```
実行した階層に学習用データの「giftee_train.record」と評価用データの「giftee_val.record」が作成されます。

## config作成
modelのパラメータや学習時のlearning rate,tfrecordファイルのパスやラベルテキストのパスなどを設定するconfigを作成します。

今回は[チュートリアル](https://github.com/tensorflow/models/blob/master/object_detection/samples/configs/faster_rcnn_resnet101_pets.config)のものを一部修正して使用します。

```
# Faster R-CNN with Resnet-101 (v1) configured for the Oxford-IIIT Pet Dataset.
# Users should configure the fine_tune_checkpoint field in the train config as
# well as the label_map_path and input_path fields in the train_input_reader and
# eval_input_reader. Search for "PATH_TO_BE_CONFIGURED" to find the fields that
# should be configured.

model {
  faster_rcnn {
    num_classes: 1 
    image_resizer {
      keep_aspect_ratio_resizer {
        min_dimension: 600
        max_dimension: 1024
      }
    }
    feature_extractor {
      type: 'faster_rcnn_resnet101'
      first_stage_features_stride: 16
    }
    first_stage_anchor_generator {
      grid_anchor_generator {
        scales: [0.25, 0.5, 1.0, 2.0]
        aspect_ratios: [0.5, 1.0, 2.0]
        height_stride: 16
        width_stride: 16
      }
    }
    first_stage_box_predictor_conv_hyperparams {
      op: CONV
      regularizer {
        l2_regularizer {
          weight: 0.0
        }
      }
      initializer {
        truncated_normal_initializer {
          stddev: 0.01
        }
      }
    }
    first_stage_nms_score_threshold: 0.0
    first_stage_nms_iou_threshold: 0.7
    first_stage_max_proposals: 300
    first_stage_localization_loss_weight: 2.0
    first_stage_objectness_loss_weight: 1.0
    initial_crop_size: 14
    maxpool_kernel_size: 2
    maxpool_stride: 2
    second_stage_box_predictor {
      mask_rcnn_box_predictor {
        use_dropout: false
        dropout_keep_probability: 1.0
        fc_hyperparams {
          op: FC
          regularizer {
            l2_regularizer {
              weight: 0.0
            }
          }
          initializer {
            variance_scaling_initializer {
              factor: 1.0
              uniform: true
              mode: FAN_AVG
            }
          }
        }
      }
    }
    second_stage_post_processing {
      batch_non_max_suppression {
        score_threshold: 0.0
        iou_threshold: 0.6
        max_detections_per_class: 100
        max_total_detections: 300
      }
      score_converter: SOFTMAX
    }
    second_stage_localization_loss_weight: 2.0
    second_stage_classification_loss_weight: 1.0
  }
}

train_config: {
  batch_size: 1
  optimizer {
    momentum_optimizer: {
      learning_rate: {
        manual_step_learning_rate {
          initial_learning_rate: 0.0003
          schedule {
            step: 0
            learning_rate: .0003
          }
          schedule {
            step: 900000
            learning_rate: .00003
          }
          schedule {
            step: 1200000
            learning_rate: .000003
          }
        }
      }
      momentum_optimizer_value: 0.9
    }
    use_moving_average: false
  }
  gradient_clipping_by_norm: 10.0
  #fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED/model.ckpt"
  from_detection_checkpoint: true
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
}

train_input_reader: {
  tf_record_input_reader {
    input_path: "PATH_TO_BE_CONFIGURED/giftee_train.record"
  }
  label_map_path: "PATH_TO_BE_CONFIGURED/giftee_label_map.pbtxt"
}

eval_config: {
  num_examples: 2000
}

eval_input_reader: {
  tf_record_input_reader {
    input_path: "PATH_TO_BE_CONFIGURED/giftee_train.record"
  }
  label_map_path: "PATH_TO_BE_CONFIGURED/giftee_label_map.pbtxt"
  shuffle: false
  num_readers: 1
}
```
主な変更点はclass_numを1にしたのと、ファイル名の変更、
fine_tune_checkpointのコメントアウトくらいで、
それ以外はサンプルのものを使用しています。

fine_tune_checkpointは既に学習済みのデータが存在していたときに
使うためのものみたいですが、今回は０から学習するのでコメントアウトしてます。

PATH_TO_BE_CONFIGUREDは今回はGoogle Cloud Storageを使用しているので、
チュートリアルどおりにそのパスで置換しました。

```bash
$ sed -i "s|PATH_TO_BE_CONFIGURED|"gs://${YOUR_GCS_BUCKET}"/data|g" \
    object_detection/samples/configs/faster_rcnn_resnet101_giftee.config
```

## Google Compute Cloudの設定
### Google Cloud Storage
ここまでに作成したファイルをGoogle Cloud Storageにアップロードします。
アップロード後のパスは下記のようになりました。

```
+ ${YOUR_GCS_BUCKET}/
  + data/
    - faster_rcnn_resnet101_giftee.config
    - giftee_label_map.pbtxt
    - giftee_train.record
    - giftee_val.record
```

## Google Cloud ML Engine
Google Cloud ML Engineで使用するファイルを用意します。
チュートリアルの通りにmodelsの階層で

```bash
$ python setup.py sdist
$ cd slim && python setup.py sdist
```
とすると、必要なファイル

* dist/object_detection-0.1.tar.gz
* and slim/dist/slim-0.1.tar.gz.

が生成されます。

# トレーニング
Google Cloud ML Engineを使ってトレーニングを実行します。

最初macローカルでトレーニングをやってみましたが、余りに時間がかかりすぎて断念しました。
ML Enigneに変更してみたら30倍くらい高速になりました。

コマンドはチュートリアルを参考に下記を実行します。

```bash
# From tensorflow/models/
$ gcloud ml-engine jobs submit training `whoami`_object_detection_`date +%s` \
    --job-dir=gs://${YOUR_GCS_BUCKET}/train \
    --packages dist/object_detection-0.1.tar.gz,slim/dist/slim-0.1.tar.gz \
    --module-name object_detection.train \
    --region us-central1 \
    --config object_detection/samples/cloud/cloud.yml \
    -- \
    --train_dir=gs://${YOUR_GCS_BUCKET}/train \
    --pipeline_config_path=gs://${YOUR_GCS_BUCKET}/data/faster_rcnn_resnet101_giftee.config
```

```
`whoami`_object_detection_`date +%s`
```
の部分はjobの名前です。

```bash
$ echo `whoami`_object_detection_`date +%s`
```
とすると、どんな名前が付けられるのかが確認できます。

--job-dir,--train_dirには学習経過を格納するディレクトリを指定します。
一定時間置きにデータが更新されていきます。

--packagesには先ほど作成したtar.gzファイルを指定します。

--configではjobの数やスペックなどを変更できるようです。今回はデフォルトのものを使用します。

このコマンドを実行するとML Engine上でjobが動きます。
GCPのML Engineでjobの状態やログなどを確認できます。

## 学習時間
今回は２時間くらい動かして、30000ステップのところで停止しました。
GCPの料金的には4000円くらいかかってしまいました。。


# eval
学習したパラメータをもとに、学習に使用していないイメージを使って評価を行います。
これもチュートリアルを参考に下記コマンドを実行して、ML Enigne上でjobを走らせます。

```bash
# From tensorflow/models/
$ gcloud ml-engine jobs submit training `whoami`_object_detection_eval_`date +%s` \
    --job-dir=gs://${YOUR_GCS_BUCKET}/train \
    --packages dist/object_detection-0.1.tar.gz,slim/dist/slim-0.1.tar.gz \
    --module-name object_detection.eval \
    --region us-central1 \
    --scale-tier BASIC_GPU \
    -- \
    --checkpoint_dir=gs://${YOUR_GCS_BUCKET}/train \
    --eval_dir=gs://${YOUR_GCS_BUCKET}/eval \
    --pipeline_config_path=gs://${YOUR_GCS_BUCKET}/data/faster_rcnn_resnet101_giftee.config
```

## tensorborad
tensorboardをつかうと、lossの推移などがグラフィカルに確認できます。
また、実際に学習したパラメータでロゴ判定した結果の画像もIMAGESタブで見ることができます。

tensorboardの起動はGoogle Cloud Storageのパスを与えて実行して、

```bash
$ tensorboard --logdir=gs://${YOUR_GCS_BUCKET}
```
ブラウザでlocalhost:6006にアクセスすると、推移を確認できます。



![TensorBoard.png](https://qiita-image-store.s3.amazonaws.com/0/99106/f9b30c02-1b3e-feeb-8f70-8d8545615c5c.png "TensorBoard.png")


# Tensorflow Graphへの変換
学習済みデータをつかってロゴの判定をさせるためには、一旦Tensorflow graphというものへの変換が必要です。

まず、Google Cloud Storageからローカルにデータをダウンロードして、

```bash
$ gsutil cp gs://${YOUR_GCS_BUCKET}/train/model.ckpt-${CHECKPOINT_NUMBER}.* .
```

変換スクリプトを実行します。

```bash
python object_detection/export_inference_graph.py \
    --input_type image_tensor \
    --pipeline_config_path object_detection/samples/configs/faster_rcnn_resnet101_giftee.config \
    --checkpoint_path model.ckpt-${CHECKPOINT_NUMBER} \
    --inference_graph_path output_inference_graph.pb
```

CHECKPOINT_NUMBERには学習データの中で数字が最大のものをいれればいいようです。

このスクリプトを実行すると変換された

* output_inference_graph.pb

が出力されます。



# ロゴ判定
いよいよロゴ判定を行ってみます。

判定用のpythonスクリプトは[こちらのチュートリアル](https://github.com/tensorflow/models/blob/master/object_detection/object_detection_tutorial.ipynb)をもとにJupyter上で実行していきます。

ほぼチュートリアル通りで進められますが、Tensorflow graphとラベルデータのパスの箇所を修正しておきます。

```py
# What model to download.
#MODEL_NAME = 'ssd_mobilenet_v1_coco_11_06_2017'
#MODEL_FILE = MODEL_NAME + '.tar.gz'
#DOWNLOAD_BASE = 'http://download.tensorflow.org/models/object_detection/'

# Path to frozen detection graph. This is the actual model that is used for the object detection.
PATH_TO_CKPT = 'data/output_inference_graph.pb'

# List of the strings that is used to add correct label for each box.
PATH_TO_LABELS = 'data/giftee_label_map.pbtxt'

NUM_CLASSES = 1
```

またモデルのダウンロードも行わないのでコメントアウトします。

```py
#opener = urllib.request.URLopener()
#opener.retrieve(DOWNLOAD_BASE + MODEL_FILE, MODEL_FILE)
#tar_file = tarfile.open(MODEL_FILE)
#for file in tar_file.getmembers():
#   file_name = os.path.basename(file.name)
#  if 'frozen_inference_graph.pb' in file_name:
#        tar_file.extract(file, os.getcwd())
```

最後にイメージパスを修正して、実行します。
判定に使うファイル名はimage1.jpg,image2.jpgとしておきます。

```
# For the sake of simplicity we will use only 2 images:
# image1.jpg
# image2.jpg
# If you want to test the code with your images, just add path to the images to the TEST_IMAGE_PATHS.
PATH_TO_TEST_IMAGES_DIR = 'gifteeimages'
TEST_IMAGE_PATHS = [ os.path.join(PATH_TO_TEST_IMAGES_DIR, 'image{}.jpg'.format(i)) for i in range(1, 3) ]

# Size, in inches, of the output images.
IMAGE_SIZE = (12, 8)
```

## 実行結果
幾つかのイメージでは下記のように正しくロゴが検出できました。

![jupyter_image1.png](https://qiita-image-store.s3.amazonaws.com/0/99106/0b2f4d5a-4f48-0e9b-98bb-09979500c289.png "jupyter_image1.png")

![jupyter_image2.png](https://qiita-image-store.s3.amazonaws.com/0/99106/e31b7b2b-5401-4fb2-59bf-8cda170bf4f1.png "jupyter_image2.png")

# 最後に
２時間ほどの学習でおもったより精度が高く判定ができるようになりました。
今回は教師データも40個程度と少なめだと思われるので、
教師データの増量と学習時間を長めに行えばさらに精度が良くなるのではないかと思います。
(GCPの利用料金がネックですが。。)