OpenCV

【C++】OpenCVで作る画像モーフィング入門:特徴点検出からアニメーション生成まで

OpenCVとC++なら、顔など二枚の画像を滑らかに変形し合成するモーフィングを手軽に組めます。

DlibやOpenCVで特徴点を取得し、Delaunay三角形分割でメッシュを共有、各三角をcv::getAffineTransformcv::warpAffineで変形し、αブレンドすれば自然な遷移が得られます。

リアルタイムでも高解像度でもGPU支援で速度が伸び、動画への応用も可能です。

処理フローの全体像

画像モーフィングをC++とOpenCVで実装する際は、いくつかの主要な工程を順に進めていきます。

ここでは、全体の流れを理解しやすいように、各ステップの役割と処理内容を解説します。

入力データの前処理

モーフィングの対象となる画像は、まず前処理を行います。

前処理の目的は、後続の処理がスムーズに行えるように画像の形式やサイズを整えることです。

  • 画像の読み込み

OpenCVのcv::imread関数を使って画像ファイルを読み込みます。

カラー画像の場合はBGR形式で読み込まれます。

  • サイズの統一

モーフィングでは2枚の画像を重ね合わせて変形させるため、画像サイズが異なると処理が複雑になります。

cv::resizeを使って、両画像のサイズを同じに揃えます。

  • 色空間の変換(必要に応じて)

顔検出や特徴点検出の精度を上げるために、グレースケール画像に変換することがあります。

cv::cvtColorでBGRからグレースケールに変換します。

  • ノイズ除去や平滑化(オプション)

画像のノイズが多い場合は、cv::GaussianBlurなどで軽く平滑化しておくと、特徴点検出の精度が向上します。

このように前処理を行うことで、後の特徴点検出や変形処理が安定して動作します。

特徴点検出工程

モーフィングの肝となるのが、画像間で対応付ける特徴点の検出です。

特に顔画像の場合は、目や鼻、口などのランドマークを検出して、それらを基準に変形を行います。

  • 顔検出

まず顔の位置を検出します。

OpenCVのCascadeClassifierやDlibの顔検出器を使うことが多いです。

  • 特徴点(ランドマーク)検出

Dlibの68点ランドマーク検出器やOpenCVのFacemark APIを利用して、顔の主要な特徴点を抽出します。

これらの点は、目の角、鼻の先端、口の輪郭など、顔の形状を表す重要なポイントです。

  • 手動補正(必要に応じて)

自動検出がうまくいかない場合は、GUIツールなどで手動でポイントを修正することもあります。

  • 特徴点の正規化

検出した特徴点は画像座標系で表されるため、画像サイズに合わせてスケーリングや座標調整を行います。

この工程で得られた対応点のセットが、後の三角形分割や変形の基礎となります。

メッシュ生成工程

特徴点が揃ったら、次はそれらを結んで三角形メッシュを作成します。

メッシュは画像を小さな三角形に分割し、それぞれの三角形単位で変形を行うための構造です。

  • Delaunay三角形分割

OpenCVのSubdiv2Dクラスを使って、特徴点群に対してDelaunay三角形分割を行います。

Delaunay分割は、三角形の内角が極端に小さくならないように分割するため、変形時の歪みを抑えられます。

  • 三角形のインデックス管理

分割結果として得られる三角形は、特徴点のインデックスで管理します。

これにより、対応する三角形同士を簡単に特定できます。

  • 境界点の追加

顔の輪郭外の領域も変形させる場合は、画像の四隅や境界に追加のポイントを入れてメッシュを補強します。

  • メッシュの可視化

デバッグや調整のために、三角形メッシュを画像上に描画して確認することが多いです。

このメッシュ構造が、画像の局所的な変形を可能にし、自然なモーフィングを実現します。

変形およびブレンディング工程

メッシュができたら、対応する三角形間でアフィン変換を行い、画像を変形させます。

さらに、変形した2枚の画像をブレンドして滑らかな遷移を作ります。

  • アフィン変換行列の計算

OpenCVのcv::getAffineTransform関数を使い、対応する三角形の3点からアフィン変換行列を求めます。

これにより、1つの三角形を別の三角形に写像できます。

  • 画像のワーピング

cv::warpAffine関数で、計算した変換行列を使って画像の該当部分を変形します。

三角形ごとに処理を繰り返し、全体の画像を変形します。

  • αブレンドによる合成

変形した2枚の画像を、モーフィングの進行度合い(0.0〜1.0)に応じて重み付け合成します。

例えば、進行度が0.3なら、元画像を70%、変形後画像を30%の割合で合成します。

  • 境界の滑らかさ調整

三角形の境界で色の不連続が起きないように、ブレンドや補間方法を工夫します。

この工程で、2つの画像が滑らかに変化していくアニメーションの各フレームが生成されます。

出力生成工程

最後に、変形とブレンドを繰り返して得られた画像を出力します。

出力形式は静止画の連番や動画ファイルが一般的です。

  • フレームシーケンスの作成

モーフィングの進行度合いを0から1まで変化させ、複数の中間画像を生成します。

これらを連続して表示することでアニメーションになります。

  • 動画ファイルへの書き出し

OpenCVのcv::VideoWriterを使い、生成したフレームを動画ファイル(例:MP4、AVI)として保存します。

コーデックやフレームレートの設定もここで行います。

  • ファイル保存

静止画として保存する場合は、cv::imwriteでPNGやJPEG形式で保存します。

  • 表示

cv::imshowでリアルタイムにモーフィングの進行を確認しながら処理を進めることも可能です。

これらの出力を活用して、モーフィングの結果を視覚的に楽しんだり、他のアプリケーションに組み込んだりできます。

特徴点検出

画像モーフィングにおいて、対応する特徴点の検出は非常に重要です。

ここでは、自動検出と手動アノテーションの方法を詳しく解説します。

自動検出

Dlibの顔ランドマーク

Dlibは高精度な顔ランドマーク検出機能を持つライブラリで、C++からも利用可能です。

68点の顔特徴点を検出し、目や鼻、口の輪郭などを細かく捉えます。

Dlibを使う場合、まず顔検出器で顔領域を特定し、その後にランドマーク検出器で特徴点を抽出します。

以下はDlibを用いた特徴点検出のサンプルコードです。

#include <dlib/image_processing/frontal_face_detector.h>
#include <dlib/image_processing.h>
#include <dlib/opencv.h>
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // Dlibの顔検出器とランドマーク検出器の初期化
    dlib::frontal_face_detector detector = dlib::get_frontal_face_detector();
    dlib::shape_predictor sp;
    dlib::deserialize("shape_predictor_68_face_landmarks.dat") >> sp;
    // 画像の読み込み
    cv::Mat img = cv::imread("face.jpg");
    if (img.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    // OpenCVの画像をdlib形式に変換
    dlib::cv_image<dlib::bgr_pixel> dlib_img(img);
    // 顔検出
    std::vector<dlib::rectangle> faces = detector(dlib_img);
    if (faces.empty()) {
        std::cerr << "顔が検出されませんでした。" << std::endl;
        return -1;
    }
    // 最初の顔に対してランドマーク検出
    dlib::full_object_detection shape = sp(dlib_img, faces[0]);
    // 検出した特徴点をOpenCV画像に描画
    for (unsigned int i = 0; i < shape.num_parts(); ++i) {
        cv::circle(img, cv::Point(shape.part(i).x(), shape.part(i).y()), 2, cv::Scalar(0, 255, 0), -1);
    }
    cv::imshow("Landmarks", img);
    cv::waitKey(0);
    return 0;
}

