アニメ顔の色情報に基づいた画像検索のデモ


Imager::AnimeFaceを使ったちょっとした応用例として画像検索のデモを作りました。
Imager::AnimeFaceを知らない方は Perlでアニメ顔を検出&解析するImager::AnimeFace - デーを参照してください。
ウェブサービスとしてではなく、デモやサンプルの意図で作っていて、方針としては、

  • Imager::AnimeFaceで得られる情報以上のことは考えない
  • 難しいことは無視して簡単に作る(コーディング1日〜2日で作れる程度)

です。Imager::AnimeFaceから得られる色情報はオマケみたいなもので、検索に使うには情報量が少なすぎる気がしますが、これくらいはできるよ!というデモになります。
この記事ではデモと同等のものを実装するに必要なアルゴリズム(DB作成と検索)について簡単に説明します。注意として、この記事ではPerlで解説しますが、デモの実装はSQLiteSQLでほとんどの計算を行っています。実際に使っているソースコードは最後にURLを掲載するので気になる方またはそのまま使いたい方は参考にしてください。

デモでできること

見てもらえれば一番だと思いますが、簡単に解説。
まず『髪の色で検索』をクリックすると色を選択するダイアログが出るので、テキトウな色を選択すると、選択した色と髪の色が似たキャラクターの顔が表示されます。顔以外のサムネイルは誤検出により発生した悲しい画像です。
f:id:ultraist:20090520092553p:image
f:id:ultraist:20090520092554p:image
『画像閲覧モード』の状態で顔サムネイルをクリックすると、元画像のサムネイルが表示されます。黒いのを少しでも白くするためにサムネイルになっています。元画像を見ると画像から顔ごとの情報を取り出して検索可能にしていることが分かります。この部分にImager::AnimeFaceの顔検出機能が使われています。(先日youtubeが破滅云々言っておきながら元サムネはクリックするまで表示されていません……)
f:id:ultraist:20090520092555p:image
『類似色 顔検索モード』の状態で顔サムネイルをクリックすると、選択した顔と類似した色の顔を検索します。うまいくパターンだとこんな感じです。
f:id:ultraist:20090520092556p:image
f:id:ultraist:20090520092557p:image
髪、肌、目とパーツごとの色の類似で検索しているので、色に特徴があったり目が大きかったりすると同じキャラクターがでてくることがあります。

以上。

このデモのようなものを作成するために必要なこと

手順
  1. アニメ顔DB作成
  2. アニメ顔DB検索

アニメ顔DB作成では大量の画像からImager::AnimeFaceを使って顔領域のサムネイル(表示用)を作りながら、Imager::AnimeFaceで取得できる各色情報を顔ごとにレコードとして保存します。
このDBを『髪の色を指定したクエリ』『DB内のある顔(のキー)を指定したクエリ』で色が類似したデータを検索できるようにします。

色の類似で検索

『色の類似で検索する』というのは、クエリの色とDB内各レコードの色について距離を計算して、色間の距離が近いものを表示することになります。距離が近い色とは、距離で昇順ソートして先頭のほうにくる色ということになります。
今回のデモはかなり似たものを先頭に持ってくることを重要視していて、あまり似ていないもの同士の前後関係はどうだっていい……と考えています。

色の距離

色には様々な表現方法がありますが、よく使われるものにRGB表色系があります。HTMLのカラー指定などはこれです。Imager::Animeが色情報として返却するImager::Color型もRGBの情報を持っています。これは(R,G,B)という3つの数値が(赤,緑,青)という3原色の明るさを表していて、全体としてひとつ色を表しています。つまりRGB表色系の色とは3次元のベクトルであるとか空間上の点であると考えることできます。(R,G,B)=(X,Y,Z)と考えると分かりやすいかもしれません。各色の明るさは0-255の数値で大きいほど明るく小さいほど暗くなります。つまり(0,0,0)=黒,(255,255,255)=白となります。RGB表色系の色とは255x255x255の立方体の中の点であるということになります。そうすると2つの色$A=[R,G,B],$B=[R,G,B]のユークリッド距離

$距離 = sqrt(
	($A->[0] - $B->[0]) ** 2 
	+ ($A->[1] - $B->[1]) ** 2 
	+ ($A->[2] - $B->[2]) ** 2
);

