天天看點

http://www.cvvision.cn/2888.html

圖像處理(一)圖像變形(1)矩形全景圖像還原-Siggraph 2014

作者:cvvision

最近發現,看過的文章,沒幾天就忘了,于是開始寫點東西記錄一下,所學習過的算法。廢話不多說,今天看了這篇文獻“Rectangling Panoramic Images via Warping”,是以做一下記錄。這篇文獻算法分為兩步:

1、第一步需要通過計算圖像最小能量線,對圖像進行預變性。

看懂這篇文章首先要看懂Seam Carving,這個算法作者有源碼,可以下載下傳下來好好解讀一下算法原理。通過這一步可以把圖像放大為矩形圖像,這一步又稱之為局部變形。在這一步每一次 疊代的過程中,源圖像上的每個像素點都會有可能移動位置,假設input圖像的源像素點I(x,y)經過Seam Carving算法放大後,位置移動到I'(x,y),那麼我們可以計算得其運動場(光流場),u(x,y)=I‘(x,y)-I(x,y)。算法第一步 的目的便是要計算出input圖像變為local warping圖像後,每個像素點的u(x,y)運動向量。

2、第二步對local warping圖像矩形進行網格劃分

記錄劃分後,每個格子頂點的坐标v(x,y),ok,通過第一步我們知道了每個像素點經過u(x,y)運動場,變形後才有了local warping 圖像,接着我們把圖像local warping通過

-u(x,y)運動場,變形回去,同時記錄下v(x,y)每個頂點的新位置v’(x,y),其實這一步不需要對圖像的資料進行變形,隻需要定劃分的網格根據-u(x,y)進行直接變形,記錄下每個網格頂點的新位置。

3、接着我們以這些網格頂點新位置v’(x,y)為控制頂點,利用相似變換的變形方法,對最開始的input 圖像進行全局變形。變形的控制頂點為v’(x,y),變形後的位置為v(x,y),據此可以得到全局的圖像變形結果

學習這篇文獻,需要先好好學習:“Seam Carving for Content-Aware Image Resizing”和“As-rigid-as-possible_shape_manipulation”這兩篇文獻網上可以下載下傳到作者寫的源代碼,所 以比較容易學會,搞定這兩篇paper,這篇文獻就簡單多了。

http://www.cvvision.cn/5493.html

著作權歸作者所有。

商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

連結:http://www.cvvision.cn/5493.html

來源:CV視覺網

一、相關理論圖像變形可以說是很多圖像、動畫領域的一個非常常見的功能,就說ps、天天P圖、美圖秀秀、可牛等這些每個軟體,有好多個功能都要用到圖像變形,比如圖像方向校正、圖像全景、視訊防抖等,在另外一篇博文全景矩形還原,就要用到圖像變形算法。

變形算法實作圖像姿态調整

變形算法實作物體方向位置調整

可以說ps中的一些圖像扭曲都是通過變形方法實作的,比如這篇paper:《As- Rigid-As-Possible Image Registration for Hand-drawn Cartoon Animations》就有講到它的變形算法與ps的效果分析,然而我今天要講的算法無論是在視屏防抖《Content-Preserving Warps for 3D Video Stabilization》《Bundled Camera Paths for Video Stabilization》,還是在圖像全景《Rectangling Panoramic Images via Warping》等都用到這個算法,這幾篇都是Siggraph上的文獻,對于搞圖形圖像的人來說,siggraph上的paper是每年必看的文章。保 相似的變形算法就相當于是一個經典又好用的圖像變形算法,具有保相似性,避免圖像變形過程中,發生嚴重錯切變換。好了到底是哪篇文獻這麼好用,那就是 2005 siggraph上的一篇文獻《As-Rigid-As-Possible Shape Manipulation》,這篇文獻在三維網格曲面圖形變形方面也具有重要的作用。

好了廢話不多說,這裡開始講解《As-Rigid-As-Possible Shape Manipulation》,因為這篇paper分為兩個步驟,

第一步主要是實作盡量保證變形前後,每個三角形都于原來的對應三角形相似,又稱之為網格模型的保相似變形。

第二步主要是為了盡量保證每個三角形與原三角形盡量全等。由于第一步中算法隻是保相似,這樣變形後三角形會有的被放大,有的被縮小,出現圖像的局部放大與縮小,是以需要對每個三角形進行适合的縮放調整,這一步又稱為拟合步,拟合變形後的每個三角形與原網格三角形全等。

