まいにちくらくら

アニメの考察感想とか

【OpenCV】Lab色空間と閾値処理【画像処理】

OpenCVに関しての記事をいくつか書いてきましたが、今回は単純に作ったものの公開になります。

大したものではないですが、 Lab色空間に関しての復習がてら書いていきます。

環境

OS macOS
エディタ Xcode
言語 C++
使用ライブラリ OpenCV4系

今回作るもの

入力画像に対して、Lab画像への変換を行い、それに対して閾値処理行う。その結果を可視化し、任意の状態のパラメータを保存する。 結果はこんな感じ。

f:id:yamachi_9rakura:20190901165335p:plain
入力画像
f:id:yamachi_9rakura:20190901165411p:plain
Lab画像
f:id:yamachi_9rakura:20190902015411p:plain
可視化ウィンドウ(上部に閾値変更トラックバー)

Lab色空間って?

Lab色空間(エル・エー・ビーいろくうかん、英: Lab color space)は補色空間の一種で、明度を意味する次元 Lと補色次元の a および b を持ち、CIE XYZ 色空間の座標を非線形に圧縮したものに基づいている。

RGBやCMYKとは異なり、Lab色空間は人間の視覚を近似するよう設計されている。知覚的均等性を重視しており、L成分値は人間の明度の知覚と極めて近い。したがって、カラーバランス調整を正確に行うために出力曲線を a および b の成分で表現したり、コントラストの調整のためにL成分を使ったりといった利用が可能である。

(出典: [フリー百科事典 ウィキペディアWikipedia)] https://ja.wikipedia.org/wiki/Lab%E8%89%B2%E7%A9%BA%E9%96%93

引用元に色々書いてありますが

・人間の感覚に合わせて考案されたよ

・環境光の影響を受けにくいらしいよ

って感じです。

とりあえずコード

include文等省略。main文のみ。

using namespace std;
using namespace cv;

int main(int argc, const char * argv[]) {

ofstream writing_file;

Mat src_img; 
src_img = cv::imread(FILE_NAME); 
if (src_img.empty()) { 
    fprintf(stderr, "読み込み失敗\n");
    return (-1);
}

namedWindow(WINDOW_NAME, 1);


int iSliderValue_a_max = 120+20;
createTrackbar("a Channel_max", WINDOW_NAME, &iSliderValue_a_max, 255);

int iSliderValue_a_min = 120-20;
createTrackbar("a Channel_min", WINDOW_NAME, &iSliderValue_a_min, 255);

int iSliderValue_b_max = 120+20;
createTrackbar("b Channel_max", WINDOW_NAME, &iSliderValue_b_max, 255);

int iSliderValue_b_min = 120-20;
createTrackbar("b Channel_min", WINDOW_NAME, &iSliderValue_b_min, 255);


Mat lab_img, a_img, b_img;
cvtColor(src_img, lab_img, COLOR_BGR2Lab); 
imshow("lab_img", lab_img);
vector<Mat> planes;
split(lab_img, planes);


while (true){
    Mat result;  

    threshold(planes[2], b_img, iSliderValue_b_min, 255., THRESH_TOZERO);
    threshold(planes[1], a_img, iSliderValue_a_min, 255., THRESH_TOZERO);

    threshold(b_img, b_img, iSliderValue_b_max, 255., THRESH_BINARY_INV);
    threshold(a_img, a_img, iSliderValue_a_max, 255., THRESH_BINARY_INV);
    bitwise_and(b_img, a_img, b_img);

    if(iSliderValue_a_max < iSliderValue_a_min || iSliderValue_b_max < iSliderValue_b_min){
        cout << "閾値が不正です." << endl;
    }

    cout << "a:" << iSliderValue_a_min << "~" << iSliderValue_a_max << endl;
    cout << "b:" << iSliderValue_b_min << "~" << iSliderValue_b_max << endl;

    src_img.copyTo(result, b_img);
    imshow(WINDOW_NAME, result);

    int iKey = waitKey(50);


    if (iKey == 32){
        writing_file.open(SAVE_FILE_NAME, ios::app);
        writing_file << "file_name:";
        writing_file << FILE_NAME << endl;
        writing_file << "a_par:"+to_string(iSliderValue_a_min)+"~"+to_string(iSliderValue_a_max) << endl;
        writing_file << "b_par:"+to_string(iSliderValue_b_min)+"~"+to_string(iSliderValue_b_max) << endl << endl;
    }


    if (iKey == 27){
        break;
    }
}

return 0;
}

細かく見ていく

では細分化して細かく見ていきましょう。

// ファイル出力用
ofstream writing_file;

//画像の入力
Mat src_img; //画像の型と変数
src_img = cv::imread("FILE_NAME"); //画像の読み込み
if (src_img.empty()) { //入力失敗の場合
    fprintf(stderr, "読み込み失敗\n");
    return (-1);
}

パラメータを記録する出力ファイルを保持する変数を宣言。 画像の入力を処理を行なっています。

//window生成
namedWindow(WINDOW_NAME, 1);

//aチャンネル用トラックバー生成(初期値120,値域0~255)
int iSliderValue_a_max = 120+20;
createTrackbar("a Channel_max", WINDOW_NAME, &iSliderValue_a_max, 255);

