OpenCVによる画像の透視変換を詳しく解説(理論から実装、実践まで)

1. アフィン変換と透視変換

         アフィン変換と透視変換の違いがよく分からなかったので、2つの変換の内容を詳しく調べて式を書き直し、自分なりの意見も述べてみました。

1. アフィン変換

アフィン変換は透視変換特殊なケースである        と考えることができます

        アフィン変換は2 次元座標2 次元座標の間の線形変換、つまり、1 つの平面内の 2 次元グラフィックスのみを含む線形変換です。

        グラフィックスの平行移動、回転ずらし拡大縮小などはアフィン変換の変換行列で表現できます

        これは、2 次元グラフィックスの 2 つのプロパティを維持します。

       ①「直線性」:直線は変形後も直線のままです。直線は、平行移動回転千鳥配置拡大縮小されても直線のままです。

        ②「平行度」:平行線は変形後も平行線であり、直線上の点の位置順序は変わりません。

        直感的には、コンピューター上で写真をドラッグしたり、反転したり、引き伸ばしたりしても、写真の画角は変わりません。

すべてのアフィン変換は、行列を乗じた座標ベクトル        として表現できます。以下は、いくつかのアフィン変換の行列形式です。

        ズーム:

\begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ \end{array} \right ] = \left[ \begin{array}{c} T_{x}x\ \ T_{y}y\\ \end{array} \right ] = \left[ \begin{array}{ccc} T_{x}& 0 \\ 0& T_{y} \\ \end{array} \right ] \left[ \begin{array}{c} x\\ y\\ \end{array} \right ] \end{equation}

        回転:

\begin{方程式}\left[\begin{array}{c}x'\\y'\\\end{array}\right] = \left[\begin{array}{c}xcos\theta-ysin\ theta\\xsin\theta+ycos\theta\\end{array}\right] = \left[\begin{array}{cc}cos\theta& -sin\theta\\sin\theta& cos\theta\\\ end {配列} \right ] \left[ \begin{array}{c} x\\ y\\ \end{array} \right ] \end{equation}

        ミスカット:

\begin{equation} \left[ \begin{array}{c} x'\\ y'\\ \end{array} \right ] = \left[ \begin{array}{c} x+ytan\phi\ \ y+xtan\varphi\\ \end{array} \right ] = \left[ \begin{array}{cc} 1& Tan\phi \\ Tan\varphi& 1 \\ \end{array} \right ] \left [ \begin{array}{c} x\\ y\\ \end{array} \right ] \end{equation}

        上記の変換は 2x2 行列のみで直接変換できますが、2x2 行列ではいくら掛けても一定量の変換ができないため、平行移動はできません。したがって、元の2 次元座標ベクトルを同次座標に変換する、つまり 2 次元ベクトルを 3 次元ベクトルで表現する必要があります。

        \left[ \begin{array}{c} x\\ y\\ \end{array} \right ] => \left[ \begin{array}{c} x\\ y\\ 1\\ \end{配列} \right ]

        パン:

        \begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ \end{array} \right ] = \left[ \begin{array}{c} x+T_{x} \\ y+T_{y}\\ \end{array} \right ] = \left[ \begin{array}{ccc} 1& 0 &T_{x}\\ 0& 1 &T_{y}\\ \end{array } \right ] \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] \end{equation}

同次座標になった後、元の 2x2 行列の拡大縮小、回転、ずらしなどの変換を        実現するには、次数だけT_{x}=T_{y}=0が必要です。

        上記の変換はすべて線形変換であるため、アフィン変換は次の一般式で表すことができます。これはインターネット上で一般的な形式です。

        \begin{equation} \left[ \begin{array}{c} x'\\ y'\\ \end{array} \right ] = \left[ \begin{array}{c} a_{11}x+ a_{12}y+a_{13}\\ a_{21}x+a_{22}y+a_{23}\\ \end{array} \right ] = \left[ \begin{array}{ccc} a_{11}& a_{12}&a_{13}\\ a_{21}& a_{22} &a_{23}\\ \end{array} \right ] \left[ \begin{array}{c} x \\ y\\ 1\\ \end{配列} \right ] \end{式}

        このとき、アフィン変換の変換行列は2×3T= \left[ \begin{array}{ccc} a_{11}& a_{12}&a_{13}\\ a_{21}& a_{22} &a_{23}\\ \end{array} \right 】の行列となる

        したがって、座標変換式は次のようになります。

        \begin{方程式} \left\{ \begin{行列}{} x'=a_{11}x+a_{12}y+a_{13} \\ y'=a_{21}x+a_{22} y+a_{23} \\ \end{行列} \right。 \end{式}

6 つの未知の係数があり、解決するには3 組のマッピング ポイント (相互に独立している場合)が必要で        あることがわかります。6 つの変数には当然少なくとも 6 つの方程式を計算する必要があり、1 組のマッピング点から2 つの方程式が得られることを理解するのは難しくありません

        同時に、3 つの点は平面を一意に決定し、線形変換のため他の 3 つのマッピング点は同じ平面内になければならないため、アフィン変換は平面内の図形変換であると言えます。

2. 視点の変換

        遠近法変換は、画像を新しい表示面に投影することであり、プロジェクション マッピングとも呼ばれます。

        これは、 2 次元から3 次元(x,y)、そして別の2 次元空間へのマッピングです(X、Y、Z)(x',y')

        アフィン変換とは対照的に、これは単なる線形変換ではありません。これにより、1 つの四角形領域を別の四角形領域にマッピングする柔軟性が向上します。

        透視変換も、 3x3行列を使用した行列乗算によって実装されます。行列の最初の 2 行はアフィン行列と同じです。つまり、アフィン変換透視変換のすべての変換も実現できます。3行目は、透視変換を実装するために使用されます。

        透視変換でも、同次座標を使用して2D ベクトルを表します。

        \begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] = \left[ \begin{array}{ccc} a_{ 11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_{31}& a_{32} &a_{33}\\ \end{配列} \right ] \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] \end{equation}

        

        このとき、透視変換の変換行列は3x3の行列T= \left[ \begin{array}{ccc} a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_{31}& a_{ 32} &a_{33}\\ \end{配列} \right ]となります。

        透視変換は\left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ]最終的な座標ではないため、さらに変換が必要です。

\begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] = z' \left[ \begin{array}{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ] \end{equation}

        \left[ \begin{array}{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ]は、最終的に変換された座標です。つまり、次のようになります。

\begin{方程式} \left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ] = \left[ \begin{array}{c} \ frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ] \end{equation}

        実際、ここで、なぜアフィン変換が透視変換の特殊なケースであるのかを理解できます。アフィン変換後の座標ベクトルも同次座標 で表される場合\left[ \begin{配列}{c} x'\\ y'\\ 1\\ \end{配列} \right ]2x3行列から3x3行列への変換は次のようにみなすことができます。

T_{アフィン}= \left[ \begin{array}{ccc} a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ 0& 0 &1\ \ \end{配列} \right ]

        このとき、アフィン変換と透視変換の形式は統一されており、アフィン変換の過程は以下のようにみなされる。

\begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ 1\\ \end{array} \right ] = \left[ \begin{array}{ccc} a_{11 }& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ 0& 0 &1\\ \end{array} \right ] \left[ \begin{array}{c } x\\ y\\ 1\\ \end{配列} \right ] \end{式}

        したがって、アフィン変換は、透視変換行列の 3行目の特殊なケースに\left[ \begin{行列} 0&0&1\\ \end{行列} \right ]すぎませんこのとき、 3次元ベクトルまで拡張されていますが、実際にはアフィン変換は空間座標系の平面、つまりz方向の値に対して行われる面内画像変換であることも理解できます。はどのように変化しても 1 であり、常に平面内に限定されます。Z=1Z=1

        視点変換の話に戻りますが、視点変換の全体的なプロセスは次のとおりです。

\begin{equation} \begin{aligned} \left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ] = \left[ \begin{array }{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ] = \frac{1}{z'} \ left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] = \frac{1}{z'} \left[ \begin{array}{ccc } a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_{31}& a_{32} &a_{33}\\ \end{配列} \right ] \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] \\= \frac{1}{a_{31}x+a_{32 }y+a_{33}} \left[ \begin{array}{ccc} a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_ {31}& a_{32} &a_{33}\\ \end{array} \right ] \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] \end{整列} \end{方程式}

        したがって、座標変換式は次のようになります。

\begin{equation} \left\{ \begin{matix}{} x''=\frac{x'}{z'}=\frac{a_{11}x+a_{12}y+a_{13} }{a_{31}x+a_{32}y+a_{33}} \\ y''=\frac{y'}{z'}=\frac{a_{21}x+a_{22}y +a_{23}}{a_{31}x+a_{32}y+a_{33}} \\ \end{matix} \right。 \end{式}

合計9 つの未知のパラメータ        がありますが、これはさらに8 つの未知のパラメータに単純化でき、これがインターネット上の一般的な形式です。

ここでの私の考えは、変換行列で表される変換T= \left[ \begin{array}{ccc} a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_{31}& a_{ 32} &a_{33}\\ \end{配列} \right ]と変換行列(k は定数) が等しい        ことを証明することです。T'=kT= \left[ \begin{array}{ccc} to_{11}& to_{12} &to_{13}\\ to_{21}& to_{22} &to_{23}\\ to_{31} &to_{32} &to_{33}\\\end{配列}\right]

        変換行列を に設定して、次の演算に代入します。

\begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] =T' \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] =kT \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] = \left [ \begin{array}{ccc} ka_{11}& ka_{12} &ka_{13}\\ ka_{21}& ka_{22} &ka_{23}\\ ka_{31}& ka_{32} &ka_{ 33}\\ \end{配列} \right ] \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right ] \end{equation}

        方程式は次のように得られます。

