【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

这篇具有很好参考价值的文章主要介绍了【快速阅读二】从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函数,这个函数不是核心,所以稍微提及一下:

【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化
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时,则在第二步体现了不同:
  用原文的公式表示即为:

         【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

   翻译为我们能看懂的意思就是: Mixed的模式下,如果前景的梯度差异大于背景的差异,则直接进行线性混合,否则就直接用背景的梯度。这个模式下可以获得更为理想的融合效果。

   我想办法把论文中的一些测试图像抠出来,然后进行了一系列测试,确实能获得一些不错的效果。

 【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化          【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化        【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

            字符                               背景纹理                        融合结果  

    【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化    【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化      【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

          海景                              彩虹                              融合后

  上面为不带mask时全图进行融合,可以看到融合后前景基本完美的融合到了背景中,但是前景的颜色还是发生了一些改变。

  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化   【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化   【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化   【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

        背景图                  前景图                    蒙版图                  合成图

  上面这一幅测试图中,太阳以及太阳在水中的倒影也完美的融合到背景图中,相当的自然。

  以下为多福图像和成到一幅中的效果。

  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化      【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化 

            前景1                          前景1蒙版

       【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化      【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

            前景2                          前景2蒙版

       【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化         【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

            背景图                          融合后结果

    上面所有的融合方式都是选择的MIXED_CLONE。

  其实注意到MIXED_CLONE里梯度的混合原则,要达到上图这样较好的融合效果,对前景图实际上还是有一丝丝特别的要求的,那就是在前景图中,我们不希望保留的特征一定要是梯度变化比较小的区域,比如纯色范围,或者很类似的颜色这样的东西。

   opencv里还有个MONOCHROME_TRANSFER这个Flag可以选,这个其实直接把前景图像变为彩色模式的灰度图就能得到一样的结果了。

  对于任意的两幅图,进行这中无缝的泊松融合,也能出现一些奇葩的效果,比如下面这样的图。

  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

  不过这种图并没有什么实际意义。

  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);
}

  这个的基础是下面的公式:

           【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

   通过调整Alpha和Beta值,改变原始的亮度,然后再将改变亮度后的图和原始的图进行泊松融合,所以这里的前景图是由背景图生成的。

   这里的代码再一次体现opencv的艺术家的特性:pow(mag,-1*beta, multx_temp);这么耗时的操作居然执行了两次。

   这里的核心其实还是在算法的第二步:根据一定的原则计算融合后的图像的梯度场,其他的过程和标准的无缝融合是一样的。

  不过不可理解的是,为什么这个函数opencv不使用类似seamlessclone的boundingRect函数缩小需要计算的范围了,这样实际是可以提速很多的。

  这个函数用论文提供的自带图像确实有较为不错的效果,比如下面这个橙子的高光部分从视觉上看确实去掉的比较完美,但是也不是所有的高光都能完美去掉。

  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化   【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

          原始图                          蒙版                      结果图(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);
}

  这个其实就在前景图上的梯度上乘上不同的系数,然后再和原图融合,这种调整可能比直接调整颜色要自然一些。

  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化  【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化   【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

  4、cv里还提供了一个纹理平整化的算法,叫textureFlatten,我没感觉到这个算法有多大的作用。所以就没有怎么去实现。

  其实算法论文里还有个Seamless tilingg功能的,我自己尝试去实现,暂时没有获取正确的结果,如下图所示:

                       【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

  这个效果再有些场景下还是很有用的。

  最后谈及下算法速度吧,因为整体都是翻译自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,有兴趣的朋友可以自行测试看看效果。

  总的来说,这个泊松融合要想获取自己需要的结果,还是要有针对性的针对第二步梯度的融合多做些考虑和调整,才能获取到自己需要的结果。 

【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

    测试Demo及测试图片下载地址: https://files.cnblogs.com/files/Imageshop/PossionBlending.rar?t=1705395766&download=true

         如果想时刻关注本人的最新文章,也可关注公众号或者添加本人微信:  laviewpbt

                             【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化

翻译

搜索

复制文章来源地址https://www.toymoban.com/news/detail-794645.html