が類似の基準となるのではないか、と思えてきます。問題としては、この距離が人間の感覚な色の違いとマッチしているかということです。人間が『似た色』と感じるふたつの色の距離が遠かったり『違う色』と感じるふたつ色の距離が近いと、この距離でソートした結果を出しても、うまくいってないと思われてします。実際、人間は様々な環境光の下で同じ色を見分けれる能力を持っているので、各色の明るさを基準としたRGB表色系の空間では似ていると感じる色の距離が遠くなることがあります。
というわけで距離を計算するために色の空間を定義することから始めます。
今回は簡単に実装するという方針に従って簡単に変換できる色空間として、はてなダイアリーで紹介されている方法を使いました。
まずImager::Colorからこの色空間に変換する関数を

# 色変換
sub _rgb2ec
{
  my $rgb = shift;
  my ($r, $g, $b, undef) = $rgb->rgba();
  my $c1 = int(($r + $g + $b) / 3);
  my $c2 = int(($r + (255 - $b)) / 2);
  my $c3 = int(($r + 2 * (255 - $g) + $b) / 4);

  return [$c1, $c2, $c3];
}

と作ります。Imager::Colorはrgba()というメソッドでRGB(A)の値を取り出すことができます。c1がR,G,Bの平均なので明るさになっていて、c2とc3で各色の割合(=色彩的なもの)になっていると解釈しています。
次にふたつの色の距離を計算する関数を、

# 色の距離
sub _color_dist
{
  my ($c1, $c2) = @_;
  my $e1 = ($c1->[0] - $c2->[0]);
  my $e2 = ($c1->[1] - $c2->[1]);
  my $e3 = ($c1->[2] - $c2->[2]);
  return $e1 * $e1 + $e2 * $e2 + $e3 * $e3;
}

とします。sqrtは計算量が大きいので取りました。式としては二乗誤差になります。これを類似の基準とします。小さいほど色が似ていることにします。以降『距離』と言ったときは、比較するもの同士が似ているときに小さくなる数値と解釈してください。
『髪の色で検索』では、クエリの色とDBに保存されている全顔の髪の色の距離を_color_dist()で計算して、昇順ソートして上位N件を表示しています。

顔の色の距離

Imager::AnimeFaceでは、髪以外に肌と目の色が使えます。さらに目の色は左右各代表的な4色を取得できるようになっています。ひとつの色を類似の基準とする場合は、上記の_color_distでいいのですが、顔全体として複数の色の組み合わせで類似を考えるとうまくやるのは難しくなってきます。
ただ今回は簡単に実装するのが方針なので、難しいことは考えず単純な方法としてパーツごとの距離を重み付けて加算します。
まず準備としてImager::AnimeFaceから返された顔情報から色情報のみ取り出します。このとき色を変換しておきます。

# 顔の色情報
sub _face_color
{
  my $face = shift;
  my $face_color = { skin => [], hair => [], leye => [], reye => []};
  
  $face_color->{skin} = _rgb2ec($face->{skin_color});
  $face_color->{hair} = _rgb2ec($face->{hair_color});
  for (my $i = 0; $i < 4; ++$i) {
    $face_color->{leye}->[$i] = _rgb2ec($face->{eyes}->{left}->{colors}->[$i]);
    $face_color->{reye}->[$i] = _rgb2ec($face->{eyes}->{right}->{colors}->[$i]);
  }
  # 目の色は色彩情報でソートしておく
  @{$face_color->{leye}} = sort { $b->[2] <=> $a->[2] } @{$face_color->{leye}};
  @{$face_color->{reye}} = sort { $b->[2] <=> $a->[2] } @{$face_color->{reye}};
  
  return $face_color;
}

目の代表的な4色は、並び順が比較に影響するのでc3でソートしておきます。このソートはうまくいかないパターンもありますが、大量のデータからいい結果だけ表示できればいいので今回は気にしません。(Imager::AnimeFaceとしては、色をベクトル量子化して作ったコードブックのヒストグラムで比較することを推奨しています。)

最後に顔の色の距離を計算する関数を作ります。

