使用Unity Shader 实现透明度抠绿(AlphaMatting)
Posted 倾月_Lei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Unity Shader 实现透明度抠绿(AlphaMatting)相关的知识,希望对你有一定的参考价值。
使用Unity Shader 实现透明度抠绿(AlphaMatting)
这是一个已应用到一些项目的抠绿方案
1、使用CbCr空间抠绿
YCbCr:是色彩空间的一种,通常会用于影片中的影像连续处理,或是数字摄影系统中。Y’为颜色的亮度(luma)成分、而
CB和CR则为蓝色和红色的浓度偏移量成份。Y’和Y是不同的,而Y就是所谓的亮度(luminance),表示光的浓度且为非线性,使用伽马修正(gamma correction)编码处理。
计算代码:
float3 RGB_To_YCbCr(float3 rgb)
float Y = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
float Cb = 0.564 * (rgb.b - Y);
float Cr = 0.713 * (rgb.r - Y);
return float3(Cb, Cr, Y);
不过这里我们先不用这么复杂的,我们将它变成更简单的计算方式
float2 RGB_To_MyCbCr(float3 rgb)
float Cb = rgb.b - rgb.g;
float Cr = rgb.r - rgb.g;
return float2(Cb, Cr) * 0.5 + 0.5;
这样每个颜色都可以通过这个函数获取它的CbCr坐标,因此相应的CbCr坐标:绿(0,0),黑白:(0.5,0.5),紫(1,1)。通过距离我们选中的颜色(绿色)的距离即可判断这个颜色是否应该被抠掉或是保留。
通过这种方式,我们可根据:选择的扣绿颜色、扣绿的最小裁剪距离和最大保留距离(计算时,裁剪距离 <= 保留距离),可以计算某个颜色的不透明度
一个颜色在CbCr空间中的保留情况:
2、边缘虚化
为了让图像更好的融入背景,我们需要做边缘虚化
这里使用了一个3*3、Sigma=1的高斯卷积核计算当前像素和周围8个像素组成的九宫格算子。通过算子,计算出一个透明度值(比如当前颜色不透明,但周围颜色都是绿色,那么这个颜色很有可能是边缘,需要被虚化)。
(这里+0.00016%是为了保证整体总权重和为100%)
实现:
使用tex2D对input.texcoord0位置像素和周围8个(_Flaming_Pear_Range_Temp * X / width,± _Flaming_Pear_Range_Temp * Y / height)其中X和Y取值是(-1,0,1),目的是对九宫格UV取值,将最终混合后的a值存储在sumColorA中。
sumColorA = pow(sumColorA, _Flaming_Pear_Lucency_Degree * 3);//取值变为曲线,而非线性
c.a = sumColorA;
3、简单去绿
半透明物体会和背景的绿色进行混合,细小物体(如头发)会因为还没有像素大,从而也无法精确获取它原本的颜色。
比如一杯水或是一根头发在绿幕中间,我们间的绿,来自它们本身不会反射过多的绿色,这种情况是无法通过颜色矫正处理掉的。
这一步主要是为了处理一些意外,让场景中不再有绿色通过扣绿之后的颜色,仍然可能出现没有去除掉的绿色。这一步通过检查rgb通道,进行一个简单的去绿。因为这步会改变原色,有时需要谨慎使用。
那么一个颜色去绿可以通过两种方式:1减小G通道值,2同时增加等量的RB通道值。
实现:
首先,去绿的目标颜色肯定是绿色(G>R && G>B)。
其次具体减小G或增加RB量多少,取决于这个颜色的亮度和G与相距其最小的R或B通道的差。
float getColorLuminance(float4 rgb) //获取颜色亮度
return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
亮度 = getColorLuminance();
暗度 = 1 - 亮度;
最大变换量 = (c.r > c.b)?(c.g - c.r):(c.g - c.b);
红蓝颜色增量 = 亮度 * 最大变换量;
绿色减量 = 暗度 * 最大变换量;
从图像上看,到这一步图像该透明的地方已经透明了,也没有绿色的出现了。但是,肉眼仍然可能发现场景中有绿色,我们的肉眼觉得黄色相比橙色更绿,所以这个黄色在橙色旁边的话,这个黄色有些显绿。所以我们还需要颜色校正。
4、颜色矫正处理
场景中的物体被光打亮,最终这些光汇聚到摄像机上,才看到了现在的图像,因为我们身处在一个房间里,我们身上可能会被绿幕或绿棚的漫反射出来的光经过并反射出多余的绿色。这一步就是为了将这些漫反射的绿色去除。
假设屏幕空间的光是均匀的,只考虑漫反射(绿幕或绿棚的镜面反射的程度很低),根据物体反射出绿是多少,就可以计算当前色块对应绿色吸收率(比如理论上,黑色和白色对绿色的吸收率不同,场景中均匀的漫反射绿光打上去,增加的G值就不同)。因为这一步的目的是降低绿光对场景的影响,可以不考虑将画面提亮(不对RB通道进行处理)。
实现:
设置一个[0,1]的场景中绿光漫反射强度greenIntensity,0为最强(保持当前图像状态,不对图像颜色进行任何处理),1为最弱。
所以理论上的代码是这样的:
fixed overMainColor = c.g * greenIntensity;
c.g = lerp(c.g, overColorGreen, c.g);
但为了效果更好,可以把Lerp的程度改为矫正率,这样更贴近实际我们选择的颜色
首先计算颜色矫正最多矫正到什么颜色,然后根据假设的绿光漫反射强度和矫正率计算最终颜色
fixed rectify = 使用简单去绿之前的颜色值进行上图取值;
fixed overMainColor = c.g * greenIntensity;
c.g = lerp(c.g, overColorGreen, rectify);
5、更多优化
上面只是做了一些基础的抠绿处理,想实现更高级的处理可以通过:
·尝试YCbCr空间进行计算
·使用更大的边缘虚化的算子
·使用更多的抠绿颜色采样
·使用遮罩,对某部分特殊处理(如处理影子或是边框)
·保存并分析前几帧的数据,进行动态抠绿,这个方案需要使用Unity后处理那套机制了,而不是简单的Shader
·使这个Shader对蓝幕进行处理(跟绿幕同理,这里就不展开了)(再括号 不用想着红幕,没有客户有那种需求)
·阴影和其他后处理:
阴影处理我想到的有2种:
1就是在场景中打光的时候留意人的影子,别补光把影子补没了,这样扣绿扣不下去,会留在图像中。这种影子也更真实。
2使用假影子:因为Unity不支持半透明阴影,这里可以由我们自己写一个假阴影,这步其实就是扣绿流程的简化,并最终输出黑白色的面片放在场景中
其他后处理就看想象力了,比如加入一点环境光,就如同根据图像调风格化渲染一样。
·硬件方面可以通过调整图像输入设备(相机)、显示器分辨率(显卡要支持)来进行优化,使最终输出图像有更好的效果
6、效果展示
(图像来源是截的图,分辨率不如原视频高,再加上抠绿技术没专业抠绿软件成熟,肯定没人家做的好)
CbCr图测试处理(这个CbCr图不标准):
肢体因动作模糊的处理:
发丝处理:这里想处理好发丝跟图像分辨率关系很大(相当大),这里可以看出头发仍然有点绿,这是因为颜色矫正无法处理这种混合色,并且简单去绿算法过于简单,无法很好的处理这种混合颜色,之后有时间我再优化一下。
透明物体:
阴影处理:
代码的话,之后等有时间整理后发布
(这是我的第一篇技术分享文章,如有错误或不严谨的地方,还望指正)
引用:https://www.cnblogs.com/mtcnn/p/9411978.html
图像来源:
https://www.bilibili.com/video/BV1bf4y1R7oa?spm_id_from=333.999.0.0
https://www.bilibili.com/video/BV1SK4y1j7oh/?spm_id_from=333.788.recommend_more_video.0
https://www.bilibili.com/video/BV1C44y177a7/?spm_id_from=333.788.recommend_more_video.9
以上是关于使用Unity Shader 实现透明度抠绿(AlphaMatting)的主要内容,如果未能解决你的问题,请参考以下文章
Unity Shader入门精要学习笔记 - 第8章 透明效果
Unity Shader ------ UV动画原理及简易实现
Unity Shader ------ 高级纹理之渲染纹理及镜子与玻璃效果的实现