このコードでは、Dlibの68点ランドマークモデルを使い、顔の特徴点を検出して緑色の円で描画しています。

shape_predictor_68_face_landmarks.datはDlibの公式サイトからダウンロード可能です。

OpenCVのFacemark API

OpenCVにもFacemark APIがあり、顔の特徴点検出が可能です。

FacemarkはLBF(Local Binary Features)やAAM(Active Appearance Model)など複数のアルゴリズムをサポートしています。

カスケードファイル・学習済みlbfmodel.yamlの用意

カスケード分類器(顔検出器)の利用にはカスケードファイルが必要で、学習済みのlbfmodel.yamlも必要です。

カスケードファイルは、OpenCVをインストールすると、多くの環境ではhaarcascade_frontalface_default.xmlファイルが自動的に含まれています。以下のようなパスに存在することが多いです。

/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml
opencv\sources\data\haarcascades\haarcascade_frontalface_default.xml

カスケードファイルをコピーしてカレントディレクトリに配置するなどをして、使える状態にしておきましょう。

lbfmodel.yamlの入手方法はいくつかありますが、こちらで一般公開されているものが利用可能です。

wget https://raw.githubusercontent.com/kurnianggoro/GSOC2017/master/data/lbfmodel.yaml

以下はFacemark LBFを使った例です。