到了这里,关于【快速阅读二】从OpenCv的代码中扣取泊松融合算子(Poisson Image Editing)并稍作优化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 神经网络算子融合的几种方式

    Kernel融合:将多个卷积核合并为一个更大的卷积核,从而减少计算量。 层融合:将多个连续的神经网络层合并为一个更大的层,减少内存访问和计算开销。 数据重用:在计算过程中,将多个算子共享输入数据,减少数据读取次数。 并行计算:将多个独立的神经网络算子并行

    2024年02月16日
    浏览(28)
  • AI编译器-图常见优化算法-算子融合

    算子融合(Operator Fusion)是深度学习编译器中的一种优化技术,它可以将多个算子合并为一个更大的算子,以减少计算和内存访问的开销。以下是一些常见的算子融合例子: 卷积和池化融合:将卷积层和池化层融合为一个算子,减少内存访问和计算的开销。 多个全连接层融

    2024年02月10日
    浏览(47)
  • python使用opencv对图像添加(高斯/椒盐/泊松/斑点)噪声

    导读 这篇文章主要介绍如何利用opencv来对图像添加各类噪声,原图 高斯噪声 高斯噪声就是给图片添加一个服从 高斯分布的噪声 ,可以通过调节高斯分布 标准差(sigma) 的大小来控制添加噪声程度, sigma 越大添加的噪声越多图片损坏的越厉害 椒盐噪声 椒盐噪声就是给图片添

    2024年02月15日
    浏览(55)
  • 【精选】OpenCV多视角摄像头融合的目标检测系统:全面部署指南&源代码

    随着计算机视觉和图像处理技术的快速发展,人们对于多摄像头拼接行人检测系统的需求日益增加。这种系统可以利用多个摄像头的视角,实时监测和跟踪行人的活动,为公共安全、交通管理、视频监控等领域提供重要的支持和帮助。 在传统的行人检测系统中,通常只使用单

    2024年01月22日
    浏览(90)
  • 《opencv实用探索·十一》opencv之Prewitt算子边缘检测,Roberts算子边缘检测和Sobel算子边缘检测

    1、前言 边缘检测: 图像边缘检测是指在图像中寻找灰度、颜色、纹理等变化比较剧烈的区域,它们可能代表着物体之间的边界或物体内部的特征。边缘检测是图像处理中的一项基本操作,可以用于人脸识别、物体识别、图像分割等多个领域。 边缘检测实质上是计算当前点和

    2024年02月22日
    浏览(52)
  • OpenCV-25sobel算子(索贝尔算子)

    前面所提到的滤波都是用于降噪的,去掉噪声,而算子是用来找边界,来识别图像的边缘。 边缘是 像素值发生跃迁 的值,是图像的显著特点之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。 人眼如何识别图像的边缘呢? 比如有一幅画,图里面有一条线

    2024年01月20日
    浏览(41)
  • 改进YOLOv8 | 特征融合篇 | YOLOv8 应用轻量级通用上采样算子CARAFE | 《特征的内容感知重组》

    特征上采样是现代卷积神经网络架构中的关键操作,例如特征金字塔。其设计对于密集预测任务,如目标检测和语义/实例分割至关重要。在本研究中,我们提出了一种称为内容感知特征重组(CARAFE)的通用、轻量级且高效的操作符,以实现这一目标。CARAFE具有以下几个优点:

    2024年02月07日
    浏览(55)
  • Opencv图像边缘检测——Roberts算子(手写)、Sobel算子(手写和调包)、Scharr算子、Laplacian算子

    Roberts算子即交叉微分算子,是基于交叉差分的梯度算子。此算法通过局部差分来计算检测图像的边缘线条,对噪声敏感。 Roberts 交叉微分算子分别为主对角线和副对角线方向的算子,有两个2*2的滤波算子组成: 对于图像而言,如果im表示图像像素矩阵,则可以如下计算(i,

    2024年02月04日
    浏览(72)
  • OpenCV(7):边缘检测之Sobel算子,Scharr算子,Laplacian算子和Canny算子边缘检测

    Sobel算子、Scharr算子、Laplacian算子和Canny算子都是常用的图像边缘检测算法。它们可以用来识别图像中物体之间的边界,从而对物体进行定位、跟踪、分割、识别等处理。 Sobel算子和Scharr算子都是基于卷积运算实现的边缘检测算法。Sobel算子使用两个3×3的矩阵对原始图像进行卷

    2024年02月05日
    浏览(51)
  • openCV实战-系列教程4:图像梯度计算(Sobel算子/开运算/梯度计算方法/scharr算子/lapkacian算子)、源码解读 ?????OpenCV实战系列总目录

    打印一个图片单独做出一个函数: 先读进来一个原型白色图 打印结果: 如图有两个3*3的卷积核,其中A是原始图片中的一个3*3的区域,这个A和3*3的卷积核所谓对应位置相乘的结果就分别是左右梯度和上下梯度 假如A是这个矩阵:  那么Gx的计算结果就为:-x1+x3-2x4+2x6-x7+x9 代码实

    2024年02月10日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包