本文介绍了拍摄场景中染发作用的完成。首要涉及头发分割才干的接入,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 的烘托链中,每个滤镜传递给下个滤镜的帧数据类型是 GPUImageFramebuffer
,GPUImageFilter
在完成单次烘托后,烘托成果保存在 GPUImageFramebuffer
的 renderTarget
中,renderTarget
是 CVPixelbufferRef
类型。
咱们知道,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
。
CVPixelbufferRef
转 MTLTexture
的核心代码如下:
- (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 纹路才干运用。
与上面 CVPixelbufferRef
转 MTLTexture
相似,为了烘托的高功能,纹路转换进程需求避免额外的数据拷贝。
这儿正确的做法是创建一个 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
。这样可能会导致在 CVPixelbufferRef
转 MTLTexture
时,读取到无效数据,形成闪屏。因此在 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
读取成果。
原图和作用图比照:
能够看到头发区域 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);
}
亮度批改前后比照:
能够看到亮度批改后作用更加天然。
源码
请到 GitHub 上查看完整代码。
参考
- 怎样用 rgb 三元组理解色相、亮度和饱和度?
获取更佳的阅览体验,请访问原文地址【Lyman’s Blog】在 GPUImage 中完成染发作用