iOS视觉(十五) -- 灰度滤镜、马赛克滤镜

1,281 阅读7分钟

一、灰度滤镜

从前篇了解到,滤镜的实现就是基于着色器上的处理,灰度滤镜顾名思义就是将整个图像转化为灰色。

那么就应该是在片元着色器中,读取到每一个像素点后将其进行一个灰度处理。

每一个像素点都是一个rgb值,如何将它转化为灰色呢? 前辈们已经为我们做好了相应的公式:

  1. 浮点算法:Gray = R * 0.3 + G * 0.59 + B * 0.11
  2. 整数⽅法:Gray = (R * 30 + G * 59 + B * 11) / 100
  3. 移位⽅法:Gray = (R * 76 + G * 151 + B * 28)>>8
  4. 平均值法:Gray = (R + G + B)/3
  5. 仅取绿⾊:Gray = G

既然有了公式,那么灰色处理变的简单了许多:

precision highp float;//精度
varying lowp vec2 varyTextCoord;//纹理坐标
uniform sampler2D colorMap;//纹理数据

const highp vec3 Gray = vec3(0.3, 0.59, 0.11);//灰度滤镜公式

void main() {
    vec4 mask = texture2D(colorMap, varyTextCoord);//纹素
	float grayMask = dot(mask.rgb, Gray);//将灰色滤镜公式与纹素进行点成得到新的纹素rgb
    gl_FragColor = vec4(vec3(grayMask), 1.0);//渲染纹素
}

实现效果如下:

二、马赛克滤镜

不难发现,日常中最常见的马赛克处理方式都是图片的内容变为了一个个的小正方体, 仅仅能大概的猜测这张图片的内容,并不能正确知道具体内容。这种常见的马赛克方式也就是正方形马赛克。其次,还有一些其他的马赛克,例如:六边形、圆形、三角形等。

马赛克的原理呢其实就是将一张图片等份的划分为若干个区域,在这若干个区域内取一个像素点,将之代替整个区域的像素点。

2.1、正方形马赛克

首先是划分好等份的正方形区域(不要在意这些细节):

再来对每一个方块的像素点进行处理:

例如 ① ② ③分别为此方块中任意一个像素点,我们直接将这些像素点设置为左上角的像素点,也就意味着这个方块内的所有内容都是左上角的那个像素点。

捋清楚了之后就需要在片元着色器中进行处理了。

首先划分的问题,我们在进行一个纹理数据加载的时候是可以读取到这个纹理的宽高,每个方块的大小可以由我们直接写死或者是通过UI界面来调节进行传递到片元着色器上。

为了方便,这里直接定义图片宽高为400, 每一个方块的宽高均为10.

下面就来实现片元着色器:

  1. 获取当前像素点坐标(0 ~ 1)
  2. 获取当前像素在图片中的坐标位置 (0 ~ 图片分辨率宽高)
  3. 通过计算得到当前像素点所在的方块的横向纵向位置
  4. 拿到位置后,根据以上步骤逆向转化为对应的方块的第一个纹理坐标(0 ~ 1)
  5. 新的纹理坐标与纹理生成纹素
precision highp float;

varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

const vec2 TexSize = vec2(400.0, 400.0);
const vec2 mosaicSize = vec2(10.0, 10.0);

void main() {

	//获取当前像素点的纹理坐标 (0~1)
    vec2 intXY = vec2(varyTextCoord.x, varyTextCoord.y);
    
    //得到此像素点在第几个方块中
    vec2 hor_ver = vec2(floor(intXY.x * TexSize.x / mosaicSize.x), floor(intXY.y * TexSize.y / mosaicSize.y));
    
    //获取当前左上角的点
    vec2 newVaryTextCoord = vec2(hor_ver.x * mosaicSize.x / TexSize.x, hor_ver.y * mosaicSize.y / TexSize.y);
    
    //生成新的纹素
    vec4 mask = texture2D(colorMap, newVaryTextCoord);

    gl_FragColor = mask;
}

前后对比:

PS.每一个小方块越大,马赛克也就打的越重。

2.2、六边形马赛克

六边形马赛克也就是将上述的正方形划分换为六边形划分。由于是一个多边形,处理起来肯定不如正方形处理来的方便。

如图划分:

但是我们做处理的时候,并不是按照一个一个六边形来进行处理的,而是按照一个一个矩形来处理的:

随意取一个像素点,我们需要做的就是判断这个像素点在矩形内,距离哪个矩形的边角点较近,就取哪一个像素点。 这步操作其实就是处理这个像素点在哪一个六边形内:

这里就需要用到一些基本的三角函数,用来计算一个像素点分别到两个顶点的距离,取最近的距离用于渲染。

但是究竟是取左上角右下角还是右上角左下角? 我们取的点肯定是六边形的核心点,所以需要针对两种情况:

