内容简介:视频滤镜,顾名思义,是在视频素材上duang特效的一种操作。 随着H5页面越做越炫酷的趋势,单一的视频播放已经不能满足我们的需求,视频滤镜在Web页面上的应用越来越广泛。如何实现视频滤镜呢?最容易想到的方案是使用CSS3内置的滤镜。CSS3为我们封装了一些常用的滤镜算法,如模糊,灰阶、饱和度等,使用filter属性来定义,详细参见
视频滤镜,顾名思义,是在视频素材上duang特效的一种操作。 随着H5页面越做越炫酷的趋势,单一的视频播放已经不能满足我们的需求,视频滤镜在Web页面上的应用越来越广泛。
问题概述
如何实现视频滤镜呢?最容易想到的方案是使用CSS3内置的滤镜。
CSS3为我们封装了一些常用的滤镜算法,如模糊,灰阶、饱和度等,使用filter属性来定义,详细参见 https://www.w3cplus.com/css3/ten-effects-with-css3-filter
除了作用于图片,该属性也可以作用于video标签,即视频滤镜。 同理,svg的filter也可以实现类似的效果,实现方式大同小异。
小伙伴的IceVideo组件 便置入了基于CSS3 filter实现的视频滤镜,链接内有包括案例在内的详细说明,本文不再赘述。
本文主要讨论的是上述方案无法覆盖的场合。 对于一些特殊风格化、定制化的效果,我们很难通过现有的filter来做出,比如
上述的抠图效果、旧电视雪花效果,本身计算方式复杂,无法使用简单的规则来定义。 对于这类“很难归类”需求,难道就没有一种更加自由的,泛用的滤镜实现方式,可以满足复杂场景吗? 答案当然是有的。 本文便介绍一种“百搭”的解决办法——WebGL着色器。 使用WebGL提供的api,在像素操作级别,定制只属于你的一款滤镜。
(示例中的视频均来自QQ-AR项目合作商的线上素材)
为了探索合适的方案,我们需要从问题的本质入手分析。
问题一、视频滤镜的本质是什么?
滤镜的本质是一种映射。即通过某种特定的算法,将图像中的像素点从一个值,映射成另一个。 对于视频,则是对每一个图像帧进行映射。 映射算法的设计,是图形图像处理的内容,目前已经有很多成熟的算法。
举几个简单的例子:
灰阶的映射算法。new rgb = (0.2989*r + 0.5870*g + 0.1140*b)
反相(底片)的映射算法new r=1.0 - r; new g=1.0 - g; new b=1.0 - b;
通过调节其中的计算参数,就可以控制效果的强弱。
在Web上,如何实现这些算法呢?
我们不能够直接操作video标签的内容,但我们能够做一个“中转”,把video绘制到canvas里,然后直接使用canvas提供的绘制api,修改像素值。 具体的方式,在我的另一篇 介绍“视频吸色”的文章 中有详细描述。
概括地说,代码如下。
function playCanvas() { var mycanvas = $(_this).find("#mycanvas")[0] var myvideo = $(_this).find("#myvideo")[0] var context = mycanvas.getContext("2d") context.drawImage(myvideo, 0, 0, opts.videoWidth, opts.videoHeight) colorData = getPixelColor(mycanvas, canvasMousePos.x, canvasMousePos.y) requestAnimationFrame(playCanvas) }
将原始的video标签设为隐藏,然后使用requestAnimationFrame回调,不断地用video的内容来更新canvas。 使用canvas方案,我们有了处理单帧图片的方法,而且它的兼容性比CSS3 filter要好,只要支持canvas的浏览器都可以渲染。 这种方法对于图片来说是足够的,几乎没有时间延迟,但处理每秒24-60帧的视频,就会产生较大的延迟,引发严重的性能问题。
上图是使用canvas的像素操作实现灰阶滤镜时,在chrome console录制的资源消耗图 可以看到,cpu的主线程已经被占满,在电脑上有明显卡顿,在手机上几乎是无法使用的。
这种方案的问题在于,将所有的像素都输入给cpu,逐点串行,没有考虑并行化的可能。 那么视频滤镜操作能否并行呢?主要取决于滤镜的实现方式,即“像素是怎么映射的”。
问题二、能否并行?
笔者考察了图形图像处理中,常见的滤镜实现方式,将其归纳总结为以下三类。
1、单像素映射法 对单个像素的颜色值进行操作。 比如反相,灰阶,变亮变暗,饱和度效果。 乃至在笔者的需求中遇到的,更为复杂的绿幕视频抠图效果(后文会有详细叙述)。
2、区域卷积法 计算一个像素时,同时使用邻近n个像素的值。 可以描述为卷积操作,使用一个矩阵作为卷积核,遍历整个图像。 比如模糊,浮雕等效果,都是用这种方式做出的。
3、颜色查表法 对于一些高度风格化的处理,很难采用单一算法描述,此时可以将颜色保存在一个512x512的表里,通过查找和差值,推算出每个像素的映射结果。 这种算法叫做Color Lookup Table,简称Color LUT,最经典的实现来自于ios内置算法库GPUImage。 该算法库已开源,github地址 https://github.com/BradLarson/GPUImage
以上三种类别,虽然原理各异,但都是局限在图像局部的操作,空间复杂度是O(1)级别的。
那么,这些算法,一定是可以并行化的。
问题三、如何并行?
实际上,css3中的filter属性,和我们熟悉的transform一样,是强制使用强制使用GPU渲染的。 也就是说,如果我们给video标签设置一个filter,像素间的计算便已经并行化了。
如果不使用css3中定义的属性,而自定义计算方式,仅靠video或者canvas方案,都无法唤起cpu,前面说的“中转”方案也无法直接使用。 这时候,我们就需要用到前端的一个强大武器——WebGL。
WebGL是一套实现了OpenGL标准的Web API,这其中也包括像素级的并行计算API——着色器(Shader)。 着色器定义了一个三维空间中的点,如何渲染成为屏幕上的一个像素点。 可以理解为WebGL渲染管道的最后一个步骤。 分为顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)两个步骤,具体的工作原理有很多介绍OpenGL的教程都有提及,此处不再赘述。
利用WebGL提供的api,我们可以定义自己的Shader。 虽然是在Web上实现,但并不是使用Javascript语法,而是使用GLSL语法书写的。 关于具体的语法,这里也不再展开赘述。
在Web上使用自定义Shader进行渲染的过程,可以用下图来概括。
落实到具体实现过程,可以分为三步。
1、建立一个场景,并且把视频作为材质,贴到一个平面物体上。
2、对这个材质指定顶点着色器和片元着色器。
3、将物体置入场景,在屏幕中的canvas对象中渲染出来。
因为物体是简单的平面,所以我们的顶点着色器很简单,只要计算出每个像素的UV纹理坐标,传递给片元着色器就可以了。
varying vec2 vUv; void main() { vUv = uv; vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_Position = projectionMatrix * mvPosition; }
在片元着色器里,我们通过下面的语句
gl_FragColor = texture2D( texture, vUv );
取到这个点的实际色值,然后开始真正的像素映射计算。
灰阶:
float gray = 0.2989*gl_FragColor.r+0.5870*gl_FragColor.g+0.1140*gl_FragColor.b; gl_FragColor = vec4(gray,gray,gray , gl_FragColor.a);
反向:
float reverser=1.0 - gl_FragColor.r; float reverseg=1.0 - gl_FragColor.g; float reverseb=1.0 - gl_FragColor.b; gl_FragColor = vec4(reverser,reverseg,reverseb,gl_FragColor.a);
下面是两个较为复杂的效果实现。
雪花怀旧效果:
float dx = fract(sin(dot(vUv ,vec2(12.9898,78.233))) * 43758.5453); vec3 cResult = gl_FragColor.rgb + gl_FragColor.rgb * clamp( 0.1 + dx, 0.0, 1.0 ); vec2 sc = vec2( sin( vUv.y * 4096.0 ), cos( vUv.y * 4096.0 ) ); cResult += gl_FragColor.rgb * vec3( sc.x, sc.y, sc.x ) * 0.025; cResult = gl_FragColor.rgb + clamp( 0.35, 0.0,1.0 ) * ( cResult - gl_FragColor.rgb ); if( false ) { cResult = vec3( cResult.r * 0.3 + cResult.g * 0.59 + cResult.b * 0.11 ); } float oldr=0.393*cResult[0]+0.769*cResult[1]+0.189*cResult[2]; float oldg=0.349*cResult[0]+0.686*cResult[1]+0.168*cResult[2]; float oldb=0.272*cResult[0]+0.534*cResult[1]+0.131*cResult[2]; gl_FragColor = vec4( oldr,oldg,oldb , gl_FragColor.a);
(参考了Threejs官方范例)
绿幕抠图Chroma Keying:
float rgb2cb(float r, float g, float b){ return 0.5 + -0.168736*r - 0.331264*g + 0.5*b; } float rgb2cr(float r, float g, float b){ return 0.5 + 0.5*r - 0.418688*g - 0.081312*b; } float smoothclip(float low, float high, float x){ if (x <= low){ return 0.0; } if(x >= high){ return 1.0; } return (x-low)/(high-low); } vec4 greenscreen(vec4 colora, float Cb_key,float Cr_key, float tola,float tolb, float clipBlack, float clipWhite){ float cb = rgb2cb(colora.r,colora.g,colora.b); float cr = rgb2cr(colora.r,colora.g,colora.b); float alpha = distance(vec2(cb, cr), vec2(Cb_key, Cr_key)); alpha = smoothclip(tola, tolb, alpha); float r = max(gl_FragColor.r - (1.0-alpha)*color.r, 0.0); float g = max(gl_FragColor.g - (1.0-alpha)*color.g, 0.0); float b = max(gl_FragColor.b - (1.0-alpha)*color.b, 0.0); if(alpha < clipBlack){ alpha = r = g = b = 0.0; } if(alpha > clipWhite){ alpha = 1.0; } if(clipWhite < 1.0){ alpha = alpha/max(clipWhite, 0.9); } return vec4(r,g,b, alpha); } float tola = 0.0; float tolb = u_threshold/2.0; float cb_key = rgb2cb(color.r, color.g, color.b); float cr_key = rgb2cr(color.r, color.g, color.b); gl_FragColor = greenscreen(gl_FragColor, cb_key, cr_key, tola, tolb, u_clipBlack, u_clipWhite);
(参考了github上的开源项目greenscreen)
以Chroma Keying算法为例,看起来代码比较长,我们可以分解一下它的核心原理,简要描述如下:
1、计算key色的红、蓝分量,组成向量A。 2、计算目标颜色的红蓝分量,组成向量B。 3、计算两个向量的距离(一个分量在另一个分量上的投影) 当AB向量接近,alpha趋于1 AB向量很远,alpha趋于0 4、以alpha作为过滤指标,滤掉目标颜色rgb值中的key色分量,计算出该点的rgb值 5、将1-alpha作为该点的透明度值(rgba中的a) 6、将该点像素值设置为新的rgba
提取分量A、B,计算alpha值,并设置新颜色的算法,可以用下图表示
通过这样的映射,我们可以很好地处理半透明边缘、模糊边缘
上图是应用在QQ-AR透明Webview项目中的案例
更多的滤镜算法,可以参考其他图形图像方面的资料。
虽然看似复杂,但上述所有算法,都是局部像素的浮点数计算。 我们把它们放进GPU中充分并行之后
得到是Chrome console资源消耗图
可以看出,计算重心转移到了GPU,cpu仍是相对空闲的。
我们对QQ-AR透明Webview中的示例进行帧率考察
可以看出,在使用gpu并行计算时,滤镜几乎不会引发掉帧。
除了定义Shader之外,我们在建立场景时,还要考虑如何完成从3D到2D的合理映射。 如何把视频作为材质渲染到场景中,并且刚好填满视口? 我们知道,一个三维场景是通过摄像机来映射到二维视口的。
传统的投影相机,有近大远小的问题。 实际上,我们很难通过视频素材本身的宽高,计算出最终视口的宽高。
这里要用到OrthographicCamera(正交相机)
正交相机没有投影变形,所以也就不存在近大远小准则。 在建立场景时,只要保证相机视口的尺寸和渲染物体的尺寸相同。 渲染物体尺寸又根据视频本身的长宽来取。 就可以建立一个视频同等大小的WebGL Canvas场景。
下面是核心代码
(使用了Three.js操作WebGL api)
//取到video标签 var video = document.getElementById(videoId); //设置场景 var scene = new THREE.Scene(); var renderer = new THREE.WebGLRenderer( { antialias: true,alpha: true } ); document.getElementById(container).appendChild(renderer.domElement); renderer.setClearColor(0xffffff,0); renderer.setSize( video.width, video.height ); //设置正交相机 var camera = new THREE.OrthographicCamera(-2, 2, 1.5, -1.5, 1, 10); camera.position.set(0, 0, 1); scene.add(camera); //设置平面物体,并将视频作为材质 var movieMaterial = new ChromaKeyMaterial(videoId, video.width, video.height, 0x00ff05,0); var movieGeometry = new THREE.PlaneGeometry(4, 3); var movie = new THREE.Mesh(movieGeometry, movieMaterial); movie.position.set(0, 0, 0); movie.scale.set(1, 1, 1); movie.visible=false; scene.add(movie); //开始动画 video.play(); animate(); function animate() { if( (video.currentTime>1) && movie.visible==false){ movie.visible=true; } requestAnimationFrame(animate); renderer.render(scene, camera); }
说明1:ChromaKeyMaterial是继承了Three默认的ShaderMeterial实现的自定义材质类。
自定义类的代码较长,此处不再贴出,详细可以右键本文提供的案例代码。
说明2:animate函数里,通过video.currentTime来切换movie物体的显示隐藏,是为了预防平面物体在材质贴图完成前(视频还在载入时)的一段时间黑屏,实际项目中可以加入一些loading效果,以保证体验
问题四、兼容性如何?
不是所有的设备都兼容CSS3 filter(仅限Chrome内核) 也不是所有设备都支持WebGL标准(比如万恶的ie) 这是CANIUSE提供的WebGL兼容性结果。
这是腾讯大数据中心对移动设备兼容WebGL的统计结果。
实际上,由于x5内核的存在,在手机QQ中兼容WebGL的比例要比图上的16%更高一些。 下面则是我们使用上报的方式,对移动设备进行考察,得到的结果。
在移动端大部分设备都越来越先进的今天,为了duang出更好更酷炫的效果,在必要的场合使用WebGL方案是可取的。
总结
以上就是本文主要介绍的内容,在文章结尾,我们再重新看一遍开头的例子。
例子中,左边是一个普通视频,右边是使用Chroma Keying算法进行抠图的绿幕视频。 我对二者都应用了自定义的滤镜,并且开放了一部分参数由用户控制。
从例子中可以看出。 1、滤镜是可以叠加的(因为这些滤镜算法本质都是像素计算,只要把算法叠加起来就好了) 2、参数是可控的(因为算法的实现完全透明,所以我们对它有全权控制权,用起来足够灵活)
当然代价就是实现成本比较高,所以,对于简单的需求,我们仍推荐使用简单的方案(比如css3滤镜,svg滤镜)。 对于复杂的需求,再来使用本文提出的方案,定制个性化特效。
并且注意对于不兼容情况的降级处理(推荐降级成使用普通video标签来渲染,放弃滤镜)
WebGL的强大之处绝不仅于此,使用自定义Shader,我们还可以做更多的事情,比如曲面视频,球面视频等等,详细的应用场景,有待各位看官大神继续发掘。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Web 高级着色语言(WHLSL) - 为WebGPU设计的Web图形着色语言
- OpenGL ES 入门之旅--OpenGL ES顶点着色器和片元着色器
- WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码
- OpenGL/OpenGL ES入门: 顶点着色器与片元着色器(OpenGL过渡OpenGL ES)
- [译]背景:着色的物理和数学(3)
- 参考近百篇文献,“图像着色” 最全综述
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Ruby on Rails社区网站开发
布拉德伯纳 / 柳靖 / 2008-10 / 55.00元
《Ruby on Rails社区网站开发》全面探讨创建完整社区网站的开发过程。首先介绍开发一个内容简单的管理系统,之后逐渐添加新特性,以创建更完整的、使用Ruby on Rails 的Web 2.0 社区网站。还给出了开发和测试中的一些建议和提示,同时指导如何使网站更生动以及维护得更好。《Ruby on Rails社区网站开发》也探讨了如何与Flickr 、Google Maps 等其他平台集成,......一起来看看 《Ruby on Rails社区网站开发》 这本书的介绍吧!