iOS:OpenGLES 实验室之2D篇 第一弹 の 智能弹幕

news2024/11/16 10:39:27

3c06ca5da17d4d0ace3b902bc799a827.jpeg

 9d7490cfb701ec1231b1a81cc3459952.gif 

本文字数: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 播放的效果:

45612913c3f0c469c96c49084789209c.gif


结构图

在播放器原先单层画面显示的基础上添加多一个图层画面,并对此图层开启 alpha 混合模式,再将弹幕图层放置于这两个图层之间,从而实现本次实验。

e75834e571e48fab9b55106559e5351e.png

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 同时显示视频画面。由于是叠在一起,所以看不出来,但实际已经是两个了。

bd03dee5eea98b8029530a16898ea81e.png

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 实现对静止图片中人脸的识别 & 智能弹幕的结合来实现

效果如下:

59049141708793963f1eead82a012b39.gif

里面使用了 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173611.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【复习 自用】JavaScript知识汇总(DOM)

注&#xff1a;之前学过JavaScript&#xff0c;本贴仅用于复习(自用)&#xff0c;建议没基础的朋友先学基础。会混入typescript&#xff01; 更新中~~~~~ Dom核心内容 创建节点 ① document.write() 是直接将内容写入页面的内容流&#xff0c;但是文档流执行完毕&#xff0c…

云原生技能树-docker caontainer 操作

运行 一个Docker镜像(image)运行后&#xff0c;就是一个容器实例&#xff0c;称为container 以镜像hello-world为例&#xff0c;启动容器&#xff1a; docker container run -it hello-world 可以看到输出了Hello World 信息&#xff1a; 以下描述错误的是&#xff1f; 答…

数字逻辑理论——从卡诺图到门电路

卡诺图化简 卡诺图化简 第一步&#xff1a;在卡诺图中圈出相邻为1的小方格&#xff08;方格的个数为2m2^{m}2m&#xff09;&#xff0c;圈里面的1越多越好&#xff0c;并且这个小方格可以重复使用。 第二步&#xff1a;上一步中的方格或者圈出来的方框——每一个都代表一个与…

Linux软件安装及管理程序

Linux安装及管理程序Linux软件安装及管理程序一、Linux应用程序基础二、RPM软件包管理工具2.1、RPM介绍2.2、RPM命令三、源码编译安装四、yum安装Linux软件安装及管理程序 一、Linux应用程序基础 应用程序与系统命令的关系 角色系统命令应用程序文件位置般在/bin和/sbin目录…

linux系统中实现智能家居的基本方法

大家好&#xff0c;今天主要和大家分享一下&#xff0c;智能家居物联网的基本实现与操作方法。 目录 第一&#xff1a;智能家居基本简介 第二&#xff1a;测试WIFI模块功能 第三&#xff1a;智能家居物联UI界面开发 第四&#xff1a;核心代码的具体实现 第五&#xff1a;最…

【阅读笔记】《重构》 第三四章

第三章 代码的味道 DuplicatedCode(重复代码) 同一个类的两个函数含有相同的表达式两个互为兄弟的子类含有相同表达式两个毫不相干的类出现重复代码 LongMethod(过长函数) 函数不宜过长&#xff0c;函数越长越难理解如果想利用单个类做太多事情&#xff0c;其内往往就会出现…

联合证券|港股再融资“春江水暖” 资本争购热门赛道企业

进入2023年&#xff0c;港股再融资商场有所回暖。到1月18日&#xff0c;已有27家港股上市公司发布拟配售股份&#xff08;简称“配股”&#xff09;再融资&#xff0c;募资总额164.01亿港元&#xff0c;较上一年同期增加148.16%。其间&#xff0c;微盟集团的配股再融资吸引了众…

fpga实操训练(lcd测试)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 前面我们测试过vga输出,当时是找了一个老式的显示器来完成的,也就是本身自带vga接口的显示器。但是,现在市面上大部分显示器都是默认支持hdmi接口的。所以说,如果真的想用fpga测…

都说InnoDB好,那还要不要使用Memory引擎?

我在上一篇文章末尾留给你的问题是:两个 group by 语句都用了 order by null,为什么使用内存临时表得到的语句结果里,0 这个值在最后一行;而使用磁盘临时表得到的结果里,0 这个值在第一行? 今天我们就来看看,出现这个问题的原因吧。 内存表的数据组织结构 为了便于分…

