LoginSignup
3
2

More than 3 years have passed since last update.

完全自動モーフィングへの道<そのハマりどころ>

Last updated at Posted at 2020-12-24

完全自動モーフィングへの道

モーフィングの歴史

モーフィングは古くて新しい技術である。1988年に Industrial Light & Magic が映画 "Willow"の中で魔法のシーーン使ったのが最初とされ、1991年のマイケル・ジャクソンの"Black or White"で次々と色んな人種の人がモーフイングしてくシーンで誰もが知るイフェクトとなった。もう30年以上も前に世に出た技術であるが、最近はDeep LerningやGPUの発達によってリアルタイムや半自動でできるようになった。
 当初は二つの画像のマッチングポイント(たとえば顔のモーフィングの場合、顔の部品の輪郭など)を手作業で指定してつなぐ、たいへんな作業であった。

自動モーフィングをopenframeworksだけで作る

現代の誰もが使える技術で自動モーフィングができるアプリケーションを作ってみるというのが本稿のテーマで、openframeworksだけで作ってみた。羊頭狗肉になるといけないので最初に断っておくが、今回は顔のモーフィングに特化しており、また、顔の周りの髪などは対応していないが、OpenCVなどを援用して、なるべく顔の部品以外にも対応していきたい。だから「完全自動モーフィングへの道」というわけである。本稿の本来の目的は自動モーフィングのインプリメント中に出くわす思わぬハマりどころやTIPSを紹介することにある。

手順

想定される自動モーフィングの手順は以下の通りである。

二つの画像からface landmarksを抽出 → その特徴点からそれぞれのメッシュの生成 → メッシュの各頂点の座標値の差を求め、画像Aで生成されたメッシュをBの頂点の座標値へ変形し、同時に画像Bで生成されたメッシュをAの頂点の座標値へ変形するアニメーションとそれぞれのメッシュにそれぞれの画像をマッピングする。画像はαチャンネルを使ってクロスディゾルブする。

というものである。一言で云うと、

  morphing = warping + cross dissolve

となる。

顔の画像からfacelandmaksを求め、それを頂点としたメッシュを生成する。

下記に「顔の画像からfacelandmaksを求め、それを頂点としたメッシュを生成する」パートを示す。

void ofApp::loadFace(string face) {
    //cout << face << std::endl;               //faceは画像ファイルのpath
    src[faceNum].loadImage(face);              //画像のロード
    src[faceNum].resize(ofGetWidth() / 2, ofGetHeight()); //画像のリサイズ
    srcTracker.update(toCv(src[faceNum]));         //顔の画像処理
    srcPoints[faceNum] = srcTracker.getImagePoints();   //facelandmarksを求める
    cout << srcPoints[faceNum].size() << std::endl;    //その数をデバッグ用に求める

    delaunay[faceNum].reset();                //ドロネー配列をリセット

    delaunay[faceNum].addPoint(ofPoint(0.0, 0.0));     // 画面左上の点を追加

    delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(0, 0));//そのuv値を追加

    for (int i = 0; i < srcPoints[faceNum].size(); i += 1) { 

        //int j = srcPoints[faceNum].size() - i - 1;
        int j = i;
         // landmarksをすべて追加
        delaunay[faceNum].addPoint(ofVec2f(srcPoints[faceNum].at(j).x, srcPoints[faceNum].at(j).y));   
         // そのuv値を追加
        delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(srcPoints[faceNum].at(j).x, srcPoints[faceNum].at(j).y)); 
    }

    int wi = src[faceNum].getWidth();
    int hi = src[faceNum].getHeight();
    // add points for corner 残りのコーナーの3点を追加
    delaunay[faceNum].addPoint(ofPoint(wi, 0.0));
    delaunay[faceNum].addPoint(ofPoint(wi, hi));
    delaunay[faceNum].addPoint(ofPoint(0.0, hi));
    // add texcoord for corner 残りのコーナーのuv値を追加
    delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(wi, 0));
    delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(wi, hi));
    delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(0, hi));
    // triangulate
    delaunay[faceNum].triangulate();
    // add dummy uv for display  おまじない
    delaunay[faceNum].triangleMesh.addTexCoord(glm::vec2(0.0, 1.0));

}

ここまでのハマりどころ

上のリストのコメントにそれぞれの行でやっていることを記したが、ドロネーでメッシュを作る場合のハマりどころは、頂点の追加の順番である。最初、顔のランドマークから追加していたら、メッシュの構造に破綻を生じる。ここで一日は無駄に費やした。だから、最初に左の(0,0)を追加し、次にランドマークを追加し、最後に残りの四隅の三頂点を追加してうまくいった。また、addTexCoordは0-1ではなく、頂点と同じ値にする。これもハマりどことである。

おまじない

そして、最後の行で、どの頂点にも対応しないuv値を追加しないとメッシュが生成しない。

GIT

上のメソッドを含む動作中のコードを 
https://github.com/ultrahamlet/morphing
にアップしてあるので参考にしていただきたい。dataの下に、画像0000.jpgと0001.jpgという二つの顔
画像を置いて起動すると、自動的にモーフィングが始まる(まだ、変形アニメーションが完成していない。このコードは頻繁にデバッグ中なのでバージョンによっては動作しない)。
(一応動くようになりました。 shape_predictor_68_face_landmarks.dat を bin/data フォルダに置いてください。↓のリンクからダウンロードできます。
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2 
動作画面の動画です。Windows 64bit用。
https://youtu.be/0M10RVVmIgs

上のソースコードをbuildできない方は、実行ファイルはこちらから→
https://dcf.jp/AutoFaceMorph/AutoFaceMorph.zip
AFM.exeを実行する前に、http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
から、shape_predictor_68_face_landmarks.dat.bz2
をダウンロードして、解凍し、dataフォルダの下に置いてください。
AFM.exe の操作法の動画です。AFM.exeはAutoMorphのフロントエンドで、画像をドロップして、0000.pngと、0001.pngを生成します。画像の回転情報も修正しています。
https://www.youtube.com/watch?v=gJc-kKpHQ4M

(その他のコードの解説とコードの変更を追っていたしますので、followしていただければ、更新の度に
お知らせします。)

ドロネーを描画してみる

さて、上のメソッドで作成したメッシュは以下の方法で、チェックできる。

src[0].bind();           // テクスチャーをバインドする
delaunay[0].triangleMesh.draw();
src[0].unbind();           // テクスチャーをアンバインドする。

これで、その画像が描けたらメッシュへの画像のマッピングがうまくいっているということである。
上の二行目をdelaunay[0].triangleMesh.drawWireframe(); にすると、ワイアフレームで描かれるので、ただの画像でないことがわかる。

3
2
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
3
2