OpenCVを用いた逆光画像の判定

## はじめに
こんにちは、kanuu0504 と申します。
ABEJAでInsight for Retailという小売業向けサービスのカスタマーサポートを担当しています。

最近、Netflixで観た[ゲームチェンジャー](https://www.netflix.com/jp/title/81157840)というドキュメンタリーの影響を受けてビーガン生活を送っています。
でも、たまに二郎とか家系とかコッテリ系ラーメンを食べてしまうので、中々体重が減りません。

エンジニアではないのですが、頑張って書いていきます。
尚、こちらはABEJA Advent Calendar 2019 13 日目 の記事です。


## きっかけ
ABEJAではエンジニアが本当に楽しそうにものづくりをしているなぁと感じます。

勿論仕事もそうですが、仕事に関係ないものもあります。
例えば、[株AI](https://qiita.com/peisuke/items/f836aa245eb54b703cee)とか[IoTデバイスを使ったしいたけ栽培](https://twitter.com/xecus/status/1197158668073979904)とか。

2018年にABEJAにジョインして、日々彼らと一緒に仕事をしているうちに、僕自身も何かつくってみたいという気持ちが大きくなってきました。

また、ABEJAは[テクノプレナーシップ](https://tech-blog.abeja.asia/entry/technopreneurship)を大切にしていて、ビジネスメンバーでもテクノロジーを活用してこそイノベーションを起こせるのだという考えが浸透しているというのもありました。

## Insight for Retailというサービスのカスタマーサポートの役割
ところで、僕が担当しているInsight for Retailというサービスを非常にざっくりと説明すると、

・小売店舗にカメラなどのIoTデバイスを設置してデータを取得する
・取得したデータをAIが解析する
・解析したデータを集計して分かりやすく可視化する
・そして、データドリブンな店舗運営を実現する

というものです。

その為、サービス導入の際には店舗でのIoTデバイスの設置が必ず発生するのですが、その施工作業もカスタマーサポートの役割に含まれます。
もっというと「IoTデバイスから解析に適した綺麗なデータをAIに提供する」というところがInsight for Retailのカスタマーサポートの重要なミッションの一つなのです。

（本記事とは関係ないですが、新たなIoTデバイスの調査や検証などもカスタマーサポートがやったりします。勿論、カスタマーサポートの本分であるお問い合わせ対応やリリース告知なども。なので、普通のSaaS企業のカスタマーサポートと比べると大分守備範囲が広いと感じています。この辺りも機会があればご紹介したいと思います。）

## 逆光の検知がしたい
さて、カスタマーサポートとして日々IoTデバイスが取得したデータを扱っていると、本当にリアル空間でのデータ取得は難しいのだなと実感します。
例えば、逆光。

以下は今年の夏に友人と富士山に登った時の写真です。ご来光の逆光が凄い。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/b006922a-dc76-82b4-8556-5ac89535b740.jpeg" width="300px">

Insight for Retailのサービスの一つに来店者の顔画像を基に年齢性別やリピーターかどうかをAIが推定するというものがあるのですが、こんな真っ黒な顔画像だと、どんなに優れたAIモデルでも精度が出ないですよね。

実際の店舗でここまでの逆光が発生するケースは稀ですが、逆光で顔が暗くなってしまう事象はたまに発生します。
しかも、逆光は時間によって出たり出なかったりするので、設置時に検知することが難しいのです。

なので、逆光の検知を自動化する仕組みをつくれば、仕事にも役立つし、且つプログラミングの勉強になるし、更には画像処理の勉強にもなるなと思いやってみることにしました。

## 逆光とはどのような状態なのか
そもそも逆光とはどのような状態なのでしょうか。
これを知るために、逆光画像とそうでない画像のヒストグラムを描画してみます。

ヒストグラムの説明は[OpenCVのチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_histograms/py_histogram_begins/py_histogram_begins.html)が分かりやすいですが、ざっくりと説明すると画像中の各ピクセルが持つ画素値という値の分布を表示したものです。
画素値は明るいと大きい値に、暗いと小さい値になるため、ヒストグラムを見ると画像中の明暗の分布が分かります。

（サラッと書きましたが、ここに辿り着くために画像処理の本を読んだり、エンジニアにOpenCVという便利な画像処理・CVのライブラリがあることを教えてもらったりと色々ありました。。。）

ヒストグラムを描画したコードは以下です。

```python

import cv2
import numpy as np
from matplotlib import pyplot as plt

# 標準入力
filename = input()

# 画像を読み込む
img = cv2.imread(filename)

# 画像のヒストグラムを描画
plt.hist(img.ravel(),256,[0,256]); plt.show()
```


まずは逆光画像のヒストグラム。画像は先ほどの富士登山の写真の顔部分をトリミングしました。
逆光画像は顔が暗くなるため、画素値が小さい方に偏っています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/9fe1377c-3f74-5368-d2bd-66556c416e2a.jpeg" width="150px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/0fe609fc-df5c-fc7f-4f3d-76bac76f4777.png" width="300px">

次に逆光ではない画像のヒストグラム。7年前の僕です。
こちらの画像は逆光画像と比較すると画素値がまんべんなく分布しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/05567a9a-fd2e-d8ed-8650-ef266a7a2fa3.jpeg" width="150px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/f2b1b1d4-0e35-4202-7e03-e6d728ff6034.png" width="300px">


なので、画像の画素値の中央値或いは平均値がある値よりも小さい場合に逆光と判定できるのではないかという仮説を立てました。

## 顔が明るくなる逆光もある
しかし、Webで逆光画像を検索していると、こんなケースがあることが分かりました。
（手持ちの画像で良いものが無かったのでPIXTAで購入。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/6f91b858-9cfa-5241-b5f7-d3861c411667.jpeg" width="300px">

この逆光画像は逆に顔が明るくなりすぎて、これはこれで判定精度に影響しそうです。

ヒストグラムは以下のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/68260c42-52b9-9b6b-ea1a-d1f681d0aa38.png" width="300px">

今度は画素値が大きい方に偏っています。
先ほどの画素値の平均値の多寡で判定するという作戦は無理そうです。
さてどうするか。

ここで改めて各画像のヒストグラムを見てみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/ba4a175f-bee0-9e55-6ff6-3e0f854eac7c.png">

これを見るとどちらのケースの逆光画像も、逆光でない画像と比較して画素値の分布に偏りがあります。
ということは、この偏り具合で逆光を判定できるのではないかと思いました。
具体的には、画像の画素値の標準偏差（データの散らばりの度合いを示す値）を計算すれば良さそうです。

## 画像の画素値の標準偏差を基に逆光判定する
ということで、OpenCVを用いて画像の画素値の標準偏差を計算してみます。

コードはこちら。
OpenCVはこういう計算も数行のコードで書けてすごく便利です。

```python

from imutils import paths
import argparse
import cv2
import os
import numpy as np

# 標準入力
filename = input()

# 画像を読み込む。
img = cv2.imread(filename)

# 画像をグレースケールにする。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 標準偏差を計算する。
mean, stddev = cv2.meanStdDev(gray)
print('stddev',stddev)

```

結果は以下の通りです。

まずは、逆光ではない画像から。こちらの標準偏差は<font color="Red">55.6</font>でした。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/05567a9a-fd2e-d8ed-8650-ef266a7a2fa3.jpeg" width="200px">


次に暗い顔になる逆光。標準偏差は<font color="Red">32.0</font>でした。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/9fe1377c-3f74-5368-d2bd-66556c416e2a.jpeg" width="200px">

最後に明るい顔になる逆光。標準偏差は<font color="Red">36.7</font>でした。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556409/6f91b858-9cfa-5241-b5f7-d3861c411667.jpeg" width="200px">

まとめると、
・逆光でない画像：標準偏差は<font color="Red">55.6</font>
・逆光画像（暗い顔）：標準偏差は<font color="Red">32.0</font>
・逆光画像（明るい顔）：標準偏差は<font color="Red">36.7</font>

ということで、閾値を40-50辺りに設けて、「標準偏差が閾値以下の画像は逆光とする」ことで判定ができそうです。
まだまだ本番環境に適用するには閾値の調整以外にも画像から顔だけを切り取る処理を入れたり課題は多いですが、それは追々。

## やってみた感想
これまでプログラミングの勉強をしようと思ったときは、技術書を買ってきて1ページ目から読む、なんてことをして10ページくらいで挫折するということを繰り返してきたのですが、今回のようにテーマを持って実際につくってみると身につきやすいし、何より試行錯誤も含めて楽しく取り組めるのだなぁと感じました。
プログラミングは実践してナンボということですね。

また、ABEJAのエンジニアは分からないことがあると皆優しく教えてくれるので、ビジネスメンバーがテクノロジーを身に付けるにはとても良い環境だと改めて感じました。多謝。
今後もテーマを決めて、継続して勉強していこうと思います。