商汤美颜
更新时间: 2024/03/15 17:26:15
NERTC SDK 支持接入商汤(SenseAR Effects SDK)等第三方专业美颜滤镜厂商,实现美颜、美妆、滤镜、贴纸等美颜特效。在娱乐社交、在线教育等场景中,您可以快速构建具备美颜特效能力的应用,让用户在进行视频通话或直播时,呈现更良好的肌肤状态和精神面貌。
准备工作
- 请联系商汤技术支持获取如下文件:
- 商汤美颜 SDK
- 商汤美颜 SDK 的 License 证书
- 商汤美颜资源文件
- 商汤美颜示例
- 已实现音视频通话
功能原理
-
用户A 通过 NERTC SDK 的
onNERtcEngineVideoFrameCaptured: rotation:
接口,将采集到的视频图像数据通过回调传给商汤美颜 SDK。 -
商汤美颜 SDK 通过回调获取视频图像数据,进行美颜处理后,通过参数返回给 NERTC SDK。
-
NERTC SDK 将美颜后的数据进行编码和传输。
示例项目源码
网易云信提供 商汤美颜示例项目源码,您可以参考该源码实现商汤美颜。
步骤1:集成商汤美颜 SDK
-
导入商汤美颜(SenseAR Effects iOS SDK)相关资源,包括:
-
License 文件:将License 文件放在商汤美颜 SDK 目录下,例如
Beauty-iOS-ObjC/ST_SDK/license
。 -
头文件
-
静态库
-
模型
-
-
添加链接库。
SenseME Effects 依赖 c++。
选择 TARGETS > Build Settings > Linking > Other LinkerFlags,添加 -lc++。
-
关闭 Bitcode。
SenseMe Effects 不支持 Bitcode。
在 Xcode 中选择 TARGETS > Build Settings > Build Options,将 Enable Bitcode 参数设置为 NO。
步骤2:导入辅助工具类
- 请将以下两个辅助工具类从商汤提供的示例中复制到您的项目目录,并进行引用。
- EFRender 文件夹:samples/SenseMeEffects/SenseMeEffects/EFRender。
- global_singleton 文件夹:samples/SenseMeEffects/SenseMeEffects/datasource/global_singleton。
因辅助工具类来自 sample,会与不需要用到的功能耦合,因此需要删除辅助工具类中的无用代码,解除与 sample 业务功能的耦合。
-
选择
EFRender/Effects.m
,注释掉对于EFBaseEffectsProcess.h
的引用。// #import "EFBaseEffectsProcess.h"
-
选择
EFRender/Effects.m
,注释掉函数_modul_state_change_callback
的内容,保持空实现。_modul_state_change_callback
函数中的内容涉及sample
中的业务功能,暂时可以忽略,不影响美颜功能的实现。st_result_t _modul_state_change_callback(st_handle_t handle, st_effect_module_info_t* p_module_info) { return ST_OK; }
-
为 EFRender 添加转换纹理为 NV12 buffer 的方法。
-
在
EFRender/Effects.h
中添加如下代码:- (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture outputBuffer:(void *)outputBuffer size:(CGSize)size;
-
在
EFRender/Effects.m
中添加如下代码:- (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture outputBuffer:(void *)outputBuffer size:(CGSize)size { if (!outputBuffer || !glIsTexture(texture) || CGSizeEqualToSize(CGSizeZero, size)) { NSLog(@"%s input param error", __func__); return; } st_result_t ret = st_mobile_rgba_tex_to_nv12_buffer(_hConvertHandle, texture, size.width, size.height, outputBuffer); if (ret != ST_OK) { NSLog(@"st_mobile_rgba_tex_to_nv12_buffer error %d", ret); } }
-
在
EFRender/EffectsProcess.h
中添加如下代码:- (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture outputBuffer:(void *)outputBuffer size:(CGSize)size;
-
在
EFRender/EffectsProcess.m
中添加如下代码:- (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture outputBuffer:(void *)outputBuffer size:(CGSize)size { [self.effect convertRGBATextureToNV12BufferWithTexture:texture outputBuffer:outputBuffer size:size]; }
-
步骤3:初始化商汤美颜 SDK
-
导入头文件
EffectsProcess.h
。#import "EffectsProcess.h"
-
添加属性和成员变量。
@property (nonatomic, strong) EAGLContext* glContext; @property (nonatomic, strong) EffectsProcess* stEffectProcess; @property (nonatomic, assign) BOOL stModelLoaded; GLuint _currentFrameWidth; GLuint _currentFrameHeight; GLuint _outTexture; CVPixelBufferRef _outputPixelBuffer; CVOpenGLESTextureRef _outputCVTexture; unsigned char* _outputBuffer;
-
初始化商汤美颜 SDK。
- [self.stEffectProcess setModelPath:modelPath] 方法会同步执行,可能需要较长时间,会阻塞当前线程。因此,不建议在主线程或 RTC 的视频采集回调方法中执行此方法,建议在子线程中执行。
- 如果在 RTC 视频回调触发时,模型加载尚未完成,则最初几帧可能无法进行美颜处理。因此,建议尽早初始化美颜 SDK。
- (void)initSTSDK { // 鉴权(xxx.lic 为 license 文件) NSString* licensePath = [[NSBundle mainBundle] pathForResource:@"<#your license#>" ofType:@"lic"]; BOOL result = [EffectsProcess authorizeWithLicensePath:licensePath]; if (!result) { NSLog(@"***** error: license is invalid *****"); } // 初始化 EAGLContext self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; // 初始化 EffectProcess self.stEffectProcess = [[EffectsProcess alloc] initWithType:EffectsTypePreview glContext:self.glContext]; // 添加 model,异步执行 dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.stModelLoaded = NO; NSString* modelPath = [[NSBundle mainBundle] pathForResource:@"model" ofType:@"bundle"]; [self.stEffectProcess setModelPath:modelPath]; self.stModelLoaded = YES; }); }
步骤4:图像处理和渲染
-
在加入房间之前,调用
setParameters
接口,将kNERtcKeyVideoCaptureObserverEnabled
的值设置为 YES,开启摄像头采集数据的回调。NSDictionary *params = @{kNERtcKeyVideoCaptureObserverEnabled : @YES}; [[NERtcEngine sharedEngine] setParameters:params];
-
在成功加入房间后调用
enableLocalVideo
方法开启本地视频采集。调用
enableLocalVideo
开启本地视频采集时,请设置streamType
为kNERtcStreamChannelTypeMainStream
,否则美颜效果不会生效。 -
在 NERtcSDK 的
onNERtcEngineVideoFrameCaptured:rotation:
中,将原始视频图像数据通过回调发给相芯美颜的接口,做相应的美颜处理。- 在进行人脸检测时,可以选择前置摄像头或后置摄像头。示例代码中使用了前置
AVCaptureDevicePositionFront
,您可以根据需要自行调整 cameraPosition。 - RTC 默认使用前置摄像头。如果您使用
kNERtcKeyVideoStartWithBackCamera
设置过偏好的摄像头,则每次启动摄像头时都会根据该偏好来选择方向。 - 业务层需要自行维护摄像头方向的值,因为 RTC 没有提供获取摄像头方向的接口。
if (!self.stModelLoaded) { return; } CVPixelBufferRetain(pixelBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, 0); st_rotate_type rotationType = ST_CLOCKWISE_ROTATE_0; switch (rotation) { case kNERtcVideoRotation_0: rotationType = ST_CLOCKWISE_ROTATE_0; break; case kNERtcVideoRotation_90: rotationType = ST_CLOCKWISE_ROTATE_90; break; case kNERtcVideoRotation_180: rotationType = ST_CLOCKWISE_ROTATE_180; break; case kNERtcVideoRotation_270: rotationType = ST_CLOCKWISE_ROTATE_270; break; default: break; } size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t height = CVPixelBufferGetHeight(pixelBuffer); // craete texture if needed if (!_outTexture || _currentFrameWidth != width || _currentFrameHeight != height) { _currentFrameWidth = (uint32_t)width; _currentFrameHeight = (uint32_t)height; if (_outTexture) { CVPixelBufferRelease(_outputPixelBuffer); _outputPixelBuffer = NULL; CFRelease(_outputCVTexture); _outputCVTexture = NULL; } if (_outputBuffer) { free(_outputBuffer); _outputBuffer = NULL; } _outputBuffer = (unsigned char*)malloc(width * height * 3 / 2); [self.stEffectProcess createGLObjectWith:(int)width height:(int)height texture:&_outTexture pixelBuffer:&_outputPixelBuffer cvTexture:&_outputCVTexture]; } // face detect st_mobile_human_action_t detectResult; memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); st_mobile_animal_result_t animalResult; memset(&animalResult, 0, sizeof(st_mobile_animal_result_t)); st_result_t result = [self.stEffectProcess detectWithPixelBuffer:pixelBuffer rotate:rotationType cameraPosition:AVCaptureDevicePositionFront humanAction:&detectResult animalResult:&animalResult]; // render result = [self.stEffectProcess renderPixelBuffer:pixelBuffer rotate:rotationType humanAction:detectResult animalResult:&animalResult outTexture:_outTexture outPixelFormat:ST_PIX_FMT_BGRA8888 outData:nil]; // convert rgb texture to nv12 buffer [self.stEffectProcess convertRGBATextureToNV12BufferWithTexture:_outTexture outputBuffer:_outputBuffer size:CGSizeMake(width, height)]; // copy nv12 buffer to pixel buffer [self copyNV12BufferToPixelBufferWithBuffer:_outputBuffer width:(uint32_t)width height:(uint32_t)height pixelBuffer:pixelBuffer]; CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); CVPixelBufferRelease(pixelBuffer);
- 在进行人脸检测时,可以选择前置摄像头或后置摄像头。示例代码中使用了前置
-
将 NV12 格式的视频帧数据复制到 CVPixelBuffer 中。
- (void)copyNV12BufferToPixelBufferWithBuffer:(unsigned char*)buffer width:(uint32_t)width height:(uint32_t)height pixelBuffer:(CVPixelBufferRef)pixelBuffer { if (!buffer) { NSLog(@"%s, buffer is invalid", __func__); return; } OSType type = CVPixelBufferGetPixelFormatType(pixelBuffer); if (type != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange && type != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) { NSLog(@"%s, pixel buffer format %u is not supported", __func__, type); return; } size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer); size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer); if (width != pixelWidth || height != pixelHeight) { NSLog(@"%s, pixel buffer width %zu or height %zu is invalid", __func__, pixelWidth, pixelHeight); return; } // y 分量 unsigned char* yData = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); size_t yBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); // uv 分量 unsigned char* uvData = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); size_t uvBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); if (width == yBytesPerRow) { size_t yLength = yBytesPerRow * height; size_t uvLength = uvBytesPerRow * height / 2; memcpy(yData, buffer, yLength); memcpy(uvData, buffer + yLength, uvLength); } else { // 逐行拷贝 size_t yBufferLength = width * height; for (uint32_t i = 0; i < height; i++) { memcpy(yData + yBytesPerRow * i, buffer + width * i, width); } for (uint32_t i = 0; i < (height / 2); i++) { memcpy(uvData + uvBytesPerRow * i, buffer + yBufferLength + width * i, width); } } }
-
调整美颜效果。
使用
EffectsProcess
的setEffectType:value:
方法调整美颜特效的参数。以下示例代码展示将美白特效值调整为0.5。
[self.stEffectProcess setEffectType:EFFECT_BEAUTY_BASE_WHITTEN value:0.5];
更多美颜特效类型可参考头文件
st_mobile_effect.h
。
步骤5:销毁商汤美颜 SDK
- (void)destroySTSDK {
self.stModelLoaded = NO;
self.stEffectProcess = nil;
self.glContext = nil;
_currentFrameWidth = 0;
_currentFrameHeight = 0;
if (_outTexture) {
_outTexture = 0;
CVPixelBufferRelease(_outputPixelBuffer);
_outputPixelBuffer = NULL;
CFRelease(_outputCVTexture);
_outputCVTexture = NULL;
}
if (_outputBuffer) {
free(_outputBuffer);
_outputBuffer = NULL;
}
}