#include <opencv2/opencv.hpp>
#include <opencv2/face.hpp>
#include <iostream>
int main()
{
    // Facemarkの初期化
    cv::Ptr<cv::face::Facemark> facemark = cv::face::FacemarkLBF::create();
    facemark->loadModel("lbfmodel.yaml");
    // 顔検出器の初期化
    cv::CascadeClassifier face_cascade("haarcascade_frontalface_default.xml");
    // 画像の読み込み
    cv::Mat img = cv::imread("face.jpg");
    if (img.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    // グレースケール変換
    cv::Mat gray;
    cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
    // 顔検出
    std::vector<cv::Rect> faces;
    face_cascade.detectMultiScale(gray, faces);
    if (faces.empty()) {
        std::cerr << "顔が検出されませんでした。" << std::endl;
        return -1;
    }
    // ランドマーク検出
    std::vector<std::vector<cv::Point2f>> landmarks;
    bool success = facemark->fit(img, faces, landmarks);
    if (success) {
        for (size_t i = 0; i < landmarks[0].size(); ++i) {
            cv::circle(img, landmarks[0][i], 2, cv::Scalar(0, 0, 255), -1);
        }
    } else {
        std::cerr << "ランドマーク検出に失敗しました。" << std::endl;
    }
    cv::imshow("Facemark Landmarks", img);
    cv::waitKey(0);
    return 0;
}

このコードでは、OpenCVのHaar Cascadeで顔を検出し、Facemark LBFモデルで特徴点を検出しています。

lbfmodel.yamlはOpenCVのGitHubリポジトリなどから入手可能です。

手動アノテーション

キーポイントの配置ガイドライン

自動検出が難しい場合や、より正確な対応点を指定したい場合は、手動で特徴点をアノテーションします。

手動アノテーションでは、以下のポイントを意識して配置すると良いです。

  • 顔の主要部位をカバーする

目の内外角、眉毛の端、鼻の先端と両側、口の輪郭、顎のラインなど、顔の形状を特徴づける点を均等に配置します。

  • 対応点の数を揃える

モーフィング対象の両画像で同じ数のポイントを同じ順序で配置することが重要です。

  • 境界点の追加

顔の輪郭外側や画像の四隅にもポイントを追加し、背景の変形を制御します。

  • 密度の調整

変形が大きくなる部分はポイントを多めに、変形が少ない部分は少なめに配置すると自然な変形になります。

アノテーションツールの利用

手動で特徴点を配置するには、以下のようなツールを使うと便利です。

  • OpenCVのGUIを使った簡易ツール

OpenCVのマウスイベントを利用して、画像上にポイントをクリックで配置し、座標を保存する自作ツールを作成できます。

  • LabelMe

Webベースのアノテーションツールで、画像にポイントやポリゴンを描画し、JSON形式で保存可能です。

  • dlibのface_landmark_detectionサンプルの改造

Dlibのサンプルプログラムを改造して、手動でポイントを追加・修正することもできます。

  • その他の専用アノテーションソフト

画像処理や機械学習用のアノテーションツール(例えば、VGG Image Annotatorなど)も利用可能です。

手動アノテーションは手間がかかりますが、特に顔以外の対象や特殊なケースでは有効です。

正確な対応点を用意することで、モーフィングの品質が大きく向上します。

メッシュ生成

モーフィングにおいて、特徴点を結んで三角形メッシュを作成することは、画像の局所的な変形を滑らかに行うために欠かせません。

ここでは、Delaunay三角形分割の実装方法と、三角形の管理方法について詳しく説明します。

Delaunay三角形分割

Subdiv2Dクラスの使い方

OpenCVのSubdiv2Dクラスは、2D平面上の点群に対してDelaunay三角形分割を行うための便利なクラスです。

Delaunay分割は、三角形の内角が極端に小さくならないように点を結ぶため、変形時の歪みを抑えられます。

Subdiv2Dの基本的な使い方は以下の通りです。

  1. 領域の初期化

分割を行う矩形領域を指定してSubdiv2Dオブジェクトを作成します。

通常は画像サイズに合わせた矩形を指定します。

  1. 特徴点の挿入

insertメソッドで特徴点を1つずつ追加します。

  1. 三角形リストの取得

getTriangleListメソッドで、分割された三角形の頂点座標リストを取得します。

以下にサンプルコードを示します。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
int main()
{
    // 画像サイズに合わせた矩形領域を定義
    cv::Rect rect(0, 0, 500, 500);
    // Subdiv2Dオブジェクトの作成
    cv::Subdiv2D subdiv(rect);
    // 特徴点の例(画像内の座標)
    std::vector<cv::Point2f> points = {
        {100, 100}, {200, 80}, {300, 150}, {400, 200},
        {250, 300}, {150, 250}, {350, 350}, {100, 400}
    };
    // 点をSubdiv2Dに挿入
    for (const auto& pt : points) {
        subdiv.insert(pt);
    }
    // 三角形リストを取得
    std::vector<cv::Vec6f> triangleList;
    subdiv.getTriangleList(triangleList);
    // 三角形の描画
    cv::Mat img = cv::Mat::zeros(rect.height, rect.width, CV_8UC3);
    for (const auto& t : triangleList) {
        cv::Point pt1(cvRound(t[0]), cvRound(t[1]));
        cv::Point pt2(cvRound(t[2]), cvRound(t[3]));
        cv::Point pt3(cvRound(t[4]), cvRound(t[5]));
        // 三角形の頂点が矩形内にあるかチェック
        if (rect.contains(pt1) && rect.contains(pt2) && rect.contains(pt3)) {
            cv::line(img, pt1, pt2, cv::Scalar(0, 255, 0), 1);
            cv::line(img, pt2, pt3, cv::Scalar(0, 255, 0), 1);
            cv::line(img, pt3, pt1, cv::Scalar(0, 255, 0), 1);
        }
    }
    // 特徴点の描画
    for (const auto& pt : points) {
        cv::circle(img, pt, 3, cv::Scalar(0, 0, 255), cv::FILLED);
    }
    cv::imshow("Delaunay Triangulation", img);
    cv::waitKey(0);
    return 0;
}

このコードでは、指定した特徴点をSubdiv2Dに挿入し、Delaunay三角形分割を行っています。

三角形の頂点が矩形領域内にあるものだけを描画しているため、画像外の三角形は表示されません。

境界条件の設定

Delaunay三角形分割では、境界条件の設定が重要です。

特に顔画像のモーフィングでは、顔の輪郭外の領域も変形させるために、画像の四隅や境界に追加のポイントを入れてメッシュを補強します。

  • 境界点の追加

画像の四隅や上下左右の中央などにポイントを追加し、メッシュの外枠をしっかり囲みます。

これにより、変形時に画像の端が不自然に引き伸ばされたり切れたりするのを防げます。

  • 矩形領域の設定

Subdiv2Dの初期化時に指定する矩形は、画像全体をカバーするように設定します。

これにより、全ての特徴点が有効に扱われます。

  • 境界点の管理

境界に追加したポイントは、対応する画像の境界にも同様に追加し、対応関係を保つ必要があります。

境界条件を適切に設定することで、モーフィングの際に画像の端が破綻しにくくなり、自然な変形が可能になります。

三角形インデックスの管理

対応表の作成

モーフィングでは、2枚の画像の特徴点に基づいて三角形メッシュを作成しますが、対応する三角形同士を正しく結びつけるために、三角形のインデックス管理が必要です。

  • 特徴点のインデックス

特徴点はベクトルなどの配列で管理し、各点にインデックスを割り当てます。

  • 三角形の頂点インデックス

Subdiv2DgetTriangleListは座標を返しますが、これを元の特徴点のインデックスに変換する必要があります。

座標と特徴点の座標を比較し、最も近い点のインデックスを特定します。

  • 対応三角形のペアリング

2枚の画像で同じインデックスの特徴点を使って三角形を作成するため、三角形の頂点インデックスの組み合わせが一致します。

これにより、対応する三角形間でアフィン変換を適用できます。

以下は、座標からインデックスを取得する例です。

int findIndex(const std::vector<cv::Point2f>& points, const cv::Point2f& pt, float epsilon = 1.0f)
{
    for (size_t i = 0; i < points.size(); ++i) {
        if (cv::norm(points[i] - pt) < epsilon) {
            return static_cast<int>(i);
        }
    }
    return -1; // 見つからなかった場合
}

この関数を使い、三角形の3頂点の座標からインデックスを取得し、三角形のインデックスリストを作成します。

データ構造の選択

三角形のインデックス管理には、効率的で扱いやすいデータ構造を選ぶことが重要です。

  • 三角形リスト(ベクトル)

三角形は3つの頂点インデックスの組み合わせで表現されるため、std::vector<cv::Vec3i>のような形で管理します。

Vec3iは3つの整数を格納できるOpenCVの型です。

  • 対応関係の保持

2枚の画像で同じインデックスの三角形を対応付けるため、同じインデックス順で三角形を格納します。

  • 高速検索用のマップ(必要に応じて)

座標からインデックスを頻繁に検索する場合は、座標をキーにしたハッシュマップやKDツリーを使うと高速化できます。

  • 三角形の属性管理

変形時に三角形ごとの変換行列や重みを管理する場合は、三角形構造体に属性を持たせることもあります。

データ構造用途メリット
std::vector<cv::Vec3i>三角形の頂点インデックス管理シンプルで扱いやすい
std::unordered_map<cv::Point2f, int>座標からインデックス検索高速検索可能(座標のハッシュ化が必要)
独自構造体三角形の属性管理変換行列や重みを一元管理可能

適切なデータ構造を選ぶことで、モーフィング処理の効率と保守性が向上します。

アフィン変形

画像モーフィングにおいて、対応する三角形間での変形はアフィン変換を用いて行います。

ここでは、変換行列の計算方法とピクセル補間の詳細について解説します。

変換行列の計算

cv::getAffineTransform

OpenCVのcv::getAffineTransform関数は、3点の対応関係からアフィン変換行列を計算します。

アフィン変換は、平行移動、回転、拡大縮小、せん断を含む線形変換で、三角形の形状を保ちながら変形できます。

関数のシグネチャは以下の通りです。

cv::Mat getAffineTransform(const cv::Point2f src[], const cv::Point2f dst[]);
  • src:変換元の3点座標配列
  • dst:変換先の3点座標配列

戻り値は2×3の変換行列cv::Matです。

以下は、3点からアフィン変換行列を求め、画像の一部を変形する例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    cv::Mat src_img = cv::imread("source.jpg");
    if (src_img.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    // 変換元の三角形の3点
    cv::Point2f srcTri[3] = { {50, 50}, {200, 50}, {50, 200} };
    // 変換先の三角形の3点
    cv::Point2f dstTri[3] = { {70, 70}, {220, 80}, {80, 210} };
    // アフィン変換行列の計算
    cv::Mat warpMat = cv::getAffineTransform(srcTri, dstTri);
    // 出力画像のサイズを指定
    cv::Mat dst_img = cv::Mat::zeros(src_img.size(), src_img.type());
    // アフィン変換の適用
    cv::warpAffine(src_img, dst_img, warpMat, dst_img.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT_101);
    cv::imshow("Original", src_img);
    cv::imshow("Affine Warp", dst_img);
    cv::waitKey(0);
    return 0;
}

このコードでは、srcTriの三角形をdstTriの三角形に変形しています。

cv::warpAffineの補間方法はcv::INTER_LINEARを指定し、境界処理はcv::BORDER_REFLECT_101で反射境界を使っています。

拡張: cv::estimateAffinePartial2D

cv::estimateAffinePartial2Dは、より柔軟にアフィン変換行列を推定できる関数です。

主に対応点が複数ある場合に使い、外れ値を除外しながら最適な変換を求めます。

シグネチャは以下の通りです。

cv::Mat estimateAffinePartial2D(
    InputArray from,
    InputArray to,
    OutputArray inliers = noArray(),
    int method = RANSAC,
    double ransacReprojThreshold = 3,
    size_t maxIters = 2000,
    double confidence = 0.99,
    size_t refineIters = 10
);
  • from:変換元の点群(2D座標)
  • to:変換先の点群
  • method:RANSACなどの外れ値除去手法
  • ransacReprojThreshold:RANSACの閾値

この関数は、3点以上の対応点がある場合に使い、ノイズや誤対応に強い変換行列を推定します。

モーフィングの三角形単位ではなく、より大きな領域の変形に適しています。

ピクセル補間

逆変換とサンプリング

アフィン変換を画像に適用する際は、通常「逆変換」を用います。

これは、出力画像の各ピクセル位置に対して、元画像の対応位置を計算し、元画像から色をサンプリングする方法です。

逆変換を使う理由は、出力画像の全ピクセルに対して値を確実に割り当てられるため、穴あきや重複を防げるからです。

具体的には、変換行列 A が与えられたとき、出力画像の座標 (x,y) に対して元画像の座標 (x,y)

[xy1]=A1[xy1]

で計算し、元画像の (x,y) の色を補間します。

OpenCVのcv::warpAffineはこの逆変換方式を内部で自動的に行います。

境界外処理

変換後の座標が元画像の範囲外になる場合の処理も重要です。

OpenCVのcv::warpAffineでは、borderModeパラメータで境界外のピクセルの扱いを指定できます。

主なオプションは以下の通りです。

borderMode説明
cv::BORDER_CONSTANT境界外は指定した色borderValueで埋める
cv::BORDER_REPLICATE境界の最も近いピクセル値を繰り返す
cv::BORDER_REFLECT境界を鏡映して繰り返す
cv::BORDER_REFLECT_101境界を鏡映して繰り返す(境界点を重複しない)
cv::BORDER_WRAP画像を周期的に繰り返す

例えば、顔画像のモーフィングでは、cv::BORDER_REFLECT_101を使うと境界の不自然な色の飛びが抑えられます。

cv::warpAffine(src, dst, warpMat, dst.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT_101);

このように、補間方法と境界処理を適切に設定することで、変形後の画像の品質を高められます。

ブレンディング

モーフィングでは、変形した2つの画像を滑らかに合成することが重要です。

ここでは、基本的な重み付け合成から高度なマルチバンドブレンディングまで、代表的な手法を解説します。

重み付け合成

線形αブレンド

線形αブレンドは、2つの画像を単純に重み付けして合成する最も基本的な方法です。

モーフィングの進行度合い α(0から1の範囲)に応じて、以下の式で合成画像を作成します。

Iblend=(1α)×I1+α×I2

ここで、I1I2 はそれぞれ変形後の2つの画像です。

C++とOpenCVでの実装例は以下の通りです。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    cv::Mat img1 = cv::imread("warped1.jpg");
    cv::Mat img2 = cv::imread("warped2.jpg");
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    double alpha = 0.3; // モーフィングの進行度合い
    cv::Mat blended;
    cv::addWeighted(img1, 1.0 - alpha, img2, alpha, 0.0, blended);
    cv::imshow("Linear Alpha Blend", blended);
    cv::waitKey(0);
    return 0;
}