一、理論講解

(1)相對坐标的概念。開始這個算法之前,我需要先給講解一個三角形中相對坐标的概念:給定一個三角形v0v1v2,如下圖所示:

那麼我們定義,v2相對于v0、v1的坐标為(x01,y01),滿足:

也就是說,我們可以通過已知的一個三角形,然後建立一個以三角形的一個頂點為原點,以其鄰接邊、及其對應的垂直旋轉90度後的方向分别作為x軸、y軸,求取三角形另外一個頂點的相對于這個坐标系的坐标。OK,如果我給你一個三角形,我要你計算出(x01,y01),你要怎麼計算?這個是一個簡單的數學問題,同時也是本文算法開始的第一步,具體x01、y01怎麼計算,那是高中問題了,不解釋。

(2)整體保相似。保相似圖像變形算法的原理很簡單,說白了就是盡量保證變形前後,相對坐标不變,也就是說保證每個三角形的相對坐标(x12,y12)、(x20,y20)、(x01,y01)不變。因為如果兩個三角形的這個相對坐标一樣,那麼這兩個三角形就是相似三角形。那麼算法具體是怎麼實作盡量保證,每個三角形變形前後的這三個相對坐标不變的呢?

假設變形後的三角形頂點坐标為v0’,v1’,v2’。因為我們是要盡量的保證相對坐标不變,我們可以根據下面的計算公式:

計算出v2desired,然後使得如下誤差最小化:

上面的能量公式越小,就代表變形後的三角形與原三角形越相似。而對于有多個三角面片的網格模型,那麼總誤差能量公式為:

這個問題其實歸結為求解一個最小二乘問題,大家還記得嗎,最小二乘其實就是使得誤差能量最小化,是以到了這裡其實我們就可以猜到,這個算法最後歸結為求解一個稀疏的線性系統。我們的目的是使得上面的總能量最小化,設相對坐标v’=,那麼:

我們的問題是要使得E1{v’}最小化,而最小化說白了就是使得其偏導數為0,是以我們需要對E1{v’}求偏導。開始前先把公式5,換另外一種形式來表達:

其中,q表示控制頂點,u表示自由頂點。對上面的式子求偏導:

把上面的式子簡寫為:

需要注意的是:對于控制頂點不變的模型,G’、B是一個固定的稀疏矩陣,是以我們可以通過上式求解u,得到自由頂點的坐标。

(3)局部完全保相似,及局部縮放調整。每個三角形分開重新調整,使得其與原三角形更為相似,同時對其做縮放調整。

首先,在經過上面的整體相似變換後,三角形可能進行放大或者縮小,因為我們用了三角形的内在屬性相對坐标,這樣隻能保證所有三角形的盡量相似,并不能保證全等。 如果算法直到這裡就結束了,那麼變形後的圖像将會出現局部放大與縮小。就拿上面的瘦臉圖檔來說,如果隻經過上面相似變換步驟,我們隻是要移動臉型輪廓的特 征點,而最後變形出來的結果可能就會出現眼睛等局部位置的放大,或縮小,是以我們要進行大小調整,瘦臉後,眼睛的大小與原來圖像的大小相等。

就像上面圖像中,圖(a)是原圖,圖(c)的結果就是隻經過相似變換後的結果,可以看到(c)中的右手部變粗了許多,而左手部變細了許多,是以我們接着這一步要做的事就是調整大小,使得(a)變形後,使得圖像變形後不會出現局部放大與縮小的情況,得到如(d)結果圖。

另一方面,在經過上面的整體相似變換後,因為我們用了整個三角網格模型進行整體調整,使得能量最小化,這個就像最小二乘一樣,最後求解出來的結果并不能保證每個三角形與原三角形完全相似,也會發生錯切變換,誤差還是存在的。就像最小二乘,求解出來的結果并不能保證最後每個方程的殘差為0。

在經過(2)中的相似變換後,因為 我們是對網格模型的所有三角形一起調整的,保證總三角形錯切變形能最小化,這個時候得到的每個三角形也不全是完全相似的一個三角形,且大小也發生了變化, 如上圖所示。我們得到了v0’v1’v2’,這個三角形我們稱之為中介三角形,接着我們要把每個三角形分開拟合成跟原三角形v0v1v2完全全等的三角 形,我們定義拟合階段的三角形變形能為:

當然我們還需要保證變形前後的也與原三角形相似:

這樣聯立公式(9)、(10),同時對其求偏導,使得變形能最小化:

記住,這一步是對每個三角形進行分開調整,這樣才能得到與原網格模型的三角形完全相似。同時在滿是上面式子後,對整個三角形以重心坐标為縮放原點,對每個三角形進行縮放調整

(4)整體拟合

上面那一部是對每個三角形分開調整,這個時候,每個三角形是完全獨立的,我們需要對齊對整體連接配接調整,否則兩個鄰接三角形的共用邊的兩個頂點的位置各不一樣,這樣根本是不是三角網格模型。

整個三角網格模型的總能量為:

H是一個表示整個網格模型拓撲連接配接關系的矩陣,對上式換另外一種表達方式,并對其求偏導,求取最小值:

簡化為:

然後重新求解,可得到自由頂點u的坐标。

二、算法流程

輸入:原三角網格模型,控制頂點

輸出:變形後的三角網格模型

1、整體相似變換

(1)計算原網格模型的相對坐标。根據給定的原三角網格模型,周遊每一個三角面片,

并計算每個頂點,相對于另外兩個頂點的坐标。以計算v2相對于邊v0v1的坐标為例:

(x01,y01)表示的是一個相對坐标,如果三角形的三個頂點的坐标已知v0,v1,v2,其實計算這個相對坐标是非常容易的。

(1)建立局部坐标軸。先求出邊向量v0v1,然後利用v0v1求出其機關向量即為局部坐标系的x軸,接着隻需要把x軸旋轉90度,就可以得到y軸了。

(2)計算相對坐标(x01,y01),這一步首先需要把向量v0v2投影到x軸,y軸。這裡需要注意的是我們上面的公式中x軸指的是v0v1,y軸指的是把v0v1旋轉90度後的向量,是以我們需要對投影結果做一個縮放處理

相關代碼實作如下:

// 周遊網格模型的每個三角面片

for ( unsigned int i = 0; i < nTris; ++i ) {

    Triangle & t = m_vTriangles[i];//第i個三角面片

    //計算目前三角形每個頂點相對于另外兩個頂點的坐标

    for ( int j = 0; j < 3; ++j ) {

        unsigned int n0 = j;

        unsigned int n1 = (j+1)%3;

        unsigned int n2 = (j+2)%3;//三角面片三個頂點的索引

        Eigen::Vector2d v0 = GetInitialVert( t.nVerts[n0] );

        Eigen::Vector2d v1 = GetInitialVert( t.nVerts[n1] );

        Eigen::Vector2d v2 = GetInitialVert( t.nVerts[n2] );//三角面片三個頂點的坐标

        //建立局部坐标系的 x軸 與y 軸

        Eigen::Vector2d v01( v1 – v0 );

        Eigen::Vector2d v01N( v01 );

        v01N.normalize();//邊向量v0v1的方向向量   x軸

        Eigen::Vector2d v01Rot90( v01(1), -v01(0) );

        Eigen::Vector2d v01Rot90N( v01Rot90 );  v01Rot90N.normalize();//垂直于邊v0v1的方向向量    y軸

        //計算局部相對坐标

        Eigen::Vector2d vLocal(v2 – v0);

        float fX = vLocal.dot(v01) /SquaredLength(v01);//計算局部相對坐标x01

        float fY = vLocal.dot(v01Rot90) /SquaredLength(v01Rot90);//計算局部相對坐标y01

        Eigen::Vector2d v2test(v0 + fX * v01 + fY * v01Rot90);

        float fLength = Length(v2test – v2);

        t.vTriCoords[j] = Eigen::Vector2d(fX,fY);//局部相對坐标(x01,y01)

    }

}

(2)構造公式(7)的相關矩陣。

void RigidMeshDeformer2D::PrecomputeOrientationMatrix()

