OpenCV

【C++】OpenCVで実現する画像アフィン変換の基本操作と実装テクニック

C++とOpenCVで実現するアフィン変換は、画像の拡大、縮小、回転、平行移動などが簡単に扱える手法です。

cv::getAffineTransformで算出した変換行列をcv::warpAffineに適用することで、直感的に画像処理が行えるため、シンプルなコードで効率的な編集が可能です。

画像アフィン変換の基礎知識

アフィン変換の定義

画像の寸法や形状の変化を行う方法として、アフィン変換はシンプルな幾何学的手法です。

画像内の各点が、直線性を保ったまま位置を変更する処理で、平行線同士の関係が変わらない特性があります。

アフィン変換は、拡大・縮小、回転、平行移動などの操作がひとつの変換行列にまとめられるため、画像処理においてとても扱いやすい技法です。

数学的背景

変換行列の基本構成

アフィン変換は、2次元の場合次のような\(2 \times 3\)の変換行列で表せます。

\[\begin{pmatrix}a & b & c \\d & e & f\end{pmatrix}\]

この行列は、平面上の点\((x, y)\)を新たな位置\((x’, y’)\)へ写像する役割を持ち、写像は以下のように計算されます。

\[\begin{pmatrix}x’ \\y’\end{pmatrix}=\begin{pmatrix}a & b \\d & e\end{pmatrix}\begin{pmatrix}x \\y\end{pmatrix}+\begin{pmatrix}c \\f\end{pmatrix}\]

この数式は、各要素がどのように組み合わされて画像変換に寄与するかを示しており、具体的な操作内容に応じた数値が設定されます。

拡大・縮小の要素

拡大・縮小は対角成分に影響を与えます。

座標\((x, y)\)に対して、拡大率\(s_x\)と\(s_y\)を適用すると、式は以下のように表されます。

\[\begin{pmatrix}s_x & 0 \\0 & s_y\end{pmatrix}\]

この変換行列をもとの座標に乗じると、画像の各点が水平方向と垂直方向に拡大または縮小されます。

実際の処理では、対応点を選んで変換行列を作成する際に、拡大率が内在的に反映されるようになっています。

回転の要素

回転に関しては、中心点を基準とする回転行列が用いられ、角度\(\theta\)に基づいた係数で表されます。

\[\begin{pmatrix}\cos\theta & -\sin\theta \\\sin\theta & \cos\theta\end{pmatrix}\]

この行列が適用されると、画像の各点が指定された角度分だけ回転されます。

OpenCVでは、\(cv::getRotationMatrix2D\)関数を使って、この回転行列を簡単に取得できる仕組みになっています。

平行移動の要素

平行移動は、変換行列の右側のベクトル部分に当たります。

\(c\)と\(f\)がそれに該当し、平行移動量を表します。

\[\begin{pmatrix}c \\f\end{pmatrix}\]

画像内のすべての点に同じ量を加えるような処理として、平行移動は他の変換と組み合わせることで画像全体をシフトさせることが可能です。

OpenCVによるアフィン変換の仕組み

主要なOpenCV関数

cv::getAffineTransformの機能

cv::getAffineTransformは、3つの対応点セットをもとにアフィン変換行列を計算する関数です。

入力される座標のセットと出力される座標のセットを指定すると、内部で連立方程式が解かれて変換行列が求められます。

このプロセスは、画像の任意の部分を狙った変換が必要な場合に非常に有効です。

cv::warpAffineの機能

cv::warpAffineは、取得したアフィン変換行列をもとに画像全体に対して実際の変換を適用する関数です。

補間法や境界処理のオプションを指定することができ、変換後の画像を任意のサイズに合わせたり、背景の色を設定するなどの細かい調整が可能です。

対応点設定の流れ

変換前後の対応点の選定

画像変換を適切に行うためには、変換前後で対応する3つの点(または必要な点の組み合わせ)を正確に選ぶことが重要です。

例えば、顔画像の場合は目や口の位置を対応点に設定することで自然な変換が可能になります。

対応点は、元画像と変換後の画像における重要なランドマークとして選定されます。

対応点の設定方法

対応点の設定は、プログラム内でcv::Point2f型の配列に格納して指定します。

下記のコード例では、画像内の特定の3点を取得して、対応する変換後の点を別途設定する方法が示されています。