このコードでは、cv::addWeighted関数を使って線形αブレンドを行っています。

alphaの値を変えることで、画像の重み付けを調整できます。

コサイン補間

線形補間は単純ですが、モーフィングの進行が直線的でやや不自然に感じることがあります。

コサイン補間は、進行度合いに滑らかなイージング効果を加え、より自然な変化を実現します。

コサイン補間の重みは以下の式で計算します。

w=1cos(απ)2

この重みを使って合成します。

C++での計算例は以下の通りです。

double alpha = 0.3;
double w = (1 - cos(alpha * CV_PI)) / 2.0;
cv::Mat blended;
cv::addWeighted(img1, 1.0 - w, img2, w, 0.0, blended);

コサイン補間を使うことで、モーフィングの開始と終了が緩やかになり、視覚的に滑らかなアニメーションになります。

マルチバンドブレンディング

ピラミッド構築

マルチバンドブレンディングは、画像を複数の周波数帯域に分解し、それぞれの帯域で合成を行う高度な手法です。

これにより、境界部分の色やテクスチャの違いを自然に馴染ませられます。

まず、画像のラプラシアンピラミッドを構築します。

ラプラシアンピラミッドは、ガウシアンピラミッドの各レベルの差分画像で構成され、画像の詳細情報を周波数ごとに分離します。

OpenCVでのガウシアンピラミッド構築例:

std::vector<cv::Mat> gaussianPyramid;
cv::Mat currentImg = img.clone();
for (int i = 0; i < levels; ++i) {
    cv::Mat down;
    cv::pyrDown(currentImg, down);
    gaussianPyramid.push_back(down);
    currentImg = down;
}

ラプラシアンピラミッドは、各レベルのガウシアン画像と次のレベルのアップサンプル画像の差分で作成します。

std::vector<cv::Mat> laplacianPyramid;
for (int i = 0; i < levels - 1; ++i) {
    cv::Mat up;
    cv::pyrUp(gaussianPyramid[i + 1], up, gaussianPyramid[i].size());
    cv::Mat lap = gaussianPyramid[i] - up;
    laplacianPyramid.push_back(lap);
}
laplacianPyramid.push_back(gaussianPyramid[levels - 1]);

周波数ごとの合成

マルチバンドブレンディングでは、2つの画像のラプラシアンピラミッドを作成し、各レベルで重み付け合成を行います。

重みはマスク画像のガウシアンピラミッドを使って滑らかに変化させます。

合成後、ラプラシアンピラミッドを逆変換して元の解像度に復元します。

合成の流れは以下の通りです。

  1. 2つの画像のラプラシアンピラミッドを作成
  2. マスクのガウシアンピラミッドを作成
  3. 各レベルで以下の合成を行う

Lblendi=MiL1i+(1Mi)L2i

ここで、L1i,L2i はそれぞれの画像のラプラシアンピラミッドの第 i レベル、Mi はマスクのガウシアンピラミッドの第 i レベルです。

  1. 合成したラプラシアンピラミッドを逆変換して画像を復元

OpenCVでの逆変換例:

cv::Mat blended = laplacianPyramid[levels - 1];
for (int i = levels - 2; i >= 0; --i) {
    cv::Mat up;
    cv::pyrUp(blended, up, laplacianPyramid[i].size());
    blended = up + laplacianPyramid[i];
}

マルチバンドブレンディングは、特に境界部分の色ムラやテクスチャの違いを目立たなくし、自然なモーフィングを実現します。

顔画像のモーフィングでも、目や口の境界などで効果的に使われます。

出力の生成

モーフィング処理で生成した中間画像を連続的に表示・保存するために、フレームシーケンスの生成と動画ファイルへの書き出しを行います。

ここでは、イージングを用いたモーフ比率の調整やループアニメーションの作成方法、OpenCVのcv::VideoWriterを使った動画保存の設定について詳しく説明します。

フレームシーケンスの生成

モーフ比率のイージング

モーフィングの進行度合い(モーフ比率)を単純に線形で変化させると、変化が急で不自然に感じることがあります。

イージング関数を使うことで、変化の速度を滑らかに調整し、より自然なアニメーションを作成できます。

代表的なイージング関数の一つに「イーズイン・イーズアウト(ease-in-out)」があります。

これは、開始と終了がゆっくりで、中間が速い変化を表現します。

数式は以下の通りです。

f(t)=1cos(πt)2,t[0,1]

ここで、t は線形のモーフ比率(0から1)です。

C++での実装例:

double easeInOut(double t)
{
    return (1 - cos(t * CV_PI)) / 2.0;
}

この関数を使い、フレームごとのモーフ比率を計算して画像の重み付けに利用します。

for (int frame = 0; frame <= totalFrames; ++frame) {
    double t = static_cast<double>(frame) / totalFrames;
    double alpha = easeInOut(t);
    // alphaを使って画像のブレンディングや変形を行う
}

イージングを使うことで、モーフィングの開始と終了が滑らかになり、視覚的に心地よいアニメーションになります。

ループアニメーション

モーフィングアニメーションを繰り返し再生する場合、ループのつなぎ目が不自然にならないように工夫が必要です。

単純に0から1までのモーフ比率を繰り返すと、最後のフレームと最初のフレームの差が大きく目立ちます。

ループを滑らかにする方法の一つは、モーフ比率を0から1、そして1から0へと往復させることです。

これにより、アニメーションが往復運動のように連続して見えます。

C++での例:

for (int frame = 0; frame <= totalFrames; ++frame) {
    double t = static_cast<double>(frame) / totalFrames;
    double alpha = (frame <= totalFrames / 2) ? (2 * t) : (2 * (1 - t));
    // alphaを使ってブレンディング
}

