本文介绍了拍摄场景中染发作用的完成。首要涉及头发分割才干的接入,Metal 与 OpenGL 之间的纹路转换。

前言

完成染发作用首要需求依靠头发分割才干辨认头发区域,然后在 Shader 中对头发区域做染色处理。

一、头发分割才干

之前咱们有介绍过推理结构 TNN 的运用。TNN 不仅开源了代码,并且还提供了一些算法模型,其间就有咱们需求的头发分割才干

在项目中运用 TNN 头发分割才干分为三步:

第一步:SDK 集成

TNN 的集成进程在之前的文章现已介绍过了,不再赘述。

算法模型的履行流程也相似,首要分为预处理、履行网络、后处理三步。

不同的是算法模型的预处理和后处理参数(MatConvertParam),这个也能够在 TNN 的附带 Demo 里找到。

预处理参数:

MatConvertParam HairSegmentation::GetConvertParamForInput(std::string tag) {
    MatConvertParam input_convert_param;
    input_convert_param.scale = {1.0 / (255 * 0.229), 1.0 / (255 * 0.224), 1.0 / (255 * 0.225), 0.0};
    input_convert_param.bias  = {-0.485 / 0.229, -0.456 / 0.224, -0.406 / 0.225, 0.0};
    return input_convert_param;
}

后处理参数:

TNN_NS::MatConvertParam TNNSDKSample::GetConvertParamForOutput(std::string name) {
    return TNN_NS::MatConvertParam();
}

第二步:原始帧获取

这儿的「原始帧」是指用于跑算法模型的原始数据。

在 GPUImage 的烘托链中,每个滤镜传递给下个滤镜的帧数据类型是 GPUImageFramebufferGPUImageFilter 在完成单次烘托后,烘托成果保存在 GPUImageFramebufferrenderTarget 中,renderTargetCVPixelbufferRef 类型。

咱们知道,iOS 渠道的 TNN 结构是基于 Metal 运行的,数据输入类型为 MTLTexture 或许 MTLBuffer

所以在咱们这个例子中,在履行染发滤镜前,需求把上一个滤镜的烘托成果转成 MTLTexture,也便是把 CVPixelbufferRef 转成 MTLTexture

CVPixelbufferRef 是一种支撑缓冲区同享的图像格局,在运用 IOSurface 的情况下,能够将缓冲区扩展成 OpenGL 或 Metal 的纹路。

也便是说,在烘托进程中,CVPixelbufferRef 允许 OpenGL 和 Metal 运用同一个纹路,而不用做额外的数据拷贝。这无疑能极大进步烘托的功能。

咱们来看一下 GPUImageFramebuffer 中创建 renderTarget 的代码:

CFDictionaryRef empty;
CFMutableDictionaryRef attrs;
empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
CVReturn err = CVPixelBufferCreate(kCFAllocatorDefault, (int)_size.width, (int)_size.height, kCVPixelFormatType_32BGRA, attrs, &renderTarget);

能够看到 renderTarget 的创建的确运用了 IOSurface,也便是说 renderTarget 能够直接转成 MTLTexture

CVPixelbufferRefMTLTexture 的核心代码如下:

- (id<MTLTexture>)textureWithPixelBuffer:(CVPixelBufferRef)pixelBuffer {
    if (!pixelBuffer) {
        return nil;
    }
    CVPixelBufferRetain(pixelBuffer);
    CVMetalTextureRef texture = nil;
    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                self.textureCache,
                                                                pixelBuffer,
                                                                nil,
                                                                MTLPixelFormatBGRA8Unorm,
                                                                CVPixelBufferGetWidth(pixelBuffer),
                                                                CVPixelBufferGetHeight(pixelBuffer),
                                                                0,
                                                                &texture);
    if (status != kCVReturnSuccess) {
        NSLog(@"texture create fail");
        CVPixelBufferRelease(pixelBuffer);
        return nil;
    }
    id<MTLTexture> result = CVMetalTextureGetTexture(texture);
    CFRelease(texture);
    CVPixelBufferRelease(pixelBuffer);
    return result;
}

得到 MTLTexture 后,还需求 resize 成算法模型的输入大小,然后就能够丢给模型处理。

留意: 这儿的 resize 也是一次 Metal 烘托,整个进程需求保证所有的 Metal 烘托在同一个 MTLCommandQueue 上履行,否则会有同步或功能问题。

第三步:辨认成果应用

算法模型处理完成后,会输出 MTLTexture 格局的头发 Mask 图,咱们需求把 Mask 图转回 OpenGL 纹路才干运用。

与上面 CVPixelbufferRefMTLTexture 相似,为了烘托的高功能,纹路转换进程需求避免额外的数据拷贝。

