shikailun的日志

人脸变形算法的实现——双线性插值

我们在对图像做仿射变换时得到的点一般情况下并不会是整数点,在改进篇中,简单起见,我直接对得到的点直接进行了取整,所以在面部有的地方看起来会很模糊。

除了取整,最常用的有二种方法:最邻近插值法双线性插值法,今天我们就用双线性插值法来改进原来的算法。

首先介绍一下双线性插值(Bilinear Interpolation),因为三角形变形时我们不能保证像素之间的一一对应关系,但要尽量保持像素点之间的连续性;

比如三角形DEF上某个点经过仿射变换后的坐标是(1.9,1.6),在上篇中我是直接当做(1,1),可以看到这种做法其实是非常不合适的,在最邻近插值法我们会取(2,2),简单且直观,但得到的图像质量不高。而双线性插值法认为,该点的颜色值应由它上下左右四个像素值确定,即(1.9, 1.6)像素的颜色值应该由(1, 1)、(2, 1)、(1, 2)、(2, 1)四个像素共同决定,而这四个像素的加权关系则由该点到四个像素的距离决定。

一般的f(i+u,j+v)由原图像中坐标为 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所对应的周围四个像素的值决定,即:

f(i+u,j+v) = (1-u)(1-v)f(i,j) + (1-u)vf(i,j+1) + u(1-v)f(i+1,j) + uvf(i+1,j+1)

其中f(i,j)表示图像(i,j)处的的像素值。

相比前者,双线性内插值法计算量大,但缩放后图像质量高,不会出现像素值不连续的的情况。

现在开始改进我们的仿射变换类,不再进行取整,直接返回计算得到的坐标值

public static double[] getAffineTransformationQ2(Point p, Point A, Point B, Point C, Point D, Point E, Point F)
{
    int XA = A.X; int XB = B.X; int XC = C.X; int XP = p.X;
    int YA = A.Y; int YB = B.Y; int YC = C.Y; int YP = p.Y;
    int a1 = XA - XC;
    int b1 = XB - XC;
    int c1 = XP - XC;
    int a2 = YA - YC;
    int b2 = YB - YC;
    int c2 = YP - YC;
    double x = (c1 * b2 - c2 * b1 + 0.0) / (a1 * b2 - a2 * b1 + 0.0);
    double y = (c1 * a2 - c2 * a1 + 0.0) / (a2 * b1 - a1 * b2 + 0.0);
    int XD = D.X; int XE = E.X; int XF = F.X;
    int YD = D.Y; int YE = E.Y; int YF = F.Y;
    double XQ = (x * XD + y * XE + (1 - x - y) * XF);
    double YQ = (x * YD + y * YE + (1 - x - y) * YF);
    double[] points = new double[2];
    points[0] = XQ;
    points[1] = YQ;
    return points;
}

然后计算双线性内插值

public static Color getBilinearInterpolationColor(Bitmap bmp, double x, double y)
{
    int A3 = (int)((1 - (x - (int)x)) * (1 - (y - (int)y)) * bmp.GetPixel((int)x, (int)y).A
            + (1 - (x - (int)x)) * (y - (int)y) * bmp.GetPixel((int)x, (int)y + 1).A
            + (x - (int)x) * (1 - (y - (int)y)) * bmp.GetPixel((int)x + 1, (int)y).A
            + (x - (int)x) * (y - (int)y) * bmp.GetPixel((int)x + 1, (int)y + 1).A);
    int R3 = (int)((1 - (x - (int)x)) * (1 - (y - (int)y)) * bmp.GetPixel((int)x, (int)y).R
            + (1 - (x - (int)x)) * (y - (int)y) * bmp.GetPixel((int)x, (int)y + 1).R
            + (x - (int)x) * (1 - (y - (int)y)) * bmp.GetPixel((int)x + 1, (int)y).R
            + (x - (int)x) * (y - (int)y) * bmp.GetPixel((int)x + 1, (int)y + 1).R);
    int G3 = (int)((1 - (x - (int)x)) * (1 - (y - (int)y)) * bmp.GetPixel((int)x, (int)y).G
            + (1 - (x - (int)x)) * (y - (int)y) * bmp.GetPixel((int)x, (int)y + 1).G
            + (x - (int)x) * (1 - (y - (int)y)) * bmp.GetPixel((int)x + 1, (int)y).G
            + (x - (int)x) * (y - (int)y) * bmp.GetPixel((int)x + 1, (int)y + 1).G);
    int B3 = (int)((1 - (x - (int)x)) * (1 - (y - (int)y)) * bmp.GetPixel((int)x, (int)y).B
            + (1 - (x - (int)x)) * (y - (int)y) * bmp.GetPixel((int)x, (int)y + 1).B
            + (x - (int)x) * (1 - (y - (int)y)) * bmp.GetPixel((int)x + 1, (int)y).B
            + (x - (int)x) * (y - (int)y) * bmp.GetPixel((int)x + 1, (int)y + 1).B);
    return Color.FromArgb(A3, R3, G3, B3);
}

通过这几篇文章的介绍,我们已经可以进行任意人脸的变形了。

从连续性角度来说,双线性插值法已经可以满足一般性的需求了。

变形过程中可能存在着像素或细节损失,这是不可避免的。

下面几个GIF就是运用以上算法实现 :)

Posted on
This entry was posted in technology  and tagged BabyMaker  图像处理  算法