15
5

More than 1 year has passed since last update.

【画像処理】PythonでOpenCVを使った色空間の変換

Last updated at Posted at 2022-07-26

PythonでOpenCVを使った色空間の変換

今回は「PythonでOpenCVを使った色空間の変換」を学んでいきます。
それぞれのバージョンはPython 3.10.4、OpenCV 4.5.5になります。また、今回の記事の内容はOpenCVの公式ドキュメントを参考にしています。

OpenCVで色空間の変換処理

OpenCVではcvtColorという色空間の変換処理を行う関数が実装されています。引数に変換前後の色空間を指定することで、様々な色空間への変換を行うことができます。

cvtColor
cv.cvtColor(src, code[, dst[, dstCn]])
Parameters 説明
src 符号なしの8ビット or 16ビットまたは32ビット浮動小数点数(CV_32F)の画像
code 色空間変換コード
dst srcと同じサイズと深さの画像
dstCn 出力画像のチャンネル数(0の場合は元画像より自動的に算出)

各チャンネルの扱い

OpenCVでは各チャンネルをBGRの順で保持し、値は以下の3つの型のどれかで表されます。

範囲
CV_8U 0 - 255
CV_16U 0 - 65535
CV_32F 0 - 1

ただし、最近は色空間の変換時に16bitの画像がサポートされていない場合が多いため、実質CV_8UまたはCV_32Fのみとなります。

変換時の注意点

非線形色空間への変換

公式のドキュメントでは、RGBからLuv等の非線形の色空間に変換する際は、正しい結果を得るためにcvtColorで変換する前に適切な値の範囲に正規化を自身で行う必要があると記載があります。

src_img = (1.0 / 255) * src_img.astype(np.float32)
dst_img = cv.cvtColor(src_img, cv2.COLOR_BGR2Luv)

ただし、非線形色空間への変換時に8bitまたは16bitの画像が入力された場合は変換前に自動で適切な正規化が内部で行われるため、以下で紹介するように一部でも情報が失われると困る場合を除き、特に事前の正規化等は必要なさそうです。

8bit画像の変換

通常、8bit画像での変換では一部の情報が失われます。もし、全範囲の色を必要とする場合や読み込み時に画像を変換して元に戻すような処理をする場合は、32bit画像を使用するほうが良いでしょう。

アルファチャンネルを追加する変換

変換によってアルファチャンネルが追加される場合、その値は対応するチャンネル範囲の最大値に設定されます。
CV_8Uの場合は255、CV_16Uの場合は65535、CV_32Fの場合は1となります。

色空間変換コードの種類について

OpenCVでは様々な色空間への変換ができるように200以上の変換コードが実装されています。ここではすべて紹介できないため、実装で使用する一部の変換コードとそれに対応する逆変換コードのみを以下に記載します。

説明 色空間変換コード 色空間逆変換コード
BGRをグレースケールに変換 cv.COLOR_BGR2GRAY cv.COLOR_GRAY2BGR
BGRにアルファチャンネルを追加 cv.COLOR_BGR2BGRA cv.COLOR_BGRA2BGR
BGRをHSVに変換 cv.COLOR_BGR2HSV cv.COLOR_HSV2BGR
BGRをLabに変換 cv.COLOR_BGR2Lab cv.COLOR_Lab2BGR
BGRをLuvに変換 cv.COLOR_BGR2Luv cv.COLOR_Luv2BGR
BGRをHLSに変換 cv.COLOR_BGR2HLS cv.COLOR_HLS2BGR
BGRをYUVに変換 cv.COLOR_BGR2YUV cv.COLOR_YUV2BGR
BGRをXYZに変換 cv.COLOR_BGR2XYZ cv.COLOR_XYZ2BGR
BGRをYCrCbに変換 cv.COLOR_BGR2YCrCb cv.COLOR_YCrCb2BGR

OpenCVのすべての色空間変換コードはこちらから確認できます。
また、変換に使用している数式に関してはこちらから確認できます。

実装

実際の実装はこのようになります。

cvt_gray.py
import sys
import cv2


def main() -> None:
    input_path = sys.argv[1]
    src_img = cv2.imread(input_path)
    if src_img is None:
        raise Exception()

    cvt_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2GRAY)
    cv2.imwrite("gray.png", cvt_img)


if __name__ == "__main__":
    main()
1.1 入力画像 1.2 出力画像
spc_logo.pngsciencepark.png gray.pnggray.png

BGR(「1.1 入力画像」)からグレースケールに変換するコード(cv2.COLOR_BGR2GRAY)を使用して、グレースケール画像(「1.2 出力画像」)を取得することができました。
さらに、各変換コードを使用した結果を各チャンネルごとに以下に記載します。

cv2.COLOR_BGR2BGRA

2.1 出力画像 2.2 出力画像 2.3 出力画像 2.4 出力画像
BGRA_0.pngB BGRA_1.pngG BGRA_2.pngR BGRA_3.pngA

cv2.COLOR_BGR2HSV