如何在SpringBoot项目中访问静态资源

在springboot项目中如果要在不集成templates的情况下访问静态资源需要做以下配置 1.在项目的application.yml文件中做如下配置 spring:profiles:active: devmvc:view:prefix: /suffix: .html 重点在 配置后生成为WebMvcProperties 配置类。该配置类中有一个内部类View Conf…

各式各样图标的特点

笔者近期在翻看各种样式的图标&#xff0c;逛了下 Iconfont、IconFinder 等图标网站&#xff0c;现根据自己的经验&#xff0c;总结了一些图标特点的描述用语 线性/面性/线面组合平面/立体无层次感&#xff08;或阴影&#xff09;/有层次感&#xff08;或阴影&#xff09;无填…

Oracle强制加了hint实效三种连接方式使用场合判断

开发写了一个语句使用了connect by level函数 SELECT DISTINCT CTMID FROM ( SELECT CTMID, REGEXP_SUBSTR(FLTUSERIDSTR, ‘[^;]’, 1, l) AS userid FROM s_userinfo,(SELECT LEVEL l FROM DUAL CONNECT BY LEVEL<300) b WHERE l < LENGTH(FLTUSERIDSTR) - LENGTH(rege…

链表的算法题

目录 题型一、克隆含有rand指针的链表 笔试&#xff1a;哈希表 面试&#xff1a;不用容器&#xff0c;模拟哈希表的功能 题型二、给一个单链表头节点Head&#xff0c;判断是否构成回文 题型三、将单链表按某值划分为左边小&#xff0c;中间相等&#xff0c;右边大 6个变量…

Allegro如何设置创建Pin Pair的快捷键操作指导

Allegro如何设置创建Pin Pair的快捷键操作指导 在做PCB设计的时候需要做一组信号的等长,需要使用到创建Pin Pair的功能,如下图,如果每个网络都右键去选择添加比较浪费时间,如下图 Allegro支持给创建一个Create Pin Pair的快捷键位 具体操作如下 打开规则管理器选择Tools

单调栈与单调队列

单调栈与单调队列一、单调栈1.1 思路1.2 例题&#xff1a;单调栈二、单调队列2.1 思路2.2 例题&#xff1a;滑动窗口一、单调栈 1.1 思路 单调栈主要解决以下问题&#xff1a; 1️⃣ 寻找下一个更大元素 2️⃣ 寻找前一个更大元素 3️⃣ 寻找下一个更小元素 4️⃣ 寻找前一个…

理性和感性 - 如何对待错误

上次的博客&#xff0c; 我写了一些关于 软件开发中的理性和感性决定 的故事。 不论是感性还是理性&#xff0c;我们的目的就是要把软件交给用户去用&#xff0c; 在软件行业中有这样一句俗话&#xff1a; 当你把产品交给用户的时候&#xff0c;你的学习才刚刚开始。 当然每个团…

【设计模式】结构型模式·桥接模式

学习汇总入口【23种设计模式】学习汇总(数万字讲解体系思维导图) 写作不易&#xff0c;如果您觉得写的不错&#xff0c;欢迎给博主来一波点赞、收藏~让博主更有动力吧&#xff01; 一.概述 将抽象与实现分离&#xff0c;使它们可以独立变化。用组合关系代替继承关系&#xff0c…

华为数字化转型之道 结语 数字化转型的8个成功要素

结语 数字化转型的8个成功要素 华为开展数字化转型的过程中,积累了一些经验和教训,总结起来有如下成功要素。 1. 一把手担责 要做好数字化转型,企业家就要有战略决心、信心、耐心。数字化转型一定是企业“一把手工程”,需要企业家自上而下地推动并在企业内达成广泛共识。…

免费开题报告|基于SpringBoot+Vue的校内跑腿平台

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

Android 深入系统完全讲解(29)

MediaMuxer 封装器 MediaMuxer 最多仅支持一个视频 track 和一个音频 track&#xff0c;所以如果有多个音频 track 可以先 把它们混合成为一个音频 track 然后再使用 MediaMuxer 封装到 mp4 容器中。 MediaMuxer 支持输出格式为 MP4&#xff0c;webm 和 3gp. 默认我们就用 mp4.…