Metal图像处理——颜色查找表(Color Lookup Table)

正文

一张1024x1024的普通图片,是由1024 * 1024=1048576个像素点组成,每个像素点包括RGBA共32bit,常见的图像处理是对相邻像素点颜色、像素点本身颜色做处理。 在对像素点本身颜色做处理的情况下,需要把某个颜色映射成另外一个颜色,比如说把颜色rgb(0.2, 0.3, 0.4) * colorMatrix = rgb(0.1, 0.2, 0.3),可以使用shader实现这个颜色转变对图片进行处理。但实际过程中的颜色映射计算过程可能会更加复杂,并且会有很多冗余运算(比如我们对相同的颜色会有重复的运算),我们希望用空间换取时间,把相同颜色值的运算结果缓存下来。

如何避免冗余运算?

假如我们用一个三维数组colorConvert来缓存这个结果,那么rgb(0.2, 0.3, 0.4) * colorMatrix处理就变成数组访问操作rgb(0.2, 0.3, 0.4) =colorConvert[0.2 * 255][0.3 * 255][0.4 * 255]=rgb(0.1, 0.2, 0.3),运算效率会有较高的提升。

但是数组长度有512* 512 * 512= 134 217 728,太占用内存!我们可以减少数组每一维的大小,把512种可能减少为64种。同时为了有更好的过渡效果,每次计算的时候我们可以用相邻的结果进行线性结合。

我们以一维的情况为例,用数组a[64]来缓存512种颜色的映射结果。假如某个点的值是102,那么有102/4=25.5,映射结果为a[25] * 0.5+a[26] * 0.5,即两边各取一半;假如某个点的值是101,那么有101/4=25.25,映射结果为a[25] * 0.25 + a[26] * 0.75,按照小数点进行分配。

这样可以用合理的数组大小缓存运算结果,并且可以在PC端提前计算出映射的数组。 接下来的问题是:

如何把映射数组传递给shader?

直接的方案是使用文本记录映射结果,然后把移动端加载文本,读取结果后存入内存的数组buffer,再把buffer作为shader的一个参数。

这里我们肯定不采用这种办法,而是采用颜色查找表(Color Lookup Table)。 我们的映射数组是colorConvert3[64][64][64],相当于64个二维数组colorConvert2[64][64]。如果我们colorConvert2[i][j]的结果写入一张64 * 64的图片第(i, j)个像素点,即用一张64 * 64的图片来缓存这个结果,如下:

对于colorConvert3[64][64][64],可以采用把64张图片拼成一个8 * 8个小图组成的大图,如下:

最后,问题只有:

如何从图片读取对应运算结果?

图片有64个正方形,每个小正方存着64 * 64的运算结果。对于颜色rgb(x, y, z),我们先用z值算出正方形的位置,再用(x,y)读取对应结果。 整个过程如下:(shader中的颜色值都是归一化后的结果,区间为[0, 1])

  • 1、用蓝色值计算正方形的位置,得到quad1和quad2;
  • 2、根据红色值和绿色值计算对应位置在整个纹理的坐标,得到texPos1和texPos2;
  • 3、根据texPos1和texPos2读取映射结果,再用蓝色值的小数部分进行mix操作;

整个shader如下:

constant float SquareSize = 63.0 / 512.0;
constant float stepSize = 0.0; //0.5 / 512.0;