下記サンプルコードでは、対応点の設定やアフィン変換行列の計算、そして変換画像の生成までが一連の流れとして記述されています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // 入力画像の読み込み
    cv::Mat src_img = cv::imread("input_image.jpg");
    if (src_img.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // 変換前の対応点(元画像上の重要な位置)
    cv::Point2f src_points[3] = {
        cv::Point2f(100.0f, 100.0f),  // 左上の座標
        cv::Point2f(200.0f, 100.0f),  // 右上の座標
        cv::Point2f(100.0f, 200.0f)   // 左下の座標
    };
    // 変換後の対応点(新たな位置に合わせる)
    cv::Point2f dst_points[3] = {
        cv::Point2f(150.0f, 150.0f),
        cv::Point2f(250.0f, 150.0f),
        cv::Point2f(150.0f, 250.0f)
    };
    // アフィン変換行列の計算
    cv::Mat affine_matrix = cv::getAffineTransform(src_points, dst_points);
    // バウンディングボックスを計算し、出力画像サイズを決定
    cv::Rect bbox = cv::RotatedRect(cv::Point2f(), src_img.size(), 0).boundingRect();
    cv::Mat dst_img;
    // アフィン変換を適用
    cv::warpAffine(src_img, dst_img, affine_matrix, bbox.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0));
    // 変換結果をウィンドウで表示
    cv::imshow("Source Image", src_img);
    cv::imshow("Transformed Image", dst_img);
    cv::waitKey(0);
    return 0;
}
平行移動したもの

このコードは、画像の読み込みから対応点の設定、アフィン変換行列の計算、そして最終的な画像の変換と表示までの流れを丁寧に示しています。

C++実装における画像処理の詳細

入力画像の準備と前処理

画像処理の実装ではまず入力画像の読み込みが必要です。

cv::imreadを使って画像ファイルからデータを取得します。

読み込み後、画像サイズやチャンネル数のチェックを行い、必要に応じて前処理(例:グレースケール変換、ノイズ除去)を実施します。

また、画像が取得できなかった場合はプログラム内でエラー処理を実施することで、後続処理が正しく動作するよう工夫しています。

変換行列の算出手順

対応点からの行列算出

変換行列の算出は、先に設定した対応点の配列を利用します。

C++では、cv::Point2fの配列に対象となる3点を格納し、cv::getAffineTransform関数に渡すだけで、簡単に変換行列が算出されます。

以下のコード例は、シンプルな対応点から行列を生成し、画像に対して適用する流れを示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // 入力画像の読み込みチェック
    cv::Mat inputImage = cv::imread("sample.jpg");
    if (inputImage.empty()) {
        std::cerr << "画像読み込みエラー" << std::endl;
        return -1;
    }
    // 元画像上の3点(例:画像内のランドマーク)
    cv::Point2f srcPts[3] = {
        cv::Point2f(80.0f, 80.0f),
        cv::Point2f(180.0f, 80.0f),
        cv::Point2f(80.0f, 180.0f)
    };
    // 変換後のターゲット位置
    cv::Point2f dstPts[3] = {
        cv::Point2f(100.0f, 100.0f),
        cv::Point2f(200.0f, 100.0f),
        cv::Point2f(100.0f, 200.0f)
    };
    // アフィン変換行列の計算
    cv::Mat transMatrix = cv::getAffineTransform(srcPts, dstPts);
    // 画像の変換の実行
    cv::Mat outputImage;
    cv::warpAffine(inputImage, outputImage, transMatrix, inputImage.size());
    // 画像表示
    cv::imshow("入力画像", inputImage);
    cv::imshow("変換後画像", outputImage);
    cv::waitKey(0);
    return 0;
}
[サンプル実行結果]
- "入力画像" ウィンドウに元画像表示
- "変換後画像" ウィンドウにアフィン変換が適用された画像表示

計算手順のポイント

変換行列算出時のポイントとして、対応点の順番や位置の正確さに注意します。

誤った順番で対応点を設定すると、期待していない変換結果になってしまう可能性があるため、座標の整合性を確認することが大切です。

また、数値計算の際には浮動小数点数の精度にも意識を向け、微小なズレを防ぐ工夫が求められます。

変換結果の生成と評価

