Pythonによる画像圧縮(特異値分解)


はじめに

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

実装

まずは結果を載せます。
結果

original

r = 10

r = 100

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

r = 10

r = 100

カラー画像.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番目まで打ち切ります。
そうするともとの行列にほぼ等しくなります。

特異値分解

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

行列の近似

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/