取左下角与右上角: 取左上角与右下角:

接下来的处理就与正方形马赛克处理非常相似了。

大致的思路就是如此,接下来就细分一下如何实现这个处理:

  1. 定义每一个马赛克的大小,也就是确定一下六边形的边长,声明变量mosaicSize
  2. 通过马赛克大小后,可通过三角函数求得每一个矩形的宽高比为 333:\sqrt{3},后续即可通过宽高比乘以六边形边长即可得到具体宽高
  3. 通过奇偶行列区别当前像素点究竟是去哪两个顶点做比较, 像素点所在矩形的位置就会出现四种情况:
    1. 奇列奇横,取左上右下点
    2. 奇列偶横,取左下右上点
    3. 偶列奇横,取左下右上点
    4. 偶列偶横,取左上右下点
  4. 计算出具体最近的纹理坐标,生成纹素进行渲染

上代码:

precision highp float;

varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

const float mosaicSize = 0.03;//马赛克边长

void main() {
    
    float mosaicLength = mosaicSize;//马赛克边长.
    //宽高比 TB : TR = 3 : √3
    float TB = 1.5;//矩形宽比例
    float TR = 0.866025;//矩形高比例
    
    float x = varyTextCoord.x;//任意像素点x坐标
    float y = varyTextCoord.y;//任意像素点y坐标
    
    
    // TB * mosaicLength 得到矩形宽度, 在用x除以宽度得到此像素点在横向第几个矩形内
    int wx = int(x / (TB * mosaicLength));
    // TR * mosaicLength 得到矩形高度, 在用y除以高度得到此像素点在纵向第几个矩形内
    int wy = int(y / (TR * mosaicLength));
    
    //分别记录 左边顶点,右边顶点,最终需要的顶点
    vec2 v1, v2, vn;

    //判断wx为奇数列
    if (wx / 2 * 2 != wx) {
        //判断wy为奇数横
        if (wy / 2 * 2 != wy) {
            //奇列奇横, 取左上与右下点
            v1 = vec2(float(wx) * TB * mosaicLength, float(wy) * TR * mosaicLength);//左上
            v2 = vec2(float(wx + 1) * TB * mosaicLength, float(wy + 1) * TR * mosaicLength);//右下
        } else {
            //奇列偶横, 取左下与右上
            v1 = vec2(float(wx) * TB * mosaicLength, float(wy + 1) * TR * mosaicLength);//左下
            v2 = vec2(float(wx + 1) * TB * mosaicLength, float(wy) * TR * mosaicLength);//右上
        }
    } else {
        //判断wy为奇数横
        if (wy / 2 * 2 != wy) {
            //偶列奇横, 取左下与右上
            v1 = vec2(float(wx) * TB * mosaicLength, float(wy + 1) * TR * mosaicLength);//左下
            v2 = vec2(float(wx + 1) * TB * mosaicLength, float(wy) * TR * mosaicLength);//右上
        } else {
            //偶列偶横, 取左上与右下点
            v1 = vec2(float(wx) * TB * mosaicLength, float(wy) * TR * mosaicLength);//左上
            v2 = vec2(float(wx + 1) * TB * mosaicLength, float(wy + 1) * TR * mosaicLength);//右下
        }
    }
    
    //计算像素点到达顶点的距离(三角函数 x^2 + y^2 = z^2)
    float s1 = sqrt(pow(x - v1.x, 2.0) + pow(y - v1.y, 2.0));
    float s2 = sqrt(pow(x - v2.x, 2.0) + pow(y - v2.y, 2.0));
    
    //判断哪个点距离最近就取哪个点
    if (s1 < s2) {
        vn = v1;
    } else {
        vn = v2;
    }
	//生成纹素
    vec4 mask2 = texture2D(colorMap, vn);

    gl_FragColor = mask2;
}

效果如下:

注:运行起来会感觉卡顿是因为在模拟器上由于CPU模拟GPU渲染造成的,使用真机就不会出现卡顿的情况

三、总结

归根到底,上述滤镜的处理都需要对片元着色器的每一个像素点进行一次处理,需要用到一些基本的数学知识,由此可见在一些复杂的图像处理上将会用到许多的数学相关函数,这个时候数学功底就慢慢变的比较重要了。

上述正方形马赛克是直接取的左上角,而六边形是取的中心点,通过计算也可以在正方形马赛克上取中心点,六边形马赛克上取左上角点等。

除去正方形马赛克、六边形马赛克,还有一些其他形式的马赛克,但是万变不离其宗,变化的只是马赛克图形的变化,我们需要做的就是通过代码来找到像素点属于在哪一个马赛克图形的区域之内,再对其进行一次变换生成纹素即可形成马赛克图形。