盒子
盒子
文章目录
  1. 前言
  2. 流程图
  3. 具体实现
    1. 统计人脸平均肤色
    2. 皮肤检测
    3. 肤色变换
    4. 融合

Android图像处理-简单的实时肤色变换实现

前言

肤色变换是一件非常有意思的事情,问题的定义是:给定一张图片且图中只有一个人,一个目标颜色(Target Color),将图中人的肤色变为目标颜色。比如图中有一个白人,目标颜色设为c,经过肤色变换,图中白人的肤色就自然的过渡为c。

看到这个问题,凭空想觉得方法非常简单,无非两步:(1)检测出皮肤区域(2)对皮肤区域进行颜色变换。但在实现过程中会有几个问题:

  • 肤色检测不准确。
  • 如果只是纯粹把肤色区域直接变为目标颜色,会让皮肤失去皮肤纹理,变得很不真实。

因此本文针对这些问题,实现了一个比较简单的肤色变换功能。

流程图

本文的肤色变换流程图如下图:

为了加快整个过程的处理速度,每个步骤的算法都是OpenGL Shader实现的,整体速度非常快。

具体实现

统计人脸平均肤色

这个需要依赖现有的人脸检测库,获得人脸检测库返回的83个点后,利用83个点中的部分点画出人脸中的局部mask(此处并不需要很精确的获得整个人脸,只需要获得部分肯定为皮肤的光滑区域),具体方法为:利用第0到18点画出部分区域,然后扣除眼睛、鼻子、嘴巴区域。效果如下:

接着统计Mask中白色区域的平均颜色,即获得了当前人脸的平均肤色,记为avgColor。

皮肤检测

本文中,皮肤检测(Skin Detection)是基于像素的多尺度动态阈值算法,总共分为二步。

第一步是皮肤检测算法,本文的皮肤检测是基于YCgCb颜色空间的多尺度动态阈值模型,生成SkinMask,该Mask的每个像素的取值为0到1,表示是皮肤的概率,该算法的特点:

  • 判断当前像素是否为皮肤不依赖其他像素,这也方便了我们实现为shader版本。
  • 动态阈值的意思是皮肤的判断基于当前人脸统计的平均肤色,而不是静态的模型,而是avgColor为中心的概率模型。
  • 多尺度的意思是对原图分别进行1/2,1/4,1/8的缩放,分别生成SkinMask后,再进行融合生成最终的SkinMask。

效果如下:

后来发现利用深度学习构建卷积神经网络进行皮肤检测效果比基于像素的皮肤检测好。

具体的fragment shader如下,其中x和y为avgColor的Cg和Cb分量值:

precision highp float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform float x;//中心点x坐标
uniform float y;//中心点y坐标
float isSkinColor(vec4 color) {
float cg1 = dot(vec3(-81.085,112.0,-30.915), color.rgb);
float cb1 = dot(vec3(-37.797,-74.203,112.0), color.rgb);
float cg = cg1+128.0-x;
float cb = cb1+128.0-y;
float v = max(sqrt(cg*cg+cb*cb)-14.0, 0.0);
float s = pow(2.718281828459, -v / 4.0);
float gray = dot(vec3(0.3,0.6,0.1), color.rgb);
float s1 = min(1.0, gray/0.25);
s = s * (s1*s1);
s = min(1.0, s);
return s;
}
void main()
{
vec4 texColor = texture2D(inputImageTexture, textureCoordinate);
float s = isSkinColor(texColor);
gl_FragColor = vec4(s,s,s, 1.0);
}

可以看出第一步获得的SkinMask可能是不精确的,如果背景有物体的颜色和当前人的肤色一样,会被认为是皮肤,为了让SkinMask更精确,我们需要使用人像分割技术(Fabby),把背景抠除。

第二步是利用现有的Fabby库对原图做操作,将背景扣除,获得FabbyMask,效果如下:

接着我们结合SkinMask和FabbyMask生成最终的FinalMask,效果如下:

肤色变换

肤色变换实际上是颜色变换(Color Transfer)的一种应用,最早的颜色变换论文是”Color Transfer Between Images”,该论文给定一张source image,给定一张target image,将source image的颜色基调变为target image的颜色基调。效果如下:

但是这篇论文中的颜色变换会对source image的整体颜色进行变换(global image recoloring),因此”Interactive Local Color Transfer Between Images”提出了局部颜色变换的概念,即对source image的一个部分进行颜色变换(尽量不改变其中的其他区域,实践证明效果一般,针对论文中的图片效果还不错)。该论文提出了一种交互式的局部颜色变换算法,先在source image中圈出一小块你需要变色的区域(不需要很精确,或指定一个颜色,记为sacolor),然后指定target color,接着该算法能自动将source image中越接近sacolor的颜色变换为target color。

