OpenCVやPILで画像を表示させる（だけ）の話

# はじめに
OpenCVやPILの話。
Qiitaに限らずウェブ上にいくらでもある情報ではあるが、記事が細切れになっているものが多く比較するには適していないのでその点に注意してまとめてみた。
その過程で先人が触れていない事項にたどり着いた（ように思える）ので、お読みいただけると幸いです。


# OpenCV
CVはComputer Visionの略で、単に画像を加工するだけでなくさまざまな機能を持つ。
まあ、今回は画像加工どころか画像を表示するだけの話なのだが。

## 画像を読み込む　cv2.imread()
cv2.imread(*filename*, *flags*)という使い方をする。
*filename*はファイル名。これが適切でない場合でもエラーにならずNoneを返す。
*flags*については後述する。

```Python:画像読み込み
import cv2

filename = "hoge.png"
imgCV = cv2.imread(filename)  # flagsは省略（デフォ値＝1）
```

## 画像を表示する　cv2.imshow()

cv2.imshow(*winname*, *mat*) という使い方をする。
*winname*はウィンドウの名前。ヌルストリングでもいいが指定は必須。
*mat*はマトリックス、すなわち行列。要するに画像データ。
デフォではサイズ変更できないが、cv2.imshow()する前にcv2.namedWindow()で「これこれこういう名前でサイズ変更可能なウィンドウを作るよ」としてやればサイズ変更できるようになる。
cv2.imread()を失敗してNoneが返ってきた場合、ここでエラーになる。

```Python:サイズ変更不可のウィンドウで画像表示 
cv2.namedWindow("image", cv2.WINDOW_AUTOSIZE)  # この一文、なくてもよい
cv2.imshow("image", imgCV)
```

```Python:サイズ変更可のウィンドウで画像表示
cv2.namedWindow("image", cv2.WINDOW_NORMAL)  # cv2.WINDOW_NORMALの値は0なので0を指定してもよい
cv2.imshow("image", imgCV)
```
## 追記：フルスクリーン表示
フルスクリーン表示するには、まずウィンドウサイズを変更可能にし、その後実際にフルスクリーンに設定するという処理をおこなう。

```Python:フルスクリーン表示したりしなかったりする関数
def cv2_imshow_fullscreen(winname, img, flag_fullscreen):
    if flag_fullscreen:
        cv2.namedWindow(winname, cv2.WINDOW_NORMAL)
        cv2.setWindowProperty(winname, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
    cv2.imshow(winname, img)

# 使い方例
cv2_imshow_fullscreen("fullscreen", img , True) # フルスクリーン表示
cv2_imshow_fullscreen("window", img , False) # ウィンドウ表示
```

調子に乗って両対応の関数を書いてみたが、フルスクリーン表示しない場合は普通にcv2.imshow()すればいいわけで、だったらifで分岐しないフルスクリーン表示専用の関数にしたほうが良かったな。


## OpenCVの画像データについて
画像データはnumpy.ndarrayの型で構成されている。シェイプで次元を調べることができるし内容を確認するのも容易だ。
**カラーはBGRの順で格納されている**ことに注意。

### 画像のシェイプ
flagsを指定して画像を読み込んだとき、画像データがどのようなカタチになっているか確認してみよう。

|flags  |cv2.IMREAD_COLOR   |cv2.IMREAD_GRAYSCALE|cv2.IMREAD_UNCHANGED     |
|:------|:-----------------:|:------------------:|:-----------------------:|
|**値** |1<br>（デフォルト値）|0                   |-1                       |
|**処理**|カラー画像として読み込む|グレースケールで読み込む|そのままの仕様で読み込む| 
|**例1<br>RGBA画像**<br>![original.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/36211b40-ee65-4deb-1667-ad094648debb.png)<br><font size="-1">実際の背景は市松模様ではなく透明</font>|![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/2276579e-1016-0629-9c20-f6eaa9663874.png)<br>shape=(200, 182, 3)|![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/01d7b537-4e75-0f40-6045-65738d6e7561.png)<br>shape=(200, 182)|![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/48811433-edf2-7b3d-b91c-5f03453a239c.png)<br>shape=(200, 182, 4)|
|**例2<br>グレースケール画像**<br>![reiwa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d42ed907-e039-14d9-d5fa-bd2208b0ed49.png)|![r3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/bcf04f96-37e8-8310-871a-ca439253cd5b.png)<br>shape=(192, 144, 3)|![r1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/1c04db79-6116-9353-2353-74f5a6b00e98.png)<br>shape=(192, 144)|![r4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/dd4d2634-6bac-2c64-4588-52a398a78f79.png)<br>shape=(192, 144)|