{

    std::vector<constraint,eigen::aligned_allocator > vConstraintsVec;

    std::set::iterator cur(m_vConstraints.begin()), end(m_vConstraints.end());

    while ( cur != end )

        vConstraintsVec.push_back( *cur++ );

    unsigned int nVerts = (unsigned int)m_vDeformedVerts.size();

    m_mFirstMatrix.resize( 2*nVerts, 2*nVerts);

    for ( unsigned int i = 0; i < 2*nVerts; ++i )

        for ( unsigned int j = 0; j < 2*nVerts; ++j )

            m_mFirstMatrix(i,j) = 0.0;

    size_t nConstraints = vConstraintsVec.size();

    unsigned int nFreeVerts = nVerts – nConstraints;

    unsigned int nRow = 0;

    m_vVertexMap.resize(nVerts);

    for ( unsigned int i = 0; i < nVerts; ++i ) {

        Constraint c(i, vec2(0,0));

        if ( m_vConstraints.find(c) != m_vConstraints.end() )

            continue;

        m_vVertexMap[i] = nRow++;

    for ( unsigned int i = 0 ; i < nConstraints; ++i )

        m_vVertexMap[vConstraintsVec[i].nVertex ] = nRow++;

    size_t nTriangles = m_vTriangles.size();

    for ( unsigned int i = 0; i < nTriangles; ++i ) {

        Triangle & t = m_vTriangles[i];

        double fTriSumErr = 0;

        for ( int j = 0; j < 3; ++j ) {

            double fTriErr = 0;

            int n0x = 2 * m_vVertexMap[ t.nVerts[j] ];

            int n0y = n0x + 1;

            int n1x = 2 * m_vVertexMap[ t.nVerts[(j+1)%3] ];

            int n1y = n1x + 1;

            int n2x = 2 * m_vVertexMap[ t.nVerts[(j+2)%3] ];

            int n2y = n2x + 1;

            float x = t.vTriCoords[j](0);

            float y = t.vTriCoords[j](1);

            m_mFirstMatrix(n0x,n0x)+= 1 – 2*x + x*x + y*y;

            m_mFirstMatrix(n0x,n1x)+= 2*x – 2*x*x – 2*y*y;

            m_mFirstMatrix(n0x,n1y)+= 2*y;

            m_mFirstMatrix(n0x,n2x)+= -2 + 2*x;

            m_mFirstMatrix(n0x,n2y)+= -2 * y;

            m_mFirstMatrix(n0y,n0y)+= 1 – 2*x + x*x + y*y;

            m_mFirstMatrix(n0y,n1x)+= -2*y;

            m_mFirstMatrix(n0y,n1y)+= 2*x – 2*x*x – 2*y*y;

            m_mFirstMatrix(n0y,n2x)+= 2*y;

            m_mFirstMatrix(n0y,n2y)+= -2 + 2*x;

            m_mFirstMatrix(n1x,n1x)+= x*x + y*y;

            m_mFirstMatrix(n1x,n2x)+= -2*x;

            m_mFirstMatrix(n1x,n2y)+= 2*y;

            m_mFirstMatrix(n1y,n1y)+= x*x + y*y;

            m_mFirstMatrix(n1y,n2x)+= -2*y;

            m_mFirstMatrix(n1y,n2y)+= -2*x;

            m_mFirstMatrix(n2x,n2x)+= 1;

            m_mFirstMatrix(n2y,n2y)+= 1;

        }

    Eigen::MatrixXd mG00( 2*nFreeVerts, 2*nFreeVerts );

    ExtractSubMatrix( m_mFirstMatrix, 0, 0, mG00 );

    Eigen::MatrixXd mG01( 2 * (int)nFreeVerts, 2 * (int)nConstraints );

    ExtractSubMatrix( m_mFirstMatrix, 0, 2*nFreeVerts, mG01 );

    Eigen::MatrixXd mG10( 2 * (int)nConstraints, 2 * (int)nFreeVerts );

    ExtractSubMatrix( m_mFirstMatrix, 2*nFreeVerts, 0, mG10 );

    // 

    Eigen::MatrixXd mGPrime = mG00 + mG00.transpose();

    Eigen::MatrixXd mB = mG01 + mG10.transpose();

    Eigen::MatrixXd mGPrimeInverse=mGPrime.inverse();

    Eigen::MatrixXd mFinal = mGPrimeInverse * mB;

    mFinal *= -1;

    m_mFirstMatrix = mFinal;

2、局部完全相似,及大小縮放調整。

計算公式(11)的F、C矩陣:

//計算公式11 F、C矩陣

void RigidMeshDeformer2D::PrecomputeScalingMatrices( unsigned int nTriangle )

     //周遊每個三角形

    Triangle & t = m_vTriangles[nTriangle];

    t.mF =Eigen::MatrixXd(4,4);

    t.mC = Eigen::MatrixXd(4,6);

    double x01 = t.vTriCoords[0](0);

    double y01 = t.vTriCoords[0](1);

    double x12 = t.vTriCoords[1](0);

    double y12 = t.vTriCoords[1](1);

    double x20 = t.vTriCoords[2](0);

    double y20 = t.vTriCoords[2](1);

    double k1 = x12*y01 + (-1 + x01)*y12;

    double k2 = -x12 + x01*x12 – y01*y12;

    double k3 = -y01 + x20*y01 + x01*y20;

    double k4 = -y01 + x01*y01 + x01*y20;

    double k5 = -x01 + x01*x20 – y01*y20 ;

    double a = -1 + x01;

    double a1 = pow(-1 + x01,2) + pow(y01,2);

    double a2 = pow(x01,2) + pow(y01,2);

    double b =  -1 + x20;

    double b1 = pow(-1 + x20,2) + pow(y20,2);

    double c2 = pow(x12,2) + pow(y12,2);

    double r1 = 1 + 2*a*x12 + a1*pow(x12,2) – 2*y01*y12 + a1*pow(y12,2);

    double r2 = -(b*x01) – b1*pow(x01,2) + y01*(-(b1*y01) + y20);

    double r3 = -(a*x12) – a1*pow(x12,2) + y12*(y01 – a1*y12);

    double r5 = a*x01 + pow(y01,2);

    double r6 = -(b*y01) – x01*y20;

    double r7 = 1 + 2*b*x01 + b1*pow(x01,2) + b1*pow(y01,2) – 2*y01*y20;

    //計算F矩陣

    // F矩陣的第一行

    t.mF(0,0)= 2*a1 + 2*a1*c2 + 2*r7;

    t.mF(0,1)= 0;

    t.mF(0,2)= 2*r2 + 2*r3 – 2*r5;

    t.mF(0,3)= 2*k1 + 2*r6 + 2*y01;

    // F矩陣的第二行

    t.mF(1,0)= 0;

    t.mF(1,1)= 2*a1 + 2*a1*c2 + 2*r7;

    t.mF(1,2)= -2*k1 + 2*k3 – 2*y01;

    t.mF(1,3)= 2*r2 + 2*r3 – 2*r5;

    // F矩陣的第三行

    t.mF(2,0)= 2*r2 + 2*r3 – 2*r5;

    t.mF(2,1)= -2*k1 + 2*k3 – 2*y01;

    t.mF(2,2)= 2*a2 + 2*a2*b1 + 2*r1;

    t.mF(2,3)= 0;

    //F矩陣的第四行

    t.mF(3,0)= 2*k1 – 2*k3 + 2*y01;

    t.mF(3,1)= 2*r2 + 2*r3 – 2*r5;

    t.mF(3,2)= 0;

    t.mF(3,3)= 2*a2 + 2*a2*b1 + 2*r1;

    // 求逆

    Eigen::MatrixXd mFInverse=t.mF.inverse();

    mFInverse *= -1.0;

    t.mF =  mFInverse;

    //計算C矩陣

    // C矩陣的第一行

    t.mC(0,0)= 2*k2;

    t.mC(0,1)= -2*k1;

    t.mC(0,2)= 2*(-1-k5);

    t.mC(0,3)= 2*k3;

    t.mC(0,4)= 2*a;

    t.mC(0,5)= -2*y01;

    // C矩陣的第二行

    t.mC(1,0)= 2*k1;

    t.mC(1,1)= 2*k2;

    t.mC(1,2) = -2*k3;

    t.mC(1,3)= 2*(-1-k5);

    t.mC(1,4)= 2*y01;

    t.mC(1,5)= 2*a;

    // C矩陣的第三行

    t.mC(2,0)= 2*(-1-k2);

    t.mC(2,1)= 2*k1;

    t.mC(2,2)= 2*k5;

    t.mC(2,3)= 2*r6;

    t.mC(2,4)= -2*x01;

    t.mC(2,5)= 2*y01;

    //C矩陣的第四行

    t.mC(3,0)= 2*k1;

    t.mC(3,1)= 2*(-1-k2);

    t.mC(3,2)= -2*k3;

    t.mC(3,3)= 2*k5;

    t.mC(3,4)= -2*y01;

    t.mC(3,5)= -2*x01;

并求解公式11,得每個三角面片新頂點的位置

3、全局連接配接拟合。

構造公式(16)的相關矩陣:

void RigidMeshDeformer2D::PrecomputeFittingMatrices()

    for ( unsigned int i = 0; i < nVerts; ++i )

    {

        Constraint c(i,vec2(0,0));

    Eigen::VectorXd gUTestX( nVerts ), gUTestY(nVerts);

        int nRow = m_vVertexMap[i];

        gUTestX[nRow] = m_vInitialVerts[i].vPosition(0);

        gUTestY[nRow] = m_vInitialVerts[i].vPosition(1);

    for ( unsigned int i = 0; i < nConstraints; ++i ) {

        int nRow = m_vVertexMap[ vConstraintsVec[i].nVertex ];

        gUTestX[nRow] = vConstraintsVec[i].vConstrainedPos[0];

        gUTestY[nRow] = vConstraintsVec[i].vConstrainedPos[1];

    Eigen::MatrixXd mHX( nVerts, nVerts );

    Eigen::MatrixXd mHY( nVerts, nVerts );

        for ( unsigned int j = 0; j < nVerts; ++j )

            mHX(i,j) = mHY(i,j) = 0.0;

            int nA = m_vVertexMap[ t.nVerts[j] ];

            int nB = m_vVertexMap[ t.nVerts[(j+1)%3] ];

            mHX(nA,nA)+= 2;

            mHX(nA,nB)+= -2;

            mHX(nB,nA)+= -2;

            mHX(nB,nB)+= 2;

            mHY(nA,nA)+= 2;

            mHY(nA,nB)+= -2;

            mHY(nB,nA)+= -2;

            mHY(nB,nB)+= 2;

    Eigen::MatrixXd mHX00( (int)nFreeVerts, (int)nFreeVerts );

    Eigen::MatrixXd mHY00( (int)nFreeVerts, (int)nFreeVerts );

    ExtractSubMatrix( mHX, 0, 0, mHX00 );

    ExtractSubMatrix( mHY, 0, 0, mHY00 );

    Eigen::MatrixXd mHX01( (int)nFreeVerts, (int)nConstraints );

    Eigen::MatrixXd mHX10( (int)nConstraints, (int)nFreeVerts );

    ExtractSubMatrix( mHX, 0, nFreeVerts, mHX01 );

    ExtractSubMatrix( mHX, nFreeVerts, 0, mHX10 );

    Eigen::MatrixXd mHY01( (int)nFreeVerts, (int)nConstraints );

    Eigen::MatrixXd mHY10( (int)nConstraints, (int)nFreeVerts );

    ExtractSubMatrix( mHY, 0, nFreeVerts, mHY01 );

    ExtractSubMatrix( mHY, nFreeVerts, 0, mHY10 );

    m_mHXPrime = mHX00;

    m_mHYPrime = mHY00;

    m_mDX = mHX01;

    m_mDY = mHY01;

    m_mLUDecompX=Eigen::FullPivLU(m_mHXPrime);

    m_mLUDecompY=Eigen::FullPivLU(m_mHYPrime);

這篇paper的總思路就是分成三步,因為如果控制頂點、和自由頂點的沒有發生變化, 相關的矩陣隻要經過第一次求解就可以了,是以可以實作實時拖拽更新,這個我在三維圖形學網格曲面變形的時候,用到的拉普拉斯網格變形的方法思路大體相同, 需要對原網格模型做預處理操作,然後才能實作實時變形。具體這個算法要和圖像結合起來,就要對圖像進行三角剖分,然後計算每個三角形的放射變換矩陣,根據 仿射變換矩陣計算每個像素點值的變化,才能對圖像進行變形,具體就不貼代碼了,因為這一些代碼都是比較簡單的,沒有什麼技術含量在裡面。

參考文獻

1、http://www-ui.is.s.u-tokyo.ac.jp/~takeo/research/rigid/

2、《As-Rigid-As-Possible Shape Manipulation》