変換後の画像は、cv::warpAffine関数を使って生成します。

画像サイズや補間方法、境界補完の設定が適切に行われていれば、滑らかな変換結果が得られます。

出力された画像を視覚的に確認した上で、変形後の幾何学的な整合性を評価することで、変換の品質を判断することが可能です。

注意点とエラー処理

画像読み込みエラーへの対策

画像の読み込み時にファイルパスの誤りやファイルの破損があると、処理が停止してしまうため、cv::imreadの返り値が空かどうかを必ずチェックする必要があります。

事前にファイルの存在確認などを行うと、エラー発生時の対策がしやすくなります。

境界補完処理の工夫

変換処理の際、画像の境界部分が欠落する問題が発生することがあります。

cv::warpAffineでは、cv::BORDER_CONSTANTcv::BORDER_REPLICATEなどのオプションを指定することで、境界部分の補完方法を選ぶことができます。

状況に応じた補完方法を採用することで、見栄えの良い結果が得られます。

数値精度と計算誤差への配慮

アフィン変換は浮動小数点数の計算に依存するため、わずかな数値誤差が最終結果に影響する場合があります。

特に大きな画像や高精度な変換を行う際は、計算方法やパラメータ設定をきめ細かく調整し、誤差が最小限に抑えられるように工夫します。

なお、複数の変換を連続して行う場合、累積誤差に注意が必要です。

画像アフィン変換の応用事例

単一変換の事例

回転変換のみの実装例

画像を回転する場合、中心位置と回転角度を指定して回転行列を作成する方法が採用されます。

下記のコード例は、入力画像を30度回転するケースを示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // 画像読み込み
    cv::Mat srcImage = cv::imread("rotate_sample.jpg");
    if (srcImage.empty()) {
        std::cerr << "画像読み込みエラー" << std::endl;
        return -1;
    }
    // 回転の中心座標を画像中心に設定
    cv::Point2f center(srcImage.cols / 2.0f, srcImage.rows / 2.0f);
    // 回転角度と拡大率の設定(ここでは30度回転、拡大率は1.0)
    double angle = 30.0;
    double scale = 1.0;
    // 回転行列の取得
    cv::Mat rotationMatrix = cv::getRotationMatrix2D(center, angle, scale);
    // 結果画像の作成
    cv::Mat rotatedImage;
    cv::warpAffine(srcImage, rotatedImage, rotationMatrix, srcImage.size());
    cv::imshow("元画像", srcImage);
    cv::imshow("回転後画像", rotatedImage);
    cv::waitKey(0);
    return 0;
}
回転処理した写真

このサンプルは、単純な回転処理を行う方法を分かりやすく示しており、回転中心や角度の指定が重要なポイントとなることがわかります。

平行移動変換のみの実装例

画像全体を任意の距離だけ平行移動させる場合、変換行列の右側の成分に平行移動量を設定します。

次の例は、画像を水平方向に50ピクセル、垂直方向に30ピクセルシフトするコードです。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // 画像の読み込み
    cv::Mat srcImage = cv::imread("translate_sample.jpg");
    if (srcImage.empty()) {
        std::cerr << "画像読み込みエラー" << std::endl;
        return -1;
    }
    // 平行移動量の設定
    float tx = 50.0f;  // 水平シフト量
    float ty = 30.0f;  // 垂直シフト量
    // 単位行列に平行移動量を追加
    cv::Mat translationMatrix = (cv::Mat_<double>(2, 3) << 1, 0, tx, 0, 1, ty);
    // 画像の平行移動を実施
    cv::Mat translatedImage;
    cv::warpAffine(srcImage, translatedImage, translationMatrix, srcImage.size());
    cv::imshow("元画像", srcImage);
    cv::imshow("平行移動後画像", translatedImage);
    cv::waitKey(0);
    return 0;
}
[サンプル実行結果]
- "元画像" ウィンドウに元の画像表示
- "平行移動後画像" ウィンドウに水平方向に50ピクセル、垂直方向に30ピクセルシフトした画像表示
ウィンドウに水平方向に50ピクセル、垂直方向に30ピクセルシフトした画像

平行移動のみの場合、変換行列には回転や拡大の要素がなくシンプルな構造となっているため、意図したシフトがわかりやすく実現できます。

複合変換の事例

複数変換の連携例

