OpenCVに入門してみた

「こたつにはいってぬくぬくしている間に画像の下処理が終わってたらよいな～」などと思いましたが、サンタさんに頼んでも多分来ないので、作ることにしました。

ふわ～っとした知識で書いているので、「こんなことができるんだ～」程度に見ていただければと思います。マサカリ大歓迎です。

# OpenCVとは
https://ja.wikipedia.org/wiki/OpenCV

> 画像処理・画像解析および機械学習等の機能を持つC/C++、Java、Python、MATLAB用ライブラリ

だそう。

# やること
資料作成だったり動画制作の際に行う**白抜き**を自動化します。
背景の白を透明色に置き換える操作ですね。

1~3枚ならツールで手作業でもよいのですが、40~50枚と数がかさばってくるとかなりストレスですよね。
こんなものはさっさと自動化してしまいましょう。

# やってみた
## キホン
ライブラリをインストールして...
```shell
pip install opencv-python
```

使いたいファイル内でインポートすればOK。
ついでにセットでよく使う**NumPy**も入れましょう。
```py
import cv2
import numpy as np
```

NumPyに関しては別途インストールする必要はありません。
実は、セットで使われることを見越してOpenCVのインストール時に一緒についてきてくれます。やさしい。

## 仕様
* ファイル名を入力すると、そのファイルを白抜きする
* 拡張子は**PNG**に変換する

## やってみた
まずは画像を読み込んで...
```py
img = cv2.imread(imgFileName)
```

RGBA方式（透明度を扱える色画像）に変換...
```py
rgbaImg = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
```

ここでちょっとだけ補足。
上の二行では変数にファイルを格納してるけど、実際に格納されているのは**画像の各ピクセルが持つデータが格納されたndarray**です。

### 補足：画像の扱い方
どうせ数日後に見返したおれが「なんやねんそれ...」になるので、詳しく補足。

まず、`ndarray`について。
これは**NumPyが提供する多次元配列**みたいな感じで、Python標準の多次元配列よりもいろいろなことができる、って感じだと思う。
たとえば、`3x3x3`の多次元配列に「1, 2, 3, ..., 27」って感じで数値を入れたいなら、

```py
import numpy as np

arr = np.arange(1, 28).reshape([3, 3, 3])
```

ってカンジ。
こんな感じで、多次元配列を手軽に扱えるのが`ndarray`のよいところ。[^1]

---
次に、画像データの扱いについて。

画像のデータは各ピクセルごとのRGBA値が`ndarray`で管理されてて、例えば
```py
img[0][0][0] # インデックスはそれぞれx, y, RGBA(0~3)
```

なら「画像の(0, 0)のRの値を参照するで！」ということになります。

---
### 本題にもどる
ということで続き。
RGBA変換した画像のうち、白の部分を透過していきます。
```py
rgbaImg[..., 3] = np.where(np.all(img == 255, axis=-1), 0, 255)
```

はい。急に訳がわからなくなりました。
順番に整理していきます。

### obj[..., 0]
こいつについて。
一見すると「引数めんどくさいから略したんか？」みたいなナリをしていますが、れっきとした**NumPyの構文**です。

一応こいつは書き換えると
```py
obj[:, :, 0]
```

となります。これでもよくわからん。

これ、実はPythonの**スライス**という構文になります。
スライスは簡単に言うと「配列をわざわざforループでまわすのめんどくさいし、短く書いたろ！」みたいなカンジの機能です。知らんけど。

そのなかでも、コロン単体のスライスは**配列全体を取り出す**という意味となります。
つまり、この三点リーダは **「省略した引数たちがとり得る値の組み合わせ」を全部出す** という意味を持ちます。

長ったらしく説明しましたが、とりあえず`rgbaImg[..., 3] =`が示す意味は、
> `rgbaImg`内のピクセルすべての透明度に対して代入するで～！

ってカンジです。


### np.where()
右辺もひとつずつさらっていきましょう。

この場合における`np.where()`は、いわば**三項演算子**の役割を持っています。
今回の場合、
1. `np.all(~)`が真偽値を返す
2. 結果が`True`であれば`0（透明）`、`False`であれば`255（透明ではない）`を返す

といった感じですね。単純。

### np.all()
さて。あとはこいつです。
こいつなんですが、**与えた評価式を評価する**という仕事を持ちます。
もちろんそれだけではありません。この引数に多次元配列を与えた場合、その多次元配列の`すべての要素に対して順番に評価`します。

今回の場合`img == 255`がそれですね。
これは`img`変数と数値を比較しているわけではなく、**`img`変数の各要素**と比較している感じですね。

`axis`オプションについても説明しようと思いましたが、今回はとくに重要ではないのと、とても分かりやすく解説されているサイトがあったので、よければ参考までに覗いてみてください（他力本願）
https://snowtree-injune.com/2020/04/29/axis-z005/#toc6

### 帰還
さて、舞い戻ってきました。どこまでいったっけ。

画像に対する処理は完了したので、あとは完成したファイルを**PNG形式で**保存します。
```py
imgName = imgFileName.split(".")[0]
cv2.imwrite("result/result_" + imgName + ".png", rgbaImg)
```

これでオッケー。

## 全体像
今後のことも考え、いいカンジに関数化しておきます。
（白抜きのことを表す英動詞がいまいち見つからなかったので、それっぽい`hollow`にしています。かっこいい。）

```py
import cv2
import numpy as np

def hollow(imgFileName):
    img = cv2.imread(imgFileName)
    rgbaImg = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
    rgbaImg[..., 3] = np.where(np.all(img == 255, axis=-1), 0, 255)
    
    imgName = imgFileName.split(".")[0]
    cv2.imwrite("result/result_" + imgName + ".png", rgbaImg)
    print("hollowed: " + imgFileName)
    
def __main__():
    imgFileName = input("ファイル名を入力してください（拡張子を含めること）")
    hollow(imgFileName)
    
__main__()
```

## 実行
今回はいらすとやから画像をお借りしてきました。
![merge_ramen_oil_man.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/fcfa63b7-97f7-e184-164f-fa898182a796.jpeg)
「ラーメンの油をまとめる人」のイラストです。


準備ができたらメインファイルを実行して、ファイル名を入力すると...
```shell
ファイル名を入力してください（拡張子を含めること）merge_ramen_oil_man.jpg
hollowed: merge_ramen_oil_man.jpg
```

**できた！！！！！！！！！！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/fae1dd70-b9b4-3131-6edd-8587ee63b074.png)

ん...？

なんか...すごく、汚いです。
丼、穴空いてますし。

## 反省
+ 対象の周囲がうまく白抜きできていない
+ 白を全部抜いてしまうため、**イラスト内の白**も抜いてしまう
+ 閾値（白と判定する範囲）が低すぎる

こんなところでしょうか。
ともあれ、よい経験になりました。


[^1]: ほかにも「要素に対して型が割り当てられる」（PythonのリストはすべてObject型）などなど様々な面で便利だけど、全容はつかみきれなかった；；