这儿正确的做法是创建一个 OpenGL 和 Metal 能够同享缓冲区的 CVPixelbufferRef,把它扩展成 OpenGL 纹路和 Metal 纹路。

然后将 Metal 纹路作为算法模型的烘托方针,则烘托完成后,OpenGL 纹路也能得到头发 Mask 图。

因为 GPUImageFramebuffer 在创建的时分,就附带了 CVPixelbufferRef 格局的 renderTarget,所以咱们直接创建 GPUImageFramebuffer 就能够:

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    CVPixelBufferRef inputPixelBuffer = firstInputFramebuffer.renderTarget;
    if (inputPixelBuffer) {
        [[SCAIManager shareManager] hairSegmentationWithSrcPixelBuffer:inputPixelBuffer dstTexture:self.hairTexture];
    }
    [super newFrameReadyAtTime:frameTime atIndex:textureIndex];
}
- (GPUImageFramebuffer *)hairFramebuffer {
    if (!_hairFramebuffer) {
        _hairFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:CGSizeMake(firstInputFramebuffer.size.width, firstInputFramebuffer.size.height) onlyTexture:NO];
    }
    return _hairFramebuffer;
}
- (id<MTLTexture>)hairTexture {
    if (!_hairTexture) {
        _hairTexture = [self.textureConverter textureWithPixelBuffer:self.hairFramebuffer.renderTarget];
    }
    return _hairTexture;
}

这儿首要看第 4 行,在创建了 hairFramebuffer 后,将 renderTarget 转成 hairTexture,然后将 hairTexture 作为算法模型的终究输出,则算法模型履行完成后,会将成果写到 hairTexture 上。

hairTexture 写入完成后,就能够拿到 OpenGL 纹路开始做下一步烘托:

GLint maskUniform = [filterProgram uniformIndex:@"hairMask"];
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, self.hairFramebuffer.texture);
glUniform1i(maskUniform, 3);

这儿咱们拿到了 hairFramebuffer 对应的纹路 ID,将它传入 OpenGL 的 Shader 中做后续处理。

留意: CVPixelbufferRef 在同享缓冲区时,需求确保烘托指令被提交后才干同步。而 GPUImageFilter 在履行 glDrawArrays 后,并没有调用 glFlush。这样可能会导致在 CVPixelbufferRefMTLTexture 时,读取到无效数据,形成闪屏。因此在 GPUImageFilter+BugFix.m 里补充了 glFlush 调用。

二、染发作用

在上面的进程中,咱们现已将头发的 Mask 图纹路传到 Shader 中,下面开始染发作用的完成。

完成天然的染发作用需求多个处理进程,比如 Mask 图边际含糊处理、LUT 滤镜叠加等。

今天咱们只做最简略的染发作用完成:色彩通道叠加

咱们知道,在 RGB 色彩空间中,色彩分为 R、G、B 三个通道。那么只要把 R 通道的数值进步,就能让头发的色彩偏红。

Shader 要害代码如下:

void main (void) {
   vec4 mask = texture2D(hairMask, textureCoordinate);
   vec4 color = texture2D(inputImageTexture, textureCoordinate);
   color.r = color.r + 0.3 * mask.g;
   gl_FragColor = color;
}

这儿的 mask 是头发的 Mask 图,因为成果保存在 G 通道里,所以通过 mask.g 读取成果。

原图和作用图比照:

在 GPUImage 中实现染发效果

能够看到头发区域 R 通道数值进步后,色彩的确变红了。但一起也导致了头发区域亮度提升,整体不太天然。

在 HSL 空间中,能够由 RGB 算出亮度:

M = max(R, G, B)
m = min(R, G, B)
L = (M + m) / 2

其间 L 就表明色彩的亮度。

由上面的公式能够看出,色彩通道的数值进步,的确会影响色彩的亮度。

因此在单通道数值批改后,需求做一下亮度批改。

具体做法是算出色彩批改前后的亮度值,然后依据前后亮度比,对终究色彩的三个通道数值做等比缩放,保证前后亮度一致。

要害代码如下:

void main (void) {
   vec4 mask = texture2D(hairMask, textureCoordinate);
   vec4 color = texture2D(inputImageTexture, textureCoordinate);
   float originLightness = lightness(color.rgb);
   color.r = color.r + 0.3 * mask.g;
   float resultLightness = lightness(color.rgb);
   gl_FragColor = vec4(color.rgb * (originLightness / resultLightness), 1.0);
}

亮度批改前后比照:

在 GPUImage 中实现染发效果

能够看到亮度批改后作用更加天然。

源码

请到 GitHub 上查看完整代码。

参考

  • 怎样用 rgb 三元组理解色相、亮度和饱和度?

获取更佳的阅览体验,请访问原文地址【Lyman’s Blog】在 GPUImage 中完成染发作用