初回更新以来の投稿になります。渕上です。

以前、node.jsを触ってみたくなり、
試しにかんたんなチャットを作ってみたことがありました。

その時、Webカメラをつないでビデオチャットを作ってみたい、
恥ずかしがりやさんのために笑い男マークで顔をマスクする機能を付けたら面白そう。
などと妄想していたのですが、なかなか行動に移すきっかけがありませんでした。

折角いただいた、仕事以外の技術を調べる機会ですので、
今回からは何回かにわたって笑い男ビデオチャットの実装にチャレンジしてみたいと思います。

とはいえ、いきなり動画の顔識別とかビデオチャット実装なんてできません。
今回は静止画の顔を識別して、他の画像と置き換える処理を実装してみたいと思います。

画像の顔部分を検出するためには、”OpenCV”というライブラリを利用することが一般的のようです。
OpenCVは様々な機能を持つオープンソースの画像処理ライブラリで、
機能の一つに、画像内の特定の物体を検出する機能が含まれています。

今回の目標

  • 仮想環境にOpenCVをインストールする
  • 顔識別してほかの画像に置き換えるスクリプトを書いてみる

用意した環境

  • Windows 8 (ホストマシン)
  • Ubuntu 14.04 (ゲストマシン)
  • OpenCV 2.4.9

OpenCVのインストール

まずはOpenCVのインストールを行います。
OpenCVは依存関係のあるライブラリが相当数あるようで、
いきなり一つ一つ解決していくのはハードルが高そうです。

依存関係を纏めてインストールするバッチをgithubで公開されている方がいましたので、
今回はそのバッチを利用させていただきました。
(Ubuntu, ArchLinux, Redhat系 向けのシェルが用意されているようです。)

gitコマンドでリポジトリをcloneして、インストール用のシェルを起動します。

$ git clone https://github.com/jayrambhia/Install-OpenCV.git
$ cd ~/Install-OpenCV/Ubuntu/
$ ./opencv_latest.sh

opencv_latest.sh は最新版を検索してインストールしてくれるシェルのようです。
3.0.0 がダウンロードされましたが、インストール時にエラーになってしまいました。

諸事情からあまり時間がとれないので、ここは割り切ります。
今回はエラーの原因究明をせず、バージョン2.4.9のインストールを行いました。

$ cd ~/Install-OpenCV/Ubuntu/2.4.9
$ bash ./opencv_2.4.9.sh

OpenCVを扱う

OpenCVはC,Pythonなど様々な言語から扱うことができるようです。
今回は、Python向けのバインディングを利用してみました。

OpenCVで顔認識の仕組み

分類器と呼ばれる仕組みを使って顔の検出を行うそうです。

分類器は顔の検出に特化したものではなく、
セットされる”学習済みデータ”によって様々な物体の検出ができるとのこと。

この”学習済みデータ”というのは機械学習プログラムに大量の画像と、
画像内の顔部分の座標を与えて作成できるものだそうで、
いくつかの学習済みデータが付属しています。

私はこの仕組みを全く理解できていないのですが、
今のところは、
「大量のサンプルを与えることによって、物体の識別ができるようになる賢い小人」
として認識しておくことにします。

今回は付属のhaarcascade_frontalface_alt_tree.xmlを使ってみました。

顔認識するスクリプトを書いてみる

顔の部分を識別して、結果を出力してみます。
うまくいけば[x座標, y座標, 顔部分のwidth, 顔部分のheight] のリストが返ってくるはずです。

step1.py

# -*- coding: utf-8 -*-
import cv2
import sys

# 学習済みデータのパス
CASCADE_FILE_PATH = "/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt_tree.xml"

# 顔識別する対象のファイルパス
base_image_path = sys.argv[1]
# 分類器のセットアップ
cascade         = cv2.CascadeClassifier(CASCADE_FILE_PATH)
# グレースケールに変換する
grayscale_image = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)

# 顔識別結果の確認
print cascade.detectMultiScale(grayscale_image, scaleFactor=1.1, minNeighbors=1, minSize=(100, 100))

結果

$ python step1.py /vagrant/input/test.jpg
[[465 300 144 144]
 [924 171 175 175]
 [712 155 177 177]]

どうやら、きちんと3人分検出できたようです。

顔検出して、他の画像を重ね合わせる

顔の検出をしても、座標を出力するだけでは実感がわきません。
いよいよ顔部分を笑い男化してみたいと思います。

OpenCVでは特定の座標に対して同サイズの画像を加算することで、画像の重ね合わせが可能です。

ベース画像

一人で撮影するのがさみしかったので、近くの人間を捕まえて写真撮影しました。
快く受け入れてくださったお二人、ありがとうございます。
test

笑い男画像