複数の変換操作(例:拡大、回転、平行移動)をひとまとめにして適用するケースでは、各操作を順次組み合わせた変換行列が利用されます。

まず各変換の行列を算出し、その後に行列の積を取ることで一度のwarpAffine呼び出しで複数操作を実現できます。

下記のサンプルコードは、画像を拡大し、30度回転させ、シフトする連携例を示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
    // 画像読み込み
    cv::Mat srcImage = cv::imread("composite_sample.jpg");
    if (srcImage.empty()) {
        std::cerr << "画像読み込みエラー" << std::endl;
        return -1;
    }
    // 画像中心点の計算
    cv::Point2f center(srcImage.cols / 2.0f, srcImage.rows / 2.0f);
    // 回転と拡大のための行列を取得:拡大率1.2、回転角度30度
    double scale = 1.5;
    double angle = 30.0;
    cv::Mat rotateMatrix = cv::getRotationMatrix2D(center, angle, scale);
    // 平行移動行列の設定(例:x方向200ピクセル、y方向100ピクセルのシフト)
    cv::Mat translationMatrix = (cv::Mat_<double>(2, 3) << 1, 0, 200, 0, 1, 100);
    // 複合変換行列の連携:まず回転と拡大、次に平行移動
    cv::Mat compositeMatrix = translationMatrix.clone();
    // rotation行列の形式と合わせるための調整(2x3形式)
    compositeMatrix.at<double>(0, 0) = rotateMatrix.at<double>(0, 0);
    compositeMatrix.at<double>(0, 1) = rotateMatrix.at<double>(0, 1);
    compositeMatrix.at<double>(0, 2) = rotateMatrix.at<double>(0, 2) + translationMatrix.at<double>(0, 2);
    compositeMatrix.at<double>(1, 0) = rotateMatrix.at<double>(1, 0);
    compositeMatrix.at<double>(1, 1) = rotateMatrix.at<double>(1, 1);
    compositeMatrix.at<double>(1, 2) = rotateMatrix.at<double>(1, 2) + translationMatrix.at<double>(1, 2);
    // 複合変換の適用
    cv::Mat compositeImage;
    cv::warpAffine(srcImage, compositeImage, compositeMatrix, srcImage.size());
    cv::imshow("元画像", srcImage);
    cv::imshow("複合変換後画像", compositeImage);
    cv::waitKey(0);
    return 0;
}
拡大・回転・平行移動が連携して適用された画像

複合変換では、変換行列の各項目が異なる変換要素を担っており、細かな調整を行いながら理想の変換結果を追求することが求められます。

性能評価と最適化の考慮点

計算負荷の把握

画像サイズが大きい場合や連続して多くの変換を行う場合、計算負荷が上がる可能性があるため、各種処理の処理時間の把握が必要です。

OpenCVの関数には高速化された実装が多く組み込まれているため、各変換操作の計算時間やフレームレートに注目し、最適化の目安として利用できます。

実行速度向上の取り組み

パフォーマンス改善の工夫

パフォーマンス改善には、以下の工夫が検討されます。

  • 複数の画像処理操作を並列化し、マルチスレッド処理やGPUアクセラレーションを活用する
  • 変換行列の計算や画像の再サンプリングにおけるアルゴリズムの最適化を行う
  • 必要な部分のみに変換処理を限定し、全体画像に一律の処理を避ける

評価手法の紹介

実行速度の評価には、cv::getTickCountcv::getTickFrequencyを組み合わせた計測手法が利用できます。

これらの関数を使うことで、各処理ブロックの実行時間をミリ秒単位で測定可能なため、どの処理がボトルネックになっているかを把握し、さらなる最適化に役立つ情報が得られます。

まとめ

今回紹介した画像アフィン変換の実装例は、C++とOpenCVを使って柔軟な画像処理が実現できることを示しています。

各変換要素がどのように変換行列に組み込まれるかを理解することで、様々な画像変換操作に応用しやすくなります。

実装時には入力画像の前処理、対応点の正確な設定、さらには計算精度や実行速度についても配慮することで、理想的な変換結果を得ることが可能です。

今回の内容を参考に、用途に合わせたカスタマイズや性能最適化に取り組むと、より高度な画像処理アプリケーションの構築につながることでしょう。

関連記事

Back to top button