\begin{equation} \left\{ \begin{matix}{} x''=\frac{k(a_{11}x+a_{12}y+a_{13})}{k(a_{31} x+a_{32}y+a_{33})}=\frac{a_{11}x+a_{12}y+a_{13}}{a_{31}x+a_{32}y+a_{ 33}} \\ y''=\frac{k(a_{21}x+a_{22}y+a_{23})}{k(a_{31}x+a_{32}y+a_{33 })}=\frac{a_{21}x+a_{22}y+a_{23}}{a_{31}x+a_{32}y+a_{33}} \\ \end{matix} \右。 \end{式}

        可以发现最后て计算出来的x''、y''与使用变换矩阵T计算出来的结果是一样的。

        因此对于T= \left[ \begin{array}{ccc} a_{11}& a_{12} &a_{13}\\ a_{21}& a_{22} &a_{23}\\ a_{31}& a_{ 32} &a_{33}\\ \end{配列} \right ],我们总可以等价为\frac{T}{a_{33}}= \left[ \begin{array}{ccc} \frac{a_{11}}{a_{33}}& \frac{a_{12}}{a_{33 }} &\frac{a_{13}}{a_{33}}\\ \frac{a_{21}}{a_{33}}& \frac{a_{22}}{a_{33}} &\ frac{a_{23}}{a_{33}}\\ \frac{a_{31}}{a_{33}}& \frac{a_{32}}{a_{33}} &1\\ \end{配列} \right ]所带来的转换。

        对上述变量重新命名即可得到变换矩阵T:

T= \left[ \begin{array}{ccc} b_{11}& b_{12} &b_{13}\\ b_{21}& b_{22} &b_{23}\\ b_{31}& b_{ 32} &1\\ \end{配列} \right ]

        此时只有8个未知参数,变换方程组也变成如下:

\begin{equation} \left\{ \begin{matix}{} x''=\frac{x'}{z'}=\frac{b_{11}x+b_{12}y+b_{13} }{b_{31}x+b_{32}y+1} \\ y''=\frac{y'}{z'}=\frac{b_{21}x+b_{22}y+b_{ 23}}{b_{31}x+b_{32}y+1} \\ \end{配列} \right。 \end{式}

        求解8个未知数需要8个等式,1组映射点提供2个等式,所以需要找到4组映射点,这也是为什么我们需要提供变换前后4个点来表示透视变换。

        至此我们也可以理解为什么透视变换是二维到三维,三维又到二维的过程。变换之前的点为\left[ \begin{配列}{c} x\\ y\\ 1\\ \end{配列} \right ],这是三维空间上的点,但我们认知上它在二维平面Z=1上的投影是(x,y)。通过矩阵变换成三维空间中的点\left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ],再通过除以三维中Z轴的值z'得到三维空间的点\left[ \begin{array}{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ],最后投影到二维平面Z=1得到点(x',y')。整个过程就是把二维转到三维,再转映射回之前的二维空间。

        透視変換効果は、観察者の視点が変化したときに観察される画像の変化に相当します。

なぜこの視点の変更がマトリックス        によって可能になるのでしょうか? 具体的な数学的原理はインターネット上で見つけることができず、ほとんどの記事が変換行列Tに止まっており、具体的な理由についての詳しい説明はなく、長い間悩みました。

        演繹すると、視点変換のプロセスは次のようになると思います。

        まず、観測点は原点に位置し、次に(0,0,0)z 軸の正の方向を見ると、(x,y,1)ディスプレイ上でオブジェクトが見える場所が投影面になります。

T透視変換行列を変更する        と、元の(x,y,1)になります(変換された点が依然として空間内の同じ平面(x',y',z')上にあることが証明できます)。このとき、座標は平面上だけではなく、三次元空間全体にあります。つまり、行列は平面内の元のグラフィックスを空間内の特定の平面のグラフィックスに変換しますこれが、変換行列が画像の視野角を変更する理由です。Z=1TZ=1

        そして、グラフの各点を視点(原点)に結び、Z=1投影面に投影してグラフを形成する(x'',y'')数値的なパフォーマンスは、3 つの座標値が で除算されることですz'。この理由は、実際には幾何学的なスケーリングです。

\frac{0-1}{0-z'}=\frac{0-x''}{0-x'}=\frac{0-y''}{0-y'}

        これは私の長年の混乱も解決します。つまり、遠近法変換によって引き起こされる写真の遠近法の変化は、観察者の遠近法の変化ではなく、一定の遠近法によるオブジェクトの空間位置の変化です。観察者の視点が変化し、その結果、私たちが見るものは変わります。これは、私たちが歩き回っているときに物体の角度が変化するのではなく、人はずっと静止しており、他の人が物体を動かすことで物体の角度が変化するのが見えると理解できます。

2. 透視変換の実現

        アプリケーションでは、OpenCV の 2 つの関数getPerspectiveTransform()warpPerspective()を使用する必要があります。

1.getPerspectiveTransform()

Mat getPerspectiveTransform(InputArray src, InputArray dst, int solveMethod = DECOMP_LU)

① 機能説明

4 組のマッピング点        から透視変換の変換行列Tを計算します。返される行列のデータ型はMatです。\left[ \begin{配列}{c} x\\ y\\ 1\\ \end{配列} \right ]ここで、1 組のマッピング点はとを参照します\left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ]が、変換行列はと にT作用することに注意してくださいたった今:\left[ \begin{配列}{c} x\\ y\\ 1\\ \end{配列} \right ]\left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ]

\left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] = T \left[ \begin{array}{c} x\\ y\\ 1\\ \end{配列} \right ]

②パラメータの説明

        パラメータsrc :ソース画像の四角形の4 つの頂点座標。

        パラメータdst :対象画像は四角形の4 つの頂点座標に対応します。

        パラメータsolveMethod : cv::solve(#DecompTypes) に渡される計算メソッド。デフォルトはDECOMP_LUです。通常、このパラメータを入力する必要はありません。

        戻り値: warpPerspective()関数で直接使用できるMat型変換行列

2.warpPerspective()     

void warpPerspective(
	InputArray src,
	OutputArray dst,
	InputArray M,
	Size dsize,
	int flags=INTER_LINEAR,
	int borderMode = BORDER_CONSTANT, 
	const Scalar& borderValue = Scalar());

① 機能説明

        変換マトリックスをT元のイメージに適用して、そのイメージを目的のイメージに透視変換します。

②パラメータの説明

        パラメータ src : 入力画像。

        パラメータ dst : 出力画像。結果を保存するには空の行列を初期化する必要があります。行列のサイズを設定する必要はありません。

        パラメータ M : 3x3変換行列。

        パラメータ dsize : 出力画像のサイズ。

        パラメータフラグ: 補間方法を設定します。デフォルトは、バイリニア補間の場合はINTER_LINEAR最近傍補間の場合はINTER_NEAREST 、逆変換 (dst->src) として M の場合はWARP_INVERSE_MAP です。

        参数borderMode:像素外推方法,默认为BORDER_CONSTANT,指定常数填充。翻阅官方文档发现还有一个选项是BORDER_REPLICATE

        参数borderValue:常数填充时边界的颜色设置,默认是(0,0,0),表示黑色。这就是为什么透视变换后图片周围是黑色的原因。这里需要注意的是类型为Scalar (B, G, R)

3.函数的使用

        代码如下:

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

void main()
{
    Mat img = imread("test.png");  
    Point2f AffinePoints0[4] = { Point2f(0, 0), Point2f(img.cols, 0), Point2f(0, img.rows), Point2f(img.cols, img.rows) };//变化前的4个节点
    Point2f AffinePoints1[4] = { Point2f(100, 0), Point2f(img.cols - 100, 0),Point2f(0, img.rows), Point2f(img.cols, img.rows) };//变化后的4个节点

    Mat Trans = getPerspectiveTransform(AffinePoints0, AffinePoints1);//由4组映射点得到变换矩阵
    Mat dst_perspective;//存储目标透视图像
    warpPerspective(img, dst_perspective, Trans, Size(img.cols, img.rows));//执行透视变换

    imshow("原图像", img);
    imshow("透视变换后", dst_perspective);
    waitKey();
}

        执行结果如下:

        可以看到,原本的效果是从正面平视电脑屏幕,现在透视变换后是仰视电脑屏幕(或者说电脑屏幕向后倾斜)。

4.映射点标记

        为了更清晰地知道变换前后的映射点,我们可以在图上标注出来,这里使用到OpenCV的circle函数。

①函数原型

        void circle()

circle (
	InputOutputArray img, 
	Point center, 
	int radius, 
	const Scalar &color, 
	int thickness=1, 
	int lineType=LINE_8, 
	int shift=0)

②函数功能

        在图像上画一个具有给定中心和半径的空心或实心圆。

③函数参数

        参数img:画圆绘制的图像。

        参数center:画圆的圆心坐标,类型为Scalar(x, y)

        参数radius:圆的半径。

        パラメータ color : 円の色、ルールは (B,G,R)、タイプはScalar(B,G,R)です。

        パラメータの太さ:正の数値の場合は、円を構成する線の太さを示します負の数値の場合は、円が塗りつぶされているかどうかを意味します。たとえば、FILLED は塗りつぶされた円を描くことを意味します。

        パラメータ line_type : 線のタイプ。デフォルトはLINE_8です。

        パラメータ シフト: 中心座標点と半径値の小数点以下の桁数。デフォルトは0です。

④ コード処理

    for (int i = 0; i < 4; i++)//显示4组映射点的位置
    {
        //画一个圆心在映射点(转换前),半径为10,线条粗细为3,红色的圆
        circle(img, AffinePoints0[i], 10, Scalar(59, 23, 232), 3);
        //画一个圆心在映射点(转换后),半径为10,线条粗细为3,蓝色的圆
        circle(dst_perspective, AffinePoints1[i], 10, Scalar(139, 0, 0), 3);
    }

        実行効果:

        変換前後の 4 つの点がすべて描画されていることがわかります。マッピング点を角に設定したため、円の一部しか見えていません。写真の中央に設定すると、円全体が表示されます。

5.ライブラリ関数の実装

        一般に透視変換は上記の関数を直接呼び出すことで実現できますが、ここでは変換処理をよりよく理解するために上記2つの関数に透視変換関数を実装しました(変換行列を求める関数は純粋に数学的な問題であり、ここでは繰り返しません)。これは、公式のライブラリ関数の実装とは異なる場合があります。

        warpPerspectiveの実現アイデアは次のとおりです。

変換行列ソース イメージが        与えられるとソース イメージをターゲット イメージに透視変換するために変換行列を使用する必要があります。このプロセスは、上で理解した順方向プロセスではありません。つまり、変換行列 T に元の画像のすべての座標を乗算して、ターゲット画像の座標を取得します。これにより、ターゲット画像上の個々の座標がマッピングされなくなるためです。逆のプロセスを使用して元の画像にマッピングしてターゲットのピクセル値を見つけます。逆マッピングの導出プロセスは次のとおりです。\left[ \begin{配列}{c} x\\ y\\ 1\\ \end{配列} \right ]\left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ]\left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ]

        既知の:

\begin{方程式} \left[ \begin{array}{c} x'\\ y'\\ z'\\ \end{array} \right ] = T \left[ \begin{array}{c} x \\ y\\ 1\\ \end{配列} \right ] \end{式}

        で割るz':

\begin{equation} \left[ \begin{array}{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \end{array} \right ] = T \left[ \begin{array}{c} \frac{x}{z'}\\ \frac{y}{z'}\\ \frac{1}{z'}\\ \end{配列} \right ] \end{式}

        と乗算しますT^{-1}:

\begin{equation} T^{-1} \left[ \begin{array}{c} \frac{x'}{z'}\\ \frac{y'}{z'}\\ 1\\ \ end{array} \right ] = \left[ \begin{array}{c} \frac{x}{z'}\\ \frac{y}{z'}\\ \frac{1}{z'} \\ \end{配列} \right ] \end{式}

        たった今:

\begin{equation} T^{-1} \left[ \begin{array}{c} x''\\ y''\\ 1\\ \end{array} \right ] = \left[ \begin{ array}{c} \frac{x}{z'}\\ \frac{y}{z'}\\ \frac{1}{z'}\\ \end{array} \right ] \end{equation }

        取得した変換行列 T の逆行列が次のようにT^{-1}なると仮定します。

T^{-1}= \left[ \begin{array}{ccc} c_{11}& c_{12} &c_{13}\\ c_{21}& c_{22} &c_{23}\\ c_{ 31}& c_{32} &c_{33}\\ \end{配列} \right ]

        展開後、以下を取得できます。

\begin{方程式} \left\{ \begin{行列}{} x=(c_{11}x''+c_{12}y''+c_{13})z' \\ y=(c_{21 }x''+c_{22}y''+c_{23})z'\\ z'=\frac{1}{c_{31}x''+c_{32}y''+c_{33 }}\\ \end{配列} \右。 \end{式}

この式は、ターゲット画像上の任意の座標T^{-1}について、        変換行列 T の逆行列を取得した後、上記の式を使用して元の画像上の座標に対応する位置を取得し、その位置でのピクセル値を取得できることを示しています。位置もちろん、位置は整数である必要はなく、補間が必要な場合もあります。(x'',y'')(x,y)ピクセル(x,y)

        コードの実装プロセスは次のとおりです。

//自己实现的wrapPerspective函数
void _wrapPerspective(const Mat& src, const Mat& T, Mat& dst)//src为源图像,T为变换矩阵,dst为目标图像
{
    dst.create(src.size(), src.type());//创建一个和原图像一样大小的Mat
    Mat T_inverse;//变换矩阵的逆
    invert(T, T_inverse);//求矩阵T的逆,结果存到T_inverse
    //取出矩阵中的值
    double c11 = T_inverse.ptr<double>(0)[0];
    double c12 = T_inverse.ptr<double>(0)[1];
    double c13 = T_inverse.ptr<double>(0)[2];
    double c21 = T_inverse.ptr<double>(1)[0];
    double c22 = T_inverse.ptr<double>(1)[1];
    double c23 = T_inverse.ptr<double>(1)[2];
    double c31 = T_inverse.ptr<double>(2)[0];
    double c32 = T_inverse.ptr<double>(2)[1];
    double c33 = T_inverse.ptr<double>(2)[2];
    //遍历目标图像的每个位置,求取原图像对应位置的像素值
    
    for (int y = 0; y < dst.rows; y++)
    {
        for (int x = 0; x < dst.cols; x++)
        {
            double xp = c11 * x + c12 * y + c13;
            double yp = c21 * x + c22 * y + c23;
            double z = c31 * x + c32 * y + c33;//z'
            z = z ? 1.0 / z : 0;//z'不为0时求导数,否则设为0
            xp *= z;
            yp *= z;
            //将双精度坐标限制在整型能表示的最大最小值之间
            double fx = max((double)INT_MIN, min((double)INT_MAX, xp));
            double fy = max((double)INT_MIN, min((double)INT_MAX, yp));
            //转化为int,这里简单地使用了最近邻插值
            int X = saturate_cast<int>(fx);
            int Y = saturate_cast<int>(fy);
            //是否在原图像大小范围内
            if (X >= 0 && X < src.cols && Y >= 0 && Y < src.cols)
            {
                dst.at<Vec3b>(y, x)[0] = src.at<Vec3b>(Y, X)[0];
                dst.at<Vec3b>(y, x)[1] = src.at<Vec3b>(Y, X)[1];
                dst.at<Vec3b>(y, x)[2] = src.at<Vec3b>(Y, X)[2];
            }
            else//以黑色填充
            {
                dst.at<Vec3b>(y, x)[0] = 0;
                dst.at<Vec3b>(y, x)[1] = 0;
                dst.at<Vec3b>(y, x)[2] = 0;
            }
        }
    }
}

        実行結果:

        公式の機能実装結果と比較すると、基本的には一致しており、実装の考え方が正しいことがわかります。

第三に、視点変換の適用

1. 視点変換のための対話型プログラム

①実験要件:

四角形の頂点を編集できる        対話型プログラムを設計すると、頂点の位置が変化したときに画像の変形結果をリアルタイムに更新できます。

②実験アイデア:

        上述の知識によれば、既知の 4 組のマッピング点の透視変換を実現するのは非常に簡単です。したがって、問題の難しさは、OpenCV のマウス クリック イベントを使用する必要がある対話型プログラムをどのように設計するかにあります。

void setMousecallback(const string& winname, MouseCallback onMouse, void* userdata=0)

パラメータの説明:

        winname : ウィンドウの名前。

        onMouse : マウス応答関数またはコールバック関数。ウィンドウ内でマウスイベントが発生するたびに呼び出される関数ポインタを指定します。この関数のプロトタイプはvoid on_Mouse(int events, int x, int y, int flags, void* param)です。

        userdata : コールバック関数に渡されるパラメータ。デフォルトは0です。私は個人的にこのパラメータを使用したことがありません。

void MouseCallback(int event,int x,int y,int flags,void *useradata);

パラメータの説明:

        イベント: マウスイベント。

        x : マウス イベントの x 座標。

        y : マウス イベントの y 座標。

        flags : マウスのドラッグイベントとキーボードとマウスの組み合わせイベントを表します。

        userdata : オプションのパラメータ。これまでは使用されていません。

        マウス イベントには主に次の種類があります。

                EVENT_MOUSEMOVE : マウスの動き

                EVENT_ LBUTTONDOWN : マウスの左ボタンを押した状態

                EVENT_ RBUTTONDOWN : マウスの右ボタンを押す

                EVENT_ MBUTTONDOWN : マウスの中ボタンを押す

                EVENT_ LBUTTONUP : マウスの左ボタンを放します

                EVENT_ RBUTTONUP : マウスの右ボタンを放します

                EVENT_ MBUTTONUP : 中央のボタンを放します

                EVENT_LBUTTONDBLCLK : 左ダブルクリック

                EVENT_ RBUTTONDBLCLK : 右ダブルクリック

                EVENT_ MBUTTONDBLCLK : 中ボタンのダブルクリック

        フラグには主に次の種類があります。

                EVENT_FLAG_ LBUTTON : 左クリックしてドラッグします

                EVENT_FLAG_ RBUTTON : 右クリックしてドラッグします

                EVENT_FLAG_ MBUTTON : 中ボタンのドラッグ

                EVENT_FLAG_ CTRLKEY : Ctrl を押したままにする

                EVENT_FLAG_ SHIFTKEY : Shift キーを押し続ける

                EVENT_FLAG_ALTKEY : alt キーを押し続けます

一般的なアイデア:

見た目や雰囲気を良くするには、ターゲット画像を配置するために、元の画像よりわずかに大きいキャンバス        が必要です。最初の 4 つのマッピング ポイントは、元のイメージの4 隅にあります。このキャンバス上で、4 つのマッピング点の円形の範囲にマウスを移動すると、このときに左ボタンを押したまま放さないと、マッピング点をドラッグすることができます。マウスの左ボタンを放す、マウスの位置はマッピング点の移動後の位置になります。次に、変換行列を再計算し、元の画像に透視変換を実行して表示します。

        もちろん、リアルタイム更新とは、左ボタンをドラッグしたときにマッピング点の位置が変化したときに、変換行列を計算して透視変換を実現することとも理解でき、マッピング点をある目標位置に移動することは明らかですが、ただし、移動中に不要な透視変換が行われてしまいます。

③実現プロセス

        マウスイベントの書き込みが中心で、その他の処理は先ほどの透視変換方法と同様です。

        左ボタンが押されたとき、クリックがマッピングポイントの領域内にあるかどうかを判断する必要があり、クリックが有効領域内にある場合には、どのマッピングポイントをクリックしたかを記録する必要もあります。

        左ボタンを押してドラッグすると、ドラッグ処理中に透視変換をリアルタイムで更新する必要がある場合、マッピング点の位置がリアルタイムで記録され、透視変換が実行されます。ここでの実装方法では、左ボタンを放した後に透視変換を実行することにしているため、左ボタンをドラッグしたときにマッピング ポイントの位置を記録する必要はありません。ただし、インタラクティブな効果を高めるために、位置を記録し、インタラクティブなプロンプト用の直線と円を描画します。

        左ボタンを離すと透視変換を行います。

具体的なコードは次のとおりです。

void mouseHander(int event, int x, int y, int flags, void* p)
{
    if (event == EVENT_LBUTTONDOWN)//左键按下
    {
        for (int i = 0; i < 4; i++)
        {
            //判断是否选择了某个映射点
            if (abs(x - dstPoint[i].x) <= radius && abs(y - dstPoint[i].y) <= radius)
            {
                pickPoint = i;
                beforePlace = dstPoint[pickPoint];//记录原本的位置
                break;
            }
        }
    }
    else if (event == EVENT_MOUSEMOVE && pickPoint >= 0 )//左键按下后选取了某个点且拖拽
    {
        //更改映射后坐标
        dstPoint[pickPoint].x = x, dstPoint[pickPoint].y = y;
        //在临时图像上实时显示鼠标拖动时形成的图像
        //注意不能直接在dstImg上画,否则会画很多次
        Mat tmp = dstImg.clone();
        //原本的圆
        circle(tmp, beforePlace, radius, Scalar(228, 164, 140), -1);
        //绘制直线
        line(tmp, dstPoint[0], dstPoint[1], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[1], dstPoint[2], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[2], dstPoint[3], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[3], dstPoint[0], Scalar(246, 230, 171), 5, 8);
        //重新绘制4个圆
        for (int i = 0; i < 4; i++)
        {
            if (i != pickPoint)
                circle(tmp, dstPoint[i], radius, Scalar(228, 164, 140), -1);
            else
                circle(tmp, dstPoint[i], radius, Scalar(96, 96, 240), -1);
        }
        imshow("透视变换后", tmp);
    }
    else if (event == EVENT_LBUTTONUP && pickPoint >= 0)//左键松开
    {
        //执行透视变换
        Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵
        warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换
        for (int i = 0; i < 4; i++)//显示4组映射点的位置
        {
            //画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆
            circle(dstImg, dstPoint[i], radius, Scalar(0, 215, 255), 3);
        }
        imshow("透视变换后", dstImg);
        pickPoint = -1;//重置选取状态
    }
}

④走行結果

        次に、さまざまな画像をテストします。

反対側の道路にあるポスターの視点変換後、道路を見下ろす        視点から道路を見下ろす視点に視点が変化していることがわかります

        今日は何気なく図書館の左側から撮った写真とその視点変換です。        

         調整後は視野角が正面に近づいていることがわかります。

⑤ ソースコード

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

Mat srcImg, dstImg;//原图像、目标图像
Point2f srcPoint[4], dstPoint[4];//原图像和目标图像的4个映射点
Point2f beforePlace;//记录移动映射点之前的位置
int radius;//映射点的判定半径
int pickPoint;//记录点击了哪个点

void mouseHander(int event, int x, int y, int flags, void* p)
{
    if (event == EVENT_LBUTTONDOWN)//左键按下
    {
        for (int i = 0; i < 4; i++)
        {
            //判断是否选择了某个映射点
            if (abs(x - dstPoint[i].x) <= radius && abs(y - dstPoint[i].y) <= radius)
            {
                pickPoint = i;
                beforePlace = dstPoint[pickPoint];//记录原本的位置
                break;
            }
        }
    }
    else if (event == EVENT_MOUSEMOVE && pickPoint >= 0 )//左键按下后选取了某个点且拖拽
    {
        //更改映射后坐标
        dstPoint[pickPoint].x = x, dstPoint[pickPoint].y = y;
        //在临时图像上实时显示鼠标拖动时形成的图像
        //注意不能直接在dstImg上画,否则会画很多次
        Mat tmp = dstImg.clone();
        //原本的圆
        circle(tmp, beforePlace, radius, Scalar(228, 164, 140), -1);
        //绘制直线
        line(tmp, dstPoint[0], dstPoint[1], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[1], dstPoint[2], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[2], dstPoint[3], Scalar(246, 230, 171), 5, 8);
        line(tmp, dstPoint[3], dstPoint[0], Scalar(246, 230, 171), 5, 8);
        //重新绘制4个圆
        for (int i = 0; i < 4; i++)
        {
            if (i != pickPoint)
                circle(tmp, dstPoint[i], radius, Scalar(228, 164, 140), -1);
            else
                circle(tmp, dstPoint[i], radius, Scalar(96, 96, 240), -1);
        }
        imshow("透视变换后", tmp);
    }
    else if (event == EVENT_LBUTTONUP && pickPoint >= 0)//左键松开
    {
        //执行透视变换
        Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵
        warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换
        for (int i = 0; i < 4; i++)//显示4组映射点的位置
        {
            //画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆
            circle(dstImg, dstPoint[i], radius, Scalar(0, 215, 255), 3);
        }
        imshow("透视变换后", dstImg);
        pickPoint = -1;//重置选取状态
    }
}

void main()
{
    srcImg = imread("library.jpg");  
    radius = 10;//设置四个点的圆的半径
    pickPoint = -1;
    //映射前的4个点
    srcPoint[0] = Point2f(0, 0);
    srcPoint[1] = Point2f(srcImg.cols, 0);
    srcPoint[2] = Point2f(srcImg.cols, srcImg.rows);
    srcPoint[3] = Point2f(0, srcImg.rows);
    //创建一张略大于原图像的画布
    dstImg = Mat::zeros(Size(2 * radius + 100 + srcImg.cols, 2 * radius + 100 + srcImg.rows), srcImg.type());
    //初始映射后的4个点
    dstPoint[0] = Point2f(radius + 50, radius + 50);
    dstPoint[1] = Point2f(radius + 50 + srcImg.cols, radius + 50);
    dstPoint[2] = Point2f(radius + 50 + srcImg.cols, radius + 50 + srcImg.rows);
    dstPoint[3] = Point2f(radius + 50, radius + 50 + srcImg.rows);
    Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵
    Mat dst_perspective;//存储目标透视图像
    warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换

    for (int i = 0; i < 4; i++)//显示初始4组映射点的位置
    {
        //画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆
        circle(dstImg, dstPoint[i], radius, Scalar(95, 180, 243), 3);
    }
    imshow("原图像", srcImg);
    imshow("透视变换后", dstImg);
    //鼠标事件
    setMouseCallback("透视变换后", mouseHander);
    waitKey();
}

2. バーチャル看板の実現

        上記の透視変換プロセスに従って、画像 A を看板の透視図として画像 B の特定の位置に変換するというアプリケーションを考えることができます。

        実装手順も非常に簡単で、絵Aの初期マッピング点を四隅に設定し、背景絵Bの透視変換後のマッピング点4点を選択し、指定した位置に絵Aを透視変換(設定)するだけです。画像Bの上書きの元の位置)。ここで注意すべき点は、マッピング ポイントの選択には固定の順序を指定する必要があることです。そうしないと、元のマッピング ポイントに 1 つずつ対応することができなくなります。ポイントの選択のデフォルトの順序は、左上隅から時計回りに選択することです。

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

Mat srcImg, dstImg;//原图像、目标图像
Mat resultImg;//结果图像
vector<Point2f> srcPoints, dstPoints;//原图像和目标图像的映射点
int pickNums;//目前已选取的节点

void mouseHander(int event, int x, int y, int flags, void* p)
{
    if (event == EVENT_LBUTTONDOWN)//左键按下
    {
        Mat tmp = dstImg.clone();
        if (pickNums == 4)//选取的点超过4个后,下一次点击会实现透视变换
        {
            //执行透视变换
            Mat Trans = getPerspectiveTransform(srcPoints, dstPoints);//由4组映射点得到变换矩阵
            warpPerspective(srcImg, tmp, Trans, Size(tmp.cols, tmp.rows));//执行透视变换
            resultImg = dstImg.clone();
            for (int y = 0; y < dstImg.rows; y++)
            {
                for (int x = 0; x < dstImg.cols; x++)
                {
                    if ((int)tmp.at<Vec3b>(y, x)[0] == 0 && (int)tmp.at<Vec3b>(y, x)[1] == 0 && (int)tmp.at<Vec3b>(y, x)[2] == 0)//像素点全0
                        continue;
                    else//非全0
                    {
                        resultImg.at<Vec3b>(y, x)[0] = tmp.at<Vec3b>(y, x)[0];
                        resultImg.at<Vec3b>(y, x)[1] = tmp.at<Vec3b>(y, x)[1];
                        resultImg.at<Vec3b>(y, x)[2] = tmp.at<Vec3b>(y, x)[2];
                    }
                }
            }
            imshow("虚拟广告牌", resultImg);
            dstPoints.clear();
            pickNums = 0;
        }
        else//选取的节点还没4个
        {
            dstPoints.push_back(Point2f(x, y));
            pickNums++;
            for (int i = 0; i < dstPoints.size(); i++)
            {
                circle(tmp, dstPoints[i], 5, Scalar(0, 215, 255), 3);
            }
            imshow("虚拟广告牌", tmp);
        }
    }

}

int main()
{
    srcImg = imread("test.png");//透视变换的图像,也就是广告图

    //设置原图像的4个映射点
    srcPoints.push_back(Point2f(0, 0));
    srcPoints.push_back(Point2f(srcImg.cols, 0));
    srcPoints.push_back(Point2f(srcImg.cols, srcImg.rows));
    srcPoints.push_back(Point2f(0, srcImg.rows));

    dstImg = imread("library.jpg");//背景图

    imshow("虚拟广告牌", dstImg);
    //鼠标事件
    setMouseCallback("虚拟广告牌", mouseHander);
    waitKey(0);
    return 0;
}

        アイデアは比較的単純です。つまり、毎回 4 つの点を選択し、選択後に看板画像の透視変換を対応する位置にマッピングします。

        透視変換された画像はエッジを黒ですべて 0 で塗りつぶすため、(0,0,0) には元の背景画像が使用され、(0,0,0) 以外には透視変換された画像が使用されます。これには、看板内に (0,0,0) のピクセルがある場合、背景画像をカバーできないという問題がありますが、テストの結果、掲示板には (0,0,0) 個のピクセルが少ないことがわかりました。元の画像のピクセル値 (0,0,0) に、(0,0,1) などの小さなオフセットを加えて均一に加算することもできます。これは、全体にはほとんど影響しません。

        効果を実感してください:

         クラスメートのコンピュータ画面を希望の写真に変えるために使用することもできます。

        動的な看板を実現するには、看板の画像をビデオ ファイルに置き換える必要があります。つまり、ビデオ ファイルから各フレームを読み取って透視変換を行う必要があります。

        変更されたコードは次のとおりです。

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

Mat dstImg, frame;//原图像、目标图像,视频帧
Mat resultImg;//结果图像
vector<Point2f> srcPoints, dstPoints;//原图像和目标图像的映射点
int pickNums;//目前已选取的节点

void mouseHander(int event, int x, int y, int flags, void* p)
{
	if (event == EVENT_LBUTTONDOWN)//左键按下
	{
		Mat tmp = dstImg.clone();
		if (pickNums == 4)//选取的点超过4个后,下一次点击会实现透视变换
		{
			//打开视频文件
			VideoCapture capture;
			capture.open("sdu_cut.mp4");
			if (!capture.isOpened())
			{
				cout << "无法打开视频文件!" << endl;
			}
			int num = 0;
			while (capture.read(frame))
			{
				num++;
				if (num == 1)//第一帧
				{
					//设置原图像的4个映射点
					srcPoints.push_back(Point2f(0, 0));
					srcPoints.push_back(Point2f(frame.cols, 0));
					srcPoints.push_back(Point2f(frame.cols, frame.rows));
					srcPoints.push_back(Point2f(0, frame.rows));
				}
				//执行透视变换
				Mat Trans = getPerspectiveTransform(srcPoints, dstPoints);//由4组映射点得到变换矩阵
				warpPerspective(frame, tmp, Trans, Size(tmp.cols, tmp.rows));//执行透视变换
				resultImg = dstImg.clone();
				for (int y = 0; y < dstImg.rows; y++)
				{
					for (int x = 0; x < dstImg.cols; x++)
					{
						if ((int)tmp.at<Vec3b>(y, x)[0] == 0 && (int)tmp.at<Vec3b>(y, x)[1] == 0 && (int)tmp.at<Vec3b>(y, x)[2] == 0)//像素点全0
							continue;
						else//非全0
						{
							resultImg.at<Vec3b>(y, x)[0] = tmp.at<Vec3b>(y, x)[0];
							resultImg.at<Vec3b>(y, x)[1] = tmp.at<Vec3b>(y, x)[1];
							resultImg.at<Vec3b>(y, x)[2] = tmp.at<Vec3b>(y, x)[2];
						}
					}
				}
				imshow("虚拟广告牌", resultImg);

				//中途退出
				char c = waitKey(1);
				if (c == 27)
				{
					break;
				}

			}
			dstPoints.clear();
			srcPoints.clear();
			pickNums = 0;
		}
		else//选取的节点还没4个
		{
			dstPoints.push_back(Point2f(x, y));
			pickNums++;
			for (int i = 0; i < dstPoints.size(); i++)
			{
				circle(tmp, dstPoints[i], 5, Scalar(0, 215, 255), 3);
			}
			imshow("虚拟广告牌", tmp);
		}
	}
}

int main()
{
	dstImg = imread("b.jpg");//背景图
	imshow("虚拟广告牌", dstImg);
	//鼠标事件
	setMouseCallback("虚拟广告牌", mouseHander);
	waitKey(0);
	return 0;
}

        考え方は基本的に静止看板の考え方と同じですが、主に通常の画像の読み取りからビデオ ファイルをフレームごとに読み取るように変更されたためです。ビデオ ファイルの初期マッピング ポイントを取得するには、最初のフレームを読み取り、四隅のマッピング ポイントの位置を決定する必要があります。

        上記のコードで 4 つのポイントを選択した後、新しい選択ラウンドの開始を続ける前に、ビデオ ファイルが再生されるまで待つ必要があります。そうしないと、エラーが報告されます。

        以下は実験結果です。ビデオ ファイルが大きすぎてアップロードできないため、コンテンツの一部のみがインターセプトされ、gif 表示に変換されます。

おすすめ

転載: blog.csdn.net/m0_51653200/article/details/127361624