變老特效是如何實現的呢?下面簡單講講我做變老特效時的思路。
1. 面部皺紋紋理。變老後的皺紋並不是實時繪製,而是需要先準備一系列皺紋紋理,然後通過紋理轉移算法將皺紋紋理轉移至正常面部紋理上。
2. 由於照片尺寸不同,即使相同照片尺寸,檢測的人臉尺寸也不會相同,而皺紋紋理尺寸是固定的,所以需要對皺紋紋理做合適的縮放纔可以,這個簡單,正常的圖片縮放算法即可。
3. 由於檢測的人臉姿態多變,有的頭正頸直,有的歪頭,有的仰頭,而皺紋紋理都是頭正頸直的,所以還需要對皺紋紋理做合適的變形,而非旋轉,旋轉的話匹配精度不夠。本文采用的是移動最小二乘法圖像變形(mls)算法。
4. 由於涉及到mls變形算法,所以需要控制點。本文采用的是提取人臉68個特徵點,由於標準68個特徵點主要分佈在臉頰、眉毛、眼睛、鼻子、嘴巴,爲了保證變形更精準,所以又計算增補了部分額頭控制點,共計78個特徵點作爲變形控制點。
5. 由於照片亮度與皺紋紋理亮度會有一些差異,所以還需要修正皺紋紋理亮度,本文采用的方法比較簡單,先計算檢測到的人臉亮度均值,然後採用gamma校正皺紋紋理亮度。其他一些細節,可以參考代碼。
下面是算法主流程代碼:
void FacialAging(BMPINFO *pSrcBitmap, BMPINFO *pTexBitmap, MLS_Point *src_points, MLS_Point *mls_points, MLS_Point *tex_points, int pt_count, float strength)
{
// 縮放紋理
float ratio = 0.0f;
ScaleTexture(pTexBitmap, src_points, tex_points, &ratio);
// 變換控制點及變形點
memcpy(mls_points, src_points, pt_count*sizeof(MLS_Point));
TransformPQ(mls_points, tex_points, pTexBitmap->lWidth, pTexBitmap->lHeight, ratio, pt_count);
// 計算源/紋理圖像臉部區域
Face_Rect src_face_rect, tex_face_rect;
CompFaceRect(&src_face_rect, src_points);
CompFaceRect(&tex_face_rect, mls_points);
if (!CheckFaceRect(pSrcBitmap, &src_face_rect))
{
return;
}
// 圖像變形
ImageWarp_MLS(pTexBitmap, tex_points, mls_points, pt_count);
// 提取臉部區域
BMPINFO src_facebmp = { 0 }, tex_facebmp = { 0 }, dst_facebmp = { 0 };
src_facebmp.dwPixelFormat = BMPFORMAT_RGB32_R8G8B8A8;
src_facebmp.lWidth = src_face_rect.width;
src_facebmp.lHeight = src_face_rect.height;
src_facebmp.lPitch[0] = src_facebmp.lWidth * 4;
src_facebmp.pPlane[0] = (uchar*)malloc(src_facebmp.lPitch[0] * src_facebmp.lHeight);
dst_facebmp.dwPixelFormat = BMPFORMAT_RGB32_R8G8B8A8;
dst_facebmp.lWidth = src_face_rect.width;
dst_facebmp.lHeight = src_face_rect.height;
dst_facebmp.lPitch[0] = dst_facebmp.lWidth * 4;
dst_facebmp.pPlane[0] = (uchar*)malloc(dst_facebmp.lPitch[0] * dst_facebmp.lHeight);
tex_facebmp.dwPixelFormat = BMPFORMAT_RGB32_R8G8B8A8;
tex_facebmp.lWidth = tex_face_rect.width;
tex_facebmp.lHeight = tex_face_rect.height;
tex_facebmp.lPitch[0] = tex_facebmp.lWidth * 4;
tex_facebmp.pPlane[0] = (uchar*)malloc(tex_facebmp.lPitch[0] * tex_facebmp.lHeight);
ExtractFaceRect(pSrcBitmap, &src_facebmp, &src_face_rect);
ExtractFaceRect(pTexBitmap, &tex_facebmp, &tex_face_rect);
float posx = 0.0f, posy = 0.0f;
uchar red = 0, green = 0, blue = 0;
uchar *pSrcData = dst_facebmp.pPlane[0];
for (int i = 0; i < dst_facebmp.lHeight; i++)
{
posy = ((float)i / dst_facebmp.lHeight)*tex_facebmp.lHeight;
for (int j = 0; j < dst_facebmp.lWidth; j++, pSrcData += 4)
{
posx = ((float)j / dst_facebmp.lWidth)*tex_facebmp.lWidth;
BilinearInterRGB(tex_facebmp.pPlane[0], posx, posy, tex_facebmp.lWidth, tex_facebmp.lHeight, &blue, &green, &red);
pSrcData[AXJ_BLUE] = blue;
pSrcData[AXJ_GREEN] = green;
pSrcData[AXJ_RED] = red;
}
}
// 修正皺紋紋理亮度
float sum = 0.0f, mean = 0.0f;
int size = src_facebmp.lWidth*src_facebmp.lHeight;
uchar luminance = 0;
pSrcData = src_facebmp.pPlane[0];
for (int i = 0; i < size; i++, pSrcData += 4)
{
luminance = (pSrcData[0] + pSrcData[1] + pSrcData[2]) / 3;
sum += luminance;
}
mean = sum / size;
//
free(src_facebmp.pPlane[0]);
free(tex_facebmp.pPlane[0]);
free(dst_facebmp.pPlane[0]);
}
下面是一些效果圖,由於用的皺紋紋理都是同一張,所以變老效果基本一樣。算法效果還可以繼續改進,不過現在也懶得弄了。