音视频OpenGL

关于OpenGL ES实现马赛克滤镜的思路与实现

2020-08-12  本文已影响0人  iOSer_jia

倡导文明和谐,往往需要给图片打上万恶的马赛克,对于iOS开发者来说,给图片打码需要使用OpenGL ES,编写GLSL文件给图片打码。
马赛克的原理其实就是将图片的某个区域用同一个色块填充,从而达到降低图片分辨率的效果,色块的颜色要根据该区域某个点来确定。本文将介绍正方形、正六边形、正三角形3种马赛克的实现算法。

正方形马赛克

正方形马赛克

正方形马赛克是将图片分成n*n个小的正方形色块,每个色块取一个当前色块某个点的颜色值填充。
比如一张 w*w 图片,如果我们要使用s*s的正方形马赛克滤镜,那么这个图片就要分割成 n*n 个正方形色块(n=floor(w/s), floor()是向下取整公式),每个正方形的色块颜色取这个这个正方形起始点的颜色值。比如如果当前纹理坐标为(x, y),我们可以通过公式

(floor(x/s)*s, floor(y/s)*s)

得到这个纹理坐标所处色块的起始点坐标,并取该起始点坐标的颜色值填充。

正方形马赛克原理.png

算法的具体实现在片元着色代码中。


precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
// 假定纹理的大小
const vec2 TextureSize = vec2(400.0, 400.0);
// 马赛克的大小
const vec2 MosaicSize = vec2(8.0, 8.0);

void main (void) {
    // 纹理坐标是0~1, 先将纹理坐标扩大假定纹理大小
    vec2 TextureXY = vec2(TextureCoordsVarying.x * TextureSize.x, TextureCoordsVarying.y * TextureSize.y);
    // 计算得到假定纹理大小下当前纹理坐标所处色块的起始点位置
    vec2 MosaicXY = vec2(floor(TextureXY.x/MosaicSize.x)*MosaicSize.x, floor(TextureXY.y/MosaicSize.y)*MosaicSize.y);
    // 在将起始点位置换算成标准0~1的范围
    vec2 MosaicCoord = vec2(MosaicXY.x/TextureSize.x, MosaicXY.y/TextureSize.y);
    
    vec4 mask = texture2D(Texture, MosaicCoord);
    gl_FragColor = vec4(mask.rgb, 1.0);
}

另外附上顶点着色器代码

attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;

void main() {
    TextureCoordsVarying = TextureCoords;
    gl_Position = Position;
}

顶点着色器不需要做任何与马赛克滤镜有关的操作,较为简单,后续的六边形马赛克和三角形马赛克均采用相同顶点着色器代码。

六边形马赛克

六边形马赛克

六边形马赛克滤镜是用交错的六边形将图片分割成一个个色块,与正方形不同,我们无法直接知道当前纹理坐标位于哪个六边形,但我们可以将两个相邻的六边形组成一个矩形,如下图所示。

六边形

我们可以将六边形问题转换为矩形问题,和正方形一样,我们可以算出当前坐标位于哪个矩形内,然后再判断当前坐标是离v1和v2距离,决定采用哪个v1还是v2的色值,达到我们的目标效果。

要实现这个算法我们首先需要计算矩形的长宽,我们用高中的几何知识来计算,目前的已知条件是六边形的边长ab,根据正六边形的性质,我们知道顶点a到原点v1的距离等于边长,即 v1a = ab。而a、d、v2组成的三角形,很明显是一个角度分别为30°、60°、90°的直角三角形,我们都知道直角三角形30°角所对的那条边等于斜边的一半,得出 ad = 0.5*v2a = 0.5*ab,另外根据勾股定理得出 v2d = ad*√3 = ab*0.5*√3 ≈ 0.866025*ab, 而矩形的长 v1d = v1a + ad = ab + 0.5*ab = 1.5ab。所以我们可以得出公式

width = length * 1.5
height = length * 0.866025

length为六边形的边长,width为矩形的长,height为矩形的宽

得到矩形的长宽后,我们就可以计算当前纹理坐标(x, y)所处矩形的两个原点v1、v2,因为矩形的原点的位置有两种情况,我们需要分开讨论。

矩形一

矩形1

首先需要知道当前坐标位于第几行第几列的矩形,

column = floor(x/v1d) = floor(x/(ab*1.5)) = floor(x/(1.5*length))
row = floor(y/v2d) = floor(y/(0.866025*length))

所以,

v1.x = column * width = column * length * 1.5
v1.y = row * height + height = (row+1) * length * 0.866025

v2.x = column * width + width = (column+1) * length * 1.5
v2.y = row * height = row * length * 0.866025

矩形二

矩形2

同样的,可以计算得到

v1.x = column * width = column * length * 1.5
v1.y = row * height = row * length * 0.866025

v2.x = column * width + width = (column+1) * length * 1.5
v2.y = row * height + height = (row+1) * length * 0.866025

接下来的步骤显然是得知当前坐标(x, y)所在矩形是矩形一还是矩形二,我们可以通过奇偶行列获得。

