Poisson Blending
このサンプルは,OpenCVの機能とはあまり関係なく,差分やデータの保持にcv::Mat形式を利用している程度です.このサンプルでは,ある画像の一部を別の画像にコピーする際に,それらを滑らかにブレンディングします.
コピー後の画素値は,Dirichlet条件の下でPoisson方程式を解くことで求められます.つまり,コピー元画像の画像勾配をなるべく保ったまま,コピー境界の画素値をコピー先の画素値と合うように,コピー結果画素値を決定します.
詳しくは,SIGGRAPH2003の論文 Poisson
Image Editting (PDF) を参照してください.
Souce Image, Target Image, Mask Image, Blending Result
また,Gradient Mixtureを行うことで,コピー元とコピー先の画像のうち,より強い勾配を保存することができます.
下図に,紙に書かれた文字(左)を,手のひらの合成するデモ結果を示します.
Gradient Mixtureを行わない通常のブレンディングの場合(中央),コピー元画像の背景である紙部分が上書きされ,手のシワが消えてのっぺりした画像になっています.
ここで Gradient Mixture を行うと(右),シワなどの強い勾配が保存されたブレンディング結果になります.
以下は,サンプル用のソースコードですが,実行には,OpenCV-2.2以降,Eigen2/3,UmfPack が必要です.Ubuntu 11.04
上でのみ動作を確認しています.
また,OpenCV自身の Blender クラスとはまったくの無関係です.
疎な連立方程式の解法部分は,UmfPackのソルバに丸投げですが,Gauss-Seidel 法,Mutigrid 法などで解いても良いでしょう.
モナリザの画像(375×505[pixel])を例に挙げると,Core i7 2.67[GHz] の VM 上 Linux で,約950[msec]
ほどの処理時間が必要です.
C++
poisson_blending.hpp
#ifndef __POISSON_BLENDING_H__
#define __POISSON_BLENDING_H__
#include <map>
#include <opencv2/imgproc/imgproc.hpp>
#include <Eigen/Core>
#define EIGEN_YES_I_KNOW_SPARSE_MODULE_IS_NOT_STABLE_YET
#include <Eigen/Sparse>
#include <unsupported/Eigen/UmfPackSupport>
//#include <unsupported/Eigen/SuperLUSupport>
namespace Blend {
class PoissonBlender
{
private:
cv::Mat _src, _target, _mask;
cv::Rect mask_roi1;
cv::Mat mask1;
cv::Mat dst1;
cv::Mat target1;
cv::Mat drvxy;
int ch;
std::map<int,int> mp;
template <typename T>
bool buildMatrix(Eigen::SparseMatrix<T> &A, Eigen::Matrix<T, Eigen::Dynamic, 1> &b,
Eigen::Matrix<T, Eigen::Dynamic, 1> &u);
template <typename T, typename Backend>
bool solve(const Eigen::SparseMatrix<T> &A, const Eigen::Matrix<T, Eigen::Dynamic, 1> &b,
Eigen::Matrix<T, Eigen::Dynamic, 1> &u);
template <typename T>
bool copyResult(Eigen::Matrix<T, Eigen::Dynamic, 1> &u);
public:
PoissonBlender();
PoissonBlender(const cv::Mat &src, const cv::Mat &target, const cv::Mat &mask);
~PoissonBlender() {};
bool setImages(const cv::Mat &src, const cv::Mat &target, const cv::Mat &mask);
void copyTo(PoissonBlender &b) const;
PoissonBlender clone() const;
bool seamlessClone(cv::Mat &dst, int offx, int offy, bool mix);
// bool smoothComplete(cv::Mat &dst); // implemented easily with zero boundary conditions.
};
PoissonBlender::PoissonBlender()
{
}
void PoissonBlender::copyTo(PoissonBlender &b) const
{
b.setImages(_src, _target, _mask);
}
inline PoissonBlender PoissonBlender::clone() const
{
PoissonBlender b;
copyTo(b);
return b;
}
// constructor
PoissonBlender::PoissonBlender(const cv::Mat &src, const cv::Mat &target, const cv::Mat &mask=cv::Mat())
: _src(src), _target(target), _mask(mask)
{
CV_Assert(_mask.channels()==1);
CV_Assert(_src.cols==_mask.cols && _src.rows==_mask.rows);
}
// set source, tareget and destination images
bool PoissonBlender::setImages(const cv::Mat &src, const cv::Mat &target, const cv::Mat &mask=cv::Mat())
{
_src = src;
_target = target;
_mask = mask;
CV_Assert(_mask.channels()==1);
CV_Assert(_src.cols==_mask.cols && _src.rows==_mask.rows);
return true;
}
// solver sparse linear system
template <typename T, typename Backend>
bool PoissonBlender::solve(const Eigen::SparseMatrix<T> &A, const Eigen::Matrix<T, Eigen::Dynamic, 1> &b,
Eigen::Matrix<T, Eigen::Dynamic, 1> &u)
{
Eigen::SparseLU<Eigen::SparseMatrix<T>, Backend> lu_of_A(A);
if(!lu_of_A.succeeded()) {
std::cerr<< "decomposition failed" << std::endl;
return false;
}
if(!lu_of_A.solve(b,&u)) {
std::cerr<< "solving failed" << std::endl;
return false;
}
return true;
}
// build matrix as linear system
template <typename T>
bool PoissonBlender::buildMatrix(Eigen::SparseMatrix<T> &A, Eigen::Matrix<T, Eigen::Dynamic,1> &b,
Eigen::Matrix<T, Eigen::Dynamic, 1> &u)
{
int w = mask_roi1.width;
int h = mask_roi1.height;
int nz=0;
for(int y=0; y<h-1; ++y) {
uchar *p = mask1.ptr(y);
for(int x=0; x<w-1; ++x, ++p) {
if(*p==0) continue;
int id = y*(w*ch)+(x*ch);
mp[id] = nz++; // r
mp[++id] = nz++; // g
mp[++id] = nz++; // b
}
}
A = Eigen::SparseMatrix<double>(nz, nz);
b = Eigen::VectorXd(nz);
u = Eigen::VectorXd(nz);
int rowA = 0;
A.reserve(5*nz);
for(int y=1; y<h-1; ++y) {
uchar *p = mask1.ptr(y)+1;
cv::Vec3d *drv = drvxy.ptr<cv::Vec3d>(y)+1;
for(int x=1; x<w-1; ++x, ++p, ++drv) {
if(*p==0) continue;
int id = y*(w*ch)+(x*ch);
int tidx=id-ch*w, lidx=id-ch, ridx=id+ch, bidx=id+ch*w;
// to omtimize insertion
uchar tlrb = 15; // 0b1111
if(mask1.at<uchar>(y-1,x)==0) {
*drv -= target1.at<cv::Vec3b>(y-1,x);
tlrb &= 7; //0b0111
}
if(mask1.at<uchar>(y,x-1)==0) {
*drv -= target1.at<cv::Vec3b>(y,x-1);
tlrb &= 11; //0b1011
}
if(mask1.at<uchar>(y,x+1)==0) {
*drv -= target1.at<cv::Vec3b>(y,x+1);
tlrb &= 13; //0b1101
}
if(mask1.at<uchar>(y+1,x)==0) {
*drv -= target1.at<cv::Vec3b>(y+1,x);
tlrb &= 14; //0b1110
}
for(int k=0; k<ch; ++k) {
A.startVec(rowA+k);
if(tlrb&8) A.insertBack(mp[tidx+k], rowA+k) = 1.0; // top
if(tlrb&4) A.insertBack(mp[lidx+k], rowA+k) = 1.0; // left
A.insertBack(mp[id +k], rowA+k) = -4.0;// center
if(tlrb&2) A.insertBack(mp[ridx+k], rowA+k) = 1.0; // right
if(tlrb&1) A.insertBack(mp[bidx+k], rowA+k) = 1.0; // bottom
}
b(rowA+0) = cv::saturate_cast<double>((*drv)[0]);
b(rowA+1) = cv::saturate_cast<double>((*drv)[1]);
b(rowA+2) = cv::saturate_cast<double>((*drv)[2]);
rowA+=ch;
}
}
A.finalize();
CV_Assert(nz==rowA);
return true;
}
template <typename T>
bool PoissonBlender::copyResult(Eigen::Matrix<T, Eigen::Dynamic, 1> &u)
{
int w = mask_roi1.width;
int h = mask_roi1.height;
for(int y=1; y<h-1; ++y) {
uchar *pd = dst1.ptr(y);
uchar *pm = mask1.ptr(y)+1;
for(int x=1; x<w-1; ++x, ++pm) {
if(*pm==0) {
pd += 3;
} else {
int idx = mp[y*(w*ch)+(x*ch)];
*pd++ = cv::saturate_cast<uchar>(u[idx+0]);
*pd++ = cv::saturate_cast<uchar>(u[idx+1]);
*pd++ = cv::saturate_cast<uchar>(u[idx+2]);
}
}
}
return true;
}
bool
PoissonBlender::seamlessClone(cv::Mat &_dst, const int offx, const int offy, const bool mix=false)
{
//
ch = _target.channels();
cv::Point offset(offx, offy);
cv::Point tl(_mask.size()), br(-1,-1);
// calc bounding box
for(int y=0; y<_mask.rows; ++y) {
uchar *p = _mask.ptr(y);
for(int x=0; x<_mask.cols; ++x,++p) {
if(*p==0) continue;
if(tl.x>x) tl.x=x;
if(tl.y>y) tl.y=y;
if(br.x<x) br.x=x;
if(br.y<y) br.y=y;
}
}
br.x += 1;
br.y += 1;
// add borders
cv::Rect mask_roi(tl, br);
cv::Rect mask_roi2(tl-cv::Point(2,2), br+cv::Point(2,2));
cv::Mat _srcUp, _targetUp, _maskUp, _dstUp;
cv::copyMakeBorder(_src, _srcUp, 2,2,2,2, cv::BORDER_REPLICATE);
cv::copyMakeBorder(_target, _targetUp, 2,2,2,2, cv::BORDER_REPLICATE);
cv::copyMakeBorder(_mask, _maskUp, 1,1,1,1, cv::BORDER_CONSTANT);
// allocate destination image
_dstUp = _targetUp.clone();
_dst = cv::Mat(_dstUp, cv::Rect(2,2,_dstUp.cols-2, _dstUp.rows-2));
mask_roi1 = cv::Rect(tl-cv::Point(1,1), br+cv::Point(1,1));
mask1 = cv::Mat(_mask, mask_roi1);
target1 = cv::Mat(_targetUp, mask_roi1+offset-cv::Point(1,1));
dst1 = cv::Mat(_dstUp, mask_roi1+offset-cv::Point(1,1));
cv::Mat src(_srcUp, mask_roi2);
cv::Mat target(_targetUp, mask_roi2+offset-cv::Point(2,2));
cv::Mat mask(_mask, mask_roi2);
cv::Mat dst(_dstUp, mask_roi2+offset-cv::Point(2,2));
CV_Assert(src.cols==dst.cols && src.rows==dst.rows);
// calc differential image
cv::Mat src64, target64;
int pw = mask_roi2.width-1, ph = mask_roi2.height-1;
src.convertTo(src64, CV_64F);
target.convertTo(target64, CV_64F);
cv::Rect roi00(0,0,pw,ph), roi10(1,0,pw,ph), roi01(0,1,pw,ph);
cv::Mat _src64_00(src64, roi00), _target64_00(target64, roi00);
cv::Mat src_dx = cv::Mat(src64, roi10) - _src64_00;
cv::Mat src_dy = cv::Mat(src64, roi01) - _src64_00;
cv::Mat target_dx = cv::Mat(target64,roi10) - _target64_00;
cv::Mat target_dy = cv::Mat(target64,roi01) - _target64_00;
// gradient mixture
cv::Mat Dx, Dy;
if(mix) { // with gradient mixture
cv::Mat pdx_src[ch], pdy_src[ch], pdx_target[ch], pdy_target[ch];
cv::split(src_dx, pdx_src);
cv::split(src_dy, pdy_src);
cv::split(target_dx, pdx_target);
cv::split(target_dy, pdy_target);
cv::Mat _masks_dx, _masks_dy;
for(int i=0; i<ch; i++) {
_masks_dx = cv::abs(pdx_src[i]) < cv::abs(pdx_target[i]);
_masks_dy = cv::abs(pdy_src[i]) < cv::abs(pdy_target[i]);
pdx_target[i].copyTo(pdx_src[i], _masks_dx);
pdy_target[i].copyTo(pdy_src[i], _masks_dy);
}
cv::merge(pdx_src, ch, Dx);
cv::merge(pdy_src, ch, Dy);
} else { // without gradient mixture
Dx = src_dx;
Dy = src_dy;
}
// lapilacian
int w = pw-1, h = ph-1;
drvxy = cv::Mat(Dx,cv::Rect(1,0,w,h)) - cv::Mat(Dx,cv::Rect(0,0,w,h))
+ cv::Mat(Dy,cv::Rect(0,1,w,h)) - cv::Mat(Dy,cv::Rect(0,0,w,h));
//
// solve an poisson's equation
//
Eigen::SparseMatrix<double> A;
Eigen::VectorXd b;
Eigen::VectorXd u;
// build right-hand and left-hand matrix
buildMatrix<double>(A, b, u);
// solve sparse linear system
solve<double, Eigen::UmfPack>(A, b, u);
// copy computed result to destination image
copyResult<double>(u);
return true;
}
}
#endif /* __POISSON_BLENDING_H__ */
main.cpp
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "poisson_blending.hpp"
void
show_usage()
{
// src_file: source image file.
// target_file: target image file (to which the source image is copied).
// mask_file: mask image file (whose size must be the same as soure image).
// offset_x: offset x-coordinate , which indicates where the origin of source image is copied.
// offset_y: offset y-coordinate , which indicates where the origin of source image is copied.
// mix: flag which indicates gradient-mixture is used.
std::cout << "./pb source_image target_image mask_image offset_x offset_y mix" << std::endl;
}
int
main(int argc, char* argv[])
{
if(argc < 7) {
show_usage();
return -1;
}
std::string src_file = argv[1];
std::string target_file = argv[2];
std::string mask_file = argv[3];
int offx = atoi(argv[4]);
int offy = atoi(argv[5]);
bool mix = (argv[6]=="true" || atoi(argv[6])>0) ? true : false;
cv::Mat src_img = cv::imread(src_file, 1);
if(!src_img.data) return -1;
cv::Mat target_img = cv::imread(target_file, 1);
if(!target_img.data) return -1;
cv::Mat mask_img = cv::imread(mask_file, 0);
if(mask_img.empty()) return -1;
//cv::Ptr<Blend::PoissonBlender> pb = &Blend::PoissonBlender(src_img, target_img, mask_img);
Blend::PoissonBlender pb = Blend::PoissonBlender(src_img, target_img, mask_img);
cv::Mat dst_img;
double f = 1000.0/cv::getTickFrequency();
int64 time = cv::getTickCount();
pb.seamlessClone(dst_img, offx, offy, mix);
std::cout<<(cv::getTickCount()-time)*f<<" [ms]"<<std::endl;
cv::imwrite("test.png", dst_img);
return 0;
}








