画像処理の入門

最近、弊社で画像処理の案件が増えているらしく、学生時代に研究でかじっていた私にドンピシャな案件がやってきています。
そこで、画像処理を少しでも知ってもらうように、今回は画像処理の基本であるOpenCVのライブラリを使って簡単な画像処理を実施していきたいと思います。
# 準備
* 今回はPythonを使用して、画像処理を行います。Pythonが使用できる環境を用意してください。
* ライブラリはOpenCVを使用します。Pythonがインストール出来たら下記のコマンドでライブラリをインストールしてください。
    ```
    pip install opencv-python
    pip install numpy
    ```
* ライブラリが正しくインストール出来ているか、確認します。以下のサイトから今回使用する画像をダウンロードします。
    [レナの画像](http://www.ess.ic.kanagawa-it.ac.jp/std_img/colorimage/lena_std.bmp)
    ダウンロードしたらプロジェクト直下にファイルを置いて、下記のコードをコピペして実行してください。
    コードを実行したら先ほど、ダウンロードした画像がウィンドウに表示されます。
    これで、準備は完了です。
    ``` python
    import cv2
    import numpy as np
    
    # 画像を読み込む
    img = cv2.imread("./lena_std.bmp")
    
    # 読み込んだ画像を表示する
    cv2.imshow('imshow_test', img)
    
    # キーボードが押されるまで、処理を止める
    cv2.waitKey(0)
    
    # ウィンドウを閉じる
    cv2.destroyAllWindows()
    ```

# 画像処理の基本
## 画像の読み込みと書き込み
画像処理を行うということはまず、以下の２つが出来ないと始まりません。
1. インプットとなる画像を読み込む。
1. 読み込んだ画像を加工して、ファイルとして吐き出す。

画像を読み込む方は先ほどのサンプルコードに書いた通り以下のコードで実行できます。
``` python
img = cv2.imread("ファイル名")
```
これで、指定したファイルをimgという変数に格納します。

次に書き込みは以下のコードで実行できます。
``` python
cv2.imwrite("ファイル名", img=画像データ)
```
ファイル名には書き込みするファイルの名前、画像データには読み込んだ画像変数を入れます。
imgという変数にcv2.imreadで読み込んだ場合は、imgと記載します。
試しに以下のコードを実行してみてください。
``` python
import cv2
import numpy as np

img = cv2.imread("./lena_std.bmp")
cv2.imwrite("output.bmp", img=img)
```
このコードが実行されると、lenna_std.bmpと同じ場所にoutput.bmpとファイルが出力されます。
中を見るとlenna_std.bmpと全く同じものになっているはずです。

## 画像処理を行う上の基本知識
### ピクセルとは？
実際に、画像処理を行う前に基本知識だけを抑えておきます。
画像というのは、ピクセルの集合体で構成されます。
ピクセルとは、画像を構成する色付きの小さな点のことで画像データの最小単位となります。
例えば、フルHDという言葉を聞いたことがあると思いますが、これは、1920×1080=2073600個のピクセルの集合で構成されています。イメージ的にはこんな感じでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/8ae4e11f-49b0-38e0-6d43-6902fc41c49d.png)
この四角がピクセルになります。書くのが大変なので、簡略化していますが、実際は縦に1080個、横に1920個四角があるイメージになります。
そして、このピクセルには色情報を持っています。
色情報の持ち方はカラー画像とグレースケール画像で異なります。
グレースケール画像の場合は、1ピクセルごとに0～255の値で持ちます。
カラー画像の場合は、1ピクセルごとにRGB値でそれぞれ0～255で値を持ちます。
ゲームをされている方はキャラメイクの髪色を決めるときにRGB値を調整することがあると思いますが、それと同じイメージです。
図にするとこんな感じでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/6b7d45bc-5cc1-3d40-7fd6-14938654fcbb.png)
数値はそれぞれのピクセル値を表しています。
グレースケール画像の場合は1ピクセルに1つの値を持ち、0に近づくほど黒く、255に近づくほど白くなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/0f2667a4-0847-c174-31aa-2eacdf25a938.png)
カラー画像の場合は、１ピクセルごとに赤、緑、青の３つの値を持ち、それらを組み合わせて表示します。
RGB値を全て同じにすると、白黒画像と同様の表現をすることも出来ます。