int iSliderValue_a_min = 120-20;
createTrackbar("a Channel_min", WINDOW_NAME, &iSliderValue_a_min, 255);

//bチャンネル用トラックバー生成(初期値120,値域0~255)
int iSliderValue_b_max = 120+20;
createTrackbar("b Channel_max", WINDOW_NAME, &iSliderValue_b_max, 255);

int iSliderValue_b_min = 120-20;
createTrackbar("b Channel_min", WINDOW_NAME, &iSliderValue_b_min, 255);

ウィンドウ生成したのちトラックバーの生成を行なっています。 トラックバーをスライドさせることでLab画像のaチャンネルとbチャンネルの閾値をいじることが目的です。

//Lab変換
Mat lab_img, a_img, b_img;
cvtColor(src_img, lab_img, COLOR_BGR2Lab); //rgbをlabに変換
imshow("lab_img", lab_img);
vector<Mat> planes;
split(lab_img, planes);

RGB画像である入力をcvtColorでLabに変換します。

画像の色空間を変換する関数
cv::cvtColor(入力画像, 出力画像, 変換コード, 出力画像のチャンネル数(デフォルトで入力と出力から自動で求められる))

参考 opencv.jp

その後、cv::splitで(名前的に間違えやすいけど文字列操作ではないです)3チャンネルであるLab画像のシングルチャンネルの配列に分割します。

マルチチャンネルの配列を,複数のシングルチャンネルの配列に分割する関数
cv::split(マルチチャンネルの入力配列, 出力)

参考 opencv.jp

while (true){
    Mat result;

    //下限より小さい値を0に
    threshold(planes[2], b_img, iSliderValue_b_min, 255., THRESH_TOZERO);
    threshold(planes[1], a_img, iSliderValue_a_min, 255., THRESH_TOZERO);
    //上限より大きい値を0
    threshold(b_img, b_img, iSliderValue_b_max, 255., THRESH_BINARY_INV);
    threshold(a_img, a_img, iSliderValue_a_max, 255., THRESH_BINARY_INV);
    bitwise_and(b_img, a_img, ab_img);

    if(iSliderValue_a_max < iSliderValue_a_min || iSliderValue_b_max < iSliderValue_b_min){
        cout << "閾値が不正です." << endl;
    }

    cout << "a:" << iSliderValue_a_min << "~" << iSliderValue_a_max << endl;
    cout << "b:" << iSliderValue_b_min << "~" << iSliderValue_b_max << endl;

    //mask処理
    src_img.copyTo(result, ab_img);
    imshow(WINDOW_NAME, result);

無限ループ内の処理。 thresholdで閾値処理をします。

入力配列の要素に対して、ある定数による閾値処理を行う関数
cv::threshold(入力画像, 出力画像, 閾値, 最大値, オプション)
 -オプション
  ->cv::THRESH_BINARY:閾値より大きい値を最大値に. それ以外は0
  ->cv::THRESH_BINARY_INV:閾値より小さい値を最大値に. それ以外は0
  ->cv::THRESH_TRUNC:閾値より大きい値は閾値まで切り詰められ,それ以外はそのまま
  ->cv::THRESH_TOZERO:閾値より大きい値はそのまま. それ以外は0
  ->cv::THRESH_TOZERO_INV:閾値以下の値はそのまま. それ以外は0

閾値はトラックバーをいじることで可変するため、任意の閾値を設定し処理を行います。 閾値処理により、それぞれのチャンネルごとで下限の閾値を下回る値を0にし,その後上限の閾値を下回る値を255にします。

この処理により、得たい範囲内の値をもつ要素のみが255の値をもつ配列が得られます。(a_img, b_img)

言い換えれば、任意の範囲内の値をもつ部分が白色となる輝度画像がチャンネルごとに得られます。

この二つに対して、bitwise_andで論理積をとり、マスク処理を行うためのマスク画像(表示したい部分のみを白色とする画像)を生成します。 参考:http://rs.aoyaman.com/img_pro/b5.html

各要素のビット毎の論理積を求める
bitwise_and(入力1, 入力2, 出力)

f:id:yamachi_9rakura:20190902021656p:plain
マスク画像

マスク処理を行う際にはcopyToを使います。

対象画像.copyTo(出力, マスク画像);

参考 :https://cvtech.cc/mask/

// Wait until user press some key for 50ms
    int iKey = waitKey(50);


    //if user press 'Enter' key
    //save img_name and values
    if (iKey == 32){
        writing_file.open(SAVE_FILE_NAME, ios::app);
        writing_file << "file_name:";
        writing_file << FILE_NAME << endl;
        writing_file << "a_par:"+to_string(iSliderValue_a) << endl;
        writing_file << "b_par:"+to_string(iSliderValue_b) << endl << endl;

Enterが押したら保存するよってだけ。

さいごに

ミドラーシュの髪の部分とか緑の部分だけ抜きたかったんですけど、うまく取れてないっていう……。 Labで試してみましたが、カードみたいなものだとHSVで十分そうです。

本来は黄色の物体を抜きたかったときに色々試していた中で作られたものです。Labだと緑と黄色が近いので割と一緒に取られてしまうことが多く、パラメータを吟味したくて作りました。結果色々試せました。

ただ、Labは環境光の影響を受けにくいって聞いたんですが、ほんとなんですかね……。

eveydayanime-9rakura.hatenablog.com