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