また、イージング関数と組み合わせると、より自然なループが作れます。

動画ファイルへの書き出し

cv::VideoWriterの設定

OpenCVのcv::VideoWriterクラスを使うと、生成したフレームを動画ファイルとして保存できます。

動画ファイルの形式やコーデック、フレームレートなどを設定して初期化します。

基本的な使い方は以下の通りです。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    int width = 640;
    int height = 480;
    int fps = 30;
    std::string filename = "morphing_output.mp4";
    // 動画ファイルの書き出し設定
    cv::VideoWriter writer;
    int fourcc = cv::VideoWriter::fourcc('m', 'p', '4', 'v'); // MP4用コーデック
    bool isOpened = writer.open(filename, fourcc, fps, cv::Size(width, height), true);
    if (!isOpened) {
        std::cerr << "動画ファイルの書き出しに失敗しました。" << std::endl;
        return -1;
    }
    // フレーム生成ループ(例)
    for (int i = 0; i < fps * 5; ++i) { // 5秒分のフレーム
        cv::Mat frame = cv::Mat::zeros(height, width, CV_8UC3);
        // フレームの描画処理(例としてグラデーション)
        cv::circle(frame, cv::Point(i * 10 % width, height / 2), 50, cv::Scalar(0, 255, 0), -1);
        writer.write(frame);
    }
    writer.release();
    std::cout << "動画ファイルを保存しました: " << filename << std::endl;
    return 0;
}

この例では、MP4形式で30fpsの動画を作成しています。

生成される動画データ

fourccは動画の圧縮形式を指定し、環境によって利用可能なコーデックが異なるため注意が必要です。

エンコードパラメータ

動画の品質やファイルサイズを調整するために、エンコードパラメータを設定できます。

OpenCVのVideoWriterはコーデックによってはパラメータを受け付けますが、詳細な制御は外部ライブラリやコマンドラインツールを使うことが多いです。

主なパラメータ例:

パラメータ名説明備考
フレームレート (fps)1秒あたりのフレーム数アニメーションの滑らかさに影響
解像度動画の幅と高さ入力画像サイズに合わせることが多い
ビットレート圧縮率と画質のバランスOpenCV単体では設定が難しい場合あり
コーデック動画圧縮方式環境依存、例:mp4v, XVID

Windows環境ではXVID、Mac/Linuxではmp4vH264がよく使われます。

コーデックが利用できない場合は、動画ファイルが正しく作成されないことがあるため、事前に環境を確認してください。

動画ファイルの書き出し時は、フレームのサイズや色空間(カラーかグレースケール)も正しく設定する必要があります。

例えば、カラー画像ならisColortrueに設定します。

これらの設定を適切に行うことで、高品質なモーフィング動画を生成できます。

パフォーマンス最適化

画像モーフィングは計算量が多く、特に高解像度画像やリアルタイム処理ではパフォーマンスの最適化が重要です。

ここではGPUアクセラレーションと並列処理による高速化手法を詳しく解説します。

GPUアクセラレーション

OpenCLとCUDAバックエンド

OpenCVはGPUを活用するために、OpenCLやCUDAといったアクセラレーション技術をサポートしています。

これにより、画像処理の多くの演算をCPUからGPUにオフロードし、大幅な高速化が可能です。

  • OpenCLバックエンド

OpenCVはoclモジュールを通じてOpenCLを利用できます。

OpenCLはクロスプラットフォームで、NVIDIA、AMD、Intelなど多様なGPUで動作します。

OpenCL対応の関数は自動的にGPUで実行されることもあり、特別なコード変更なしに高速化できる場合があります。

  • CUDAバックエンド

NVIDIA製GPUを使う場合は、OpenCVのCUDAモジュールを利用するとさらに高速化が期待できます。

CUDAはNVIDIA専用のAPIで、OpenCVのCUDA対応関数を使うことで、画像のリサイズ、フィルタリング、変換などをGPU上で効率的に処理できます。

CUDAを使うには、OpenCVをCUDA対応でビルドし、cv::cuda名前空間の関数を利用します。

例えば、画像のアップロードやダウンロードはcv::cuda::GpuMatを使います。

#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <iostream>
int main()
{
    cv::Mat src = cv::imread("image.jpg");
    if (src.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    // CPUからGPUへ画像を転送
    cv::cuda::GpuMat gpuSrc;
    gpuSrc.upload(src);
    // GPU上で画像をグレースケールに変換
    cv::cuda::GpuMat gpuGray;
    cv::cuda::cvtColor(gpuSrc, gpuGray, cv::COLOR_BGR2GRAY);
    // GPUからCPUへ結果をダウンロード
    cv::Mat gray;
    gpuGray.download(gray);
    cv::imshow("Gray Image", gray);
    cv::waitKey(0);
    return 0;
}

このようにCUDAを活用すると、画像処理のボトルネックをGPUに移せます。

ただし、GPUへのデータ転送コストも考慮し、処理の粒度が大きい場合に効果的です。

UMatによる自動切り替え

OpenCVのUMatは、CPUとGPUのメモリ管理を抽象化したデータ構造です。

UMatを使うと、対応する関数が自動的にOpenCLを利用してGPU処理を行い、非対応の場合はCPU処理にフォールバックします。

UMatを使うことで、コードの大幅な変更なしにGPUアクセラレーションを活用できます。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    cv::Mat src = cv::imread("image.jpg");
    if (src.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }
    // UMatに変換
    cv::UMat uSrc;
    src.copyTo(uSrc);
    // UMat上でグレースケール変換(OpenCLが有効ならGPUで実行)
    cv::UMat uGray;
    cv::cvtColor(uSrc, uGray, cv::COLOR_BGR2GRAY);
    // CPU側に結果を取得
    cv::Mat gray = uGray.getMat(cv::ACCESS_READ);
    cv::imshow("Gray Image", gray);
    cv::waitKey(0);
    return 0;
}

UMatはOpenCL対応の関数でのみGPU処理が行われるため、すべての処理が高速化されるわけではありませんが、簡単にGPU活用を試せる手段として便利です。

並列処理

TBBによるループ並列化

IntelのThreading Building Blocks(TBB)は、C++で簡単に並列処理を実装できるライブラリです。

OpenCVはTBBを内部で利用していることも多く、ユーザーコードでも明示的にTBBを使ってループの並列化が可能です。

モーフィング処理では、三角形ごとの変形やフレームごとの処理を並列化すると効果的です。

以下はTBBのparallel_forを使った例です。

#include <tbb/tbb.h>
#include <vector>
#include <iostream>
int main()
{
    std::vector<int> data(1000000, 1);
    std::vector<int> result(data.size());
    tbb::parallel_for(size_t(0), data.size(), [&](size_t i) {
        // 重い計算の代わりに単純な処理
        result[i] = data[i] * 2;
    });
    std::cout << "処理完了" << std::endl;
    return 0;
}

OpenCVのループ処理でも同様に、三角形単位の変形処理をparallel_forで並列化すると、CPUのコア数を活かして高速化できます。

マルチスレッドでの入出力

画像の読み込みや書き出し、動画のフレーム生成などのI/O処理も、モーフィング全体の処理時間に影響します。

これらを別スレッドで処理することで、計算処理とI/Oを並行して行い、全体のスループットを向上させられます。

