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

倡导文明和谐,往往需要给图片打上万恶的马赛克,对于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;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容