3.1 出力画像 3.2 出力画像 3.3 出力画像
HSV_0.pngH HSV_1.pngS HSV_2.pngV

cv2.COLOR_BGR2Lab

4.1 出力画像 4.2 出力画像 4.3 出力画像
Lab_0.pngL Lab_1.pnga Lab_2.pngb

cv2.COLOR_BGR2Luv

5.1 出力画像 5.2 出力画像 5.3 出力画像
Luv_0.pngL Luv_1.pngu Luv_2.pngv

cv2.COLOR_BGR2HLS

6.1 出力画像 6.2 出力画像 6.3 出力画像
HLS_0.pngH HLS_1.pngL HLS_2.pngS

cv2.COLOR_BGR2YUV

7.1 出力画像 7.2 出力画像 7.3 出力画像
YUV_0.pngY YUV_1.pngU YUV_2.pngV

cv2.COLOR_BGR2XYZ

8.1 出力画像 8.2 出力画像 8.3 出力画像
XYZ_0.pngX XYZ_1.pngY XYZ_2.pngZ

cv2.COLOR_BGR2YCrCb

9.1 出力画像 9.2 出力画像 9.3 出力画像
YCrCb_0.pngY YCrCb_1.pngCr YCrCb_2.pngCb

色空間変換は可逆?不可逆?検証してみた

色空間の変換後に逆変換を行い、変換前と逆変換後の画像を比較することで検証しました。
画像同士の比較には、numpyのarray_equal()を使用し、差異があった場合はnumpyのisclose()を使用して、その割合を算出しています。
8bit画像と32bit画像それぞれで検証に使用したスクリプトと結果をまとめた表を以下に記載します。

8bit画像の場合

compare.py
import sys
import numpy as np

import cv2
import cvex


def main() -> None:
    input_path = sys.argv[1]
    src_img = cvex.imread(input_path)
    if src_img is None:
        raise Exception()

    cvt_code_dict = {
        cv2.COLOR_BGR2GRAY: "GRAY",
        cv2.COLOR_BGR2BGRA: "BGRA",
        cv2.COLOR_BGR2HSV: "HSV",
        cv2.COLOR_BGR2Lab: "Lab",
        cv2.COLOR_BGR2Luv: "Luv",
        cv2.COLOR_BGR2HLS: "HLS",
        cv2.COLOR_BGR2YUV: "YUV",
        cv2.COLOR_BGR2XYZ: "XYZ",
        cv2.COLOR_BGR2YCrCb: "YCrCb",
    }

    decvt_code_dict = {
        "GRAY": cv2.COLOR_GRAY2BGR,
        "BGRA": cv2.COLOR_BGRA2BGR,
        "HSV": cv2.COLOR_HSV2BGR,
        "Lab": cv2.COLOR_Lab2BGR,
        "Luv": cv2.COLOR_Luv2BGR,
        "HLS": cv2.COLOR_HLS2BGR,
        "YUV": cv2.COLOR_YUV2BGR,
        "XYZ": cv2.COLOR_XYZ2BGR,
        "YCrCb": cv2.COLOR_YCrCb2BGR,
    }

    for code, v in cvt_code_dict.items():
        cvt_img = cv2.cvtColor(src_img, code)
        decvt_img = cv2.cvtColor(cvt_img, decvt_code_dict[v])
        flag = np.array_equal(src_img, decvt_img)
        print("{}:{}".format(v, flag))

        if not flag:
            flag_mask = np.isclose(src_img, decvt_img)
            src_diff_array = np.ravel(src_img[~flag_mask].astype(np.float32))
            cvt_diff_array = np.ravel(decvt_img[~flag_mask].astype(np.float32))
            diff_list = abs(src_diff_array - cvt_diff_array)
            diff_len = len(diff_list)
            diff_sum = sum(diff_list)
            h, w = src_img.shape[:2]
            pixels = h * w
            print("元画像と異なるピクセルの割合:{}%".format((diff_len / (pixels * 3)) * 100))
            print("異なる値の平均幅:{}".format((diff_sum / diff_len)))


if __name__ == "__main__":
    main()

色空間変換コード 色空間逆変換コード 差異の有無 異なるピクセルの割合 異なる値の平均幅(0~255)
cv.COLOR_BGR2GRAY cv.COLOR_GRAY2BGR あり 93.54188368% 17.98067933
cv.COLOR_BGR2BGRA cv.COLOR_BGRA2BGR なし 0% 0
cv.COLOR_BGR2HSV cv.COLOR_HSV2BGR あり 31.13129340% 1.06006148
cv.COLOR_BGR2Lab cv.COLOR_Lab2BGR あり 39.09743924% 1.04829014
cv.COLOR_BGR2Luv cv.COLOR_Luv2BGR あり 52.36208767% 1.12438843
cv.COLOR_BGR2HLS cv.COLOR_HLS2BGR あり 31.66710069% 1.02539713
cv.COLOR_BGR2YUV cv.COLOR_YUV2BGR あり 27.06043837% 1.00000000
cv.COLOR_BGR2XYZ cv.COLOR_XYZ2BGR あり 42.04947917% 1.50939287
cv.COLOR_BGR2YCrCb cv.COLOR_YCrCb2BGR あり 2.63856337% 1.00000000

