讀源碼學算法之poisson matting

Matting中文譯爲摳圖,是圖像處理中一個很基本的問題。跟分割不太一樣,摳圖往往更加精確,甚至連頭髮絲都要抽取出來,而分割往往很難做到。摳圖主要應用在圖像合成中,如下所示,摳出的興趣部分可以合成新的場景。
在這裏插入圖片描述

摳圖問題定義爲:I=αF+(1α)BI =\alpha F+(1-\alpha)B (1)
其中, II 表示圖像本身,由前景 FF 和前景 BB 通過透明度參數 α[0,1]\alpha \in [0,1] 合成。
最常見的比如一個玻璃瓶,那麼它的顏色基本就是自身的顏色跟背景色的合成。

因此,摳圖主要根據輸入圖像 II, 求解右側的三個未知數F,B,αF,B,\alpha, 很顯然這是一個病態問題。
這裏主要講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)求導:

I=(FB)α+αF+(1α)B\nabla I = (F-B)\nabla\alpha + \alpha\nabla F + (1-\alpha)\nabla B

假設背景和前景光滑變化,那麼:F=0,B=0\nabla F = 0, \nabla B = 0, 因此有:

I=(FB)α\nabla I = (F-B)\nabla\alpha

因此有:

α=I/(FB)\nabla \alpha = \nabla I /(F-B)

從而我們可以得到對應的poisson方程:

α=div(I/(FB))\triangle\alpha = div(\nabla I/(F-B))

在圖像中,離散的情況下,每個像素僅僅考慮四聯通域:

α=αi,j+1+αi,j1+αi+1,j+αi1,j4αi,j\triangle\alpha = \alpha_{i,j+1} + \alpha_{i,j-1} + \alpha_{i+1,j} + \alpha_{i-1,j} - 4 \alpha_{i,j}

divx(I/(FB))=(Ix(FB)I(FB)x)/(FB)2div_x(\nabla I/(F-B)) = (\nabla I_x(F-B) - \nabla I(F-B)_x) / (F-B)^2

divy(I/(FB))=(Iy(FB)I(FB)y)/(FB)2div_y(\nabla I/(F-B)) = (\nabla I_y(F-B) - \nabla I(F-B)_y) / (F-B)^2

div(I/(FB))=divx(I/(FB))+divy(I/(FB))div(\nabla I/(F-B)) = div_x(\nabla I/(F-B)) +div_y(\nabla I/(F-B))

於是有:

4αi,j=αi,j+1+αi,j1+αi+1,j+αi1,jdiv(I/(FB))4\alpha_{i,j} = \alpha_{i,j+1} + \alpha_{i,j-1} + \alpha_{i+1,j} + \alpha_{i-1,j} - div(\nabla I/(F-B))

接下來就可以迭代求解 αi,j\alpha_{i,j}, 如果其值較大,就將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));
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章