本文字数:3046字
预计阅读时间:15 分钟
iOS:OpenGLES 实验室之2D篇 第一弹 の 智能弹幕
笔者之前发表的音视频文章,有图像的处理,音频的重采样等等,都属于入门级别。通过阅读它们,读者能对音视频有了了解。可在 Gitee 上面回顾。
2023年,笔者将整理下关于 OpenGLES 的实验室系列并进行发表。首先为读者带来 2D 篇的系列,它大多是 x y 坐标,不涉及 z 坐标,所以用 2D篇。内容上,它不对 OpenGLES 的基础知识进行细说与讨论。但如果对 OpenGLES 不了解或者了解一点,仍可通过本实验室系列了解 OpenGLES。它旨在激起读者的兴趣,扩展到实际的应用上。总的来说,这些实验 & Demo 将是额外的,即对基础学习的补充,通过这些它们的实践和运用,能让读者进一步了解 OpenGLES 。
前言
本次实验室带来的是《OpenGLES 实验室之2D篇 第一弹の智能弹幕》。其实这里取这个名字有点牵强,是弹幕,却不智能哈,因为它不包含人脸识别功能,使用固定的矩形区域。
首先简单介绍下智能弹幕,它是弹幕在视频播放的时候不遮挡人物。它在很多点播视频上运用,直播也有平台支持,它让弹幕更友好,提升观看视频的体验,可以说让弹幕和视频达成一个平衡。那它的实现,其实之前刘壮童靴这篇《带你实现完整的视频弹幕系统》 最后有提到弹幕防挡探索。而笔者也写过用 mask layer 蒙层的实现《iOS 弹幕系统之智能弹幕学习篇》,分别使用 CAShapeLayer & UIBezierPath ,CGGradientRef 的圆半径方向渐变 UIImage,带 Alpha 的 UIImage 等实现不遮挡的效果,都是 native 的实现,但性能和效果都比较难满足不了直播,就不再介绍了。
进入主题,使用 OpenGLES 实现智能弹幕,核心就是人景分离,简单说就是绘制两次,一次原来的视频,一次只有人物,然后叠在一起播放,所谓叠在一起,本 Demo 是基于 IJKPlayer 分两个 opengles layer 绘制。
Demo
Demo 包含 IJKPlayer 实现本次实验的改动 Git Patch(IJKPlayer 仓库比较大就没上传到 Demo)和基于CoreImage 实现的图片人脸识别弹幕的项目QHVisionDemo。
Git 地址:QHAIDanmuMan : iOS :OpenGLES 实验室之2D篇第一弹の智能弹幕
实验
效果
这是用千帆直播的直播流在 IJKPlayer 播放的效果:
结构图
在播放器原先单层画面显示的基础上添加多一个图层画面,并对此图层开启 alpha 混合模式,再将弹幕图层放置于这两个图层之间,从而实现本次实验。
IJKSDLGLView
1、 IJKPlayer 的显示是 IJKSDLGLView ,由于要绘制两个,所以需要将它原本实现的 render 抽离到子 view 。
因此新增 QHIJKSDLGLShowView ,主要就是调用 OpenGLES 绘制,然后 IJKSDLGLView持有两个该view,一个作为前景view,一个作为后景view。
@interface QHIJKSDLGLShowView : UIView
@property(nonatomic, readonly) CGFloat fps;
@property(nonatomic) CGFloat scaleFactor;
@property (nonatomic, weak) id<IJKSDLGLViewProtocol> protocol;
@end
@interface IJKSDLGLView : UIView <IJKSDLGLViewProtocol>
@property (nonatomic, strong) QHIJKSDLGLShowView *showFV;// 前景 view
@property (nonatomic, strong) QHIJKSDLGLShowView *showBV;// 后景 view
@end
2、绘制图像数据的控制,将原本的 display 的操作,分发到前后景 View 去分别处理。这部分逻辑其实跟原来是一模一样,只是简单抽出,由一变二。
- (void)display: (SDL_VoutOverlay *) overlay
{
if (![self setupGLOnce])
return;
if (![self tryLockGLActive]) {
if (0 == (_tryLockErrorCount % 100)) {
NSLog(@"IJKSDLGLView:display: unable to tryLock GL active: %d\n", _tryLockErrorCount);
}
_tryLockErrorCount++;
return;
}
_tryLockErrorCount = 0;
// 分发到 前后景
[self.showBV display:overlay];
[self.showFV display:overlay];
[self displayInternal:overlay];
[self unlockGLActive];
}
3、分发后就会有两个 view 同时显示视频画面。由于是叠在一起,所以看不出来,但实际已经是两个了。
Render
接下来就是对前景的处理,后景保持原来的绘制
原本的 IJKSDLGLView 有下面这句代码,用于创建渲染对象,它里面就是 OpenGLES 的 Program,Shader 等的执行(这里是 OpenGLES 的基础知识,可以理解创建如何绘制图像的程序)。
_renderer = IJK_GLES2_Renderer_create(overlay);
那么这里需要修改,增加前景的入参,用于区分前后景。对应的路径修改如下:
// 前景 self.bFront = YES
_renderer = QH_IJK_GLES2_Renderer_create_for(overlay, self.bFront);
->
// bFront = YES
renderer = QH_IJK_GLES2_Renderer_create_yuv420p(bFront); break;
->
// 创建对应的 render
IJK_GLES2_Renderer *renderer = IJK_GLES2_Renderer_create_base(bFront ? IJK_GLES2_getFragmentShader_yuv420p_4Front() : IJK_GLES2_getFragmentShader_yuv420p());
->
// 创建片段着色器
const char *IJK_GLES2_getFragmentShader_yuv420p_4Front()
->
// 片段着色器的GLSL
g_shader_front;
Shader
这里主要是修改 Shader 了,也是真正实现 OpenGLES 智能弹幕的关键。fsh 通过纹理坐标,输出要绘制的图像对应的像素值,也就是图片上的一个点。
那么怎么处理呢?主要逻辑是在指定区域内,如果该纹理的坐标在区域内则原样像素输出,不在区域内则将其 Alpha 值则 0 ,即透明。
代码如下:
static const char g_shader_front[] = IJK_GLES_STRING(
precision highp float;
varying highp vec2 vv2_Texcoord;
uniform mat3 um3_ColorConversion;
uniform lowp sampler2D us2_SamplerX;
uniform lowp sampler2D us2_SamplerY;
uniform lowp sampler2D us2_SamplerZ;
// uniform mediump float v_mesh[8];
void main()
{
mediump float fx = vv2_Texcoord.x;
mediump float fy = vv2_Texcoord.y;
mediump float x[4];
mediump float y[4];
x[0] = 0.3;
x[1] = 0.6;
x[3] = 0.3;
x[2] = 0.6;
y[0] = 0.2;
y[1] = 0.2;
y[3] = 0.6;
y[2] = 0.6;
mediump float a;
mediump float b;
mediump float c;
mediump float d;//分别存四个向量的计算结果;
a = (x[1] - x[0])*(fy - y[0]) - (y[1] - y[0])*(fx - x[0]);
b = (x[2] - x[1])*(fy - y[1]) - (y[2] - y[1])*(fx - x[1]);
c = (x[3] - x[2])*(fy - y[2]) - (y[3] - y[2])*(fx - x[2]);
d = (x[0] - x[3])*(fy - y[3]) - (y[0] - y[3])*(fx - x[3]);
if ((a >= 0.0 && b >= 0.0 && c >= 0.0 && d >= 0.0) || (a <= 0.0 && b <= 0.0 && c <= 0.0 && d <= 0.0)) {
mediump vec3 yuv;
lowp vec3 rgb;
yuv.x = (texture2D(us2_SamplerX, vv2_Texcoord).r - (16.0 / 255.0));
yuv.y = (texture2D(us2_SamplerY, vv2_Texcoord).r - 0.5);
yuv.z = (texture2D(us2_SamplerZ, vv2_Texcoord).r - 0.5);
rgb = um3_ColorConversion * yuv;
gl_FragColor = vec4(rgb, 1);
}
else {
gl_FragColor = vec4(1, 1, 1, 0);
}
}
);
由于这里是正矩形,判断区域其实可以简单点写在 x[0] < fx < x[2] && y[0] < fy < y[2]。
但这里用的方式为了通用性,它兼容其他形状和多边形,它也是在下一个实验被应用到,读者可以稍微记住下该算法。它是计算点与边(两点),即三点构成的平面,类似“右手螺旋法则”判断该点是方向。然后,该点与四边的分别计算的结果,如果是同正或者同负,即为矩阵内;如果都是 0 则表示在矩阵边上;其余情况则为矩阵外。
混合
如果修改完上面的操作后再加入后面的弹幕,发现没有效果。这是虽然已经将不在区域内的 Alpha 设置 0 实现智能弹幕,但还需设置开启混合模式才能有效让 Alpha 值生效。
glEnable(GL_BLEND) // 开启混合
glBlendFunc(sourceFactor, destinationFactor) // 设置混合函数
// GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA
弹幕
最后,加入弹幕 view ,记得加在前后景 view 之间喔。Demo 的弹幕是笔者开发的一个弹幕组件,读者也可以更换自己或者其他第三方的弹幕库。
- (void)addDanmu:(UIView *)view {
[_glView insertSubview:view belowSubview:_glView.showFV];
}
CoreImage 的识别
QHVisionDemo 实现了基于 CoreImage 实现对静止图片中人脸的识别 & 智能弹幕的结合来实现
效果如下:
里面使用了 CIDetector 来识别人脸区域并将数据加载到缓存里面,再由 OpenGLES 进行渲染,实现跟上述是一样的哈。
// 将图像转换为CIImage
CIImage *faceImage = [CIImage imageWithCGImage:image.CGImage];
CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:nil options:opts];
// 识别出人脸数组
NSArray *features = [faceDetector featuresInImage:faceImage];
// 传入片段着色器
if (_bFront) {
GLfloat va[_v_mesh_a.count];
for (int i = 0; i < _v_mesh_a.count; i++) {
va[i] = [_v_mesh_a[i] floatValue];
}
glUniform1fv(_v_mesh, (GLsizei)_v_mesh_a.count, va);
}
最后
本实验目的旨在实现了弹幕防挡的原理。这里仍然缺少 1、人脸识别;2、人像抠图。
在实现 IJKPlayer 智能弹幕,固定了前景区域,没有加入人脸识别。如果要实现可借助第三方的 sdk ,不过这样 Demo 修改就比较大,还有一种就是提前识别做好蒙层(点播比较多选择这种方案),再下发识别后的该蒙层数据。所以完整的智能弹幕还需要 人像识别+人像抠图。当然笔者还没实现,如果读者有实现了,欢迎分享给笔者来进一步学习。
感谢各位读者,那就下个实验,再见啦👋!
链接
《Gitee》—— https://gitee.com/chenqihui
《带你实现完整的视频弹幕系统》—— https://mp.weixin.qq.com/s/Y0L1d124V9tWoJA7hYNRMQ
《QHAIDanmuMan: iOS:OpenGLES 实验室之2D篇 第一弹 の 智能弹幕》—— https://gitee.com/chenqihui/qhaidanmu-man
《人脸识别技术 (一) —— 基于CoreImage实现对静止图片中人脸的识别》—— https://www.jianshu.com/p/15fad9efe5ba