32bit画像の場合

compare.py
import sys
import numpy as np

import cv2
import cvex


def main() -> None:
    input_path = sys.argv[1]
    src_img = cvex.imread(input_path)
    if src_img is None:
        raise Exception()

    cvt_code_dict = {
        cv2.COLOR_BGR2GRAY: "GRAY",
        cv2.COLOR_BGR2BGRA: "BGRA",
        cv2.COLOR_BGR2HSV: "HSV",
        cv2.COLOR_BGR2Lab: "Lab",
        cv2.COLOR_BGR2Luv: "Luv",
        cv2.COLOR_BGR2HLS: "HLS",
        cv2.COLOR_BGR2YUV: "YUV",
        cv2.COLOR_BGR2XYZ: "XYZ",
        cv2.COLOR_BGR2YCrCb: "YCrCb",
    }

    decvt_code_dict = {
        "GRAY": cv2.COLOR_GRAY2BGR,
        "BGRA": cv2.COLOR_BGRA2BGR,
        "HSV": cv2.COLOR_HSV2BGR,
        "Lab": cv2.COLOR_Lab2BGR,
        "Luv": cv2.COLOR_Luv2BGR,
        "HLS": cv2.COLOR_HLS2BGR,
        "YUV": cv2.COLOR_YUV2BGR,
        "XYZ": cv2.COLOR_XYZ2BGR,
        "YCrCb": cv2.COLOR_YCrCb2BGR,
    }

    # src_cvt_img = src_img.copy()
    for code, v in cvt_code_dict.items():
        src_cvt_img = src_img.astype(np.float32) * (1.0 / 255)
        cvt_img = cv2.cvtColor(src_cvt_img, code)
        decvt_img = cv2.cvtColor(cvt_img, decvt_code_dict[v])

        flag = np.array_equal(src_cvt_img, decvt_img)
        print("{}:{}".format(v, flag))

        if not flag:
            flag_mask = np.isclose(src_cvt_img, decvt_img)
            src_diff_array = np.ravel(src_cvt_img[~flag_mask].astype(np.float32))
            cvt_diff_array = np.ravel(decvt_img[~flag_mask].astype(np.float32))

            diff_list = abs(src_diff_array - cvt_diff_array)
            diff_len = len(diff_list)
            diff_sum = sum(diff_list)
            h, w = src_cvt_img.shape[:2]
            pixels = h * w

            print("元画像と異なるピクセルの割合:{:.8f}%".format((diff_len / (pixels * 3)) * 100))
            print("異なる値の平均幅:{:.8f}".format((diff_sum / diff_len)))
            print("8bitだった場合の平均幅:{:.8f}".format((diff_sum / diff_len) * 255))


if __name__ == "__main__":
    main()

色空間変換コード 色空間逆変換コード 差異の有無 異なるピクセルの割合 異なる値の平均幅(0~1) 異なる値の平均幅(0~255)
cv.COLOR_BGR2GRAY cv.COLOR_GRAY2BGR あり 99.23470052% 0.06650403 16.95852754
cv.COLOR_BGR2BGRA cv.COLOR_BGRA2BGR なし 0% 0
cv.COLOR_BGR2HSV cv.COLOR_HSV2BGR あり 0.43793403% 0.00000012 0.00003102
cv.COLOR_BGR2Lab cv.COLOR_Lab2BGR あり 99.66080729% 0.00098905 0.25220869
cv.COLOR_BGR2Luv cv.COLOR_Luv2BGR あり 3.40386285% 0.00005605 0.01429347
cv.COLOR_BGR2HLS cv.COLOR_HLS2BGR あり 0.00792101% 0.00000023 0.00005921
cv.COLOR_BGR2YUV cv.COLOR_YUV2BGR あり 52.72493490% 0.00002682 0.00683790
cv.COLOR_BGR2XYZ cv.COLOR_XYZ2BGR あり 0.33279080% 0.00000020 0.00005223
cv.COLOR_BGR2YCrCb cv.COLOR_YCrCb2BGR あり 44.03526476% 0.00002096 0.00534422

8bit、32bitどちらもBGRにアルファチャンネルを追加する変換のみ、変換前後の差異がありませんでした。既存のチャンネルに対しての操作が一切ないために差異が発生しなかったと考えられます。
それ以外の変換に関しては、少なからず差異が発生しているため、不可逆の変換だと言えます。ただし、8bitのときと32bitのときでは明らかに差異の大きさが異なりました。
結果として、色空間の変換をする際には8bit,32bitどちらも基本的に不可逆だと考えて画像を扱うべきですが、多少の誤差を許容できる場合は32bit画像での逆変換を行うことで誤差を最小限に抑えることが可能です。

さいごに

今回は、「PythonでOpenCVを使った色空間の変換」について解説しました。

それでは引き続きよろしくお願いいたします。

目次は以下の記事からご覧になれます。

15
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
5