Matting中文譯爲摳圖,是圖像處理中一個很基本的問題。跟分割不太一樣,摳圖往往更加精確,甚至連頭髮絲都要抽取出來,而分割往往很難做到。摳圖主要應用在圖像合成中,如下所示,摳出的興趣部分可以合成新的場景。
摳圖問題定義爲: (1)
其中, 表示圖像本身,由前景 和前景 通過透明度參數 合成。
最常見的比如一個玻璃瓶,那麼它的顏色基本就是自身的顏色跟背景色的合成。
因此,摳圖主要根據輸入圖像 , 求解右側的三個未知數, 很顯然這是一個病態問題。
這裏主要講Jian Sun 和 Jiaya Jia發表於SIGGRAPH 2004的經典工作:poisson matting
論文鏈接:http://kucg.korea.ac.kr/new/seminar/2009/src/PA-09-07-29.pdf
Github代碼:https://github.com/xuhongxu96/PoissonMatting
摳圖難的部分往往在於前景的邊緣,比如人的頭髮等,除此之外的部分要麼是確定的前景,要麼是確定的背景,實際上我們只需要對那些不確定的區域求解。因此往往給定一個trimap, 如上圖所示,確定的前景用白色表示,確定的背景用黑色表示,剩下的不確定部分使用灰色表示。那麼,灰色區域也就是我們需要求解的部分。
首先對(1)求導:
假設背景和前景光滑變化,那麼:, 因此有:
因此有:
從而我們可以得到對應的poisson方程:
在圖像中,離散的情況下,每個像素僅僅考慮四聯通域:
於是有:
接下來就可以迭代求解 , 如果其值較大,就將trimap中相應的位置更新爲前景,反之更新爲背景。
流程圖(參考自github)如下所示:
加註釋的源代碼如下:
/****************************************************/
// Some useful function
#define I(x, y) (intensity(image(inY(image, y), inX(image, x))))
#define FmB(y, x) (FminusB(inY(FminusB, y), inX(FminusB, x)))
double dist_sqr(cv::Point p1, cv::Point p2) {
return (p1.x - p2.x) *(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y);
}
//顏色距離,就是兩個顏色的最大值相減
int color_dis(cv::Vec3b p1, cv::Vec3b p2) {
int t1 = fmax(fmax(p1[0], p1[1]), p1[2]);
int t2 = fmax(fmax(p2[0], p2[1]), p2[2]);
return t1 - t2;
}
//防止x,y越界
int inX(cv::Mat &image, int x) {
if (x < 0) x = 0;
if (x >= image.cols) x = image.cols - 1;
return x;
}
int inY(cv::Mat &image, int y) {
if (y < 0) y = 0;
if (y >= image.rows) y = image.rows - 1;
return y;
}
//intensity,三通道的最大值
double intensity(cv::Vec3b v) {
return fmax(fmax(v[0], v[1]), v[2]);
}
/***************************************************/
//找出所有邊界像素的位置
std::vector<cv::Point> PoissonMatting::findBoundaryPixels(const cv::Mat_<uchar> &trimap, int a, int b){
std::vector<cv::Point> result;
for (int x = 1; x < trimap.cols - 1; ++x) {
for (int y = 1; y < trimap.rows - 1; ++y) {
if (trimap(y, x) == a) {
if (trimap(y-1, x)==b || trimap(y+1, x)==b || trimap(y, x-1)==b || trimap(y, x+1)==b) {
result.push_back(cv::Point(x, y));
}
}
}
}
return result;
}
void PoissonMatting::_matting(cv::Mat _image, cv::Mat _trimap, cv::Mat &_foreground, cv::Mat &_alpha){
const cv::Mat_<cv::Vec3b> &image = static_cast<const cv::Mat_<cv::Vec3b> &>(_image);
cv::Mat_<uchar> &trimap = static_cast<cv::Mat_<uchar> &>(_trimap);
_foreground.create(image.size(), CV_8UC3);
_alpha.create(image.size(), CV_8UC1);
cv::Mat_<cv::Vec3b> &foreground = static_cast<cv::Mat_<cv::Vec3b>&>(_foreground);
cv::Mat_<uchar> &alpha = static_cast<cv::Mat_<uchar>&>(_alpha);
cv::Mat_<double> FminusB = cv::Mat_<double>::zeros(trimap.rows, trimap.cols);
for (int times = 0; times < 5; ++times) {
std::vector<cv::Point> foregroundBoundary = findBoundaryPixels(trimap, 255, 128);
std::vector<cv::Point> backgroundBoundary = findBoundaryPixels(trimap, 0, 128);
cv::Mat_<uchar> trimap_blur;
cv::GaussianBlur(trimap, trimap_blur, cv::Size(9, 9), 0);
// 構建圖像上每個位置的 F-B
for (int x = 0; x < trimap.cols; ++x) {
for (int y = 0; y < trimap.rows; ++y) {
cv::Point current;
current.x = x;
current.y = y;
if (trimap_blur(y, x) == 255) { //確定的前景部分F-(0,0,0)
FminusB(y, x) = color_dis(image(y, x), cv::Vec3b(0, 0, 0));
} else if (trimap_blur(y, x) == 0) { //確定的背景部分(0,0,0)-B
FminusB(y, x) = color_dis(cv::Vec3b(0, 0, 0), image(y, x));
} else {
// 未知區域的每個位置尋找距離最近的前景像素位置和背景像素位置
cv::Point nearestForegroundPoint, nearestBackgroundPoint;
double nearestForegroundDistance = 1e9, nearestBackgroundDistance = 1e9;
for(cv::Point &p : foregroundBoundary) {
double t = dist_sqr(p, current);
if (t < nearestForegroundDistance) {
nearestForegroundDistance = t;
nearestForegroundPoint = p;
}
}
for(cv::Point &p : backgroundBoundary) {
double t = dist_sqr(p, current);
if (t < nearestBackgroundDistance) {
nearestBackgroundDistance = t;
nearestBackgroundPoint = p;
}
}
FminusB(y, x) = color_dis(image(nearestForegroundPoint.y, nearestForegroundPoint.x),
image(nearestBackgroundPoint.y, nearestBackgroundPoint.x));
if (FminusB(y, x) == 0)
FminusB(y, x) = 1e-9;
}
}
}
// F-B高斯平滑
cv::GaussianBlur(FminusB, FminusB, cv::Size(9, 9), 0);
// Solve the Poisson Equation By The Gauss-Seidel Method (Iterative Method)
for (int times2 = 0; times2 < 300; ++times2) {
for (int x = 0; x < trimap.cols; ++x) {
for (int y = 0; y < trimap.rows; ++y) {
//白色(F) 黑色(B) 灰色(未知)
if (trimap(y, x) == 128) {
// 計算 (▽I/F-B)在所有位置的散度dvgX:(u/v)' = (u'v-uv')/v^2
double dvgX = ( (I(x + 1, y) + I(x - 1, y) - 2 * I(x, y)) * FmB(y, x)
- (I(x + 1, y) - I(x, y)) * (FmB(y, x + 1) - FmB(y, x)) )
/ (FmB(y, x) * FmB(y, x));
// 計算 (▽I/F-B)在所有位置的散度dvgY:(u/v)' = (u'v-uv')/v^2
double dvgY = ( (I(x, y + 1) + I(x, y - 1) - 2 * I(x, y)) * FmB(y, x)
- (I(x, y + 1) - I(x, y)) * (FmB(y + 1, x) - FmB(y, x)) )
/ (FmB(y, x) * FmB(y, x));
double dvg = dvgX + dvgY;
// a的散度爲:a(i+1,j)+a(i-1,j)+a(i,j+1)+a(i,j-1)-4a(i,j) = dvg, 因此得到下面的式子
double newAlpha = ((double)alpha(y, x + 1)
+ alpha(y, x - 1)
+ alpha(y + 1, x)
+ alpha(y - 1, x)
- dvg * 255.0) / 4.0;
// 根據得到的alpha更新Trimap
if (newAlpha > 253) trimap(y, x) = 255;
else if (newAlpha < 3) trimap(y, x) = 0;
if (newAlpha < 0) newAlpha = 0;
if (newAlpha > 255) newAlpha = 255;
// 更新alpha
alpha(y, x) = newAlpha;
}
else if (trimap(y, x) == 255) alpha(y, x) = 255; //前景區域不需計算
else if (trimap(y, x) == 0) alpha(y, x) = 0; //背景區域不需計算
}
}
}
}
// 得到了alpha,因此可以合成紅色背景的新圖像 I = a Image + (1-a)Red ???
for (int x = 0; x < alpha.cols; ++x) {
for (int y = 0; y < alpha.rows; ++y) {
foreground(y, x) = ((double) alpha(y, x) / 255) * image(y, x) + ((255.0 - alpha(y, x)) / 255 * cv::Vec3b(0, 0, 255));
}
}
}