### 画像の座標系
画像の座標系は、少し異なります。
一般的な座標系は下の画像の通りとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/f8bba9ae-3b88-a943-b18c-b57ffedd1954.png)
数学でよく見るやつですね。
左下の座標が(0,0)となり、右に行くほどxの値が大きくなり、上に行くほどyの値が大きくなります。
一方で、画像の座標系は下の画像のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/3df33c0a-5ce0-6e8b-9d27-2b388e5ebebf.png)
x軸は変わりませんが、y軸が反転します。画像の左上が(0,0)で下に行くほどyの値が大きくなります。
ここを勘違いすると苦戦しますので、注意してください。

## 画像をグレースケールにしてみよう
前置きも終わったので、実際にOpenCVを使ってみます。
まずは、画像をグレースケールにしてみます。
コードは以下の通りになります。
``` python
import cv2
import numpy as np

# 画像を読み込む
img = cv2.imread("./lena_std.bmp")

# 画像の高さ、幅を取得する
height = img.shape[0]
width = img.shape[1]

# 出力画像を初期化
output = np.zeros((height, width, 1), np.uint8)

# 画像の高さ分ループ
for y in range(0, height):
    # 画像の幅分ループ
    for x in range(0, width):
        # グレースケール値
        val = 0
        for col in range(0, 3):
            # ピクセルごとに入力画像のRGB値を加算
            val += img[y, x, col]

        # RGB値の平均値を出力画像に設定
        output[y, x] = val / 3

# 画像を保存する
cv2.imwrite("output.bmp", img=output)
```
コードの解説をすると以下の通りになります。
1. 入力となる画像を読み込む。
1. 入力画像の高さと幅を取得する。
1. 出力画像用の変数を初期化する
1. 画像の幅と高さの分だけ、5,6を実行する。
1. ピクセルごとに入力画像のRGB値の合計値を求める。
1. RGB値の合計から平均を求め、出力画像に値を保存する。
1. 結果を画像として、保存する。

このコードを実行すると以下のような画像が出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/8316ae0f-e661-44fc-5083-15b8d57328f4.png)
これで、カラー画像からグレースケール画像に変換することが出来ました。

## 画像を2値化してみよう
グレースケール画像に変換することが出来たので、今度は画像を2値化してみます。
2値化とは、名前の通り2つの値にすることです。2つの値とは0か255、つまり白or黒だけで画像を表現します。
コードは以下の通りになります。
``` python
import cv2
import numpy as np

# 画像を読み込む
img = cv2.imread("./lena_std.bmp")

# 画像の高さ、幅を取得する
height = img.shape[0]
width = img.shape[1]

# 出力画像を初期化
output = np.zeros((height, width, 1), np.uint8)

# 画像の高さ分ループ
for y in range(0, height):
    # 画像の幅分ループ
    for x in range(0, width):
        # グレースケール値
        sum = 0
        for col in range(0, 3):
            # ピクセルごとに入力画像のRGB値を加算
            sum += img[y, x, col]

            # グレースケール値を求める
        val = sum / 3

        # ピクセル値が128未満の場合は0にする
        if val < 128:
            output[y, x] = 0
        # ピクセル値が128以上の場合は255にする
        else:
            output[y, x] = 255

# 画像を保存する
cv2.imwrite("output.bmp", img=output)
```
コードの解説をすると以下の通りになります。
1. 入力となる画像を読み込む。
1. 入力画像の高さと幅を取得する。
1. 出力画像用の変数を初期化する
1. 画像の幅と高さの分だけ、5,6を実行する。
1. ピクセルごとに入力画像のRGB値の合計値を求める。
1. RGB値の合計から平均を求める。
1. RGB値が平均が128未満の場合は、ピクセル値を0,そうでない場合は255にする。
1. 結果を画像として、保存する。

グレースケール化の処理とベースは同じです。
グレースケールの場合は、RGBの平均値をそのまま設定しましたが、2値化はRGBの平均値が128未満なら0、128以上なら255にピクセル値を振分けます。
コードを実行すると以下の画像が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/d891e72c-7744-5049-82ae-76d3e0da4bdd.png)

