天天看點

利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/113379112

最後不少朋友問,“OpenGL ES 入門後怎麼學習寫一些濾鏡?”,“怎麼學習 shader ?”。

最近請教了一些大佬,他們一緻認為正确的做法就是“去模仿”。先去模仿别人的濾鏡怎麼實作的,比如觀察抖音的一些簡單的濾鏡,然後自己琢磨去實作一個。

當然,最有效率的方法是研究一些相關的開源項目,比如大名鼎鼎的 android-gpuimage 項目,該項目基本上實作了各種常見濾鏡,上手容易,學習 shader 、熟悉 GLSL 或者對 OpenGL 濾鏡感興趣的同學,可以研究下。

順便說下,最近看了一個項目叫 android-gpuimage-plus ,主要講是 Native 層實作的濾鏡,有一些比較不錯的思路可以參考下。

之前有一位朋友發了一副表情畫濾鏡的效果圖,就是利用不同的表情去替換不同的像素,生成一副由表情組成的圖像。表情畫濾鏡的原理其實跟字元畫相同,隻是字元換成了表情。

由于那副效果圖不友善展示,這裡就介紹下字元畫的實作原理,利用一個 shader 來實作字元畫效果。

字元畫濾鏡原理

字元畫濾鏡其實跟 LUT 濾鏡是同一個原理,本質上就是查表,像素替換。

實作字元畫濾鏡,首先想到的法子是,對圖像進行逐像素替換成字元(一個字元實際上是由多個像素組成的小圖檔)。

逐像素替換會有兩個問題:

  1. 一個像素有 RGB 24 位三個通道,一共有 256×256×256 種顔色,那麼多顔色要與字元表對應起來很麻煩;
  2. 逐像素替換字元,相當于原圖一個像素替換成多個像素,比如現在用的字元表,一個字元的大小是 16x23 = 268 像素,那麼渲染出來的圖像大小變為原來的 268 倍,顯然也不合理。

是以字元畫濾鏡實作的正确思路是先把原圖轉為灰階圖,這樣顔色種類隻有 256 種,然後做馬賽克,用一個小格子替換一個字元,保證小格子的寬高比與字元相同,確定替換後的字元不被拉伸,這樣渲染出來的圖像大小與原圖一樣。

利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

字元畫濾鏡原理一句話描述就是,原圖先做灰階圖馬賽克,再用小格子替換字元。

字元畫濾鏡實作

按照上節的原理描述,我們先對原圖做灰階圖馬賽克,擷取灰階值就直接對采樣後像素點的 RGB 分量進行灰階轉換。

//RGB 轉灰階公式
Y = 0.299R+0.587G+0.114B      

馬賽克效果原理就是将圖像分割成很多小區域,小區域内取相同的顔色,顔色值可以是該區域某些像素值的權重平均,本文取的是小矩形區域内中心點的像素值。

利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

這裡使用的字元表圖像尺寸 128x69 ,一共有 24 個字元,每個字元尺寸 16x23 像素。

灰階圖馬賽克的實作。

//灰階圖馬賽克
#version 100
precision highp float;
varying vec2 v_texcoord;
uniform lowp sampler2D s_textureY;
uniform lowp sampler2D s_textureU;
uniform lowp sampler2D s_textureV;
uniform lowp sampler2D s_textureMapping;
uniform vec2 texSize;

vec4 YuvToRgb(vec2 uv) {
    float y, u, v, r, g, b;
    y = texture2D(s_textureY, uv).r;
    u = texture2D(s_textureU, uv).r;
    v = texture2D(s_textureV, uv).r;
    u = u - 0.5;
    v = v - 0.5;
    r = y + 1.403 * v;
    g = y - 0.344 * u - 0.714 * v;
    b = y + 1.770 * u;
    return vec4(r, g, b, 1.0);
}

const vec3  RGB2GRAY_VEC3 = vec3(0.299, 0.587, 0.114);
const float MESH_WIDTH = 16.0;//一個字元的寬
const float MESH_HEIGHT= 23.0;//一個字元的高
const float MESH_ROW_NUM = 100.0;//固定小格子的行數
void main()
{
    float imageMeshWidth = texSize.x / MESH_ROW_NUM;
    //使小格子的寬高比跟字元的寬高比保持一緻,防止替換後字元被拉伸
    float imageMeshHeight = imageMeshWidth * MESH_HEIGHT / MESH_WIDTH;

    vec2 imageTexCoord = v_texcoord * texSize;//歸一化坐标轉像素坐标
    
    //取小格子中心點的像素
    vec2 midTexCoord;
    midTexCoord.x = floor(imageTexCoord.x / imageMeshWidth) * imageMeshWidth + imageMeshWidth * 0.5;//小格子中心
    midTexCoord.y = floor(imageTexCoord.y / imageMeshHeight) * imageMeshHeight + imageMeshHeight * 0.5;//小格子中心
    vec2 normalizedTexCoord = midTexCoord / texSize;//歸一化
    vec4 rgbColor = YuvToRgb(normalizedTexCoord);//采樣

    float grayValue = dot(rgbColor.rgb, RGB2GRAY_VEC3);//rgb轉灰階值
    gl_FragColor = vec4(vec3(grayValue), rgbColor.a);
}      

