《关于我马上就要考研却还花时间自学数字图像处理这档事》 -写于2021年12月21日(摸鱼摸鱼)。。
计算机图形学、数字图像处理、计算机视觉(模式识别) 这三门学科各不相同,各有交叉 (图片转自知乎用户 孙佳明 的回答)
画图表示就长下面这样: (三年美术经验,指上小学前学了一堆长大后全tm忘完了)
学了数字图像处理的我才终于意识到,原来冯女神的入门精要12章、13章讲的是数字图像处理的内容啊。怪说不得自学起来这么吃力原来我连地基都没搭就开始造房子,不塌才怪。
这里简单介绍下,unity将屏幕当成一张面片处理(别问我为什么这么处理,因为opengl是这么干的)。所以我们写的屏幕后处理效果往往更多应用的是图像处理而不是图形学的知识了。
OK,开始。 首先上模之屋把原神的胡桃和烟绯模型下载下来,用作本次示例。 接着用MMD4Mecanim这个牛逼的插件把模型转为FBX用在unity中,换上我自己写的卡通着色材质。
颜色处理
根据数字图像处理的内容,一幅RGB图像就是M×N×3大小的彩色像素数组,每个彩色像素点都是在特定空间位置的成彩色图像所对应的红绿蓝分量。RGB也可以视为三幅灰度图像形成的“堆叠”,当将他们送到彩色显示器的红绿蓝输入端时,会在屏幕上生成一副彩色图像。
反色(负片效果) 反色效果可以说是这次教程最简单的了,反色也可以叫补色,意思是某个颜色加上它的补色可以变成白色。而一般我们用的都是RGB色彩模型,1-图像的RGB值就可以得到图像的反色。
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col = 1 - col;
return col;
}
灰度图 注意,一般如下面这张图在计算机图像邻域我们称之为灰度图像而不是黑白图像。黑白图像指的是完全只有黑白两种颜色的图像,但灰度图像在黑色与白色之间还有许多级的颜色深度,比如浅灰深灰这样的区分。 另外在单色图像中,灰度=亮度。但彩色图像则不能这么理解。 在图像识别邻域中,我们往往会将彩色图像转换为灰度图像后再做处理,因为梯度因素是最关键的,梯度意味着边缘,自然就用到灰度图像了。而颜色本身就容易受到光照等因素的影响,同类的物体颜色有很多变化。所以颜色本身难以提供关键信息。 (部分摘自知乎水哥的回答)
具体将彩色图像转换为一张灰度图像的流程其实就是将RGB三色通道转换为一个通道的过程。计算公式有很多: 1.浮点算法:Gray=R0.3+G0.59+B0.11 2.整数方法:Gray=(R30+G59+B11)/100 3.移位方法:Gray =(R76+G151+B*28)>>8; 4.平均值法:Gray=(R+G+B)/3; 5.仅取绿色:Gray=G; 这里我就用冯乐乐入门精要里的计算公式:Gray=R * 0.2125 + G *0.7154 + B * 0.0721
fixed4 col = tex2D(_MainTex, i.uv);
fixed graycolor = col.r * 0.2125 + col.g *0.7154 + col.b * 0.0721;
return fixed4(graycolor,graycolor,graycolor,1.0);
黑白图 和灰度图做了区别后,我们自然知道黑白图就是只有颜色值为255和0的图像(即只有黑白)。 要想实现黑白图,可以设定一个阈值(我这里就设定0.5,也就是纯灰色),灰度值小于阈值的输出黑色,大于阈值的返回白色。 代码如下:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed graycolor = col.r * 0.2125 + col.g *0.7154 + col.b * 0.0721;
fixed splitColor = saturate(sign(graycolor - 0.5));
fixed4 finalColor = fixed4(splitColor,splitColor,splitColor,1);
return finalColor;
}
曝光处理 (图截自天津理工大学杨淑莹的数字图像处理课程)
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if(col.r < 0.5){col.r = 1 - col.r;}
if(col.g < 0.5){col.g = 1 - col.g;}
if(col.b < 0.5){col.b = 1 - col.b;}
return col;
}
(这个就不多赘述了,只是为了简单实现效果,真要写这么多if写在shader里我怕是要被同事追着打)
模糊
说到模糊效果,这个就要引入一下图像处理中的卷积概念。 我们把要处理的平面图像看做一个矩阵, 图像的每个像素分别对应着矩阵的元素, 假设屏幕的分辨率是 640*480, 那么对应的矩阵行数= 640, 列数= 480。 在数字图像处理中, 有一种处理方法叫滤波(分为线性滤波和非线性滤波),用于滤波计算的是滤波器(也叫卷积核), 滤波器通常是个方阵(即长宽相等的方形矩阵)。 进行滤波就是对于大矩阵中的每个像素, 计算它周围像素和滤波器矩阵对应位置元素的乘积, 然后把结果相加到一起, 最终得到的值就作为该像素的新值, 这样就完成了一次滤波。 尽可能简单地说,你用一个小方阵,从左到右从上到下的顺序遍历整个图像,每一次遍历都用方阵里的数值和方阵所覆盖图像范围内对应的像素值做计算。得到最后的值就是方阵中心位置对应的图像像素进行滤波后的值。 如果还是不太清楚,看看下面的图:
原图像某像素点值为1,经过卷积核加权运算后:-4 * 2 + 4 * 0 + (其他都是0*0我就不写了) = -8。以此得出经过某种滤波后该像素点值为-8。
在shader里构造一个滤波器 开头我们就提到了,unity底层是把屏幕当成一张面片处理,那我们就屏幕后处理材质来实现某些特效。 如下,在顶点着色器里构造了一个3x3矩阵。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv + _MainTex_TexelSize.xy * float2(-1,-1);
o.uv[1] = v.uv + _MainTex_TexelSize.xy * float2( 0,-1);
o.uv[2] = v.uv + _MainTex_TexelSize.xy * float2( 1,-1);
o.uv[3] = v.uv + _MainTex_TexelSize.xy * float2(-1, 0);
o.uv[4] = v.uv + _MainTex_TexelSize.xy * float2( 0, 0);
o.uv[5] = v.uv + _MainTex_TexelSize.xy * float2( 1, 0);
o.uv[6] = v.uv + _MainTex_TexelSize.xy * float2(-1, 1);
o.uv[7] = v.uv + _MainTex_TexelSize.xy * float2( 0, 1);
o.uv[8] = v.uv + _MainTex_TexelSize.xy * float2( 1, 1);
return o;
}
float2(-1,-1)对应的是滤波器左下角,float2(0,0)对应的是滤波器中心,以此类推。
均值模糊 一个3x3均值模糊卷积核如下: (也就是矩阵一共有N个元素,则每个元素的权重值为1/N。) (赶时间用画图画的,不要在意。。) 每一个元素对应的像素值乘以方阵的权重值,最后相加得到中心像素位置的值:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = 0;
for(int k = 0; k < 9 ;k++)
{ col += tex2D(_MainTex,i.uv[k]); }
return col * 0.1111;
}
结果: 再加一个float类型的_Range变量控制采样范围。
o.uv[0] = v.uv + _MainTex_TexelSize.xy * float2(-1,-1) *_Range;
o.uv[1] = v.uv + _MainTex_TexelSize.xy * float2( 0,-1) *_Range;
o.uv[2] = v.uv + _MainTex_TexelSize.xy * float2( 1,-1) *_Range;
o.uv[3] = v.uv + _MainTex_TexelSize.xy * float2(-1, 0) *_Range;
o.uv[4] = v.uv + _MainTex_TexelSize.xy * float2( 0, 0) *_Range;
o.uv[5] = v.uv + _MainTex_TexelSize.xy * float2( 1, 0) *_Range;
o.uv[6] = v.uv + _MainTex_TexelSize.xy * float2(-1, 1) *_Range;
o.uv[7] = v.uv + _MainTex_TexelSize.xy * float2( 0, 1) *_Range;
o.uv[8] = v.uv + _MainTex_TexelSize.xy * float2( 1, 1) *_Range;
_Range = 2时: 高斯模糊 高斯模糊的效果和均值模糊差不多,高斯模糊的卷积核叫高斯核,它的权重值是由高斯方程计算出来的。 (图摘自百科) 高斯模糊相比较均值模糊的优势在于它能很好地模拟邻域内每个像素对当前处理像素的影响程度——距离越近的像素影响越大,越远的像素影响越小。 当然,实时渲染当中我们就没必要把高斯方程计算出来,那样太消耗资源了,我们可以直接把高斯核的计算结果拿来用。 如下图,假定σ=1.5,模糊半径为1,得到一个3x3的矩阵。 (图片转自https://blog.csdn.net/farmwang/article/details/74452750)
将片元着色器代码修改如下:
fixed4 fragBlur (v2f i) : SV_Target
{
fixed4 sum = tex2D(_MainTex,i.uv[0]) * 0.147761;
sum += 0.0947416 * (tex2D(_MainTex,i.uv[1]) + tex2D(_MainTex,i.uv[3])
+ tex2D(_MainTex,i.uv[6]) + tex2D(_MainTex,i.uv[8]));
sum += 0.118318 * (tex2D(_MainTex,i.uv[2]) + tex2D(_MainTex,i.uv[4])
+ tex2D(_MainTex,i.uv[5]) + tex2D(_MainTex,i.uv[7]));
return sum;
}
结果如下(_Range = 1)
Bloom bloom是一种用于模拟视觉看向强光所观察到的一种强光从明亮区域的边缘渗入到周围的效果。 可能我表达不够清楚,直接上图(上图为一般效果,下图为打开bloom) (图片转自霜狼_may大佬的文章:https://www.bilibili.com/read/cv1491209/)
bloom个人认为一定要会,只要加上这玩意你就能以较低的成本换来画面上较大的提升。 bloom效果步骤分为以下: 1、提取画面中过亮的部分(比如强光源) 2、将过亮的部分做模糊处理,成新的图像 3、将得到的新图像与原图像“啪”地粘在一起,完成!
提取较亮的部分:
fixed GrayColor(fixed3 color)
{
fixed gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
return gray;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed gray = GrayColor(col.rgb);
fixed3 finalColor = fixed3(gray,gray,gray);
fixed c = clamp( gray - _Threshlod , 0.0,1.0);
return fixed4(col.rgb * c,1.0);
}
灰度图的好处在这里就体现出来了,我们将彩色图像转换为灰度图像,可以直接避开彩色的干扰来提取图像中较亮的部分(也就是灰度值接近白色的部分),设定一个阈值,让图像灰度值减去它,大于0的部分就是我们要的。
接着将提取出来的图像做模糊处理,用上述的两种模糊都可以。
最后,我们让这两张图贴贴
sampler2D _Tex;
sampler2D _MainTex;
v2f vert(a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_TARGET0
{
fixed4 a = tex2D(_MainTex,i.uv);
fixed4 b = tex2D(_Tex,i.uv);
return a + b;
}
ok,完成:
|