# ライブラリでグレースケール、2値化してみよう
ここまで、画像をピクセルごとに計算して、グレースケール及び2値化しましたが、既存のライブラリで実行することが出来ます。
``` python
import cv2
import numpy as np

# 画像を読み込む
img = cv2.imread("./lena_std.bmp")

# 画像をグレースケールにする
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite("lenna_gray.bmp", img=img_gray)

# 画像を2値化する
ret, img_2val = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU)
cv2.imwrite("lenna_2val.bmp", img=img_2val)
```
上のコードを実行すると、グレースケールと2値化をした画像が出力されます。
グレースケールや2値化は実は、様々な手法があり、先ほどの出力とは少し異なります。
グレースケールの場合は、以下のサイトを参考を見ると計算式が異なることが分かります。

https://python-debut.blogspot.com/2019/12/cvtcolor.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/77b9934e-5466-e334-0784-d4bbcdbd4096.png)
こちらが出力結果になります。
左がピクセルごとに計算した結果、右がライブラリを使った計算結果になります。
見比べてみると左の方が全体的に明るくなっていると思います。

次に2値化は大津の2値化というものを今回は使用しました。

https://ja.wikipedia.org/wiki/%E5%A4%A7%E6%B4%A5%E3%81%AE%E4%BA%8C%E5%80%A4%E5%8C%96%E6%B3%95

簡単に説明すると大津の2値化は入力画像ごとに閾値を自動で設定してくれるものになります。詳細は上のサイトを参考にしてください。
ピクセルごとに計算した場合は、閾値を128と手動で設定しましたが、大津の2値化では128になるとは言い切れません。
実際の出力結果を比較してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/05c437d8-ac2d-86ff-bbb6-e67bfb7cee95.png)
左が閾値を手動で設定したもの、右が大津の2値化を実行したものになります。
大津の2値化の方は鼻や口がより白くなっていることが分かります。
実際に閾値を確認するには、以下の箇所のretに値が入っています。
``` python
ret, img_2val = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU)
```
retの値を出力すると117でした。よって、閾値は117であることが分かります。

## 一から画像を作ってみよう
これまでは、入力画像を加工して新たに画像を作りましたが、最後に一から画像を作ってみます。
``` python
import cv2
import numpy as np

# 出力画像を初期化
output = np.zeros((1920, 1020, 3), np.uint8)

# 線を描画する
cv2.line(output, (300, 300), (700, 700), (255, 0, 0), thickness=20)

# 塗りつぶしなし円を描画する
cv2.circle(output, (800, 800), 200, (0, 0, 255),
           thickness=3, lineType=cv2.LINE_AA)

# 塗りつぶしあり円を描画する
cv2.circle(output, (1200, 800), 200, (0, 255, 255),
           thickness=-1, lineType=cv2.LINE_AA)

# 長方形を描画する
cv2.rectangle(output, (1200, 300), (1500, 800), (255, 0, 255), thickness=10)

# 画像を保存する
cv2.imwrite("output.bmp", img=output)
```

それぞれの関数の意味は次の通りになります。
* 線の描画
    ``` python
    cv2.line(出力画像, (始点のx座標, 始点のy座標), (終点のx座標, 終点のy座標), (B, G, R), 線の太さ)
    ```
    1点注意することはピクセル値の指定です。見ての通り、(255,0,0)で青色で直線が表示されているので、色の指定は(R,G,B)ではなく、(B,G,R)になっていることに注意してください。直線に限らず、その他も同様になります。

* 円
    ``` python
    cv2.circle(出力画像, (中心のx座標, 中心のy座標), 半径, (B, G, R), 線の太さ, 線の種類)
    ```
    塗りつぶしなしの円を描画する場合は線の太さを1以上にします。
    塗りつぶしありの円を描画する場合は線の太さを-1にします。

* 長方形
    ``` python
    cv2.rectangle(output, (左上x座標, 左上y座標), (右下x座標, 右下y座標), (B,G,R), 線の太さ)
    ```
    長方形は図形の左上と右下の座標を指定することで、表現します。

実際の出力結果は以下の通りになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314839/b0f130ac-224b-6edb-351e-8d059735939f.png)
画像では後から描画したものが手前に来ます。
今回は、青の直線→赤の円→黄色の円→ピンクの長方形の順番で描画したので、青の線より赤の円が手前に、黄色の円よりピンクの長方形が手前に来るようになっています。

# まとめ
今回は、画像処理の第一歩ということでOpenCVを使って色々試してみました。
画像処理を始めたい方のお役に立てれば幸いです。