C++11以降の標準スレッドライブラリを使った簡単な例:

#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <opencv2/opencv.hpp>
#include <iostream>
std::queue<cv::Mat> frameQueue;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
void producer()
{
    for (int i = 0; i < 100; ++i) {
        cv::Mat frame = cv::imread("frame" + std::to_string(i) + ".jpg");
        {
            std::lock_guard<std::mutex> lock(mtx);
            frameQueue.push(frame);
        }
        cv.notify_one();
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        finished = true;
    }
    cv.notify_one();
}
void consumer()
{
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return !frameQueue.empty() || finished; });
        if (!frameQueue.empty()) {
            cv::Mat frame = frameQueue.front();
            frameQueue.pop();
            lock.unlock();
            // フレーム処理(例:表示)
            cv::imshow("Frame", frame);
            cv::waitKey(1);
        } else if (finished) {
            break;
        }
    }
}
int main()
{
    std::thread prod(producer);
    std::thread cons(consumer);
    prod.join();
    cons.join();
    return 0;
}

この例では、画像の読み込みをproducerスレッドで行い、consumerスレッドで処理・表示しています。

モーフィング処理でも同様に、計算とI/Oを分離して効率化できます。

これらのGPUアクセラレーションと並列処理の技術を組み合わせることで、C++とOpenCVによる画像モーフィングのパフォーマンスを大幅に向上させられます。

品質向上の工夫

画像モーフィングの品質を高めるためには、特徴点(ランドマーク)の精度向上やメッシュの最適化、色調整などの工夫が欠かせません。

ここでは、ランドマーク補正、メッシュリファインメント、カラーマッチングの具体的な手法を詳しく解説します。

ランドマーク補正

外れ値のスムージング

顔の特徴点検出は高精度ですが、まれに誤検出やノイズによる外れ値が発生します。

これらの外れ値があると、モーフィング時に不自然な変形や歪みが生じるため、補正が必要です。

外れ値のスムージングには、以下の方法があります。

  • 移動平均フィルタ

複数フレームにわたる同一ランドマークの座標を平均化し、急激な変動を抑えます。

例えば、現在の座標を前後数フレームの座標の平均で置き換えます。

  • メディアンフィルタ

外れ値に強いメディアンフィルタを適用し、異常な点を除去します。

  • 閾値による除外

前フレームとの距離が一定以上離れている点を外れ値とみなし、補正または除外します。

以下は移動平均フィルタの簡単な例です。

#include <vector>
#include <opencv2/opencv.hpp>
// 過去Nフレームの座標を保持するバッファ
std::vector<cv::Point2f> smoothLandmark(const std::vector<std::vector<cv::Point2f>>& history, int index, int windowSize)
{
    cv::Point2f sum(0, 0);
    int count = 0;
    int start = std::max(0, index - windowSize);
    int end = std::min(static_cast<int>(history.size()) - 1, index + windowSize);
    for (int i = start; i <= end; ++i) {
        sum += history[i][index];
        ++count;
    }
    return sum * (1.0f / count);
}

このようにスムージングを行うことで、ランドマークの位置が安定し、変形の滑らかさが向上します。

フレーム間の一貫性

動画や連続画像でモーフィングを行う場合、フレーム間でランドマークの位置が大きく変動すると、アニメーションがちらついたり不自然になります。

フレーム間の一貫性を保つための工夫としては以下があります。

  • トラッキングの活用

顔検出・特徴点検出に加え、光学フローやトラッキングアルゴリズムを使い、前フレームのランドマーク位置を基に次フレームの位置を推定します。

  • 補間による補正

検出が失敗したフレームは、前後のフレームのランドマークを線形補間して補完します。

  • 時間的平滑化

ランドマーク座標に対してカルマンフィルタやローパスフィルタを適用し、時間的に滑らかな動きを実現します。

これらの手法を組み合わせることで、連続フレームのランドマークが安定し、自然なモーフィングアニメーションが可能になります。

メッシュリファインメント

境界エッジの再分割

三角形メッシュの境界部分は、変形時に歪みや破綻が起きやすい箇所です。

境界エッジを細かく再分割することで、変形の自由度を高め、より自然な形状変化を実現できます。

  • 境界点の追加

画像の輪郭や顔の輪郭に沿って追加の特徴点を配置し、境界の三角形を細分化します。

  • エッジ分割アルゴリズム

長いエッジを一定の長さ以下に分割し、三角形のサイズを均一化します。

  • 動的リファインメント

変形の大きい部分に応じてメッシュの細かさを調整し、計算コストと品質のバランスを取ります。

境界の再分割により、特に顔の輪郭や髪の毛の部分での不自然な引き伸ばしや折れを抑制できます。

面積バランスの最適化

メッシュ内の三角形の面積バランスも品質に影響します。

極端に大きい三角形や細長い三角形は変形時に歪みやすく、モーフィングの品質を低下させます。

  • 三角形の面積均一化

Delaunay分割後に、面積が大きすぎる三角形を分割し、均一なサイズに近づけます。

  • 形状の正則化

三角形の内角が極端に小さくならないように調整し、細長い三角形を減らします。

  • メッシュスムージング

頂点の位置を調整してメッシュ全体のバランスを改善します。

これらの最適化により、変形時の歪みが減り、より滑らかで自然なモーフィングが可能になります。

カラーマッチング

明度・色相の整合

モーフィング対象の2枚の画像は、撮影条件や照明の違いにより明度や色相が異なることがあります。

これらの差異を補正しないと、変形後の画像をブレンドした際に不自然な色ムラや境界が目立ちます。

  • 明度の正規化

画像全体の平均輝度を揃えるために、ヒストグラムの平均値や中央値を基準に明度を調整します。

  • 色相の調整

HSV色空間に変換し、色相(Hue)や彩度(Saturation)を合わせることで、色味の違いを軽減します。

  • 局所的な色補正

顔のパーツごとに色調整を行い、より自然な色のつながりを実現します。

ヒストグラムマッチング

ヒストグラムマッチングは、1枚の画像の輝度や色の分布を別の画像に合わせる手法です。

これにより、2枚の画像の色調が統一され、モーフィング時の違和感を減らせます。

OpenCVでのヒストグラムマッチングは標準関数がないため、自作するか外部ライブラリを利用します。

基本的な流れは以下の通りです。

  1. 入力画像と参照画像のヒストグラムを計算します。
  2. 累積分布関数(CDF)を求める。
  3. 入力画像の各画素の値をCDFに基づいて参照画像の画素値にマッピングします。

簡単なヒストグラムマッチングの擬似コード例:

// 入力画像と参照画像のヒストグラム計算
cv::Mat histInput, histRef;
int histSize = 256;
float range[] = {0, 256};
const float* histRange = {range};
cv::calcHist(&inputGray, 1, 0, cv::Mat(), histInput, 1, &histSize, &histRange);
cv::calcHist(&refGray, 1, 0, cv::Mat(), histRef, 1, &histSize, &histRange);
// CDF計算
cv::Mat cdfInput, cdfRef;
histInput.copyTo(cdfInput);
histRef.copyTo(cdfRef);
for (int i = 1; i < histSize; ++i) {
    cdfInput.at<float>(i) += cdfInput.at<float>(i - 1);
    cdfRef.at<float>(i) += cdfRef.at<float>(i - 1);
}
// マッピングテーブル作成
std::vector<uchar> lut(256);
int j = 0;
for (int i = 0; i < 256; ++i) {
    while (j < 255 && cdfRef.at<float>(j) < cdfInput.at<float>(i)) {
        ++j;
    }
    lut[i] = j;
}
// 入力画像の画素値をマッピング
cv::Mat matched = inputGray.clone();
for (int y = 0; y < inputGray.rows; ++y) {
    for (int x = 0; x < inputGray.cols; ++x) {
        matched.at<uchar>(y, x) = lut[inputGray.at<uchar>(y, x)];
    }
}

