泊松融合我自己寫的第一版程序大概是2016年在某個小房間里折騰出來的,當(dāng)時是用的迭代的方式,記得似乎效果不怎么樣,沒有達到論文的效果。前段時間又有網(wǎng)友問我有沒有這方面的程序,我說Opencv已經(jīng)有了,可以直接使用,他說opencv的框架太大,不想為了一個功能的需求而背上這么一座大山,看能否做個脫離那個環(huán)境的算法出來,當(dāng)時,覺得工作量挺大,就沒有去折騰,最近年底了,項目漸漸少了一點,公司上面又在搞辦公室政治,我地位不高,沒有參與權(quán),所以樂的閑,就抽空把這個算法從opencv里給剝離開來,做到了完全不依賴其他庫實現(xiàn)泊松融合樂,前前后后也折騰進半個月,這里還是做個開發(fā)記錄和分享。
在翻譯算法過程中,除了參考了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》論文理解和實現(xiàn)
? ? ? ? ? ? ? ?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)原理、實現(xiàn)與應(yīng)用
?
對應(yīng)的論文為:Poisson Image Editing,可以從百度上下載到。
泊松融合的代碼在opencv的目錄如下:
opencv-4.9.0\源代碼\modules\photo\src,其中的seamless_cloning_impl.cpp以及seamless_cloning.cpp為主要算法代碼。
我們總結(jié)下opencv的泊松融合主要是由以下幾個步驟組成的:
1、計算前景和背景圖像的梯度場;
2、根據(jù)一定的原則計算融合后的圖像的梯度場(這一步是最靈活的,通過改變他可以實現(xiàn)各種效果);
3、對融合后的梯度偏導(dǎo),獲取對應(yīng)的散度。
4、由散度及邊界像素值求解泊松方程(最為復(fù)雜)。
那么我們就一步一步的進行扣取和講解。
一、計算前景和背景圖像的梯度場。
這一部分在CV中對應(yīng)的函數(shù)名為:computeGradientX及computeGradientY,在CV中的調(diào)用代碼為:
computeGradientX(destination, destinationGradientX);
computeGradientY(destination, destinationGradientY);
computeGradientX(patch, patchGradientX);
computeGradientY(patch, patchGradientY);
以X方向的梯度為例, 其相應(yīng)的代碼為:
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函數(shù)進行處理。在這里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方式的鏡像就是這個結(jié)果
LinePD[Width - 1] = -LinePD[Width - 2]; // 最后一列
}
}
else
{
// 三通道代碼
}
}
我這里的Dest沒有用float類型,而是用的short,我的原則是用最小的內(nèi)存量+合適的數(shù)據(jù)類型來保存目標(biāo)。?
注意,opencv里默認的邊緣采用的是101的鏡像方式的,因此,對于[0, -1, 1]這種卷積和,最右側(cè)一列的值就是右側(cè)倒數(shù)第二列的負值。
2、根據(jù)一定的原則計算融合后的圖像的梯度場
這部分算法opencv寫的很分散,他把代碼放置到了好幾個函數(shù)里,這里把他們集中一下大概就是如下幾行:
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,那么下面的融合整個融合后的梯度場就完全是前景的梯度場了,和背景就毫無關(guān)系了,而進行erode后,則邊緣部分使用的就是背景的梯度場,這樣就有了有效的邊界條件,不過?erode(binaryMask, binaryMask, Kernel, Point(-1, -1), 3);最后一個參數(shù)要是3呢,這個3可是表示重復(fù)執(zhí)行三次,如果配合上前面的Kernel參數(shù),對于一個純白的圖,邊緣就會出現(xiàn)3行和3列的純黑的像素了(我測試默認參數(shù)下erode在處理邊緣時,是用了0值代替超出邊界的值),我個人感覺這里使用參數(shù)1就可以了。
從第四到第十二行其實就是很簡單的一個線性融合過程,Opencv的代碼呢寫的很向量化,我們要自己實現(xiàn)其實就下面幾句代碼:
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、對融合后的梯度偏導(dǎo),獲取對應(yīng)的散度。
這一部分對應(yīng)的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的這個函數(shù)是支持Inplace操作的,即Src=Dest時,也能得到正確的結(jié)果,為了實現(xiàn)這個結(jié)果,我們注意到這個卷積核是偏左的,即核中心偏左的元素有用,利用這個特性可以在X方向上從右向左循環(huán),就可以避免數(shù)據(jù)被覆蓋的,當(dāng)然對于每行的第一個元素就要特別處理了,同時注意這里采用了101格式的邊緣處理。
4、由散度及邊界像素值求解泊松方程。
有了以上的散度的計算,后面就是求解一個很大的稀疏矩方程的過程了,如果直接求解,將會是一個非常耗時的過程,即使利用稀疏的特性,也將對編碼者提出很高的技術(shù)要求。自己寫個稀疏矩陣的求解過程也是需要很大的勇氣的。
在opencv里,上面的求解是借助了傅里葉變換實現(xiàn)的,這個的原理我在某個論文中看到過,現(xiàn)在時間關(guān)系,我也沒有找到那一片論文了,如果后續(xù)有機會看到,我在分享出來。
CV的求解過程涉及到了3個函數(shù),分別是poissonSolver、solve、dst,也是一個調(diào)用另外一個的關(guān)系,具體的這個代碼能實現(xiàn)求解泊松方程的原理,我們也不去追究吧,僅僅從代碼層面說說大概得事情。
首先是poissonSolver函數(shù),其具體代碼如下:
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有一次把他的代碼藝術(shù)展現(xiàn)的活靈活現(xiàn)。從低6到第11行,我們看到了一個藝術(shù)家為了獲取最后的結(jié)果所做的各種行為藝術(shù)。
首先是復(fù)制原圖,然后把原圖除了第一行、最后一行、第一列、最后一列填充為黑色(rectangle函數(shù)),然后對這個填充后的圖進行拉普拉斯邊緣檢測,然后第10行做個減法,最后第11行呢,又直接裁剪了去掉周邊一個像素寬范圍內(nèi)的結(jié)果。
行為藝術(shù)家。
如果只從結(jié)果考慮,我們完全沒有必要有這么多的中間過程,我們把邊緣用*表示,中間值都為0,則一個二維平面圖如下所示:
// 拉普拉斯卷積核如下 對應(yīng)標(biāo)識如下
// 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的部位的計算值為我們需要的結(jié)果,很明顯,除了最外一圈0值的拉普拉斯邊緣檢測不為0,其他的都為0,不需要計算,而周邊一圈的0值的拉普拉斯邊緣檢測涉及到的3*3又恰好在原圖的有效范圍內(nèi),不需要考慮邊緣的值,因此,我們可以直接一步到位寫出mod_diff的值的。
ModDiff[0] = Laplacian[Width + 1] - (Image[1] + Image[Stride]); // 對應(yīng)的拉普拉斯有效值為: 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函數(shù),這個函數(shù)不是核心,所以稍微提及一下:


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];
}
他主要調(diào)用dst函數(shù),然后對dst函數(shù)處理的結(jié)果再進行濾波,然后再調(diào)用dst函數(shù),最后得到的結(jié)果進行圖像化。 不過第一次dst是使用FFT正變換,第二次使用了FFT逆變換。
從他的恢復(fù)圖像的過程看,他也是對最周邊的一圈像素不做處理,直接使用背景的圖像值。
那么我們再看看dst函數(shù),這個是解泊松方程的關(guān)鍵所在,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的數(shù)據(jù)填充中,我們看到他臨時創(chuàng)建了寬度2*Width + 2大小的數(shù)據(jù),高度為Height,其中第0列,第Width + 1列的數(shù)據(jù)為都為0,第1列到第Width +1列之間的數(shù)據(jù)為原是數(shù)據(jù),第Width + 2到2*Width + 1列的數(shù)據(jù)為原始數(shù)據(jù)鏡像后的負值。
填充完之后,在構(gòu)造一個復(fù)數(shù),然后調(diào)用FFT變換,當(dāng)invert為false時,使用的DFT_ROWS參數(shù),為true時,使用的是DFT_ROWS + DFT_SCALE + DFT_INVERSE參數(shù),那么其實這里就是一維的FFT正變換和逆變換,即對數(shù)據(jù)的每一行單獨處理,行于行之間是無關(guān)的,是可以并行的。
再進行了第一次FFT變換后,我們有創(chuàng)建一副寬度為2*Height+ 2,高度為Width大小的數(shù)據(jù),這個時候數(shù)據(jù)里的填充值依舊分為2塊,也是用黑色的處置條分開,同樣右側(cè)值的為鏡像負值分布。但是這個時候原始值是從前面進行FFT變換后的數(shù)據(jù)中獲取,而且還需要轉(zhuǎn)置獲取,其獲取的是FFT變換的虛部的值。?
填充完這個數(shù)據(jù)后,再次進行FFT變換,變換完之后,我們?nèi)∽儞Q后的虛部的值的轉(zhuǎn)置,并且舍棄第一列的值,作為我們處理后的結(jié)果。?
整個OPENCV的代碼從邏輯上是比較清晰的,他通過各種內(nèi)嵌的函數(shù)組合,實現(xiàn)了清晰的思路。但是如果從代碼效率角度來說,是非常不可取的,從內(nèi)存占用上來說,也存在著過多的浪費。這也是opencv中非核心函數(shù)通用的問題,基本上就是只在意結(jié)果,不怎么在乎過程和內(nèi)存占用。?
談到這里,核心的泊松融合基本就講完了,其各種不同的應(yīng)用也是基于上述過程。
那么我們再稍微談?wù)勊惴ǖ膬?yōu)化和加速。?
整個算法流程不算特別長,前面三個步驟的計算都比較簡單,計算量也不是很大,慢的還是在于泊松方程的求解,而求解中最耗時還是那個DFT變換,簡單的測試表面,DFT占整個算法耗時的80%(單線程下)。前面說過,這個內(nèi)部使用的是一維行方向的DFT變換,行于行之間的處理是無關(guān)的,而且,他的數(shù)據(jù)量也比較大,特別適合于并行處理,我們可以直接用簡單的omp就可以實現(xiàn)加速。
另外,我們再進行FFT時,常用的一個加速手段就是GetOptimalDftSize獲得一個和原始尺寸最為接近而又能更快實現(xiàn)FFT的大小,通常他們是3或者4或者5的倍數(shù)。有時候,這個加速也非常的明顯,比如尺寸為1023的FFT和尺寸為1024的FFT,速度可以相差好幾倍。這里我也嘗試使用這個函數(shù),但是經(jīng)過多次嘗試(包括適當(dāng)?shù)母淖償?shù)據(jù)布局),都存在一個嚴(yán)重的問題,得到的結(jié)果圖像有著不可忽視的誤差,基本無法恢復(fù)。因此,這個優(yōu)化的步驟不得已只能放棄。
前面說了很多,還忘記了一個最重要的函數(shù)的扣取,dft函數(shù),這個函數(shù)在opencv的目錄如下: opencv-4.9.0\源代碼\modules\core\src\dxt.cpp,居然是用的dxt這個文件名,開始我怎么搜都搜不到他。?
關(guān)于這個功能的扣取,我大概也花了半個月的時間,時間上OPENCV也有很多版本,比如CPU的、opencl的等等,我這里扣取的是純CPU的,而且還是從早期的CV的代碼中扣的,現(xiàn)在的版本的代碼里有太多不相關(guān)的東西了,扣取的難度估計還要更大。而且在扣取中我還做了一些優(yōu)化,這個就不在這里多說了,總之,opencv的FFT在各種開源版本的代碼中算是一份非常不錯的代碼。
具體的應(yīng)用:1、無縫的圖像合成,對應(yīng)CV的seamlessClone函數(shù),他支持背景圖和前景圖圖不一樣大小,也可以沒有蒙版等等特性,其具體的代碼如下:
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的作用正好想法,把周邊一圈設(shè)置為黑色,然后再提取出實際有效的邊界(不為0的區(qū)域),以便減少計算量,后續(xù)再根據(jù)邊界裁剪出有效的區(qū)域,交給具體的融合的函數(shù)處理。
opencv的這個函數(shù)寫的實在不怎么好,當(dāng)我們不小心設(shè)置了錯誤的p參數(shù)時,就會出現(xiàn)內(nèi)存錯誤,這個參數(shù)主要是指定前景圖像在背景圖像中的位置的, 我們必須保證前景圖像不能有任何部分跑到背景圖像的外部,在我自己寫的版本中已經(jīng)校正了這個小錯誤。
? ? ? ? 注意,這里有個Flag參數(shù),當(dāng)Flag為NORMAL_CLONE時,就是我們前面的標(biāo)準(zhǔn)過程,當(dāng)為MIXED_CLONE時,則在第二步體現(xiàn)了不同:
用原文的公式表示即為:
? ? ? ? ?
? 翻譯為我們能看懂的意思就是:?Mixed的模式下,如果前景的梯度差異大于背景的差異,則直接進行線性混合,否則就直接用背景的梯度。這個模式下可以獲得更為理想的融合效果。
我想辦法把論文中的一些測試圖像摳出來,然后進行了一系列測試,確實能獲得一些不錯的效果。
? ? ? ? ??
? ? ? ??
字符 ? ? ? ? 背景紋理 融合結(jié)果
? ??? ??
? ? ??
海景 彩虹 融合后
上面為不帶mask時全圖進行融合,可以看到融合后前景基本完美的融合到了背景中,但是前景的顏色還是發(fā)生了一些改變。
???
???
? ?
背景圖 前景圖 蒙版圖 合成圖
上面這一幅測試圖中,太陽以及太陽在水中的倒影也完美的融合到背景圖中,相當(dāng)?shù)淖匀弧?/span>
以下為多福圖像和成到一幅中的效果。
? ? ??
?
前景1 前景1蒙版
? ? ? ?? ? ??
前景2 前景2蒙版
? ? ? ?? ? ? ? ?
背景圖 融合后結(jié)果
? ? 上面所有的融合方式都是選擇的MIXED_CLONE。
其實注意到MIXED_CLONE里梯度的混合原則,要達到上圖這樣較好的融合效果,對前景圖實際上還是有一絲絲特別的要求的,那就是在前景圖中,我們不希望保留的特征一定要是梯度變化比較小的區(qū)域,比如純色范圍,或者很類似的顏色這樣的東西。
? opencv里還有個MONOCHROME_TRANSFER這個Flag可以選,這個其實直接把前景圖像變?yōu)椴噬J降幕叶葓D就能得到一樣的結(jié)果了。
對于任意的兩幅圖,進行這中無縫的泊松融合,也能出現(xiàn)一些奇葩的效果,比如下面這樣的圖。
不過這種圖并沒有什么實際意義。
2、圖像的亮度的改變,對應(yīng)illuminationChange函數(shù),其具體代碼為:
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);
}
這個的基礎(chǔ)是下面的公式:
? ? ? ? ? ?
? 通過調(diào)整Alpha和Beta值,改變原始的亮度,然后再將改變亮度后的圖和原始的圖進行泊松融合,所以這里的前景圖是由背景圖生成的。
這里的代碼再一次體現(xiàn)opencv的藝術(shù)家的特性:pow(mag,-1*beta, multx_temp);這么耗時的操作居然執(zhí)行了兩次。
這里的核心其實還是在算法的第二步:根據(jù)一定的原則計算融合后的圖像的梯度場,其他的過程和標(biāo)準(zhǔn)的無縫融合是一樣的。
不過不可理解的是,為什么這個函數(shù)opencv不使用類似seamlessclone的boundingRect函數(shù)縮小需要計算的范圍了,這樣實際是可以提速很多的。
這個函數(shù)用論文提供的自帶圖像確實有較為不錯的效果,比如下面這個橙子的高光部分從視覺上看確實去掉的比較完美,但是也不是所有的高光都能完美去掉。
??
???
原始圖 蒙版 結(jié)果圖(alpha = 0.2, beta = 0.3)
論文里還提到了可以對偏黑的圖進行適度調(diào)亮,這個我倒是沒有測試成功。
3、圖像顏色調(diào)整,對應(yīng)函數(shù)localColorChange,這個函數(shù)就更為簡單了。
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);
}
這個其實就在前景圖上的梯度上乘上不同的系數(shù),然后再和原圖融合,這種調(diào)整可能比直接調(diào)整顏色要自然一些。
??
???
4、cv里還提供了一個紋理平整化的算法,叫textureFlatten,我沒感覺到這個算法有多大的作用。所以就沒有怎么去實現(xiàn)。
其實算法論文里還有個Seamless tilingg功能的,我自己嘗試去實現(xiàn),暫時沒有獲取正確的結(jié)果,如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ?
這個效果再有些場景下還是很有用的。
最后談及下算法速度吧,因為整體都是翻譯自opencv,而且核心最耗時的FFT部分也基本是直接翻譯的,所以不會有本質(zhì)的區(qū)別,在默認情況下,我用opencv 4.0版本去測試,同樣大小的圖,如果我不開openmp,耗時比大概是10:6,其中10是我的耗時,我估計這個于CV內(nèi)部調(diào)用的DFT算法版本有關(guān)。此時我們觀察到使用CV時,CPU的使用率在35%(4核),當(dāng)在CV下加入setNumThreads(1)指令后,可以看到CPU使用在25%,此時耗時比大概是10:8。當(dāng)我使用2個線程加速我的FFT1D時,CV也使用默認設(shè)置,耗時比約為5:6,此時我的CPU占用率約為40%,因此比cv版本的還是要快一些的。
以上對比僅限于seamlessclone,對于其他的函數(shù),我做了boundRect,那就不是塊一點點了。
為了方便測試,我做了一個可視化的UI,有興趣的朋友可以自行測試看看效果。
總的來說,這個泊松融合要想獲取自己需要的結(jié)果,還是要有針對性的針對第二步梯度的融合多做些考慮和調(diào)整,才能獲取到自己需要的結(jié)果。?
?? 測試Demo及測試圖片下載地址: https://files.cnblogs.com/files/Imageshop/PossionBlending.rar?t=1705395766&download=true
? ? ? ? ?如果想時刻關(guān)注本人的最新文章,也可關(guān)注公眾號或者添加本人微信:? laviewpbt
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
翻譯
搜索文章來源:http://www.zghlxwxcb.cn/news/detail-794645.html
復(fù)制文章來源地址http://www.zghlxwxcb.cn/news/detail-794645.html
到了這里,關(guān)于【快速閱讀二】從OpenCv的代碼中扣取泊松融合算子(Poisson Image Editing)并稍作優(yōu)化的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!