OpenGL.Shader:志哥教你寫一個濾鏡直播用戶端(7)
最近在忙人生另一半的事,還有個小外單,确實是有些忙不過來。雖然是有點忙,但還是能勉強維持每月1~2篇原創知識文章的産量給大家。廢話不說,這篇文章主要是簡單介紹一些圖像處理的基本方法。内容講解基于GPU.Shader語言,但不妨礙認識層面上的了解。在OpenCV和其他圖像處理SDK當中同樣适用。事不宜遲,show the code.
1、亮度
介紹三維光照的時候已經接觸過,隻需要在原RGB三色通道的基礎上+光照值就可以了,shader代碼如下:
precision mediump float;
varying highp vec2 textureCoordinate; // 從頂點着色傳過來的紋理坐标
uniform sampler2D SamplerY; // Y分量
uniform sampler2D SamplerU; // U分量
uniform sampler2D SamplerV; // V分量
mat3 colorConversionMatrix = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0);
vec3 yuv2rgb(vec2 pos)
{
vec3 yuv;
yuv.x = texture2D(SamplerY, pos).r;
yuv.y = texture2D(SamplerU, pos).r - 0.5;
yuv.z = texture2D(SamplerV, pos).r - 0.5;
return colorConversionMatrix * yuv;
}
// yuv三分量轉rgb的方法。
uniform lowp float brightness; // 光照強度。
void main()
{
vec4 textureColor = vec4(yuv2rgb(textureCoordinate), 1.0);
gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w); // 原rgb分量基礎上+brightness,保留w
}
//光照強度建議控制在 -0.5~0.5,1的效果已經就是泛白了
void setAdjustEffect(float percent) {
mBrightness = range(percent * 100.0f, -0.5f, 0.5f);
}
float range(float percentage, float start, float end) {
return (end - start) * percentage / 100.0f + start;
}
亮度的片元着色器代碼如上,效果比較簡單就不提供上來了,大家可以在demo工程上嘗試嘗試。 具體代碼參照 https://github.com/MrZhaozhirong/NativeCppApp cpp/gpufilter/filter/GpuBrightnessFilter.hpp
2、曝光度
曝光度和亮度的原理基本上是一緻的,亮度是全方位的線性增加色值,而曝光度是基于原色值的指數型疊加(紅的會更紅,綠的會更綠,藍的會更藍,白光的會更光)shader代碼如下:
precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D SamplerY;
uniform sampler2D SamplerU;
uniform sampler2D SamplerV;
mat3 colorConversionMatrix = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0);
vec3 yuv2rgb(vec2 pos)
{
vec3 yuv;
yuv.x = texture2D(SamplerY, pos).r;
yuv.y = texture2D(SamplerU, pos).r - 0.5;
yuv.z = texture2D(SamplerV, pos).r - 0.5;
return colorConversionMatrix * yuv;
}
uniform lowp float exposure; // 曝光度效果值
void main()
{
vec4 textureColor = vec4(yuv2rgb(textureCoordinate), 1.0);
gl_FragColor = vec4((textureColor.rgb * pow(2.0, exposure)), textureColor.w); // rgb * 2^效果值
}
曝光度也應該有上下限,防止過度曝光。
void setAdjustEffect(float percent) {
// (0.0 as the default)
mExposure = range(percent * 100.0f, -5.0f, 5.0f);
}
曝光度的片元着色器代碼如上,效果比較簡單就不提供上來了,大家可以在demo工程上嘗試嘗試。 具體代碼參照 https://github.com/MrZhaozhirong/NativeCppApp cpp/gpufilter/filter/GpuExposureFilter.hpp
3、飽和度
接觸了兩個比較簡單的基礎處理方法之後,接下來看一個入門級别的。 首先來了解一下什麼叫色彩飽和度:飽和度是指色彩的鮮豔程度,也稱色彩的純度。飽和度取決于該色中含色成分和消色成分(灰色)的比例。含色成分越大,飽和度越大;消色成分越大,飽和度越小。純的顔色都是高度飽和的,如鮮紅,鮮綠。混雜上白色,灰色或其他色調的顔色,是不飽和的顔色,如绛紫,粉紅,黃褐等,完全不飽和的顔色根本沒有色調,如黑白之間的各種灰色。
概念總結:飽和度 = X·原色 + Y·灰階值,其中(x+y=1)
其中使用Luma算法求算灰階:Gray = R*0.2125 + G*0.7154 + B*0.0721
precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D SamplerY;
uniform sampler2D SamplerU;
uniform sampler2D SamplerV;
mat3 colorConversionMatrix = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0);
vec3 yuv2rgb(vec2 pos)
{
vec3 yuv;
yuv.x = texture2D(SamplerY, pos).r;
yuv.y = texture2D(SamplerU, pos).r - 0.5;
yuv.z = texture2D(SamplerV, pos).r - 0.5;
return colorConversionMatrix * yuv;
}
uniform float saturation; // 飽和度比值
const mediump vec3 luminanceWeight = vec3(0.2125, 0.7154, 0.0721); // Luma算子
void main()
{
vec4 textureColor = vec4(yuv2rgb(textureCoordinate), 1.0);
lowp float luminance = dot(textureColor.rgb, luminanceWeight); // 求算灰階值
lowp vec3 greyScaleColor = vec3(luminance);
gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);
// GLSL内置函數 mix(x,y,a) = x*(1-a)+y*a,剛好滿足飽和度的公式定義。
}
從定義公式可知,saturation飽和度比值的正常可取範圍是(0~1),既然有正常的範圍,那肯定就有不正常的範圍,大家可以恰當的調整上下限,比較其實際效果。
void setAdjustEffect(float percent) {
mSaturation = range(percent * 100.0f, 0.0f, 1.0f); // 2.0f
}
飽和度的片元着色器代碼如上,效果比較簡單就不提供上來了,大家可以在demo工程上嘗試嘗試。 具體代碼參照 https://github.com/MrZhaozhirong/NativeCppApp cpp/gpufilter/filter/GpuSaturationFilter.hpp
到這裡先運作我引導前言知識,數字圖像處理當中的三大色彩模型:RGB、HSI、CMYK(注意!這裡不是格式,是色彩模型)
(1)最常用的RGB色彩模型。
RGB是依據人眼識别的顔色定義出的空間,可表示大部分顔色。是圖像進行中最基本、最常用、面向硬體的顔色空間,是一種光混合的體系。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL5IDN5UTNwYTM1ITNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
可以看到RGB顔色模式用三維空間中的一個點表示一種顔色,每個點有三個分量,分别表示紅、綠、藍的亮度值,亮度值限定為【0,1】。在RGB模型的立方體中,原點對應的顔色為黑色,它的三個分量值都為0;距離原點最遠的頂點對應的顔色為白色,它的三個分量值都為1。從黑色到白色的灰階值分布在這兩個點的連線上,該虛線稱為灰階線;立方體的其餘各點對應不同的顔色,即三原色紅、綠、藍及其混合色黃、品紅、青色。
(2)HSI色彩模型,視覺傳輸傳播使用
HSI色彩空間是從人的視覺系統出發,用色調(Hue)、飽和度(Saturation或Chroma)和亮度 (Intensity或Brightness)來描述色彩。
H——表示顔色的相位角。紅、綠、藍分别相隔120度;互補色分别相差180度,即顔色的類别。
S——表示成所選顔色的純度和該顔色最大的純度之間的比率,範圍:[0, 1],即顔色的深淺程度。
I——表示色彩的明亮程度,範圍:[0, 1],人眼對亮度很敏感!
可以看到HSI色彩空間和RGB色彩空間隻是同一實體量的不同表示法,因而它們之間存在着轉換關系:HSI顔色模式中的色調使用顔色類别表示,飽和度與顔色的白光光亮亮度剛好成反比,代表灰色與色調的比例,亮度是顔色的相對明暗程度。
(3)CMYK模型,用于印刷品依靠反光的色彩模式
CMYK是一種依靠反光的色彩模式,我們是怎樣閱讀報紙的内容呢?是由陽光或燈光照射到報紙上,再反射到我們的眼中,才看到内容。它需要有外界光源,如果你在黑暗房間内是無法閱讀報紙的。隻要在螢幕上顯示的圖像,就是RGB模式表現的。隻要是在印刷品上看到的圖像,就是CMYK模式表現的。大多數在紙上沉積彩色顔料的裝置,如彩色列印機和影印機,要求輸入CMY資料,在内部進行RGB到CMY的轉換。
青色Cyan、品紅色Magenta、黃色Yellow是光的二次色,是顔料的顔色。而K取的是black最後一個字母,之是以不取首字母,是為了避免與藍色(Blue)混淆。當紅綠藍三原色被混合時,會産生白色,當混合青色、品紅色、黃色三原色時會産生黑色。從理論上來說,隻需要CMY三種油墨就足夠了,但是由于目前制造技術還不能造出高純度的油墨,CMY相加的結果實際是一種暗紅色。
以上三種顔色模型是一系列顔色的數學表現形式。三種最流行的顔色模型是RGB(用于計算機圖形);YIQ,YUV或YCbCr(用于視訊系統)和CMYK(用于彩色列印)。但是,這三種顔色沒有一種和我們直覺概念上的色調,飽和度,亮度有直接的聯系。這就使我們暫時去追尋其它的模型,它們能簡化程式設計,處理和終端使用者操作。
4、色調(色相)
最後一個色調,算是入門以上的圖像處理手法。理論部分較多且深,要慢慢了解。先來了解什麼是色調,如何定義色調。
色調不是指顔色的性質,是對一幅繪畫作品的整體評價。譬如一幅畫中畫面色彩的總體傾向,是大的色彩效果,這幅畫作品當中,雖然可能用了多種顔色,但總體有一種色調,是偏藍或偏紅,是偏暖或偏冷等等。這種在不同顔色的物體上,籠罩着某一種色彩,使不同顔色的物體都帶有同一色彩傾向,這樣的色彩現象就是色調。
在三大色彩模型當中,HSI色彩模型是能較好接近直覺概念上的色調,飽和度,亮度的聯系。在這方面rgb格式不友善計算,是以要先轉成為友善色調、飽和度、亮度計算的YIQ\YUV格式。這裡選用YIQ格式,有關YIQ格式的疑問,可以看這篇譯文https://www.hisour.com/zh/yiq-color-space-26084/ 轉換格式之後,然後還要轉換顔色模型,從RGB模型轉為HSI模型,友善線性調整色調。
說那麼多屁話,還是看代碼吧!
precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D SamplerY;
uniform sampler2D SamplerU;
uniform sampler2D SamplerV;
mat3 colorConversionMatrix = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0);
vec3 yuv2rgb(vec2 pos)
{
vec3 yuv;
yuv.x = texture2D(SamplerY, pos).r;
yuv.y = texture2D(SamplerU, pos).r - 0.5;
yuv.z = texture2D(SamplerV, pos).r - 0.5;
return colorConversionMatrix * yuv;
}
uniform mediump float hueAdjust; // 使用者調整值
// RGB to YIQ的矩陣向量
const highp vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);
const highp vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);
const highp vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);
// YIQ to RGB的矩陣向量
const highp vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);
const highp vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);
const highp vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);
void main()
{
//# 提取RGB
vec4 textureColor = vec4(yuv2rgb(textureCoordinate), 1.0);
//# 轉換為YIQ
highp float YPrime = dot(textureColor, kRGBToYPrime);
highp float I = dot(textureColor, kRGBToI);
highp float Q = dot(textureColor, kRGBToQ);
//# 依據HSI模型,提取色調色度
highp float hue = atan(Q, I);
highp float chroma = sqrt(I * I + Q * Q);
//# 外部動态調整色調
hue -= hueAdjust;
//# 轉變回HSI模型的YIQ格式數值
Q = chroma * sin (hue);
I = chroma * cos (hue);
//# 再轉換回RGB 用于顯示
highp vec4 yIQ = vec4 (YPrime, I, Q, 0.0);
textureColor.r = dot(yIQ, kYIQToR);
textureColor.g = dot(yIQ, kYIQToG);
textureColor.b = dot(yIQ, kYIQToB);
gl_FragColor = textureColor;
}
以上代碼都帶上注釋了,還有不明白的歡迎私信。效果視訊以後再上傳。大家可以在demo工程上嘗試嘗試。 具體代碼參照 https://github.com/MrZhaozhirong/NativeCppApp cpp/gpufilter/filter/GpuHueFilter.hpp
That is All.
興趣讨論群:703531738。暗号:志哥13567