このようにヒストグラムマッチングを行うことで、モーフィング対象画像間の色調差を効果的に補正できます。

これらの品質向上の工夫を組み合わせることで、より自然で滑らかな画像モーフィングを実現できます。

デバッグとトラブルシューティング

画像モーフィングの開発過程では、変形のずれや色ずれなどの問題が発生しやすいため、適切なデバッグ手法と対策が重要です。

ここでは、変形ずれの検出方法と色ずれアーチファクトの対処法について詳しく解説します。

変形ずれの検出

可視化オーバーレイ

変形ずれを検出するために、特徴点や三角形メッシュを画像上に重ねて可視化する方法が有効です。

これにより、どの部分で対応点のずれやメッシュの歪みが起きているかを直感的に把握できます。

  • 特徴点の描画

対応する特徴点を異なる色で描画し、両画像の対応関係を確認します。

例えば、元画像の特徴点は緑、変形後の特徴点は赤で表示します。

  • 三角形メッシュの描画

Subdiv2Dで生成した三角形のエッジを線で描画し、メッシュの形状を視覚化します。

三角形の歪みや不自然な形状が目立つ部分を特定できます。

  • 変形前後のオーバーレイ

変形前の画像と変形後の画像を半透明で重ねて表示し、ずれや歪みを肉眼で確認します。

OpenCVで特徴点とメッシュを描画する例:

void drawLandmarks(cv::Mat& img, const std::vector<cv::Point2f>& points, const cv::Scalar& color)
{
    for (const auto& pt : points) {
        cv::circle(img, pt, 3, color, cv::FILLED);
    }
}
void drawMesh(cv::Mat& img, const std::vector<cv::Vec3i>& triangles, const std::vector<cv::Point2f>& points, const cv::Scalar& color)
{
    for (const auto& tri : triangles) {
        cv::line(img, points[tri[0]], points[tri[1]], color, 1);
        cv::line(img, points[tri[1]], points[tri[2]], color, 1);
        cv::line(img, points[tri[2]], points[tri[0]], color, 1);
    }
}

これらの可視化を行うことで、変形ずれの原因となるポイントや三角形を特定しやすくなります。

誤対応三角形の特定

モーフィングで最も問題となるのは、対応する三角形の頂点が誤ってマッチングされているケースです。

誤対応があると、その三角形部分で大きな歪みや破綻が発生します。

誤対応三角形を特定する方法は以下の通りです。

  • 三角形の面積変化の検出

対応する2つの三角形の面積比を計算し、極端に大きく変化している三角形を検出します。

面積比が閾値を超える場合は誤対応の可能性があります。

  • 頂点間距離の異常検出

対応する頂点間の距離が不自然に大きい場合、その三角形は誤対応の疑いがあります。

  • 可視化による確認

問題のある三角形を赤色などで強調表示し、手動で確認します。

面積比の計算例:

double triangleArea(const cv::Point2f& a, const cv::Point2f& b, const cv::Point2f& c)
{
    return std::abs((a.x*(b.y - c.y) + b.x*(c.y - a.y) + c.x*(a.y - b.y)) / 2.0);
}
bool isMismatchTriangle(const std::vector<cv::Point2f>& srcPoints, const std::vector<cv::Point2f>& dstPoints, const cv::Vec3i& tri, double threshold = 3.0)
{
    double areaSrc = triangleArea(srcPoints[tri[0]], srcPoints[tri[1]], srcPoints[tri[2]]);
    double areaDst = triangleArea(dstPoints[tri[0]], dstPoints[tri[1]], dstPoints[tri[2]]);
    if (areaSrc < 1e-5) return false; // 面積が極端に小さい場合は除外
    double ratio = areaDst / areaSrc;
    return (ratio > threshold || ratio < 1.0 / threshold);
}

このように誤対応三角形を検出し、修正や除外を行うことで、モーフィングの品質を向上させられます。

色ずれアーチファクト

ガンマ補正

モーフィングで画像をブレンドする際、色のずれや明るさの不自然な変化が起きることがあります。

これは、画像の輝度値が線形空間ではなくガンマ補正された非線形空間で表現されているためです。

ガンマ補正を考慮せずに線形ブレンドを行うと、暗い部分や明るい部分で色の不連続が目立ちます。

対策としては、以下の手順でガンマ補正を考慮したブレンドを行います。

  1. 入力画像をガンマ補正前の線形空間に変換(逆ガンマ補正)

Ilinear=Isrgbγ

通常、γ2.2を使います。

  1. 線形空間でブレンドを実施
  2. ブレンド結果を再度ガンマ補正(ガンマ変換)して表示用に変換

Isrgb=Ilinear1/γ

OpenCVでの逆ガンマ補正とガンマ補正の例:

cv::Mat gammaCorrection(const cv::Mat& img, double gamma)
{
    cv::Mat imgFloat;
    img.convertTo(imgFloat, CV_32F, 1.0 / 255.0);
    cv::pow(imgFloat, gamma, imgFloat);
    imgFloat.convertTo(imgFloat, CV_8U, 255.0);
    return imgFloat;
}

この処理をブレンド前後に適用することで、色ずれを抑えた自然なモーフィングが可能になります。

ラップアラウンド対策

色空間の扱いによっては、色の値が0〜255の範囲を超えて循環(ラップアラウンド)し、不自然な色変化が発生することがあります。

特に色相(Hue)を扱う際に起こりやすい問題です。

対策としては、

  • 色空間の選択

RGBやHSVなどの色空間で処理する際、色相の値が0〜360度で循環することを考慮し、差分計算や補間時にラップアラウンドを正しく処理します。

  • 補間方法の工夫

色相の差分を計算する際に、360度を超えた場合は360を引くなどして最短距離を計算します。

  • 範囲制限

ブレンド後の値を明示的に0〜255の範囲にクリップし、オーバーフローやアンダーフローを防ぎます。

色相のラップアラウンド処理例(擬似コード):

float hueDiff(float h1, float h2)
{
    float d = h2 - h1;
    if (d > 180.0f) d -= 360.0f;
    else if (d < -180.0f) d += 360.0f;
    return d;
}

このように色空間の特性を理解し適切に処理することで、色ずれアーチファクトを防止できます。

これらのデバッグ手法と対策を活用しながら開発を進めることで、画像モーフィングの品質を高め、問題発生時に迅速に原因を特定・修正できます。

応用例

画像モーフィングの基本技術を応用することで、部分的な変形や複数画像を連続的に変化させる表現が可能になります。

ここでは、マスクを使った部分モーフィングと複数画像の連続モーフィングの具体例を解説します。

マスクによる部分モーフィング

目元のみ変形

顔全体ではなく、目元だけを変形させたい場合は、モーフィング対象領域を限定するマスクを利用します。

これにより、目元の表情変化や目の形状の変化を強調しつつ、他の部分は元のまま保持できます。

  • マスクの作成

目元の領域を白(255)、それ以外を黒(0)で表現したバイナリマスクを用意します。

