kivantium活動日記

プログラムを使っていろいろやります

k-meansを用いた画像の色クラスタリング

ご注文は機械学習ですか?を改良するべくDeep Learningなどに手を出しているわけですが、Deep Learningを行うためには大量の学習データが必要になります。学習データを一から手動で分類するのも馬鹿らしいので、今出来ている部分を使ってある程度自動化したいと思います。現状のプログラムだと精度が低いので何らかの方法で精度を上げたいと思います。というわけで、
アニメ作品における人物キャラクター画像の萌え特徴分析とその応用にある手法を用いて髪部分のみを抽出することで精度を上げてみようと試してみました。

処理の流れ

元論文ではメディアンカットによる減色→クラスター併合とK-means法を用いて6クラスタに併合→肌・髪・目レイヤーの特定という流れで進んでいます。実装が公開されていないのでこの情報から手法を再現する必要があります。

とりあえず、減色・クラスタリングまでを行ってみようと思います。

プログラム

メディアンカット法の実装は面倒だったので単純な減色を行います。
k-meansによるレイヤー分けは
memo: K-means クラスタリングを参考にしました。
6つのレイヤーに分ける処理はMatの配列なり3次元Matなりを使えばもっと綺麗に書けそうですが、実装速度を重視ということで非常に汚くなっています。もっといい方法があったら教えてください。

OpenCVのk-means関数については
クラスタリングと多次元空間探索に説明があります。

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

//減色用の関数
int convert(int value) {
    if (value < 32) {
        return 32;
    } else if (value < 64) {
        return 64;
    } else if (value < 96) {
        return 96;
    } else if (value < 128) {
        return 128;
    } else if (value < 160) {
        return 160;
    } else if (value < 192) {
        return 192;
    } else if (value < 224) {
        return 224;
    } else {
        return 255;
    }

    return 0;  // 未到達
}

int main(int argc, const char * argv[]){
    //画像の読み込みと表示
    Mat input = imread(argv[1]);
    if (input.empty()) {
        cout << "Failed to load image!" << endl;
        return -1;
    }
    imshow("original", input);
    
    //正規化・減色
    Mat norm(input.size(), input.type());
    Mat sample(input.size(), input.type());
    normalize(input, norm, 0, 255, NORM_MINMAX, CV_8UC3);
    for(int y=0; y<input.rows; y++){
        for(int x=0; x<input.cols; x++){
            for(int c=0; c < input.channels(); ++c){
                int index = y*input.step+x*input.elemSize()+c;
                sample.data[index] = convert(norm.data[index]);
            }
        }
    }	
    input = sample;
    imshow("down", input);

    //kmeansを行うためにフォーマットを変換
    Mat matrix = input.reshape(1, input.cols*input.rows);
    matrix.convertTo(matrix, CV_32FC1, 1.0/255.0);
     
    //kmeansの実行
    Mat centers, labels; //結果の格納
    TermCriteria criteria (TermCriteria::COUNT, 100, 1); //100回繰り返す
    int cluster_number = 6; //クラスタ数
    kmeans(matrix, cluster_number, labels, criteria, 1, KMEANS_RANDOM_CENTERS, centers);

    //レイヤーの準備(初期状態を白で統一)
    Mat layer0(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));
    Mat layer1(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));
    Mat layer2(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));
    Mat layer3(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));
    Mat layer4(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));
    Mat layer5(Size(input.cols, input.rows), CV_8UC3, Scalar::all(255));

    //色とラベル情報の準備
    MatConstIterator_<int> label_first = labels.begin<int>();
    centers.convertTo(centers, CV_8UC1, 255.0);
    centers = centers.reshape(3);
     
    //レイヤーごとに色を分ける
    for(int y=0; y<input.rows; ++y){
        for(int x=0; x<input.cols; ++x){
            const Vec3b& rgb = centers.ptr<Vec3b>(*label_first)[0];
            if(*label_first == 0) layer0.at<Vec3b>(y,x) = rgb;
            else if(*label_first == 1) layer1.at<Vec3b>(y,x) = rgb;
            else if(*label_first == 2) layer2.at<Vec3b>(y,x) = rgb;
            else if(*label_first == 3) layer3.at<Vec3b>(y,x) = rgb;
            else if(*label_first == 4) layer4.at<Vec3b>(y,x) = rgb;
            else layer5.at<Vec3b>(y,x) = rgb;
            ++label_first;
        }
    }
    //レイヤーごとに表示
    imshow("layer0", layer0);
    imshow("layer1", layer1);
    imshow("layer2", layer2);
    imshow("layer3", layer3);
    imshow("layer4", layer4);
    imshow("layer5", layer5);

    //レイヤーごとに保存
    imwrite("layer0.png", layer0);
    imwrite("layer1.png", layer1);
    imwrite("layer2.png", layer2);
    imwrite("layer3.png", layer3);
    imwrite("layer4.png", layer4);
    imwrite("layer5.png", layer5);
     
    waitKey(0);

    return 0;
}

結果

元画像と生成された6レイヤーを示します。
f:id:kivantium:20150219002600p:plain:w100f:id:kivantium:20150219002606p:plain:w100f:id:kivantium:20150219002612p:plain:w100f:id:kivantium:20150219002618p:plain:w100f:id:kivantium:20150219002622p:plain:w100f:id:kivantium:20150219002626p:plain:w100f:id:kivantium:20150219002629p:plain:w100

髪だけが抽出されたレイヤーは作られませんでした。肌と髪が同じクラスタに分類されてしまっているようです。実験を繰り返したところ、この画像の場合10レイヤー以上用意すれば肌と髪が違うクラスタに分類されるようですが、その場合髪の一部のみのレイヤーになってしまい、髪色を再現したとは言えなくなっていました。

髪の毛だけの情報を抽出したいのに、髪の毛の情報すらまともに得られなくなってしまっては元も子もないので、この手法による精度向上は諦めるしかなさそうです。