商汤美颜

更新时间: 2024/03/15 17:26:15

NERTC SDK 支持接入商汤(SenseAR Effects SDK)等第三方专业美颜滤镜厂商,实现美颜、美妆、滤镜、贴纸等美颜特效。在娱乐社交、在线教育等场景中,您可以快速构建具备美颜特效能力的应用,让用户在进行视频通话或直播时,呈现更良好的肌肤状态和精神面貌。

准备工作

  • 请联系商汤技术支持获取如下文件:
    • 商汤美颜 SDK
    • 商汤美颜 SDK 的 License 证书
    • 商汤美颜资源文件
    • 商汤美颜示例
  • 已实现音视频通话

功能原理

商汤美颜原理.png

  1. 用户A 通过 NERTC SDK 的 onNERtcEngineVideoFrameCaptured: rotation: 接口,将采集到的视频图像数据通过回调传给商汤美颜 SDK。

  2. 商汤美颜 SDK 通过回调获取视频图像数据,进行美颜处理后,通过参数返回给 NERTC SDK。

  3. NERTC SDK 将美颜后的数据进行编码和传输。

示例项目源码

网易云信提供 商汤美颜示例项目源码,您可以参考该源码实现商汤美颜。

步骤1:集成商汤美颜 SDK

  1. 导入商汤美颜(SenseAR Effects iOS SDK)相关资源,包括:

    • License 文件:将License 文件放在商汤美颜 SDK 目录下,例如 Beauty-iOS-ObjC/ST_SDK/license

    • 头文件

      商汤头文件.png

    • 静态库

      商汤库文件.png

    • 模型

      商汤模型.png

  2. 添加链接库。

    SenseME Effects 依赖 c++。

    选择 TARGETS > Build Settings > Linking > Other LinkerFlags,添加 -lc++

  3. 关闭 Bitcode。

    SenseMe Effects 不支持 Bitcode。

    在 Xcode 中选择 TARGETS > Build Settings > Build Options,将 Enable Bitcode 参数设置为 NO。

    商汤美颜设置.png

步骤2:导入辅助工具类

  1. 请将以下两个辅助工具类从商汤提供的示例中复制到您的项目目录,并进行引用。
    • EFRender 文件夹:samples/SenseMeEffects/SenseMeEffects/EFRender。
    • global_singleton 文件夹:samples/SenseMeEffects/SenseMeEffects/datasource/global_singleton。

因辅助工具类来自 sample,会与不需要用到的功能耦合,因此需要删除辅助工具类中的无用代码,解除与 sample 业务功能的耦合。

  1. 选择 EFRender/Effects.m,注释掉对于 EFBaseEffectsProcess.h 的引用。

    // #import "EFBaseEffectsProcess.h"
    
  2. 选择 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;
    }
    
  3. 为 EFRender 添加转换纹理为 NV12 buffer 的方法。

    1. EFRender/Effects.h 中添加如下代码:

      - (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture
                                          outputBuffer:(void *)outputBuffer
                                                  size:(CGSize)size;
      
    2. 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);
          }
      }
      
    3. EFRender/EffectsProcess.h 中添加如下代码:

      - (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture
                                          outputBuffer:(void *)outputBuffer
                                                  size:(CGSize)size;
      
    4. EFRender/EffectsProcess.m 中添加如下代码:

      - (void)convertRGBATextureToNV12BufferWithTexture:(GLuint)texture
                                          outputBuffer:(void *)outputBuffer
                                                  size:(CGSize)size {
          [self.effect convertRGBATextureToNV12BufferWithTexture:texture
                                                  outputBuffer:outputBuffer
                                                          size:size];
      }
      

步骤3:初始化商汤美颜 SDK

  1. 导入头文件 EffectsProcess.h

    #import "EffectsProcess.h"
    
  2. 添加属性和成员变量。

    @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;
    
  3. 初始化商汤美颜 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:图像处理和渲染

  1. 在加入房间之前,调用 setParameters 接口,将 kNERtcKeyVideoCaptureObserverEnabled 的值设置为 YES,开启摄像头采集数据的回调。

    NSDictionary *params = @{kNERtcKeyVideoCaptureObserverEnabled : @YES};
    [[NERtcEngine sharedEngine] setParameters:params];
    
  2. 在成功加入房间后调用 enableLocalVideo 方法开启本地视频采集。

    调用 enableLocalVideo 开启本地视频采集时,请设置 streamTypekNERtcStreamChannelTypeMainStream,否则美颜效果不会生效。

  3. 在 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);
    
  4. 将 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);
            }
        }
    }
    
  5. 调整美颜效果。

    使用 EffectsProcesssetEffectType: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;
    }
}
此文档是否对你有帮助?
有帮助
去反馈
  • 准备工作
  • 功能原理
  • 示例项目源码
  • 步骤1:集成商汤美颜 SDK
  • 步骤2:导入辅助工具类
  • 步骤3:初始化商汤美颜 SDK
  • 步骤4:图像处理和渲染
  • 步骤5:销毁商汤美颜 SDK