手動で作成するか、顔ランドマークの目周辺ポイントを使って多角形領域を描画して生成します。

  • 変形処理の制限

アフィン変形や三角形メッシュの変形をマスク領域内の三角形に限定します。

マスク外の領域は変形せず、元画像のまま保持します。

  • ブレンディング

変形した目元領域と元画像の背景をマスクで合成し、境界が自然になるようにぼかしやフェザー処理を行います。

OpenCVでマスクを使った部分モーフィングの例:

cv::Mat mask; // 目元領域のバイナリマスク(白:変形領域)
cv::Mat warpedImg; // 変形後の画像
cv::Mat originalImg; // 元画像
// マスク領域で変形画像を適用し、それ以外は元画像を保持
cv::Mat result = originalImg.clone();
warpedImg.copyTo(result, mask);

この方法により、目元だけが滑らかに変形し、表情の変化や目の動きを強調できます。

背景固定

顔や人物のモーフィングで背景を変形させたくない場合、背景を固定するマスクを使います。

背景の変形は不自然になりやすいため、背景領域は元画像のまま保持し、人物部分のみを変形します。

  • 背景マスクの作成

セグメンテーション技術や手動で背景領域をマスク化します。

背景は黒(0)、人物は白(255)で表現します。

  • 特徴点の制限

背景領域の特徴点は変形に含めず、人物領域の特徴点のみで三角形メッシュを作成します。

  • 変形と合成

人物領域のみ変形し、背景は元画像のまま保持。

マスクを使って合成し、境界部分はぼかしで自然に繋げます。

この手法により、背景の歪みを防ぎつつ、人物の表情や形状の変化を自然に表現できます。

複数画像の連続モーフィング

年齢変化アニメーション

複数の顔画像を用いて、年齢の変化を連続的に表現するアニメーションを作成できます。

例えば、幼少期から老年期までの顔写真を順にモーフィングさせることで、自然な年齢変化を視覚化します。

  • 画像の準備

年齢順に並んだ複数の顔画像を用意し、各画像の特徴点を検出・対応付けします。

  • 連続モーフィング

隣接する画像ペアごとにモーフィングを行い、中間フレームを生成します。

これを繰り返して連続した変化を作ります。

  • フレームの連結

生成した中間フレームを時系列に並べて動画化し、滑らかな年齢変化アニメーションを完成させます。

  • イージングの活用

モーフ比率にイージング関数を適用し、変化の速度を調整して自然な動きを演出します。

この技術は、映画やゲームのキャラクター年齢変化表現、教育コンテンツなどで活用されています。

キャラクター間クロスフェード

異なるキャラクターや人物間でのモーフィングを行い、クロスフェードのような効果を作ることも可能です。

例えば、アニメキャラクターから実写人物への変換や、異なる表情の切り替えに使えます。

  • 特徴点の対応付け

異なるキャラクター間でも、目・鼻・口などの主要な特徴点を対応させる必要があります。

手動でポイントを調整することが多いです。

  • メッシュの共通化

両画像で同じ三角形メッシュを使い、対応する三角形間でアフィン変形を行います。

  • 色調整

キャラクター間で色調が大きく異なる場合は、カラーマッチングやヒストグラムマッチングで色味を整えます。

  • アニメーション生成

中間フレームを生成し、連続的に表示することで滑らかなクロスフェード効果を実現します。

この応用は、映像制作やゲームのキャラクター切り替え、特殊効果などで活用されています。

これらの応用例を活用することで、基本的な画像モーフィング技術を拡張し、多彩な表現や実用的なシナリオに対応できます。

拡張アイデア

画像モーフィングの基本技術をさらに発展させるための拡張アイデアを紹介します。

これらの手法を取り入れることで、より高度で表現力豊かなモーフィングが可能になります。

制御点のウェイト調整

モーフィングにおける制御点(特徴点)は通常、均等に扱われますが、各点にウェイト(重み)を設定することで、変形の影響度を調整できます。

これにより、特定の領域を強調したり、逆に変形を抑えたりすることが可能です。

  • ウェイトの設定方法

各制御点に0から1の範囲でウェイトを割り当てます。

ウェイトが大きいほど、その点の変形が強く反映されます。

  • 変形計算への反映

三角形ごとのアフィン変換や頂点の移動量にウェイトを乗じて調整します。

例えば、頂点の移動ベクトルにウェイトを掛けて変形量を制御します。

  • 局所的な変形制御

目元や口元など、表情の変化を強調したい部分に高いウェイトを設定し、背景や髪の毛などは低いウェイトにして変形を抑制します。

  • インタラクティブな調整

GUIツールでウェイトを視覚的に調整しながら、リアルタイムにモーフィング結果を確認することも可能です。

このウェイト調整により、より細やかで意図的な変形表現が実現できます。

非線形ワーピング

基本的なモーフィングはアフィン変換などの線形変形を用いますが、非線形ワーピングを導入すると、より複雑で自然な変形が可能になります。

  • スプライン変形

Thin Plate Spline(TPS)などのスプライン補間を使い、制御点間の変形を滑らかに補間します。

TPSは曲線的な変形を表現でき、顔の表情変化や筋肉の動きを自然に再現できます。

  • 局所的な非線形変形

特定の領域だけ非線形変形を適用し、他の部分は線形変形のままにすることで、計算コストを抑えつつ高品質な変形を実現します。

  • 物理ベースの変形モデル

弾性体モデルや質点モデルを用いて、物理的な挙動を模倣した非線形変形を行う手法もあります。

これにより、よりリアルな変形表現が可能です。

OpenCVではTPSの直接的なサポートはありませんが、外部ライブラリや自作実装で非線形ワーピングを組み込むことができます。

音声同期アニメーション

モーフィングアニメーションを音声に同期させることで、口の動きや表情変化を音声の内容に合わせてリアルタイムに制御できます。

これにより、よりインタラクティブで説得力のあるアニメーションが作成可能です。

  • 音声特徴量の抽出

音声信号からメル周波数ケプストラム係数(MFCC)やピッチ、音素情報などを抽出します。

  • 音声と表情のマッピング

抽出した音声特徴量を基に、口の開閉や表情の変化を制御するパラメータを生成します。

例えば、音素ごとに対応する口の形状を用意し、音声に合わせてモーフィング比率を変化させます。

  • リアルタイム制御

音声入力に応じてモーフィングの進行度や制御点の位置を動的に変更し、リアルタイムでアニメーションを生成します。

  • 機械学習の活用

音声と表情の対応関係を学習したモデルを使い、より自然で多様な表情変化を実現する手法もあります。

この音声同期アニメーションは、バーチャルアバターやゲームキャラクター、インタラクティブコンテンツでの活用が期待されます。

これらの拡張アイデアを取り入れることで、C++とOpenCVを用いた画像モーフィングの表現力と応用範囲を大きく広げられます。

まとめ

本記事では、C++とOpenCVを用いた画像モーフィングの基本から応用までを詳しく解説しました。

特徴点検出やDelaunay三角形分割、アフィン変形、ブレンディングの手法を理解し、パフォーマンス最適化や品質向上の工夫も紹介しています。

さらに、部分モーフィングや連続モーフィング、拡張アイデアとして非線形ワーピングや音声同期アニメーションなど、多彩な応用例も学べます。

これにより、実践的で高品質な画像モーフィングの実装が可能になります。

関連記事

Back to top button
目次へ