背景がグレーの画像から色のついた部分を抜き出す

#この記事でやること
画像中の灰色、黒、白の部分をすべて黒に塗りつぶす。それ以外の場所は元の色のままにする。

#背景が灰色の画像
そんな画像そもそもああるのか？というのはさておき、そんな需要が私にあった。
そこでその技術をまとめたい。
使う言語はPython。使うライブラリはcv2, numpy, matplotlib。
使うエディタはjupyter notebook。

#そもそも画像とは
画面に映っている画像は、そもそもは「x,y軸を持つ平面上に画素を設置し、R(赤)G(緑)B(青)の三色を振り分けたもの[^1]」である。基本的にはこのRGBの強さの塩梅を調整して色を出力している。

##「灰色(グレー)」とは
画像を扱う上で「灰色」についてどんな色なのか考察してみると灰色とは「RGBの値がそれぞれほとんど同じ値をとっている色」である。ここでいう「灰色」とは、ざっくりと「白と黒を混ぜた色」であり、白と黒もこの範疇に含める。

#どのように色のついた部分を抜き出すか
これは複数通りの考え方があるが、スタンダードな手法として「灰色の部分を区別する」もしくは「灰色ではない部分を区別する」というものがあげられる。これらは似ているようで、考え方や実際にプログラムを設計する際には異なる場合があったり、方針が迷子になってしまわないように、これを念頭に置いてプログラムを作成する必要がある。
今回は「灰色である部分を見つけ出す」という方針でプログラムを考えた。
これを使い、どの画素の色(画素値(R,G,B))を黒((R,G,B)=(0,0,0))にするか、というマスクを作る。そのマスクを使い、目的である「画像中の灰色、黒、白の部分をすべて黒に塗りつぶす」を達成する。

#灰色の特徴
灰色の特徴として、上にも挙げたように「RGBの値がそれぞれほとんど同じ値をとっている色」ということが考えられる。そこで、「RGBの色の強さの値が近いものを灰色として認識する」というプログラムを作成する。

#灰色の検出の指標
「RGBの色の強さの値が近いもの」としてどんな指標を考えるか。
考えられる指標はいくつかあるが今回はシンプルに「ある画像上の座標のRGBの3値の最大値・最小値の差が小さいもの」とした。本当は分散を考えたりするのもありだと思うが、データ上、扱いやすいので。

#プログラムの実装
まずはライブラリをインポートして、画像を読み込む。画像処理はopencvを使う。画像はプログラムファイルと同じディレクトリに`test.jpg`という名前で保存しているとした。画像はイラストやとフリー素材を組み合わせて作成。
##諸々のimport
```python:import

import opencv
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
original_image=cv2.imread("./test.jpg")
conv_img = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
```
実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/deef4655-1c91-da8f-f57d-4d33f0afe06e.png)
雪と戯れる犬です。かわいい。

##画像の確認
`conv_img`の画素を操作する。基礎事項としてopencvの画像の配列形式がどうなっているのか説明すると、
numpyの三次元array形式で画像の配列imgに対し、img[縦][横][色]の順に並んでいる。色については、デフォルトでは0:青, 1:緑, 2:赤の順番で並んでいるが、今回はpyplotで逐次表示させる都合上、0:赤, 1:緑, 2:青の順番に並び替えている[^2]。今回使っている画像は縦:621, 横:877である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/386f1056-3b28-f635-cede-8a46a9678ea7.png)

##画素の操作
ここで試しに赤の成分だけ表示してみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/81816255-74b6-d4b3-14bf-94c40821a310.png)

このようになっている。

## マスクの作成
ここからは、灰色の背景を除去するためのマスクの作成をする。
まず画像imgに対して座標(x,y)=(i,j)を考えると、img[j][i]にはRGBの三つ色の情報が含まれている。
これを利用して、マスクを作成する。方針で考えたように「RGBの最大値と最小値の差が小さい画素」を抽出するために「RGBの最大値と最小値の差」の二次元配列を作成する。

```python:prepare_mask

mask=np.zeros((conv_img.shape[0],conv_img.shape[1]))

for i in range(conv_img.shape[0]):
    for j in range(conv_img.shape[1]):
        mask[i][j]=conv_img[i][j].max()-conv_img[i][j].min()
        
plt.imshow(mask)
plt.gray()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/40fd7a7d-73fc-7349-c362-2022c0d2725c.png)
ここで作成された画像の「黒い部分」がもともと「灰色、黒、白」だった部分にあたる[^3]。

##マスクを元画像に適用する
マスクを元画像に適用する。「色がばらついていない度合」によってどこを「黒く表示するか」が変わる。今回は色のばらつきが5以下の部分を「灰色」として黒く表示する。そのために`mask`をいったん加工し、`mask[i][j]`の値が5以下の時、`0`、それ以外の時`1`にする。
まず、新しい画像にするための三次元配列として`uint8`形式の三次元配列を用意する。
`after_process=np.zeros((conv_img.shape[0],conv_img.shape[1],3),np.uint8)`
その後、`mask`の画素値に着目し、条件を満たしていれば「元画像と同じ画素」、満たしていなければ「黒(0,0,0)」をその画素に代入する。

```python:mask_process

for i in range(conv_img.shape[0]):
    for j in range(conv_img.shape[1]):
        if mask[i][j]<=5:
            after_process[i,j]=(0,0,0)
        else:
            after_process[i][j]=conv_img[i][j]
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/b2b59c3a-5d34-8a63-8732-b65e61a3b99e.png)


こうやって背景を黒で染めた画像を作成することができた。
犬の周りがにじんでいるのは、いわゆるjpgのにじみ、のはず。

#まとめ
灰色の背景を持つ画像から、背景を除去することができた。
これを応用することで特定の色の背景を除去したりできるはずである。もちろん、色の特徴をうまく表す必要がある。


#発展形
`mask`の値を0 or 1にし、元画像の色ごとに掛け算をする形でも同様のことができる。これは「黒」が(R,G,B)=(0,0,0)であるからできることである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070392/fc0ce56e-b522-63c2-9e4c-9b7b56b8912c.png)


[^1]: ちなみに原点は画像の左上であり右へx軸下へy軸である。
[^2]: ただし今回は特別重要ではない
[^3]: 犬の目も黒くなってしまっている