基于均值坐标(Mean-Value Coordinates)的图像融合算法的具体实现
1. 概述
泊松融合是图像融合处理效果最好的算法,其来自于2004年Siggraph的经典paper:《Poisson Image Editing》。以这篇文章为发端,很多大神提出了一系列的优化算法。2009年, Zeev Farbman 在的SIGGRAPH上面提出的基于Mean-Value Coordinates方法的泊松融合加速算法《Coordinates for Instant Image Cloning》(文献二)。在这篇文章中,泊松方程被转换成拉普拉斯方程,并且提出了用均值坐标Mean-Value Coordinates来近似求解这个方程,从而达到实时运算的效果。
初步了解了一下原生的泊松融合算法和均值坐标融合算法,其原理包含的内涵十分丰富,包含一些诸如列散度、拉普拉斯算子、梯度场、泊松方程等等数学知识,要完全弄明白确实需要一定的基础。这里就重点关注一下根据《Coordinates for Instant Image Cloning》(文献二)实现图像融合的过程,有机会的话再详细推导一下其原理。
2. 实现
2.1. 准备
在OpenCV中,已经收录了泊松融合算法,也就是函数seamlessClone():

这个算法要求输入一个源图像,一个目标图像,源图像希望融合到目标图像的位置,以及一个mask图像。这个mask图像也就是一张二值化图像,用来标识图像的ROI(region of interest感兴趣区域)。均值坐标融合算法的输入参数也是一样的,不过mask图像很难以处理,OpenCV自带的GUI难以满足需求。所以我这里通过QT来做GUI,通过OpenCV将图像显示到QT窗体上,然后再QT窗体的图像区域内绘制多边形,多边形内部即为ROI。可以参考我的这两篇文章:
《使用QT显示OpenCV读取的图片》
《使用QT绘制一个多边形》
2.2. 核心
2.2.1. 均值坐标(Mean-Value Coordinates)
在论文中提出了一个很重要的概念也就是均值坐标(Mean-Value Coordinates)。对于如下多边形内部的点:

都有一系列与多边形边界相关的坐标值:

也就是说,只要确定了ROI,也就确定了ROI区域内每个点的均值坐标(Mean-Value Coordinates),每个点会有m个值(m为ROI边界多边形的顶点)。
2.2.2. ROI边界栅格化
论文中是以ROI边界多边形为例的,实际用到图像处理中是不会只用几个多边形的节点来计算的,而应该是ROI边界上连续的点。实际上不用想也知道,图像融合最关键的部分就是ROI边界部分的像素值。必须要用到ROI边界上所有的像素值来计算。
也就是说这里还需要一个工作,就是将ROI边界多边形栅格化,取得其上连续的像素位置,得到准确的栅格化多边形边界。这里可以参看我的这篇文章《矢量线的一种栅格化算法》。按照顺序逐条将多边形的边栅格化,即可以得到ROI的栅格化多边形边界。
2.2.3. 核心实现
论文给出的算法伪代码如下:

这段算法描述并不复杂,转换成自然语言如下:
- 假设ROI区域内有n个点,其边界由m个点组成。
- 那么可以求每个点的MVC(均值坐标),每个点有m个坐标值,一共有n个点,MVC就是就是一个n*m的矩阵。
- 求ROI区域边界的像素差diff,显然其是一个m*1的矩阵。
- 那么新图像ROI区域的插值为:r = MVC * diff,矩阵乘法后r为n*1矩阵。
- 将插值r与原图像g矩阵相加:f = g + r,替换目标图像相应位置的值。
核心部分具体的实现代码如下:
QTime startTime = QTime::currentTime();
//Step1:找到边界上所有的像素点
vector<Vector2d> ROIBoundPointList;
CalBoundPoint(ROIBoundPointList);
//Step2:计算范围内每个点的 mean-value coordinates
size_t srcImgBufNum = static_cast<size_t>(srcImg.cols) * static_cast<size_t>(srcImg.rows);
vector<vector<double>> MVC(srcImgBufNum);
for(size_t i = 0; i < srcImgBufNum; i++)
{
MVC[i].resize(ROIBoundPointList.size()-1, 0);
}
vector<bool> clipMap(srcImgBufNum, true); //标识范围内的点
cout<<"开始计算 mean-value coordinates..." << endl;
#pragma omp parallel for //开启OpenMP并行加速
for (int ri = 0; ri < srcImg.rows; ++ri)
{
for (int ci = 0; ci < srcImg.cols; ++ci)
{
//点是否在多边形内
size_t m = static_cast<size_t>(srcImg.cols) * ri + ci;
if(!Point_In_Polygon_2D(ci, ri, ROIBoundPointList))
{
clipMap[m] = false;
continue;
}
//逐点计算MVC
Vector2d P(ci, ri);
vector<double> alphaAngle(ROIBoundPointList.size());
for(size_t pi = 1; pi < ROIBoundPointList.size(); pi++)
{
alphaAngle[pi] = threePointCalAngle(ROIBoundPointList[pi-1], P, ROIBoundPointList[pi]);
}
alphaAngle[0] = alphaAngle[ROIBoundPointList.size()-1];
for(size_t pi = 1; pi < ROIBoundPointList.size(); pi++)
{
double w_a = tan(alphaAngle[pi-1]/2) + tan(alphaAngle[pi]/2);
double w_b = (ROIBoundPointList[pi-1] - P).Mod();
MVC[m][pi-1] = w_a / w_b;
if(_isnan(MVC[m][pi-1])==1)
{
MVC[m][pi-1] = 0;
}
}
double sum = 0;
for(size_t pi = 0; pi < MVC[m].size(); pi++)
{
sum = sum + MVC[m][pi];
}
for(size_t pi = 0; pi < MVC[m].size(); pi++)
{
MVC[m][pi] = MVC[m][pi] / sum;
}
}
}
cout<<"计算完成!" << endl;
//Step3:计算边界的像素插值
vector<int> diff;
for(size_t i = 0; i < ROIBoundPointList.size()-1; i++)
{
size_t l = (size_t) srcImg.cols * ROIBoundPointList[i].y + ROIBoundPointList[i].x;
for(int bi = 0; bi < winBandNum; bi++)
{
size_t m = (size_t) dstImg.cols * winBandNum * (ROIBoundPointList[i].y + posY)+ winBandNum * (ROIBoundPointList[i].x + posX) + bi;
size_t n = (size_t) srcImg.cols * winBandNum * ROIBoundPointList[i].y + winBandNum * ROIBoundPointList[i].x + bi;
int d = (int)(dstImg.data[m]) - (int)(srcImg.data[n]);
diff.push_back(d);
}
clipMap[l] = false; //在多边形边上的点没法计算MVC
}
//Step4:插值计算
cout<<"开始插值计算..." << endl;
//Mat rMat(srcImg.rows, srcImg.cols, CV_64FC3);
#pragma omp parallel for
for (int ri = 0; ri < srcImg.rows; ++ri)
{
for (int ci = 0; ci < srcImg.cols; ++ci)
{
size_t l = (size_t) srcImg.cols * ri + ci;
if(!clipMap[l])
{
continue;
}
vector<double> r(winBandNum, 0);
for(size_t pi = 0; pi < MVC[l].size(); pi++)
{
for(int bi = 0; bi < winBandNum; bi++)
{
r[bi] = r[bi] + MVC[l][pi] * diff[pi * winBandNum + bi];
}
}
for(int bi = 0; bi < winBandNum; bi++)
{
size_t n = (size_t) srcImg.cols * winBandNum * ri + winBandNum * ci + bi;
size_t m = (size_t) dstImg.cols * winBandNum * (ri + posY)+ winBandNum * (ci + posX) + bi;
dstImg.data[m] = min(max(srcImg.data[n] + r[bi], 0.0), 255.0);
}
}
}
cout<<"插值完成!" << endl;
QTime stopTime = QTime::currentTime();
int elapsed = startTime.msecsTo(stopTime);
cout<<"总结完成用时"<<elapsed<<"毫秒";
2.2.4. 实现中的问题
- ROI边界上的点无法计算MVC值,需要予以剔除,否则ROI边界上会出现一圈白色的点。
- 用到了OpenMP加速,可以大幅提高性能。如有必要的话,可以通过显卡加速。
3. 效果
3.1. 使用过程
程序源代码可参见文章最末的链接,是一个OpenCV结合QT的GUI的程序。编译运行后,点击"打开"按钮,界面会显示源图像:

点击"绘制"按钮,在源图像区域内绘制一个多边形,确定一个ROI:

准备一张想要融合的目标图像:

点击"融合"按钮,会加载目标图像,并会根据设置的位置,将源图像的ROI融合到目标图像中:

3.2. 效率
在Debug模式,不使用OpenMP加速的情况下,这个算法的效率大约需要50秒左右的时间。
在Debug模式,使用OpenMP加速,算法的效率可以优化到10秒,也就是不使用OpenMP加速时的5倍左右。而我使用的机器CPU是i7-8750H标压6核CPU,考虑到一些IO操作造成的性能损耗,这个优化效率是正常的。
最后在使用Release模式,使用OpenMP加速之后,算法的效率可以优化到1秒左右,这说明编译器优化对程序性能也是有很大影响的,尤其是对并行程序而言。
这个实现只是这个算法的初始实现,效率就已经达到了1秒左右,看来论文说的可以达到实时融合确实不是虚言。有机会再尝试一下论文中提到的一些性能优化实现。
4. 参考
[1] 泊松融合及其优化算法
[2] Coordinates for Instant Image Cloning
[3] 图像处理(十二)图像融合(1)Seamless cloning泊松克隆-Siggraph 2004
[4] 多尺度并行坐标插值实时图像克隆算法
基于均值坐标(Mean-Value Coordinates)的图像融合算法的具体实现的更多相关文章
- 基于均值坐标(Mean-Value Coordinates)的图像融合算法的优化实现
目录 1. 概述 2. 实现 2.1. 原理 2.2. 核心代码 2.3. 第二种优化 3. 结果 1. 概述 我在之前的文章<基于均值坐标(Mean-Value Coordinates)的图像 ...
- paper 101:图像融合算法及视觉艺术应用
1:基于泊松方程的图像融合方法,利用偏微分方程实现了不同图像上区域的无缝融合.比较经典的文章: P. Pérez, M. Gangnet, A. Blake. Poisson image editin ...
- 【图像配准】基于互信息的图像配准算法:MI、EMI、ECC算法
简单介绍: 基于互信息的图像配准算法以其较高的配准精度和广泛的适用性而成为图像配准领域研究的热点之中的一个.而基于互信息的医学图像配准方法被觉得是最好的配准方法之中的一个.基于此.本文将介绍简单的基于 ...
- 基于FPGA的线阵CCD实时图像采集系统
基于FPGA的线阵CCD实时图像采集系统 2015年微型机与应用第13期 作者:章金敏,张 菁,陈梦苇2016/2/8 20:52:00 关键词: 实时采集 电荷耦合器件 现场可编程逻辑器件 信号处理 ...
- 【VS开发】【图像处理】基于灰度世界、完美反射、动态阈值等图像自动白平衡算法的原理、实现及效果
基于灰度世界.完美反射.动态阈值等图像自动白平衡算法的原理.实现及效果 白平衡是电视摄像领域一个非常重要的概念,通过它可以解决色彩还原和色调处理的一系列问题.白平衡是随着电子影像再现色彩真实 ...
- 一种基于均值不等式的Listwise损失函数
一种基于均值不等式的Listwise损失函数 1 前言 1.1 Learning to Rank 简介 Learning to Rank (LTR) , 也被叫做排序学习, 是搜索中的重要技术, 其目 ...
- OpenCV探索之路(二十四)图像拼接和图像融合技术
图像拼接在实际的应用场景很广,比如无人机航拍,遥感图像等等,图像拼接是进一步做图像理解基础步骤,拼接效果的好坏直接影响接下来的工作,所以一个好的图像拼接算法非常重要. 再举一个身边的例子吧,你用你的手 ...
- 【HEVC帧间预测论文】P1.1 基于运动特征的HEVC快速帧间预测算法
基于运动特征的 HEVC 快速帧间预测算法/Fast Inter-Frame Prediction Algorithm for HEVC Based on Motion Features <HE ...
- [译]基于GPU的体渲染高级技术之raycasting算法
[译]基于GPU的体渲染高级技术之raycasting算法 PS:我决定翻译一下<Advanced Illumination Techniques for GPU-Based Volume Ra ...
随机推荐
- html5 一些好用的原生事件
计算机技术发展这么多年, 有些方面太老了,不适合新时代的发展, 使用新的规范,更方便开发和使用, 提高各方使用人的工作效率, 何乐而不为 drag drop html5新增属性 某些操作使用拖拽比较方 ...
- 吴裕雄--天生自然python学习笔记:python 用pygame模块加载图片
加载图片 使用几何绘图无法画出精细的图形,所以我们可以把现成的图片加载到 Pygam e 中直接使用 . 加载图片的语法为 : 图片加载后通常会用 convert 方法加以处理, 以增加显示速度,语法 ...
- elasticsearch ik中文分词器的使用详解
(基于es5.4)先喵几眼github,按照步骤安装好分词器 link:https://github.com/medcl/elasticsearch-analysis-ik 复习一下常用的操作 .查看 ...
- QTP 表格的导入导出异常信息 笔记
0 环境 系统环境:win7 1 操作 1.1 前言 与异常遍历结合 需要表格记录下来(读写) 代码大概跑了一下 若细节有错 请自行更改 1.2 导出 systemutil.Run "D:\ ...
- JAVA循环结构学校上机经常遇到的几题 笔记
package homework.class4; import java.util.*; import java.util.stream.Collectors; import java.util.st ...
- verilog求倒数-ROM实现方法
采用线性逼近法结合32段线性查找表的方式来实现1/z的计算. 首先将1/32-1/64的定点化数据存放到ROM中,ROM中存放的是扩大了2^20 次方的数字四舍五入后的整数部分.n值越大,精度越大,误 ...
- python-倒序循环
有时候循环需要用到倒序,所以整理一下倒序循环的方法 方法1: 如果要倒序遍历访问序列中的元素,可以对该序列使用reversed() 函数,reversed函数会生成一份倒序列表的拷贝,但是不会改变原列 ...
- orcale 11g安装,创建表空间,用户,授权用户
一.卸载旧oracle 用Oracle自带的卸载程序不能从根本上卸载Oracle,从而为下次的安装留下隐患,那么怎么才能完全卸载Oracle呢?那就是直接注册表清除,步骤如下: 1. 开始->设 ...
- 3DMAX 卸载工具,完美彻底卸载清除干净3dmax各种残留注册表和文件
一些同学安装3dmax出错了,也有时候想重新安装3dmax的时候会出现这种本电脑windows系统已安装3dmax,你要是不留意直接安装,只会安装3dmax的附件,3dmax是不会安装上的.这种原因呢 ...
- Java IO: 异常处理
原文链接 作者:Jakob Jenkov 译者: 李璟(jlee381344197@gmail.com) 流与Reader和Writer在结束使用的时候,需要正确地关闭它们.通过调用close()方法 ...