カラーチャンネル数は、cv2.IMREAD_COLORでは透明度があってもグレースケール画像であっても一律で3チャンネルの配列になる。cv2.IMREAD_GRAYSCALEの場合はチャンネル数が1になるのではなく、チャンネル数の指定のない(h, w)という二次元配列になる。
cv2.IMREAD_UNCHANGEDは、[OpenCV-Pythonチュートリアル「画像を扱う」](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_image_display/py_image_display.html#id3)には「アルファチャンネルも含めた画像として読み込む」とあるが、一律4チャンネルになるわけではなく、元の画像から変更なく読み込むというのが正しい。RGBA画像なら4チャンネル、RGB画像なら3チャンネル。グレースケール画像なら1チャンネルなので(h, w, 1)ではなく(h, w)を返す。
ややこしい？ いや、最初からそう言ってるでしょ、cv2.IMREAD_**UNCHANGED**って。

### サイズを取得する
元画像のタイプによって配列のカタチが異なるOpenCVの画像データ。高さや幅を取得するにはif文を使って場合分けしなくてはいけない…ということはない。
(高さ, 幅)で得られるグレースケール画像のシェイプも、(高さ, 幅, チャンネル数)となるカラー画像も、0番目に高さがあって1番目に幅があることは同じ。だから0や1で決め打ちしてやればいいのだ。

```Python:
# どちらでも可

def getSize1(imgCV):
    h = imgCV.shape[0]
    w = imgCV.shape[1]
    return h, w

def getSize2(imgCV):
    h, w = imgCV.shape[:2]
    return h, w
```

## Google Colabの場合
Google Colabではcv2.imshow()は使えないようになっている。Jupyterのセッションがクラッシュしてしまうらしい。
代わりにcv2_imshow()というGoogle Colab独自のメソッドを使うよう代替案を提示してくる。
cv2_imshow()にウィンドウ名は不要で画像データのみを指定する。これによりColabのセル内に画像が表示される。

```Python:Google&nbsp;Colab
# 1セル1文でも全部まとめても可
import cv2
from google.colab.patches import cv2_imshow

filename = "hoge.png"
imgCV = cv2.imread(filename)

cv2_imshow(imgCV)
```

## Jupyter Notebookの場合
Jupyter Notebookではcv2.imshow()は禁止されているわけではないが、クラッシュすることがあるのは変わらない。

実は、Jupyter Notebookでは、正しい処理をすることで正しく表示させることができる。
[先程のサイト](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_image_display/py_image_display.html#id4)にあるようにcv2.imshow()で表示した後、キー入力を待ってウィンドウを破棄するようにすればいいのだ。
この場合でも右上バッテンで画像ウィンドウを閉じようとしてはいけない。やはりクラッシュしてしまう。
正しく表示できるといってもミスったら死だなんて嫌すぎる。そんなのはゲームの世界だけで十分だ。

```Python:Jupyter&nbsp;Notebook
import cv2

filename = "hoge.png"
imgCV = cv2.imread(filename)

# 以下を一つのセルで実行する
cv2.imshow("image",imgCV)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

## 正しい処理とはどういうことか
画像表示するにはcv2.imshow()だぞと書いたが、この段階ですでにつまづいている人もいるかもしれない。
Pythonをインストールしたときに一緒に付いてくる開発環境IDLE上で実行するとうまくいく。だがpyファイルをダブルクリックすると期待通りの動きにならない。VS Codeでもダメ。
これは、IDLEではプログラムが終了してもシェルが生き続けているのに対し、python.exeを実行する場合はそれが終了した瞬間に画像のウィンドウも閉じてしまうから。だと思われる。
**cv2で作成したウィンドウはcv2で破棄する。**これがこの世界のジャスティスなのだ。


# PIL(Pillow)
PIL(Python Image Library)という画像処理ライブラリがあって、その後継がPillow。OpenCVとの得手不得手の差はまた今度。
Pillowをインストールしても、実際に使う際にインポートするのはPIL。

## 画像を読み込む　Image.open()
Image.open(filename)という使い方をする。
filenameが適切でない場合はエラーになる。
厳密にはmodeという引数もありデフォ値が"r"なのだが、これが何を意味しそれ以外にどんな値が使えるのかよくわからない。

## 画像を表示する　show()
読み込んだ画像データをshow()することで画像が表示される。
画像はOSごとに異なる画像ビューアが起動して表示される。ちょいと不便だな。
引数にはtitleやcommandがあるがいずれも省略可能。言うまでもないことだが、引数がなくてもカッコは必須。

```Python:ソース
from PIL import Image

filename = "hoge.png"
imgPIL = Image.open(filename)  # 画像読み込み

imgPIL.show()  # 画像表示
```

## PILの画像データについて
画像データは、たとえばpng画像ならばPIL.PngImagePlugin.PngImageFileという形式になっており、中身を確認するのは容易ではない。
その代わり、これは画像データであると自覚しているのでさまざまな属性を持っている。


### サイズを取得する
```Python:
print (imgPIL.mode)
# RGBA  # ほかに RGB L（グレースケール）などがある。
# Image.open()のmodeとの関係は不明。

print (imgPIL.size)
# (182, 200)  # タプルで、幅,高さ の順

print (imgPIL.width)
# 182

print (imgPIL.height)
# 200
```

# matplotlibのグラフとして表示
画像をmatplotlibのグラフとして表示することも多い。
matplotlib.pyplotの詳しい使い方はここでは説明しない。

python上で実行すると、インタラクティブなmatplotlibのグラフがあらわれる。拡大したり表示エリアを変えたりできる。
![uchuhikoushi_pil_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/b39b31bd-9185-5d6a-80d3-23ec7edca03f.png)

Google ColabやJupyter Notebook上では単なる画像としてグラフが表示される。
Jupyter Notebookでは%matplotlib inlineというおまじないを唱えるといいらしい。

![uchuhikoushi_pil_graph_web.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d889449d-92a0-ceb3-52fe-3682a3be8d1a.png)

え？ 普通に画像を表示する場合との違いがわからない？ 
では、こんな画像ならばどうする？
![dot-e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/9f23c0a4-55aa-3f8c-c263-0cf30e4cdb80.png) 　← ここにいる
これは6×8の画像。matplotlibのグラフはこんな小さい画像もいい感じに拡大してくれるのがありがたい。
![smallpic_pil_graph_web.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d32f9ef0-5dab-85f5-7e6e-56820b2adbde.png)

## PIL画像の場合
PIL画像はそのままではグラフ化できないのでnumpy.asarray()でnumpy.ndarrayにしてやる必要がある。

```Python:ソース
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
%matplotlib inline  # Jupyter Notebookでインライン表示する

filename = "hoge.png"
imgPIL = Image.open(filename)
arrPIL = np.asarray(imgPIL)
plt.imshow(arrPIL)
plt.show()
```

## OpenCVの場合
ここのみ諸事情によりサンプル画像がいらすとやでなく[スキマナース](https://nurse-web.jp/photo/)になっています。

これが元画像。
![nurse.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/06607f09-1cfb-a71b-0682-e5896e7dbf04.jpeg)

OpenCVの画像データはもとよりnumpy.ndarray。ならばそのままplt.imshowすればいい？
とやってみると。
![nurse_miss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/75ee07ab-b09d-ab9e-eb0e-69dabd2864dd.png)

はいダメー。OpenCVの画像はBGRだって言ったでしょ。matplotlib.pyplotは普通にRGBなのでOpenCVの画像をmatplotlibでグラフ表示する際は色を変換してやる必要があるのだ。

カラーをコンバートするにはcv2.cvtColorを使う。cv2.cvtColor(src, code)という使い方をする。
srcはソース。元の画像データ。
codeは色変換の組み込み定数。BGRをRGBにするとか、その逆とか、RGBをグレーにするとか、RGBをRGBAにするとか、いろいろある。BGRをRGBにコンバートするのはcv2.COLOR_BGR2RGB。
このひと手間によってOpenCVの画像もmatplotlibのグラフとして表示できる。

![nurse_good.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/990f9ca7-3b7c-4f41-945f-3dd3696c95e1.png)

BGR→RGBは、(高さ, 幅, BGR値)というシェイプの配列の2番目の並びをRGBと逆順にすることにほかならない。
[前回学んだスライス](https://qiita.com/mo256man/items/ed8ea21bd0ae94e5ed48)を活用することもできる。

```Python:ソース
import numpy as np
import cv2
from matplotlib import pyplot as plt

filename = "nurse.jpg"
imgCV = cv2.imread(filename)

# cv2.cvtColorを使う方法
imgCV_RGB = cv2.cvtColor(imgCV,cv2.COLOR_BGR2RGB)

# スライスを使う方法
# imgCV_RGB = imgCV[:, :, ::-1]

plt.imshow(imgCV_RGB)
plt.show()
```

ここで透過を持つpng画像を使わなかったのは、今後詳しく調べていくため。
で、できなかったからじゃないんだからね、勘違いしないでよね。
証拠として、グラフ画像として透過が正しく表現できた例を示す。
![uchuhikoushi_cv_graph_color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/6d3e20c3-43fc-cbcf-beab-992be6edd658.png)



# 次回予告
画像と画像を合成する、その際に手前の画像の透明部分は背後の画像が透けて見えるようにする。そんな80年代のゲーマーなら「ああ、スプライトね」と言いたくなるようなマスク処理を手作業でおこなっていきます。

最後に、もう一度透過画像をさまざまなflagsで表示する表をもう一度見てみよう。
~~がんばって作ったので~~予習として。


|元画像|cv2.IMREAD_COLOR   |cv2.IMREAD_GRAYSCALE|cv2.IMREAD_UNCHANGED     |
|:--------|:-----------------:|:------------------:|:-----------------------:|:-----------------------:|
|![original.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/36211b40-ee65-4deb-1667-ad094648debb.png)|![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/2276579e-1016-0629-9c20-f6eaa9663874.png)|![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/01d7b537-4e75-0f40-6045-65738d6e7561.png)|![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/48811433-edf2-7b3d-b91c-5f03453a239c.png)|![uchuhikoushi_pil_graph_web.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d889449d-92a0-ceb3-52fe-3682a3be8d1a.png)|