# 目の色の距離
sub _eye_color_dist
{
  my ($eye1, $eye2) = @_;
  
  return 0.25 * (
    _color_dist($eye1->[0], $eye2->[0])
    + _color_dist($eye1->[1], $eye2->[1])
    + _color_dist($eye1->[2], $eye2->[2])
    + _color_dist($eye1->[3], $eye2->[3])
  );
}
# 顔の色の距離
sub _face_color_dist
{
  my ($face1, $face2, $hair_w, $skin_w, $eye_w) = @_;
  my ($skin_dist, $hair_dist, $leye_dist, $reye_dist, $eye_dist) = 0.0;
  
  # 各パーツの色の距離
  if ($skin_w != 0.0) {
    # 肌
    $skin_dist = _color_dist($face1->{skin}, $face2->{skin});
  }
  if ($hair_w != 0.0) {
    # 髪
    $hair_dist = _color_dist($face1->{hair}, $face2->{hair});
  }
  if ($eye_w != 0.0) {
    # 目
    $leye_dist = _eye_color_dist($face1->{leye}, $face2->{leye});
    $reye_dist = _eye_color_dist($face1->{reye}, $face2->{reye});
    $eye_dist = 0.5 * ($leye_dist + $reye_dist);
  }
  # 重み付き距離
  return $hair_w * $hair_dist + $skin_w * $skin_dist + $eye_w * $eye_dist;
}

目の色の距離は代表4色の距離を平均し左右でも平均します。これで、『髪の色』『肌の色』『目の色』の距離が個別に出るので、重み付で足し合わせて顔間の距離とします。距離というより誤差と考えています。重みは各パーツの色の違いをどれだけ重視するかという割合で、重みが大きいと誤差が大きく加算され、重みが小さいと誤差が小さく加算されます。

デモの『類似色 顔検索モード』では、

($hair_w, $skin_w, $eye_w) = (0.8, 0.1, 0.3);

としています。この割合には特に根拠はありません。試行錯誤から決めた値になります。
『髪の色で検索』では、

($hair_w, $skin_w, $eye_w) = (1.0, 0.1, 0.0);

としています。このとき髪の色はクエリで指定された色、肌の色は肌の平均色(226,145,126)としています。肌の色は、誤検出のサムネイルをできるだけ表示しないための努力として追加しました。

ここまでをまとめると、

  1. 大量の画像からImager::AnimeFaceで顔検出を行い、顔ごとに_face_color()で得た色情報を保存する
  2. 上記類似の基準を使って、クエリと全レコードについて距離を計算し、距離で昇順ソートした上位N件を検索結果として表示する

となります。色の処理以外は取り上げるまでもないことだと思っているので、実際に使っているソースコードだけ出して終わります。
DB作成と検索CGI部分のソースコード404 Not Foundにあります。
前述したとおり、ほとんどの計算をSQLiteで行っているので、上で説明した関数は一部しか使われていません。このソースコードは参考として出したもので、実はいつの間にか破滅していてそのまま使えるものではない可能性もあると警告しておきます。
コードの内容を簡単に説明すると、

FaceColorDB.pm
DB作成と検索の関数。モジュールになっているがバッチ処理などかなりごちゃごちゃ入ってる。エラー処理がヤバイ。
create_table.sql
顔色テーブル作成SQL。sqlite3で最初に実行。
make_db.pl
make_db.pl imgdir db_file thumb_dirでimgdirの画像から顔色テーブルにレコードを追加する。サムネイルをthumb_dirに作成する。元画像サムネの存在に基づいた差分更新にも対応。
clean_thumb.pl
顔がひとつも検出されていない元画像のサムネイルを消す処理。消すと差分更新ができなくなるので注意。
search_face_cgi.txt
『類似色 顔検索モード』で使われているCGI。JSONを吐く。
search_haircolor_cgi.txt
『髪の色で検索』で使われているCGI。JSONを吐く。

となります。

まとめ

簡潔さを目指してある程度テキトウにやっても、データさえあればそこそこの応用例が作れることが分かってもらえたと思います。アニメフェースはこわくないので、みなさん、アニメフェース職人になりましょう。
なにかgdgdになってしまいました。

おもひで

最初は目の顔に対する割合や目の縦横比など部品の形状に関する情報も使おうと思っていたのですが、そう考え始めると髪の色を基準に2値化して髪型も取るべきだとか、顔の輪郭も肌の色を基準に2値化すれば取れるから……いや、むしろ顔部品位置から画像の位置合わせを行って……前に見たアレでは……と収拾が付かなくなりそうだったので、開き直って『色のみで簡単に』としました。
心に余裕ができたら次回はスゴイのを目指してがんばりたいと思います。

おまけ

笑顔抽出!!
f:id:ultraist:20090520092559p:image
説明しよう、笑顔抽出とは、口が開いてる顔を抽出することである。

なにかgdgdになってしまいました。