奇偶关系

化繁为简,我们可以从起始位置开始观察,当前坐标位于偶数行、偶数列和奇数行、奇数列时是矩形二,奇数行、偶数列和偶数行、奇数列时是矩形一,至此,我们可以拿到v1和v2的坐标。

得到v1,v2后,我们就可以判断当前坐标(x, y)离谁更近而决定采用哪个原点的颜色值。

附上着色器代码:


precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

const float mosaicSize = 0.015;

void main (void) {
    
    float length = mosaicSize;
    
    float dx = 1.5;
    float dy = 0.866025;
    
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    // 当前位于第几行和第几列矩形
    int wx = int(x/dx/length);
    int wy = int(y/dy/length);
    
    vec2 v1, v2, vn;
    
    if (wx/2 * 2 == wx) {// 偶数行
        if (wy/2 * 2 == wy) { // 偶数列
            v1 = vec2(length*dx*float(wx), length*dy*float(wy));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
        } else { // 奇数列
            v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
        }
    } else { // 奇数行
        if (wy/2 * 2 == wy) {  //偶数列
            v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
        } else { // 奇数列
            v1 = vec2(length*dx*float(wx), length*dy*float(wy));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
        }
    }
    
    // 当前坐标到v1、v2的距离
    float d1 = sqrt(pow(v1.x-x, 2.0) + pow(v1.y-y, 2.0));
    float d2 = sqrt(pow(v2.x-x, 2.0) + pow(v2.y-y, 2.0));
    
    if (d1 < d2) {
        vn = v1;
    } else {
        vn = v2;
    }

    vec4 mask = texture2D(Texture, vn);
    gl_FragColor = mask;
}

三角形马赛克

三角形马赛克

三角形马赛克是在六边形马赛克基础上变化得来的,从下图可以看到,一个正六边形可以分割成6个正三角形,而我们已经知道当前坐标(x, y)所在的六边形,只需要通过计算当前坐标(x, y)和原点的连线与起始边的夹角判断该点位于哪个3角形区域内,再取该三角形区域中心点的颜色值填充,就可以得到最终的结果。

三角形马赛克原理

夹角的计算可以使用用公式
a = atan((y-v0.y), (x-v0.x))

着色器具体代码如下:


precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

const float mosaicSize = 0.03;

void main (void) {
    
    float length = mosaicSize;
    
    float dx = 1.5;
    float dy = 0.866025;
    
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    int wx = int(x/dx/length);
    int wy = int(y/dy/length);
    
    vec2 v1, v2, vn;
    
    if (wx/2 * 2 == wx) {
        if (wy/2 * 2 == wy) {
            v1 = vec2(length*dx*float(wx), length*dy*float(wy));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
        } else {
            v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
        }
    } else {
        if (wy/2 * 2 == wy) {
            v1 = vec2(length*dx*float(wx), length*dy*float(wy+1));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy));
        } else {
            v1 = vec2(length*dx*float(wx), length*dy*float(wy));
            v2 = vec2(length*dx*float(wx+1), length*dy*float(wy+1));
        }
    }
    
    float d1 = sqrt(pow(v1.x-x, 2.0) + pow(v1.y-y, 2.0));
    float d2 = sqrt(pow(v2.x-x, 2.0) + pow(v2.y-y, 2.0));
    
    if (d1 < d2) {
        vn = v1;
    } else {
        vn = v2;
    }
    
    // 将π分为3等分
    float PI3 = 3.14159/3.0;
    // 获得弧度值
    float a = atan((y-vn.y), (x-vn.x));
    // 每个中心点与原点的xy偏移值 
    float xoffset = length*0.5;
    float yoffset = xoffset*dy;
    
    // 对象图中6个三角形区域的中心点
    vec2 area1 = vec2(vn.x+xoffset, vn.y+yoffset);
    vec2 area2 = vec2(vn.x, vn.y+yoffset);
    vec2 area3 = vec2(vn.x-xoffset, vn.y+yoffset);
    vec2 area4 = vec2(vn.x-xoffset, vn.y-yoffset);
    vec2 area5 = vec2(vn.x, vn.y-yoffset);
    vec2 area6 = vec2(vn.x+xoffset, vn.y-yoffset);
    
    // 判断当前坐标位于哪个区域
    if (a >= 0.0 && a < PI3) {
        vn = area1;
    } else if (a >= PI3 && a < PI3*2.0) {
        vn = area2;
    } else if (a >= PI3*2.0 && a <= PI3*3.0) {
        vn = area3;
    } else if (a <= -PI3*2.0 && a >= -PI3*3.0) {
        vn = area4;
    } else if (a <= -PI3 && a > -PI3*2.0) {
        vn = area5;
    } else if (a <= 0.0 && a < -PI3) {
        vn = area6;
    }

    vec4 mask = texture2D(Texture, vn);
    gl_FragColor = mask;
}

上一篇 下一篇

猜你喜欢

热点阅读