灰階圖馬賽克的效果。

利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

灰階圖馬賽克完成後,每個小格子替換一個字元,24 個字元将 0~255 的灰階值(歸一化後為 0~1.0 )分成 24 個等級,計算出灰階值後根據等級取對應的字元。

然後根據采樣坐标在小格子内的偏移計算出字元(包含一個字元的小圖檔)的采樣坐标,最後對字元采樣。

字元畫實作的完整 shader 。

//字元畫
#version 100
precision highp float;
varying vec2 v_texcoord;
uniform lowp sampler2D s_textureY;
uniform lowp sampler2D s_textureU;
uniform lowp sampler2D s_textureV;
uniform lowp sampler2D s_textureMapping;//字元表紋理
uniform float u_offset;
uniform vec2 texSize;//原圖尺寸
uniform vec2 asciiTexSize;//字元表尺寸

vec4 YuvToRgb(vec2 uv) {
    float y, u, v, r, g, b;
    y = texture2D(s_textureY, uv).r;
    u = texture2D(s_textureU, uv).r;
    v = texture2D(s_textureV, uv).r;
    u = u - 0.5;
    v = v - 0.5;
    r = y + 1.403 * v;
    g = y - 0.344 * u - 0.714 * v;
    b = y + 1.770 * u;
    return vec4(r, g, b, 1.0);
}

const vec3  RGB2GRAY_VEC3 = vec3(0.299, 0.587, 0.114);
const float MESH_WIDTH = 16.0;//一個字元的寬
const float MESH_HEIGHT= 23.0;//一個字元的高
const float GARY_LEVEL = 24.0;//字元表圖上有 24 個字元
const float ASCIIS_WIDTH = 8.0;//字元表列數
const float ASCIIS_HEIGHT = 3.0;//字元表行數
const float MESH_ROW_NUM = 100.0;//固定小格子的行數
void main()
{
    float imageMeshWidth = texSize.x / MESH_ROW_NUM;
    //使小格子的寬高比跟字元的寬高比保持一緻,防止替換後字元被拉伸
    float imageMeshHeight = imageMeshWidth * MESH_HEIGHT / MESH_WIDTH;

    vec2 imageTexCoord = v_texcoord * texSize;//歸一化坐标轉像素坐标
    
    //取小格子中心點的像素
    vec2 midTexCoord;
    midTexCoord.x = floor(imageTexCoord.x / imageMeshWidth) * imageMeshWidth + imageMeshWidth * 0.5;//小格子中心
    midTexCoord.y = floor(imageTexCoord.y / imageMeshHeight) * imageMeshHeight + imageMeshHeight * 0.5;//小格子中心
    vec2 normalizedTexCoord = midTexCoord / texSize;//歸一化
    vec4 rgbColor = YuvToRgb(normalizedTexCoord);//采樣

    float grayValue = dot(rgbColor.rgb, RGB2GRAY_VEC3);//rgb轉灰階值
    //gl_FragColor = vec4(vec3(grayValue), rgbColor.a);

    //根據采樣坐标在小格子内的偏移計算出在字元(包含一個字元的小圖檔)内的偏移
    float offsetX = mod(imageTexCoord.x, imageMeshWidth) * MESH_WIDTH / imageMeshWidth;
    float offsetY = mod(imageTexCoord.y, imageMeshHeight) * MESH_HEIGHT / imageMeshHeight;

    float asciiIndex = floor((1.0 - grayValue) * GARY_LEVEL);//根據灰階值确定第幾個字元
    float asciiIndexX = mod(asciiIndex, ASCIIS_WIDTH);
    float asciiIndexY = floor(asciiIndex / ASCIIS_WIDTH);

    //根據字元的位置和字元内的偏移,計算出字元表紋理的采樣點坐标
    vec2 grayTexCoord;
    grayTexCoord.x = (asciiIndexX * MESH_WIDTH + offsetX) / asciiTexSize.x;
    grayTexCoord.y = (asciiIndexY * MESH_HEIGHT + offsetY) / asciiTexSize.y;

    vec4 originColor = YuvToRgb(v_texcoord);//采樣原始紋理
    vec4 mappingColor = vec4(texture2D(s_textureMapping, grayTexCoord).rgb, rgbColor.a);//采樣字元表紋理

    gl_FragColor = mix(originColor, mappingColor, u_offset);//最後做個混合保留一些原圖的色彩
}      

字元畫的效果。

利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

聯系與交流

技術交流擷取源碼可以添加我的微信:Byte-Flow

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
利用 OpenGL ES 給視訊播放器和相機做個字元畫濾鏡字元畫濾鏡原理字元畫濾鏡實作聯系與交流

繼續閱讀