人脸变形算法的实现——双线性插值
我们在对图像做仿射变换时得到的点一般情况下并不会是整数点,在改进篇中,简单起见,我直接对得到的点直接进行了取整,所以在面部有的地方看起来会很模糊。
除了取整,最常用的有二种方法:最邻近插值法和双线性插值法,今天我们就用双线性插值法来改进原来的算法。
首先介绍一下双线性插值(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就是运用以上算法实现 :)