Pythonによる画像圧縮（特異値分解）

#はじめに
今回Pythonの学習も兼ねて行列近似を実装してみました。
実装は、行列の近似の応用例の1つである画像圧縮です。
行列近似を理解するには特異値分解の理解が必要ですが、筆者は説明できないのでほかサイトをご参照ください。

#実装
まずは結果を載せます。
結果

original
<img width="50%" alt="cute_dakkusu_original.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/a2fe671c-5d8a-072c-7737-c0ace4f723aa.jpeg")>

r = 10
<img width="50%" alt="cute_dakkusu_original_r10.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/5dc9c84f-5d59-d30b-a689-35009a9142f8.jpeg">

r = 100
<img width="50%" alt="cute_dakkusu_original_r100.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/f5c386dd-dd41-c235-278f-821325db012d.jpeg">

pythonコード
(docコメントの書き方間違っていたらごめんなさい)

```matrix_approximation.py
def im_comp_gray(A, r):
    """グレースケール画像のランクr近似

    Args:
        A (numpy.ndarray): 近似対象の行列
        r (int): 近似のランク

    Returns:
        numpy.ndarray: rランク近似された画像
    """

    # svdの実行, Falseでeconomy svd実行
    # Sは特異値のみなので注意、VはVの転置行列なので注意
    U, s, V = np.linalg.svd(A, False)

    # m x n の行列を定義
    compressed_im = np.ones(A.shape)

    # Uを縦ベクトル、Vを横ベクトルにスライス
    for i in range(r):
        compressed_im += s[i] * U[:, i:i+1].dot(V[i:i+1, :])
    
    return compressed_im

im_path = [input_file_path]
im_out_path = [output_file_path]
r = 100

def main():

    # グレースケールでの画像の読み込み
    im = cv2.imread(im_path, cv2.IMREAD_GRAYSCALE)
    check_image(im)

    compressed_im = im_comp_gray(im, r)

    # 結果画像の出力
    cv2.imwrite(im_out_path, compressed_im)

if __name__ == "__main__":
    main()
```

#追記
カラー画像を圧縮コードも作成しました。

結果

original
<img width="50%" alt="cute_dakkusu_original.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/878fc874-ab20-e304-8d80-4517bcef1e16.jpeg")>

r = 10
<img width="50%" alt="cute_dakkusu_original_r10.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/ccb17b11-5b35-e806-8400-65585fd4838c.jpeg">

r = 100
<img width="50%" alt="cute_dakkusu_original_r100.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606568/4434491e-2e3e-6d79-9112-1ffd7ae32fc8.jpeg">

```カラー画像.py
def comp_im(A, r):
    """カラー画像のrランク近似

    Args:
        A (numpy.ndarray): 近似対象の行列
        r (int): 近似のランク

    Returns:
        numpy.ndarray: rランク近似された画像
    """

    # カラーの情報(3)
    rgb = A.shape[2]

    # ループで処理できるようにを1次元目に設定
    # 最後に配列の順番をもとに戻す (transpose()メソッドを使用)
    compressed_im = np.ones((rgb, A.shape[0], A.shape[1]))

    # RGBを0, 1, 2として処理
    for n in range(rgb):

        # R, G, Bそれぞれをスライスして取得（1ループ目：R、2ループ目：G、3ループ目：B）
        C = A[:, :, n]

        # svdの実行, Falseでeconomy svd実行
        # Sは特異値のみなので注意、VはVの転置行列なので注意
        U, s, V = np.linalg.svd(C, False)

        # Uを縦ベクトル、Vを横ベクトルにスライス
        for i in range(r):
            compressed_im[n] += s[i] * U[:, i:i+1].dot(V[i:i+1, :])
    
    return compressed_im.transpose(1, 2, 0)

im_path = [input_file_path]
im_out_path = [output_file_path]
r = 100

def main():

    #カラー画像の読み込み
    im = cv2.imread(im_path)

    check_image(im)

    compressed_im = comp_im(im, r)

    # 結果画像の出力
    cv2.imwrite(im_out_path, compressed_im)

if __name__ == "__main__":
    main()
```

#補足
行列の近似は、合計でmこの縦ベクトルと横ベクトルの積(u * v)の和を、r番目まで打ち切ります。
そうするともとの行列にほぼ等しくなります。

特異値分解

```math
A = U\Sigma V^T = \sigma_1 u_1 v^T_1 + ... + \sigma_m u_m v^T_m
```

行列の近似

```math
A \simeq \tilde{A}_r = U_r \Sigma_r V^T_r = \sigma_1 u_1 v^T_1 + ... + \sigma_r u_r v^T_r
```

#おわりに
白黒の画像の圧縮とカラーの圧縮それぞれやってみました。

この記事を書くにあたって、Pythonの練習も兼ねて実装しましたが、Python結構難しかったです。
ただ、c言語やjava言語にない多次元行列を縦ベクトル、横ベクトルとして切り出せる「スライス」は嬉しい機能だと思いました。

qiitaで記号を載せたり画像を載せたりするのは思いの外大変でした。
丁寧に記事を書いてくださる方いつも感謝しております。

この記事が皆さんのご参考になれば嬉しいです。
なにかご指摘あれば教えていただけると幸いです。以上です。

#参考
実装にあたり以下のサイトを参考にさせていただきました。

特異値分解による行列の低ランク近似の基礎をまとめる
https://seetheworld1992.hatenablog.com/entry/2018/08/16/155013

特異値分解による画像の低ランク近似
https://qiita.com/kaityo256/items/48de63526b469235d16a

Python, OpenCVで画像ファイルの読み込み、保存（imread, imwrite）
https://note.nkmk.me/python-opencv-imread-imwrite/