ニコニコモンズで配布されていた笑い男ロゴを利用します。

step2.py

# -*- coding: utf-8 -*-
import cv2
import sys

# 学習済みデータのパス
CASCADE_FILE_PATH = "/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt_tree.xml"

base_image_path = sys.argv[1]
overlay_image_path = sys.argv[2]

base_image      = cv2.imread(base_image_path)
overlay_image   = cv2.imread(overlay_image_path)
cascade         = cv2.CascadeClassifier(CASCADE_FILE_PATH)
grayscale_image = cv2.cvtColor(base_image, cv2.cv.CV_BGR2GRAY)

results = cascade.detectMultiScale(grayscale_image, scaleFactor=1.1, minNeighbors=1, minSize=(100, 100))

if len(results) > 0:
    for rect in results:
        # 顔部分の縦横サイズ取得
        rect_w = rect[1]
        rect_h = rect[2]

        # 重ね合わせ対象の画像を、顔領域に合わせてリサイズ
        resized_overlay_image = cv2.resize(overlay_image, (rect_w, rect_h))
        resized_overlay_image_w, resized_overlay_image_h = resized_overlay_image.shape[:2]

        # 顔領域にリサイズした画像を重ねる
        base_image[rect[1]:(rect[1]+resized_overlay_image_w), rect[0]:(rect[0]+resized_overlay_image_h)] = resized_overlay_image;

    # 保存
    cv2.imwrite("/vagrant/output/detected.jpg", base_image)

結果

python step2.py /vagrant/input/test.jpg /vagrant/input/nc73730.png

res1

無事、笑い男画像を重ねることができました。
ただ、透過処理を行っていないためか透過部分が黒くなってしまっています。

アルファチャンネルを考慮する

貼り付けた画像の透過部分が黒くなってしまったので、透過できるようにしてみます。
アルファチャンネルの取り扱いは、こちらの記事を参考にしました。
(恥ずかしながらまだ仕組みが理解できていないので、ほぼそのまま使わせていただきました。
アルファチャンネルの仕組みについて、今後勉強していきたいと思います。)

step3.py

# -*- coding: utf-8 -*-
import cv2
import sys

# 学習済みデータのパス
CASCADE_FILE_PATH = "/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt_tree.xml"

base_image_path = sys.argv[1]
overlay_image_path = sys.argv[2]

base_image      = cv2.imread(base_image_path)
# アルファチャンネルを維持するために、IMREAD_UNCHANGEDフラグを与えて読み込み
overlay_image   = cv2.imread(overlay_image_path, cv2.IMREAD_UNCHANGED)

# 分類器のセットアップ
cascade         = cv2.CascadeClassifier(CASCADE_FILE_PATH)
# グレースケールに変換する
grayscale_image = cv2.cvtColor(base_image, cv2.cv.CV_BGR2GRAY)

results = cascade.detectMultiScale(grayscale_image, scaleFactor=1.1, minNeighbors=1, minSize=(100, 100))

# アルファチャンネル
# 参考サイトの処理をそのまま真似させていただきました
mask = overlay_image[:,:,3]
mask = cv2.cvtColor(mask, cv2.cv.CV_GRAY2BGR)
mask = mask / 255.0
overlay_image = overlay_image[:,:,:3]

if len(results) > 0:
    for rect in results:
        # 顔部分の縦横サイズ取得
        rect_w = rect[1]
        rect_h = rect[2]

        # 重ね合わせ対象の画像を、顔領域に合わせてリサイズ
        resized_overlay_image = cv2.resize(overlay_image, (rect_w, rect_h))
        resized_overlay_image_w, resized_overlay_image_h = resized_overlay_image.shape[:2]

        # マスクを、顔領域に合わせてリサイズ
        resized_mask = cv2.resize(mask, (rect_w, rect_h));

        # 顔領域にリサイズした画像を重ねる
        base_image[rect[1]:(rect[1]+resized_overlay_image_w), rect[0]:(rect[0]+resized_overlay_image_h)] *= 1-resized_mask;
        base_image[rect[1]:(rect[1]+resized_overlay_image_w), rect[0]:(rect[0]+resized_overlay_image_h)] += resized_overlay_image*resized_mask;

    # 保存
    cv2.imwrite("/vagrant/output/detected.jpg", base_image)

結果

python step3.py /vagrant/input/test.jpg /vagrant/input/nc73730.png

detected

透過処理を行うことができました!

次回予告

今回は、無事に静止画の加工をおこなうことができました。

笑い男ビデオチャットを作るには

  • OpenCVでwebカメラを扱う
  • Nodeでwebカメラを扱う

等、まだまだ乗り越えるべきハードルがたくさんあるので、
次回以降もひとつづつ調査、実装してみたいと思います。