fragment float4
samplingShader(RasterizerData input [[stage_in]], // stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
               texture2d<float> normalTexture [[ texture(LYFragmentTextureIndexNormal) ]], // texture表明是纹理数据,LYFragmentTextureIndexNormal是索引
               texture2d<float> lookupTableTexture [[ texture(LYFragmentTextureIndexLookupTable) ]]) // texture表明
{
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear); // sampler是采样器
    float4 textureColor = normalTexture.sample(textureSampler, input.textureCoordinate); //正常的纹理颜色
    
    float blueColor = textureColor.b * 63.0; // 蓝色部分[0, 63] 共64种
    
    float2 quad1; // 第一个正方形的位置, 假如blueColor=22.5,则y=22/8=2,x=22-8*2=6,即是第2行,第6个正方形;(因为y是纵坐标)
    quad1.y = floor(floor(blueColor) * 0.125);
    quad1.x = floor(blueColor) - (quad1.y * 8.0);
    
    float2 quad2; // 第二个正方形的位置,同上。注意x、y坐标的计算,还有这里用int值也可以,但是为了效率使用float
    quad2.y = floor(ceil(blueColor) * 0.125);
    quad2.x = ceil(blueColor) - (quad2.y * 8.0);
    
    float2 texPos1; // 计算颜色(r,b,g)在第一个正方形中对应位置
    /*
     quad1是正方形的坐标,每个正方形占纹理大小的1/8,即是0.125,所以quad1.x * 0.125是算出正方形的左下角x坐标
     stepSize这里设置为0,可以忽略;
     SquareSize是63/512,一个正方形小格子在整个图片的纹理宽度
     */
    
    texPos1.x = (quad1.x * 0.125) + stepSize + (SquareSize * textureColor.r);
    texPos1.y = (quad1.y * 0.125) + stepSize + (SquareSize * textureColor.g);
    
    float2 texPos2; // 同上
    texPos2.x = (quad2.x * 0.125) + stepSize + (SquareSize * textureColor.r);
    texPos2.y = (quad2.y * 0.125) + stepSize + (SquareSize * textureColor.g);
    
    float4 newColor1 = lookupTableTexture.sample(textureSampler, texPos1); // 正方形1的颜色值
    float4 newColor2 = lookupTableTexture.sample(textureSampler, texPos2); // 正方形2的颜色值
    
    float4 newColor = mix(newColor1, newColor2, fract(blueColor)); // 根据小数点的部分进行mix
    return float4(newColor.rgb, textureColor.w); //不修改alpha值
}

总结

颜色转换表是在网上找了一张,特此感谢——LUT(颜色查找表)的来源; Shader部分参考自GPUImageLookupFilter,demo的地址在这里

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏听雨堂

从MapX到MapXtreme2004[11]-坐标概论

        坐标的问题是Mapxtreme中最郁闷的问题,前几天在这上面耗了很多时间,没有搞定,今天又是不得不钻研,还好,小有心得。         1、...

2217
来自专栏hbbliyong

Opencv-python画图基础知识

2515
来自专栏黒之染开发日记

【easeljs】矢量形状 Shape类

一个Shape(形状)允许你在显示列表中显示矢量图。它包含一个带有所有绘制矢量图形的方法的Graphics(图形)实例。Graphics实例可以在多个Shape...

1173
来自专栏腾讯IVWEB团队的专栏

WebGL 纹理颜色原理

现在已经知道顶点着色器和片段着色器一起决定着向颜色缓冲区写入颜色信息并最终呈现出来,那么这个过程是什么样,如果图形的颜色需要用现有图片来渲染那么又该如何操作?

5641
来自专栏葡萄城控件技术团队

Spread for Windows Forms快速入门(9)---使用公式

Spread的公式计算引擎支持300多种内置函数,并支持通过内置函数和运算符来自定义公式。支持的函数包括日期、时间函数、工程计算函数、财务计算函数、逻辑函数、数...

2265
来自专栏互联网软件技术

canvas实现验证码

在通常的登录界面我们都可以看到验证码,验证码的作用是检测是不是人在操作,防止机器等非人操作,防止数据库被轻而易举的攻破。

1553
来自专栏HTML5学堂

原生JS | 导航底部横线跟随鼠标缓动

HTML5学堂(码匠):在上周当中,我们用jQuery实现了 - 在导航底部存在一条横线,跟随着鼠标缓动到相应导航项 - 的特效,今天我们来讲讲原生JS的实现方...

6558
来自专栏Android机动车

用数学思维实现雷达分析图

前段时间回看里约奥运会的国球比赛,岛国媒体给我龙队一个响亮的称号—— 六边形战士 !

1142
来自专栏数据结构与算法

Stirling数

第一类: 定义 第一类Stirling数表示表示将 n 个不同元素构成m个圆排列的数目。又根据正负性分为无符号第一类Stirling数 ? 和带符号第一类...

33610
来自专栏小李刀刀的专栏

深入解析CSS样式层叠权重值

读到《重新认识CSS的权重》这篇,鬼哥在文章最后给出了便于记忆的顺序:“important > 内联 > ID > 类 > 标签 | 伪类 | 属性选择 > 伪...

3386

扫码关注云+社区

领取腾讯云代金券