【快速閱讀二】從OpenCv的代碼中扣取泊松融合算子(Poisson Image Editing)並稍作優化

  泊松融合我自己寫的第一版程序大概是2016年在某個小房間裏折騰出來的,當時是用的迭代的方式,記得似乎效果不怎麼樣,沒有達到論文的效果。前段時間又有網友問我有沒有這方面的程序,我說Opencv已經有了,可以直接使用,他說opencv的框架太大,不想爲了一個功能的需求而背上這麼一座大山,看能否做個脫離那個環境的算法出來,當時,覺得工作量挺大,就沒有去折騰,最近年底了,項目漸漸少了一點,公司上面又在搞辦公室政治,我地位不高,沒有參與權,所以樂的閒,就抽空把這個算法從opencv裏給剝離開來,做到了完全不依賴其他庫實現泊松融合樂,前前後後也折騰進半個月,這裏還是做個開發記錄和分享。

  在翻譯算法過程中,除了參考了opencv的代碼,還看到了很多參考資料,主要有以下幾篇:

                1、http://takuti.me/dev/poisson/demo/                  這個似乎打不開了,早期的代碼好像是主要參考了這裏

     2、http://blog.csdn.net/baimafujinji/article/details/46787837      圖像的泊松(Poisson)編輯、泊松融合完全詳解

               3、http://blog.csdn.net/hjimce/article/details/45716603        圖像處理(十二)圖像融合(1)Seamless cloning泊松克隆-Siggraph 2004

    4、http://www.wangt.cc/2022/09/%E3%80%8Apoisson-image-editing%E3%80%8B%E8%AE%BA%E6%96%87%E7%90%86%E8%A7%A3%E4%B8%8E%E5%A4%8D%E7%8E%B0/#google_vignette  《Poisson Image Editing》論文理解和實現

               5、https://www.baidu.com/link?url=GgbzGxsNBzdTewEEXY4lx7RH5hB4KWxODUF79-cdVnNT4siKaGx5JSqh-pR3l7N9rXufCnyXWj2Fl40KvfRuTq&wd=&eqid=d200bfec000c06300000000665a61134    從泊松方程的解法,聊到泊松圖像融合

    6、https://blog.csdn.net/weixin_43194305/article/details/104928378    泊松圖像編輯(Possion Image Edit)原理、實現與應用

 

  對應的論文爲:Poisson Image Editing,可以從百度上下載到。

  泊松融合的代碼在opencv的目錄如下:

    opencv-4.9.0\源代碼\modules\photo\src,其中的seamless_cloning_impl.cpp以及seamless_cloning.cpp爲主要算法代碼。

  我們總結下opencv的泊松融合主要是由以下幾個步驟組成的:

    1、計算前景和背景圖像的梯度場;

    2、根據一定的原則計算融合後的圖像的梯度場(這一步是最靈活的,通過改變他可以實現各種效果);

    3、對融合後的梯度偏導,獲取對應的散度。

    4、由散度及邊界像素值求解泊松方程(最爲複雜)。

  那麼我們就一步一步的進行扣取和講解。

  一、計算前景和背景圖像的梯度場。

  這一部分在CV中對應的函數名爲:computeGradientX及computeGradientY,在CV中的調用代碼爲:

    computeGradientX(destination, destinationGradientX);
    computeGradientY(destination, destinationGradientY);

    computeGradientX(patch, patchGradientX);
    computeGradientY(patch, patchGradientY);

  以X方向的梯度爲例, 其相應的代碼爲:

void Cloning::computeGradientX( const Mat &img, Mat &gx)
{
    Mat kernel = Mat::zeros(1, 3, CV_8S);
    kernel.at<char>(0,2) = 1;
    kernel.at<char>(0,1) = -1;
    if(img.channels() == 3)
    {
        filter2D(img, gx, CV_32F, kernel);
    }
    else if (img.channels() == 1)
    {
        filter2D(img, gx, CV_32F, kernel);
        cvtColor(gx, gx, COLOR_GRAY2BGR);
    }
}

  可以看到就是簡單的一個卷積,卷積核心爲[0, -1, 1],然後使用filter2D函數進行處理。在這裏opencv爲了減少代碼量,把灰度版本的算法也直接用彩色的處理了。

  這個要拋棄CV,其實是個很簡單的過程,一個簡單的代碼如下:

//    邊緣部分採用了反射101方式,這個要和Opencv的代碼一致,支持單通道和3通道
void IM_ComputeGradientX_PureC(unsigned char *Src, short *Dest, int Width, int Height, int Stride)
{
    int Channel = Stride / Width;
    if (Channel == 1)
    {
        for (int Y = 0; Y < Height; Y++)
        {
            unsigned char* LinePS = Src + Y * Stride;
            short* LinePD = Dest + Y * Width;
            for (int X = 0; X < Width - 1; X++)
            {
                LinePD[X] = LinePS[X + 1] - LinePS[X];
            }
            //    LinePD[Width - 2] = LinePS[Width - 1] - LinePS[Width - 2]
            //    LinePD[Width - 1] = LinePS[Width - 2] - LinePS[Width - 1]            101方式的鏡像就是這個結果
            LinePD[Width - 1] = -LinePD[Width - 2];                //    最後一列
        }
    }
    else
    {
       //  三通道代碼
    }
}

  我這裏的Dest沒有用float類型,而是用的short,我的原則是用最小的內存量+合適的數據類型來保存目標。 

  注意,opencv裏默認的邊緣採用的是101的鏡像方式的,因此,對於[0, -1, 1]這種卷積和,最右側一列的值就是右側倒數第二列的負值。

  2、根據一定的原則計算融合後的圖像的梯度場

  這部分算法opencv寫的很分散,他把代碼放置到了好幾個函數裏,這裏把他們集中一下大概就是如下幾行:

 1     Mat Kernel(Size(3, 3), CV_8UC1);
 2     Kernel.setTo(Scalar(1));
 3     erode(binaryMask, binaryMask, Kernel, Point(-1,-1), 3);
 4     binaryMask.convertTo(binaryMaskFloat, CV_32FC1, 1.0/255.0);
 5     arrayProduct(patchGradientX, binaryMaskFloat, patchGradientX);
 6     arrayProduct(patchGradientY, binaryMaskFloat, patchGradientY);
 7     bitwise_not(wmask,wmask);
 8     wmask.convertTo(binaryMaskFloatInverted,CV_32FC1,1.0/255.0);
 9     arrayProduct(destinationGradientX, binaryMaskFloatInverted, destinationGradientX);
10     arrayProduct(destinationGradientY, binaryMaskFloatInverted, destinationGradientY);
11     Mat laplacianX = destinationGradientX + patchGradientX;
12     Mat laplacianY = destinationGradientY + patchGradientY;

   這個代碼裏的前面是三行一開始我感覺很納悶,這個是幹啥呢,爲什麼要對mask進行一個收縮呢,後面想一想,如果是一個純白的mask,那麼下面的融合整個融合後的梯度場就完全是前景的梯度場了,和背景就毫無關係了,而進行erode後,則邊緣部分使用的就是背景的梯度場,這樣就有了有效的邊界條件,不過 erode(binaryMask, binaryMask, Kernel, Point(-1, -1), 3);最後一個參數要是3呢,這個3可是表示重複執行三次,如果配合上前面的Kernel參數,對於一個純白的圖,邊緣就會出現3行和3列的純黑的像素了(我測試默認參數下erode在處理邊緣時,是用了0值代替超出邊界的值),我個人感覺這裏使用參數1就可以了。

  從第四到第十二行其實就是很簡單的一個線性融合過程,Opencv的代碼呢寫的很向量化,我們要自己實現其實就下面幾句代碼:

for (int Y = 0; Y < Height; Y++)
{
    int Index = Y * Width * Channel;
    int Speed = Y * Width;
    for (int X = 0; X < Width; X++)
    {
        float MaskF = MaskS[Speed + X] * IM_INV255;
        float InvMaskF = 1.0f - MaskF;
        if (Channel == 1)
        {
            LaplacianX[Index + X] = GradientX_B[Index + X] * InvMaskF + GradientX_F[Index + X] * MaskF;
            LaplacianY[Index + X] = GradientY_B[Index + X] * InvMaskF + GradientY_F[Index + X] * MaskF;
        }
        else
        {
            //    三通道
        }
    }
}

  3、對融合後的梯度偏導,獲取對應的散度。

  這一部分對應的CV的代碼爲:

    computeLaplacianX(laplacianX,laplacianX);
    computeLaplacianY(laplacianY,laplacianY);

  以X方向的散度計算爲例,其代碼如下:

void Cloning::computeLaplacianX( const Mat &img, Mat &laplacianX)
{
    Mat kernel = Mat::zeros(1, 3, CV_8S);
    kernel.at<char>(0,0) = -1;
    kernel.at<char>(0,1) = 1;
    filter2D(img, laplacianX, CV_32F, kernel);
}

  也是個卷積,沒有啥特別的,翻譯成普通的C代碼可以用如下方式:

void IM_ComputeLaplacianX_PureC(float* Src, float* Dest, int Width, int Height, int Channel)
{
    if (Channel == 1)
    {
        for (int Y = 0; Y < Height; Y++)
        {
            float* LinePS = Src + Y * Width;
            float* LinePD = Dest + Y * Width;
            for (int X = Width - 1; X >= 1; X--)
            {
                LinePD[X] = LinePS[X] - LinePS[X - 1];
            }
            LinePD[0] = -LinePD[1];            //    第一列
        }
    }
    else
    {
            //    三通道
    }
}

  注意到Opencv的這個函數是支持Inplace操作的,即Src=Dest時,也能得到正確的結果,爲了實現這個結果,我們注意到這個卷積核是偏左的,即核中心偏左的元素有用,利用這個特性可以在X方向上從右向左循環,就可以避免數據被覆蓋的,當然對於每行的第一個元素就要特別處理了,同時注意這裏採用了101格式的邊緣處理。

  4、由散度及邊界像素值求解泊松方程。

  有了以上的散度的計算,後面就是求解一個很大的稀疏矩方程的過程了,如果直接求解,將會是一個非常耗時的過程,即使利用稀疏的特性,也將對編碼者提出很高的技術要求。自己寫個稀疏矩陣的求解過程也是需要很大的勇氣的。

  在opencv裏,上面的求解是藉助了傅里葉變換實現的,這個的原理我在某個論文中看到過,現在時間關係,我也沒有找到那一片論文了,如果後續有機會看到,我在分享出來。

  CV的求解過程涉及到了3個函數,分別是poissonSolver、solve、dst,也是一個調用另外一個的關係,具體的這個代碼能實現求解泊松方程的原理,我們也不去追究吧,僅僅從代碼層面說說大概得事情。

  首先是poissonSolver函數,其具體代碼如下

 1 void Cloning::poissonSolver(const Mat &img, Mat &laplacianX , Mat &laplacianY, Mat &result)
 2 {
 3     const int w = img.cols;
 4     const int h = img.rows;
 5     Mat lap = laplacianX + laplacianY;
 6     Mat bound = img.clone();
 7     rectangle(bound, Point(1, 1), Point(img.cols-2, img.rows-2), Scalar::all(0), -1);
 8     Mat boundary_points;
 9     Laplacian(bound, boundary_points, CV_32F);
10     boundary_points = lap - boundary_points;
11     Mat mod_diff = boundary_points(Rect(1, 1, w-2, h-2));
12     solve(img,mod_diff,result);
13 }

  這裏,Opencv有一次把他的代碼藝術展現的活靈活現。從低6到第11行,我們看到了一個藝術家爲了獲取最後的結果所做的各種行爲藝術。

  首先是複製原圖,然後把原圖除了第一行、最後一行、第一列、最後一列填充爲黑色(rectangle函數),然後對這個填充後的圖進行拉普拉斯邊緣檢測,然後第10行做個減法,最後第11行呢,又直接裁剪了去掉周邊一個像素寬範圍內的結果。

  行爲藝術家。

  如果只從結果考慮,我們完全沒有必要有這麼多的中間過程,我們把邊緣用*表示,中間值都爲0,則一個二維平面圖如下所示:

    //    拉普拉斯卷積核如下                對應標識如下
    //        1                                Q1
    //    1    -4    1                   Q3    Q4    Q5    
    //        1                                Q7
    // 
    //    *    *    *    *    *    *    *    *    *
    //    *    0    0    0    0    0    0    0    *    
    //    *    0    0    0    0    0    0    0    *    
    //    *    0    0    0    0    0    0    0    *    
    //    *    0    0    0    0    0    0    0    *    
    //    *    0    0    0    0    0    0    0    *    
    //    *    *    *    *    *    *    *    *    *

  所有爲0的部位的計算值爲我們需要的結果,很明顯,除了最外一圈0值的拉普拉斯邊緣檢測不爲0,其他的都爲0,不需要計算,而周邊一圈的0值的拉普拉斯邊緣檢測涉及到的3*3又恰好在原圖的有效範圍內,不需要考慮邊緣的值,因此,我們可以直接一步到位寫出mod_diff的值的。

    ModDiff[0] = Laplacian[Width + 1] - (Image[1] + Image[Stride]);        //    對應的拉普拉斯有效值爲:    Q1 + Q3
    for (int X = 2; X < Width - 2; X++)
    {
        ModDiff[X - 1] = Laplacian[Width + X] - Image[X];                //    Q1
    }
    ModDiff[Width - 3] = Laplacian[Width + Width - 2] - (Image[Width - 2] + Image[Stride + Width - 1]);        //    Q1 + Q5

    for (int Y = 2; Y < Height - 2; Y++)
    {
        unsigned char* LinePI = Image + Y * Stride;
        float* LinePL = Laplacian + Y * Width;
        float* LinePD = ModDiff + (Y - 1) * (Width - 2);
        LinePD[0] = LinePL[1] - LinePI[0];                                //    Q3
        for (int X = 2; X < Width - 2; X++)
        {
            LinePD[X - 1] = LinePL[X];                                    //    0
        }
        LinePD[Width - 3] = LinePL[Width - 2] - LinePI[Width - 1];        //    Q5
    }
    //    最後一行
    ModDiff[(Height - 3) * (Width - 2)] = Laplacian[(Height - 2) * Width + 1] - (Image[(Height - 2) * Stride] + Image[(Height - 1) * Stride + 1]);    //    Q3 + Q7
    for (int X = 2; X < Width - 2; X++)
    {
        ModDiff[(Height - 3) * (Width - 2) + X - 1] = Laplacian[(Height - 2) * Width + X] - Image[(Height - 1) * Stride + X];                        //    Q7
    }
    ModDiff[(Height - 3) * (Width - 2) + Width - 3] = Laplacian[(Height - 2) * Width + Width - 2] - (Image[(Height - 2) * Stride + Width - 1] + Image[(Height - 1) * Stride + Width - 2]);    //    Q5 + Q7

  接下來是我們的solve函數,這個函數不是核心,所以稍微提及一下:

void Cloning::solve(const Mat &img, Mat& mod_diff, Mat &result)
{
    const int w = img.cols;
    const int h = img.rows;

    Mat res;
    dst(mod_diff, res);

    for(int j = 0 ; j < h-2; j++)
    {
        float * resLinePtr = res.ptr<float>(j);
        for(int i = 0 ; i < w-2; i++)
        {
            resLinePtr[i] /= (filter_X[i] + filter_Y[j] - 4);
        }
    }

    dst(res, mod_diff, true);

    unsigned char *  resLinePtr = result.ptr<unsigned char>(0);
    const unsigned char * imgLinePtr = img.ptr<unsigned char>(0);
    const float * interpLinePtr = NULL;

     //first col
    for(int i = 0 ; i < w ; ++i)
        result.ptr<unsigned char>(0)[i] = img.ptr<unsigned char>(0)[i];

    for(int j = 1 ; j < h-1 ; ++j)
    {
        resLinePtr = result.ptr<unsigned char>(j);
        imgLinePtr  = img.ptr<unsigned char>(j);
        interpLinePtr = mod_diff.ptr<float>(j-1);

        //first row
        resLinePtr[0] = imgLinePtr[0];

        for(int i = 1 ; i < w-1 ; ++i)
        {
            //saturate cast is not used here, because it behaves differently from the previous implementation
            //most notable, saturate_cast rounds before truncating, here it's the opposite.
            float value = interpLinePtr[i-1];
            if(value < 0.)
                resLinePtr[i] = 0;
            else if (value > 255.0)
                resLinePtr[i] = 255;
            else
                resLinePtr[i] = static_cast<unsigned char>(value);
        }

        //last row
        resLinePtr[w-1] = imgLinePtr[w-1];
    }

    //last col
    resLinePtr = result.ptr<unsigned char>(h-1);
    imgLinePtr = img.ptr<unsigned char>(h-1);
    for(int i = 0 ; i < w ; ++i)
        resLinePtr[i] = imgLinePtr[i];
}
View Code

  他主要調用dst函數,然後對dst函數處理的結果再進行濾波,然後再調用dst函數,最後得到的結果進行圖像化。 不過第一次dst是使用FFT正變換,第二次使用了FFT逆變換。

  從他的恢復圖像的過程看,他也是對最周邊的一圈像素不做處理,直接使用背景的圖像值。

  那麼我們再看看dst函數,這個是解泊松方程的關鍵所在,opencv的代碼如下:

void Cloning::dst(const Mat& src, Mat& dest, bool invert)
{
    Mat temp = Mat::zeros(src.rows, 2 * src.cols + 2, CV_32F);
    int flag = invert ? DFT_ROWS + DFT_SCALE + DFT_INVERSE: DFT_ROWS;
    src.copyTo(temp(Rect(1,0, src.cols, src.rows)));
    for(int j = 0 ; j < src.rows ; ++j)
    {
        float * tempLinePtr = temp.ptr<float>(j);
        const float * srcLinePtr = src.ptr<float>(j);
        for(int i = 0 ; i < src.cols ; ++i)
        {
            tempLinePtr[src.cols + 2 + i] = - srcLinePtr[src.cols - 1 - i];
        }
    }
    Mat planes[] = {temp, Mat::zeros(temp.size(), CV_32F)};
    Mat complex;
    merge(planes, 2, complex);
    dft(complex, complex, flag);
    split(complex, planes);
    temp = Mat::zeros(src.cols, 2 * src.rows + 2, CV_32F);
    for(int j = 0 ; j < src.cols ; ++j)
    {
        float * tempLinePtr = temp.ptr<float>(j);
        for(int i = 0 ; i < src.rows ; ++i)
        {
            float val = planes[1].ptr<float>(i)[j + 1];
            tempLinePtr[i + 1] = val;
            tempLinePtr[temp.cols - 1 - i] = - val;
        }
    }
    Mat planes2[] = {temp, Mat::zeros(temp.size(), CV_32F)};
    merge(planes2, 2, complex);
    dft(complex, complex, flag);
    split(complex, planes2);
    temp = planes2[1].t();
    temp(Rect( 0, 1, src.cols, src.rows)).copyTo(dest);
}

  對Temp的數據填充中,我們看到他臨時創建了寬度2*Width + 2大小的數據,高度爲Height,其中第0列,第Width + 1列的數據爲都爲0,第1列到第Width +1列之間的數據爲原是數據,第Width + 2到2*Width + 1列的數據爲原始數據鏡像後的負值。

  填充完之後,在構造一個複數,然後調用FFT變換,當invert爲false時,使用的DFT_ROWS參數,爲true時,使用的是DFT_ROWS + DFT_SCALE + DFT_INVERSE參數,那麼其實這裏就是一維的FFT正變換和逆變換,即對數據的每一行單獨處理,行於行之間是無關的,是可以並行的。

  再進行了第一次FFT變換後,我們有創建一副寬度爲2*Height+ 2,高度爲Width大小的數據,這個時候數據裏的填充值依舊分爲2塊,也是用黑色的處置條分開,同樣右側值的爲鏡像負值分佈。但是這個時候原始值是從前面進行FFT變換後的數據中獲取,而且還需要轉置獲取,其獲取的是FFT變換的虛部的值。 

  填充完這個數據後,再次進行FFT變換,變換完之後,我們取變換後的虛部的值的轉置,並且捨棄第一列的值,作爲我們處理後的結果。 

  整個OPENCV的代碼從邏輯上是比較清晰的,他通過各種內嵌的函數組合,實現了清晰的思路。但是如果從代碼效率角度來說,是非常不可取的,從內存佔用上來說,也存在着過多的浪費。這也是opencv中非核心函數通用的問題,基本上就是隻在意結果,不怎麼在乎過程和內存佔用。 

  談到這裏,核心的泊松融合基本就講完了,其各種不同的應用也是基於上述過程。

  那麼我們再稍微談談算法的優化和加速。 

  整個算法流程不算特別長,前面三個步驟的計算都比較簡單,計算量也不是很大,慢的還是在於泊松方程的求解,而求解中最耗時還是那個DFT變換,簡單的測試表面,DFT佔整個算法耗時的80%(單線程下)。前面說過,這個內部使用的是一維行方向的DFT變換,行於行之間的處理是無關的,而且,他的數據量也比較大,特別適合於並行處理,我們可以直接用簡單的omp就可以實現加速。

  另外,我們再進行FFT時,常用的一個加速手段就是GetOptimalDftSize獲得一個和原始尺寸最爲接近而又能更快實現FFT的大小,通常他們是3或者4或者5的倍數。有時候,這個加速也非常的明顯,比如尺寸爲1023的FFT和尺寸爲1024的FFT,速度可以相差好幾倍。這裏我也嘗試使用這個函數,但是經過多次嘗試(包括適當的改變數據佈局),都存在一個嚴重的問題,得到的結果圖像有着不可忽視的誤差,基本無法恢復。因此,這個優化的步驟不得已只能放棄。

  前面說了很多,還忘記了一個最重要的函數的扣取,dft函數,這個函數在opencv的目錄如下:  opencv-4.9.0\源代碼\modules\core\src\dxt.cpp,居然是用的dxt這個文件名,開始我怎麼搜都搜不到他。 

  關於這個功能的扣取,我大概也花了半個月的時間,時間上OPENCV也有很多版本,比如CPU的、opencl的等等,我這裏扣取的是純CPU的,而且還是從早期的CV的代碼中扣的,現在的版本的代碼裏有太多不相關的東西了,扣取的難度估計還要更大。而且在扣取中我還做了一些優化,這個就不在這裏多說了,總之,opencv的FFT在各種開源版本的代碼中算是一份非常不錯的代碼。

  具體的應用:

  1、無縫的圖像合成,對應CV的seamlessClone函數,他支持背景圖和前景圖圖不一樣大小,也可以沒有蒙版等等特性,其具體的代碼如下:

void cv::seamlessClone(InputArray _src, InputArray _dst, InputArray _mask, Point p, OutputArray _blend, int flags)
{
    CV_INSTRUMENT_REGION();
    CV_Assert(!_src.empty());
    const Mat src  = _src.getMat();
    const Mat dest = _dst.getMat();
    Mat mask = checkMask(_mask, src.size());
    dest.copyTo(_blend);
    Mat blend = _blend.getMat();
    Mat mask_inner = mask(Rect(1, 1, mask.cols - 2, mask.rows - 2));
    copyMakeBorder(mask_inner, mask, 1, 1, 1, 1, BORDER_ISOLATED | BORDER_CONSTANT, Scalar(0));
    Rect roi_s = boundingRect(mask);
    if (roi_s.empty()) return;
    Rect roi_d(p.x - roi_s.width / 2, p.y - roi_s.height / 2, roi_s.width, roi_s.height);
    Mat destinationROI = dest(roi_d).clone();
    Mat sourceROI = Mat::zeros(roi_s.height, roi_s.width, src.type());
    src(roi_s).copyTo(sourceROI,mask(roi_s));
    Mat maskROI = mask(roi_s);
    Mat recoveredROI = blend(roi_d);
    Cloning obj;
    obj.normalClone(destinationROI,sourceROI,maskROI,recoveredROI,flags);
}

     copyMakeBorder這個東西,呵呵,和前面講的那個rectangle的作用正好想法,把周邊一圈設置爲黑色,然後再提取出實際有效的邊界(不爲0的區域),以便減少計算量,後續再根據邊界裁剪出有效的區域,交給具體的融合的函數處理。
  opencv的這個函數寫的實在不怎麼好,當我們不小心設置了錯誤的p參數時,就會出現內存錯誤,這個參數主要是指定前景圖像在背景圖像中的位置的, 我們必須保證前景圖像不能有任何部分跑到背景圖像的外部,在我自己寫的版本中已經校正了這個小錯誤。

        注意,這裏有個Flag參數,當Flag爲NORMAL_CLONE時,就是我們前面的標準過程,當爲MIXED_CLONE時,則在第二步體現了不同:
  用原文的公式表示即爲:

         

   翻譯爲我們能看懂的意思就是: Mixed的模式下,如果前景的梯度差異大於背景的差異,則直接進行線性混合,否則就直接用背景的梯度。這個模式下可以獲得更爲理想的融合效果。

   我想辦法把論文中的一些測試圖像摳出來,然後進行了一系列測試,確實能獲得一些不錯的效果。

                   

            字符                               背景紋理                        融合結果  

              

          海景                              彩虹                              融合後

  上面爲不帶mask時全圖進行融合,可以看到融合後前景基本完美的融合到了背景中,但是前景的顏色還是發生了一些改變。

           

        背景圖                  前景圖                    蒙版圖                  合成圖

  上面這一幅測試圖中,太陽以及太陽在水中的倒影也完美的融合到背景圖中,相當的自然。

  以下爲多福圖像和成到一幅中的效果。

         

            前景1                          前景1蒙版

             

            前景2                          前景2蒙版

                

            背景圖                          融合後結果

    上面所有的融合方式都是選擇的MIXED_CLONE。

  其實注意到MIXED_CLONE裏梯度的混合原則,要達到上圖這樣較好的融合效果,對前景圖實際上還是有一絲絲特別的要求的,那就是在前景圖中,我們不希望保留的特徵一定要是梯度變化比較小的區域,比如純色範圍,或者很類似的顏色這樣的東西。

   opencv裏還有個MONOCHROME_TRANSFER這個Flag可以選,這個其實直接把前景圖像變爲彩色模式的灰度圖就能得到一樣的結果了。

  對於任意的兩幅圖,進行這中無縫的泊松融合,也能出現一些奇葩的效果,比如下面這樣的圖。

  

  不過這種圖並沒有什麼實際意義。

  2、圖像的亮度的改變,對應illuminationChange函數,其具體代碼爲:

void Cloning::illuminationChange(Mat &I, Mat &mask, Mat &wmask, Mat &cloned, float alpha, float beta)
{
    CV_INSTRUMENT_REGION();
    computeDerivatives(I,mask,wmask);
    arrayProduct(patchGradientX,binaryMaskFloat, patchGradientX);
    arrayProduct(patchGradientY,binaryMaskFloat, patchGradientY);
    Mat mag;
    magnitude(patchGradientX,patchGradientY,mag);
    Mat multX, multY, multx_temp, multy_temp;
    multiply(patchGradientX,pow(alpha,beta),multX);
    pow(mag,-1*beta, multx_temp);
    multiply(multX,multx_temp, patchGradientX);
    patchNaNs(patchGradientX);
    multiply(patchGradientY,pow(alpha,beta),multY);
    pow(mag,-1*beta, multy_temp);
    multiply(multY,multy_temp,patchGradientY);
    patchNaNs(patchGradientY);
    Mat zeroMask = (patchGradientX != 0);
    patchGradientX.copyTo(patchGradientX, zeroMask);
    patchGradientY.copyTo(patchGradientY, zeroMask);
    evaluate(I,wmask,cloned);
}

  這個的基礎是下面的公式:

           

   通過調整Alpha和Beta值,改變原始的亮度,然後再將改變亮度後的圖和原始的圖進行泊松融合,所以這裏的前景圖是由背景圖生成的。

   這裏的代碼再一次體現opencv的藝術家的特性:pow(mag,-1*beta, multx_temp);這麼耗時的操作居然執行了兩次。

   這裏的核心其實還是在算法的第二步:根據一定的原則計算融合後的圖像的梯度場,其他的過程和標準的無縫融合是一樣的。

  不過不可理解的是,爲什麼這個函數opencv不使用類似seamlessclone的boundingRect函數縮小需要計算的範圍了,這樣實際是可以提速很多的。

  這個函數用論文提供的自帶圖像確實有較爲不錯的效果,比如下面這個橙子的高光部分從視覺上看確實去掉的比較完美,但是也不是所有的高光都能完美去掉。

       

          原始圖                          蒙版                      結果圖(alpha = 0.2, beta = 0.3)

  論文裏還提到了可以對偏黑的圖進行適度調亮,這個我倒是沒有測試成功。

  3、圖像顏色調整,對應函數localColorChange,這個函數就更爲簡單了。

void Cloning::localColorChange(Mat &I, Mat &mask, Mat &wmask, Mat &cloned, float red_mul=1.0,
                                 float green_mul=1.0, float blue_mul=1.0)
{
    computeDerivatives(I,mask,wmask);
    arrayProduct(patchGradientX,binaryMaskFloat, patchGradientX);
    arrayProduct(patchGradientY,binaryMaskFloat, patchGradientY);
    scalarProduct(patchGradientX,red_mul,green_mul,blue_mul);
    scalarProduct(patchGradientY,red_mul,green_mul,blue_mul);
    evaluate(I,wmask,cloned);
}

  這個其實就在前景圖上的梯度上乘上不同的係數,然後再和原圖融合,這種調整可能比直接調整顏色要自然一些。

       

  4、cv裏還提供了一個紋理平整化的算法,叫textureFlatten,我沒感覺到這個算法有多大的作用。所以就沒有怎麼去實現。

  其實算法論文裏還有個Seamless tilingg功能的,我自己嘗試去實現,暫時沒有獲取正確的結果,如下圖所示:

                       

  這個效果再有些場景下還是很有用的。

  最後談及下算法速度吧,因爲整體都是翻譯自opencv,而且核心最耗時的FFT部分也基本是直接翻譯的,所以不會有本質的區別,在默認情況下,我用opencv 4.0版本去測試,同樣大小的圖,如果我不開openmp,耗時比大概是10:6,其中10是我的耗時,我估計這個於CV內部調用的DFT算法版本有關。此時我們觀察到使用CV時,CPU的使用率在35%(4核),當在CV下加入setNumThreads(1)指令後,可以看到CPU使用在25%,此時耗時比大概是10:8。當我使用2個線程加速我的FFT1D時,CV也使用默認設置,耗時比約爲5:6,此時我的CPU佔用率約爲40%,因此比cv版本的還是要快一些的。

  以上對比僅限於seamlessclone,對於其他的函數,我做了boundRect,那就不是塊一點點了。

  爲了方便測試,我做了一個可視化的UI,有興趣的朋友可以自行測試看看效果。

  總的來說,這個泊松融合要想獲取自己需要的結果,還是要有針對性的針對第二步梯度的融合多做些考慮和調整,才能獲取到自己需要的結果。 

    測試Demo及測試圖片下載地址: https://files.cnblogs.com/files/Imageshop/PossionBlending.rar?t=1705395766&download=true

         如果想時刻關注本人的最新文章,也可關注公衆號或者添加本人微信:  laviewpbt

                             

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章