物体検出とボケ検出で一眼レフ風の料理写真候補抽出

この記事は [Retty Inc. Advent Calendar 2017](https://qiita.com/advent-calendar/2017/retty) 7日目です。
昨日は @ryota-yamamoto さんによる [Vue.jsでコンソールに絵を描く ~ ES6を添えて ~](https://qiita.com/ryota-yamamoto/items/3e4aa5c143195ccc5326) でした。

#はじめに
こんにちは、Rettyでエンジニアをしておりますtamatsu(@koji-t )と申します。
去年[【Darknet】リアルタイムオブジェクト認識 YOLOをTensorflowで試す](https://qiita.com/koji-t/items/a33bc8297c9a03b38c74)という記事を書いたので今年はこのオブジェクト認識と画像のピンぼけ情報を使って一眼レフ風の料理写真を抽出したいと思います。

#一眼レフ風の料理写真とは？
抽出目的の一眼レフ風の料理写真とは以下のような写したい料理部にピントが合っており、背景やその他の部分はボケている写真のことです。
また、良い料理写真として料理の配置や、大きさも重要となってきますね。

<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/ace3d2c4-78b0-cb56-7f6d-64913a00df0c.jpeg" width=26.57%> <img src="https://qiita-image-store.s3.amazonaws.com/0/121696/28956eec-8ada-f1e5-e6f5-5e061a540878.jpeg" width=30%> <img src="https://qiita-image-store.s3.amazonaws.com/0/121696/2f3ed2a5-b55f-2391-5800-12c1d6ec17c0.jpeg" width=30%>

美味しそう。。。

#抽出する際の手がかり
さて、このような画像を見つけるために、重要なことは

- 背景がボケているか
- 料理の位置
    - 構図は色々あるかと思いますが、今回は料理位置が画像の中心付近、或いは下半分に来ているもの
- 画像内に占める料理の面積比
    - アップしすぎや遠くに写っている物は弾きたい

などが挙げられます。(本当は色鮮やかだとか光のあたり具合が、、とかありますが一旦無視で)


#抽出方法
上記のような特徴を探すために今回は、Object detection(物体検出)とBlur detection(ボケ検出)という技術を用います。
簡単に説明すると物体検出とは画像内に「何がどこに写っているか」を求める技術で、ボケ検出は、その画像がボケているかどうかを判断するような技術となります。

つまり、今回やることは、**物体検出でいい感じの場所に料理が写った画像を探し、その中からボケ検出で料理周辺がボケた画像を探す**。ということです。

ちなみに、Object detectionには[Darknet YOLO](https://pjreddie.com/darknet/yolo/)を用い、[Blur detection](https://www.pyimagesearch.com/2015/09/07/blur-detection-with-opencv/)にはOpenCVを用います。

#Darknet YOLOで料理検出 -学習-
まずは機械が料理を探せるように「料理とはなんぞや」を機械に学習させます。
現在Darknetはありがたいことに、Tensorflow版、chainer版、keras版、PyTorch版など、非常に多くのライブラリで試すことができます。
その中で今回僕が学習に使ったのは、「**本家**」です。はい、学習には安心と安全の本家を使ってみました。
学習の仕方はざっくり言うと、学習用の画像と、その画像内のどこに何が(今回は料理のみ)写っているのかというアノテーション情報が記載されたtxtファイル(教師データ)を用意し、cfgというファイルにネットワークの構成などを記載し、コマンドを叩いて待つ！という感じです。詳細は[本家サイト](https://pjreddie.com/darknet/yolo/)をご覧ください。

学習用の画像はRettyのDBの中にある大量の画像からランダムに2,000枚選び、[BBox-Label-Tool](https://github.com/puzzledqs/BBox-Label-Tool)という便利なツールで以下のように手動でひたすら教師データを作りました。

![label.gif](https://qiita-image-store.s3.amazonaws.com/0/121696/a71f96e5-721b-527e-2403-9152b3578c5b.gif)

マウスのドラッグ・アンド・ドロップでひたすら料理を囲っていく作業です。

学習が終わるとweightsファイルというモデルファイルが出来上がります。
気になる精度ですが、**すいません**、実はこれ昔作ったモデルで学習精度は覚えていません、、

このモデルを用いて、新たな画像に料理がどこに写っているのかなどを調べてみましょう。

#Darknet YOLOで料理検出 -テスト-
モデルが出来上がったので、実際に料理が検出できるか試してみます。以下に用意した10枚の画像に対して試してみます。
この10枚の中には一眼レフ風の画像も混ぜて、後に上手く抽出できるかも見てみます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/1d8eaf7d-9664-2067-0e2b-28af95448538.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/1b52c180-0e8c-4ccc-bb74-0d6091fa5640.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/5bd17ee3-9c19-9e9c-b4fb-ab0e9465e6ac.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/38b4ae8b-5772-fa5a-e61d-6fed7b4354de.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/234ccfdd-de0d-71cb-0e2e-551bbc6980a5.jpeg" width=20%>

<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/8cd75cbb-f957-344f-3fce-6d28d61faf27.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d0db0fea-fed0-c31d-1ec4-f46890bcecb5.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/52200f10-eff3-105f-76ec-da9ffa34afd6.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/a36828d4-04d5-20b3-67aa-09b4b26d3824.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/9290fbe1-c064-3894-8657-ac66c398d2f2.jpeg" width=20%>



##実行環境
学習には本家を使いましたが、実行にはTensorflow版の[darkflow](https://github.com/thtrieu/darkflow)というものを使いました。
こちらは本家のモデルとcfgファイルをそのまま使用することができ、使うのも簡単です。以下のコードを実行すると指定したフォルダ内の画像に対して結果を出力することが出来ます。

```python:yolo.py
from darkflow.net.build import TFNet
import cv2
from imutils import paths
import numpy as np

options = {"model": "cfg/yolo-face.cfg",
           "load": "yolo-face_30000.weights", "thresulthold": 0.3, "gpu": 0.5}

tfnet = TFNet(options)

input_images = './input_images'
output_images = './output_images/'

for image_path in paths.list_images(input_images):
    image_name = image_path.split('/')[-1]
    if image_name[0] == ".":
        continue
    img = np.asarray(cv2.imread(image_path))
    result_image = img
    results = tfnet.return_predict(img)

    for result in results:
        if not result['confidence']:
            continue
        print(image_name, result)
        result_image = cv2.rectangle(
            img, (result['topleft']['x'], result['topleft']['y']), (result['bottomright']['x'], result['bottomright']['y']), (0, 0, 255), 5)
        cv2.putText(result_image, str(result['confidence']), (result['topleft']['x'], result[
                    'topleft']['y'] + 30), cv2.FONT_HERSHEY_TRIPLEX, 1, (0, 0, 255))
    cv2.imwrite(output_images + image_name, result_image)
```
optionsに使うモデルや、検出の際の閾値、gpuの使用率などを指定することができ、tfnet.return_predictで画像に対して物体検出が出来ます。

↑の10枚の画像に対して行ってみた結果が以下です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/1df0b43c-dac5-5689-5511-14b5567f5842.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/87cc4160-fb28-6bda-d3b5-42c8de9b1f73.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/4a49531b-b718-b408-a136-90995f25b989.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d82525e8-053e-ba3f-4267-f6a8e812a2d6.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/e5add72d-ceec-8b7d-10ec-ca4a453fe195.jpeg" width=20%>

<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/4a0eb653-2550-6d72-d902-24622b58cb6d.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/9c5de4b9-d173-15f3-4f03-a8c0a619be24.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/7be89575-310b-715f-f5b5-618ea1fd806e.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d186956c-1166-c96b-aaf7-c9190f889059.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/9290fbe1-c064-3894-8657-ac66c398d2f2.jpeg" width=20%>



左上の数字はaccuracyです、上手く料理を検出出来ていることがわかります。
ちなみに10枚目の画像はRettyのオフィスの写真です。

# ボケ検出
次にボケ検出です。ボケ検出には[Blur detection with OpenCV](https://www.pyimagesearch.com/2015/09/07/blur-detection-with-opencv/)を用います。
これは画像をグレースケールに変換し、3 x 3のラプラシアンカーネルで畳み込んで標準偏差の二乗を計算し、その値によってボケているかどうかを判断するというものです。
`cv2.Laplacian(image, cv2.CV_64F).var()`という1行で計算出来ます。
結果が低いほどボケているらしいのですが、今回は複数の画像で試して調整し、閾値を100にしました。

ボケ検出の使い方ですが、画像のどこがボケているのかという情報が欲しいので、画像を正方形にリサイズ後64分割し、分割した領域一つ一つに対してボケているか判断します。最終的にボケた領域が画像の周辺部に多いものが一眼レフ風画像の候補となります。

先程の10枚の画像をそれぞれ64分割し、ボケた領域を白で塗りつぶしたものが以下となります。
<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/6e939159-0f21-fd78-b6f5-bb659ef69ccb.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/da20ccc0-706b-7fb1-c46e-5a1cdf7d6427.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/0ec171ef-2c20-a557-9b30-771fec24391d.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/e627fbcd-b250-2713-964a-78c92c5640c0.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/38ffe749-b5a7-4108-1200-4d33c3323a52.jpeg" width=20%>

<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d56a9b1b-107a-0500-54ae-e2a35fea30ca.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/630d0b2a-90fb-4796-b44f-2c46a7ec7a5a.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/32d7754f-6ee2-1c7e-0963-a588680a9189.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/8dc40019-cab2-8335-2e43-45297417c177.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/23a94185-eee4-9400-97e1-05439fa8808d.jpeg" width=20%>

どうでしょうか、上段左から二枚目のように紛らわしい物もありますが一眼レフ風の画像は綺麗に背景部分が白で塗りつぶされた気がします。

#物体検出とボケ検出２つを組み合わせて一眼レフ風画像抽出
物体検出で料理を検出した10枚のうち、料理の位置、画像内に占める料理の面積比を考慮すると生き残る画像は以下になりました。
<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/1df0b43c-dac5-5689-5511-14b5567f5842.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/87cc4160-fb28-6bda-d3b5-42c8de9b1f73.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/e5add72d-ceec-8b7d-10ec-ca4a453fe195.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/4a0eb653-2550-6d72-d902-24622b58cb6d.jpeg" width=20%><img src="https://qiita-image-store.s3.amazonaws.com/0/121696/7be89575-310b-715f-f5b5-618ea1fd806e.jpeg" width=20%>
<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d186956c-1166-c96b-aaf7-c9190f889059.jpeg" width=20%>


ここから料理周辺部がボケた画像を絞り込むと以下の２つが生き残りました。
<img src="https://qiita-image-store.s3.amazonaws.com/0/121696/7be89575-310b-715f-f5b5-618ea1fd806e.jpeg" width=20%> <img src="https://qiita-image-store.s3.amazonaws.com/0/121696/d186956c-1166-c96b-aaf7-c9190f889059.jpeg" width=20%>
お寿司の画像は残念ながら検出された料理の面積が大きく、弾かれてしまいましたね。

#まとめ
どうでしょうか？今回内部で使った、面積比の値や中心位置、ボケ領域の分析部分は省きましたが、パラメータを調整することで確実に微妙な写真を弾くことが出来ます。
また、あくまで候補を抽出するのが今回の主題であり画像が多くなってくると例外も多く、機械のみで一発で良い画像だけに絞り込むようことはまだまだ出来ておらず、最後には人の手を借りることになるのが実情です。が、膨大な画像から対象外を一気に取り除くことが出来るので画像を選別する際の効率化には便利な技術でした。

皆さんも気になったら遊んでみてはいかがでしょうか！

明日は、 @saku の「swiftでHTMLからNSAttributedStringを作る方法」です、お楽しみに！