本チュートリアルでは、画像上の特定の三角形領域を別の形状の三角形に変形する手法について解説します。3D グラフィックスや画像処理において、三角分割は曲面を近似するための基本的なアプローチです。OpenCV は直接的な三角形変換関数を備えていませんが、いくつかの組み込み関数を組み合わせることでこの処理を実現できます。
アフィン変換の基礎知識
アフィン変換は、2 次元または 3 次元空間内の点集合に対して平行移動、拡大縮小、回転、せん断などの幾何学的操作を適用する変換手法です。重要な特性として、変換後の平行線は依然として平行に保たれます。このため、三角形から任意の三角形への変形には適していますが、四角形の一般的な変形には制約があります。
OpenCV では、アフィン変換は 2 行 3 列の行列によって表現されます。最初の 2 列は回転やスケーリング、shear を表し、最終列は平行移動を示します。
実装アプローチ
画像全体に対してアフィン変換を直接三角形領域に適用することはできないため、以下の手順で処理を行います:
- 源三角形と目標三角形を定義する
- 各三角形を囲む最小矩形を検出する
- 入力画像から矩形領域を切り出す
- 三角形座標を矩形座標系に調整する
- アフィン変換行列を計算・適用する
- 変換結果にマスクを適用して不要部分を表示しないようにする
ステップ 1:データ準備と初期設定
まず、入力画像を読み込み、浮動小数点数形式に変換します。出力用バッファも用意します。三角形の頂点座標を指定します。
// C++ バージョン
#include <opencv2/opencv.hpp>
using namespace cv;
// 入力画像の読み込みと型変換
Mat sourceImage = imread("input_image.jpg");
sourceImage.convertTo(sourceImage, CV_32FC3, 1.0f / 255.0f);
// 白色背景の出力画像を作成
Mat destinationImage = Mat::ones(sourceImage.size(), sourceImage.type());
destinationImage *= 1.0f;
// 変換元三角形の 3 頂点
vector<Point2f> srcTriangle;
srcTriangle.emplace_back(360.f, 200.f);
srcTriangle.emplace_back(60.f, 250.f);
srcTriangle.emplace_back(450.f, 400.f);
// 変換先三角形の 3 頂点
vector<Point2f> dstTriangle;
dstTriangle.emplace_back(400.f, 200.f);
dstTriangle.emplace_back(160.f, 270.f);
dstTriangle.emplace_back(400.f, 400.f);
# Python バージョン
import cv2
import numpy as np
# 入力画像の読み込み
sourceImage = cv2.imread("input_image.jpg")
# 白色の出力画像バッファ作成
destinationImage = np.ones(sourceImage.shape, dtype=np.float32) * 255.0
# 変換元の三角形座標(float32 型)
srcTriangle = np.array([[[360, 200], [60, 250], [450, 400]]], dtype=np.float32)
# 変換先の三角形座標
dstTriangle = np.array([[[400, 200], [160, 270], [400, 400]]], dtype=np.float32)
ステップ 2:バウンディングボックスの計算
三角形を囲む最小矩形を取得することで、画像全体ではなく必要な領域のみ処理し、計算効率を向上させます。
// C++ の場合
Rect srcRect = boundingRect(srcTriangle);
Rect dstRect = boundingRect(dstTriangle);
// Python の場合
srcRect = cv2.boundingRect(srcTriangle[0])
dstRect = cv2.boundingRect(dstTriangle[0])
ステップ 3:座標系の変換と画像クリップ
切り出した矩形内で三角形の位置を相対座標に変更する必要があります。これはそれぞれの頂点から矩形左上の座標を引き算することで達成されます。
// C++ 実装
vector<Point2f> clippedSrcTri, clippedDstTri;
vector<Point> clippedDstTriInt;
for(int i = 0; i < 3; ++i) {
clippedSrcTri.push_back(Point2f(srcTriangle[i].x - srcRect.x,
srcTriangle[i].y - srcRect.y));
clippedDstTri.push_back(Point2f(dstTriangle[i].x - dstRect.x,
dstTriangle[i].y - dstRect.y));
// fillConvexPoly は整数 Point 型を必要とする
clippedDstTriInt.push_back(Point(static_cast<int>(dstTriangle[i].x - dstRect.x),
static_cast<int>(dstTriangle[i].y - dstRect.y)));
}
// 入力画像から矩形部分を抜き出し
Mat croppedSource = sourceImage(srcRect).clone();
# Python 実装
clippedSrcTri = []
clippedDstTri = []
for i in range(3):
clippedSrcTri.append((srcTriangle[0][i][0] - srcRect[0],
srcTriangle[0][i][1] - srcRect[1]))
clippedDstTri.append((dstTriangle[0][i][0] - dstRect[0],
dstTriangle[0][i][1] - dstRect[1]))
# 入力画像をクロップ
croppedSource = sourceImage[srcRect[1]:srcRect[1]+srcRect[3],
srcRect[0]:srcRect[0]+srcRect[2]]
ステップ 4:アフィン変換行列の生成と適用
二つの三角形の対応関係からアフィン変換行列を計算し、切り出した画像区域に適用します。
// C++
Mat transformMatrix = getAffineTransform(clippedSrcTri, clippedDstTri);
Mat croppedDestination = Mat::zeros(dstRect.height, dstRect.width, croppedSource.type());
warpAffine(croppedSource, croppedDestination, transformMatrix,
croppedDestination.size(), INTER_LINEAR, BORDER_REFLECT_101);
# Python
transformMatrix = cv2.getAffineTransform(np.float32(clippedSrcTri),
np.float32(clippedDstTri))
croppedDestination = cv2.warpAffine(croppedSource, transformMatrix,
(dstRect[2], dstRect[3]), None,
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REFLECT_101)
ステップ 5:三角形マスクの作成と合成
最後に、三角形領域以外のピクセルを透過表示するためにマスクを作成し、変換された画像を合成します。
// C++ マスク処理
Mat mask = Mat::zeros(dstRect.height, dstRect.width, CV_32FC3);
fillConvexPoly(mask, clippedDstTriInt, Scalar(1.0f, 1.0f, 1.0f), 16, 0);
// マスクを適用して三角形内のみを残す
multiply(croppedDestination, mask, croppedDestination);
// 出力画像の該当領域を更新
multiply(destinationImage(dstRect), Scalar(1.0f, 1.0f, 1.0f) - mask, destinationImage(dstRect));
destinationImage(dstRect) += croppedDestination;
# Python マスク処理
mask = np.zeros((dstRect[3], dstRect[2], 3), dtype=np.float32)
cv2.fillConvexPoly(mask, np.int32(clippedDstTri), (1.0, 1.0, 1.0), 16, 0)
# マスク適用
croppedDestination = croppedDestination * mask
# 出力画像との合成
destinationImage[dstRect[1]:dstRect[1]+dstRect[3], dstRect[0]:dstRect[0]+dstRect[2]] \
= destinationImage[dstRect[1]:dstRect[1]+dstRect[3], dstRect[0]:dstRect[0]+dstRect[2]] \
* ((1.0, 1.0, 1.0) - mask)
destinationImage[dstRect[1]:dstRect[1]+dstRect[3], dstRect[0]:dstRect[0]+dstRect[2]] \
+= croppedDestination
完全実装例
上記の手順を一つの関数にまとめた完全なコードは以下の通りです。
// 三角形領域のアフィン変換を行うメイン関数
void warpAndTransformTriangle(Mat& inputImg, Mat& outputImg,
const vector<Point2f>& srcTri,
const vector<Point2f>& dstTri)
{
Rect srcBound = boundingRect(srcTri);
Rect dstBound = boundingRect(dstTri);
vector<Point2f> relSrcTri, relDstTri;
vector<Point> intDstTri;
for(int i = 0; i < 3; ++i) {
relSrcTri.emplace_back(srcTri[i].x - srcBound.x, srcTri[i].y - srcBound.y);
relDstTri.emplace_back(dstTri[i].x - dstBound.x, dstTri[i].y - dstBound.y);
intDstTri.emplace_back(Point(static_cast<int>(dstTri[i].x - dstBound.x),
static_cast<int>(dstTri[i].y - dstBound.y)));
}
Mat inputCropped = inputImg(srcBound).clone();
Mat transMat = getAffineTransform(relSrcTri, relDstTri);
Mat outputCropped = Mat::zeros(dstBound.height, dstBound.width, inputCropped.type());
warpAffine(inputCropped, outputCropped, transMat, outputCropped.size(),
INTER_LINEAR, BORDER_REFLECT_101);
Mat triMask = Mat::zeros(dstBound.height, dstBound.width, CV_32FC3);
fillConvexPoly(triMask, intDstTri, Scalar(1.0f, 1.0f, 1.0f), 16, 0);
multiply(outputCropped, triMask, outputCropped);
multiply(outputImg(dstBound), Scalar(1.0f, 1.0f, 1.0f) - triMask, outputImg(dstBound));
outputImg(dstBound) += outputCropped;
}
処理終了後、画像を 8bit 形式に変換して表示または保存することが一般的です。これで三角形の領域だけが他の形状に変換され、周辺部分は元のまま維持されます。