这篇论文也给出了将该算法应用到肤色变换的效果,如下:

我们这里实现的肤色变换算法就是基于这两篇论文,因此步骤为:

  • 获得sacolor:当前图片中人脸的肤色,该颜色我们已经在先前获得。
  • 指定targetcolor:目标颜色,下面指定为绿色。
  • 肤色变换,该算法基于LAB颜色空间。

LAB中每个分量的含义如下:

  • L表示亮度(0,100),其中0表示黑色,100表示白色。
  • A表示红绿(-128,127),其中-128表示绿色,127表示红色。
  • B表示黄蓝(-128,127),其中-128表示蓝色,127表示黄色。

这里对其取值范围进行了一些转换,因此取值范围有些不同。

RGB转LAB:

vec3 rgb2lab(vec4 color) {
float L = 0.3811 * color.b + 0.5783 * color.g + 0.0402 * color.r;
float M = 0.1967 * color.b + 0.7244 * color.g + 0.0782 * color.r;
float S = 0.0241 * color.b + 0.1288 * color.g + 0.8444 * color.r;
if (L == 0.0) L = 1.0;
if (M == 0.0) M = 1.0;
if (S == 0.0) S = 1.0;
L = log(L);
M = log(M);
S = log(S);
float ll = (L + M + S) / sqrt(3.0);
float aa = (L + M - 2.0 * S) / sqrt(6.0);
float bb = (L - M) / sqrt(2.0);
return vec3(ll, aa, bb);
}

LAB转RGB:

vec4 lab2rgb(vec3 lab) {
float L = lab.r / sqrt(3.0) + lab.g / sqrt(6.0) + lab.b / sqrt(2.0);
float M = lab.r / sqrt(3.0) + lab.g / sqrt(6.0) - lab.b / sqrt(2.0);
float S = lab.r / sqrt(3.0) - 2.0 * lab.g / sqrt(6.0);
L = exp(L);
M = exp(M);
S = exp(S);
float b = 4.4679 * L - 3.5873 * M + 0.1193 * S;
float g = -1.2186 * L + 2.3809 * M - 0.1624 * S;
float r = -0.0497 * L - 0.2439 * M + 1.2045 * S;
if (b > 1.0) b = 1.0;
if (b < 0.0) b = 0.0;
if (g > 1.0) g = 1.0;
if (g < 0.0) g = 0.0;
if (r > 1.0) r = 1.0;
if (r < 0.0) r = 0.0;
return vec4(r,g,b, 1.0);
}

接着就是颜色变换算法,该算法的思路为:对于原图的每个像素color,计算color与sacolor的距离,如果距离越近,那么变换成越接近targetcolor的颜色,否则,颜色改变越少。实现如下:

precision highp float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform float skinl; //当前人脸平均肤色的L分量
uniform float skina; //当前人脸平均肤色的A分量
uniform float skinb; //当前人脸平均肤色的B分量
uniform float targetl;//目标颜色的L分量
uniform float targeta;//目标颜色的A分量
uniform float targetb;//目标颜色的B分量
vec3 rgb2lab(vec4 color) {
...
}
vec4 lab2rgb(vec3 lab) {
...
}
float euclidean(vec3 e1, vec3 e2) {
return sqrt((e1.r - e2.r) * (e1.r - e2.r) + (e1.g - e2.g) * (e1.g - e2.g) + (e1.b - e2.b) * (e1.b - e2.b));
}
void main()
{
vec4 texColor = texture2D(inputImageTexture, textureCoordinate);
vec3 lab = rgb2lab(texColor);
vec3 skin = vec3(skinl, skina, skinb);
float distance = euclidean(lab, skin);
distance = distance / 5.0;
float f = 1.0 / exp(3.0 * distance * distance);
vec3 result_lab;
result_lab.r = lab.r + f * (targetl - skinl);
result_lab.g = lab.g + f * (targeta - skina);
result_lab.b = lab.b + f * (targetb - skinb);
gl_FragColor = lab2rgb(result_lab);
}

效果如下:

论文中指出,颜色变换比较适合景色的颜色变换,对肤色变换效果不一定很理想。

融合

最后用FinalMask对TransferImg进行过滤,最后生成的效果如下:

效果看起来还可以,但是边界处的变化较为明显,比较突兀。

支持一下
扫一扫,支持xiazdong