OpenGL ES 3.0 Transform Feedback實現(xiàn)圖像對比度調(diào)整

本文檔描述了在iOS上使用OpenGL ES 3.0新增的Transform Feedback功能只在頂點著色器中實現(xiàn)圖像處理等通用GPU計算功能。區(qū)別于OpenGL ES 2.0將圖像處理算法寫在片段著色器,最終輸出到離線紋理、渲染緩沖區(qū)或屏幕(默認幀緩沖區(qū))中,Transform Feedback(變換反饋)可以只用頂點著色器實現(xiàn)所需的算法,故有時也被稱為頂點變換。

目錄:
|- (頂點著色器)實現(xiàn)圖像對比度調(diào)整
|- 讀取GPU處理的結(jié)果圖像
|-- 映射GPU內(nèi)存
|-- RGBA原始數(shù)據(jù)創(chuàng)建UIImage
|- 生成紋理坐標
|- 坑
|-- 使用整數(shù)采樣器isampler2D容易出現(xiàn)的精度問題
|-- 使用整數(shù)采樣器isampler2D容易出現(xiàn)的紋理坐標問題
|- 性能比較

本人已編寫的Transform Feedback相關(guān)文檔:

基于前面所寫的iOS GPGPU 編程:GPU進行浮點計算并讀取結(jié)果,現(xiàn)在探索調(diào)整圖像對比度的簡單實現(xiàn)及讀取處理結(jié)果至主存并生成UIImage實例。下面是本文檔對應程序的運行結(jié)果示例。

原圖
改變對比度

1、(頂點著色器)實現(xiàn)圖像對比度調(diào)整

樸素實現(xiàn)如下所示。

#version 300 es

layout(location = 0) in vec2 in_texcoord;

uniform sampler2D u_sampler;
uniform float u_image_width;
uniform float u_image_height;

uniform float u_contrast_adjustment; // 默認為0.5

flat out uint out_color;

void main()
{
    vec2 normalized_texcoord = vec2(
        in_texcoord.x / u_image_width, 
        in_texcoord.y / u_image_height);
    vec3 rgb = texture(u_sampler, normalized_texcoord).rgb;
    vec3 contrast = vec3(0.0, 0.0, 0.0);
    vec3 normalized_rgb = vec3(mix(contrast, rgb, u_contrast_adjustment));

    out_color = (255u << 24) + 
        (uint(normalized_rgb.b * 255.0) << 16) + 
        (uint(normalized_rgb.g * 255.0) << 8) + 
        (uint(normalized_rgb.r * 255.0) << 0);
}

簡單分析上述代碼:

  1. 指定輸出變量out_color為flat表示不對結(jié)果進行插值,從而保持main函數(shù)的處理結(jié)果。
  2. u_image_width、u_image_height由客戶端指定需要處理的圖像維度,由于后面上傳的紋理坐標是[0, 圖像寬高],而OpenGL ES定義的紋理坐標范圍為[0, 1.0],因此進行歸一化處理。
vec2 normalized_texcoord = vec2(
        in_texcoord.x / u_image_width, 
        in_texcoord.y / u_image_height);
  1. mix函數(shù)實現(xiàn)了對比度調(diào)整。mix函數(shù)的作用對contrast和rgb兩個參數(shù),根據(jù)u_contrast_adjustment的值(表示為百分比)進行線性插值,最終將contrast與rgb所表示的兩個顏色混合到一起。如果mix函數(shù)的第三個參數(shù)為第二個參數(shù)的alpha值,此時,計算結(jié)果相當于調(diào)用glBlendFunc函數(shù)。
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  1. 將計算結(jié)果映射回[0, 255]范圍,后續(xù)在CPU上創(chuàng)建UIImage。不像Fragment Shader那樣使用vec4的原因是,CGImage需要32位(4分量、每分量一字節(jié))的數(shù)據(jù)格式,而vec4是4個浮點數(shù)據(jù),還得做數(shù)據(jù)截斷,多出了工作量。補充:根據(jù)對圖像數(shù)據(jù)的進一步了解,圖像的RGB值也可定義為浮點數(shù),具體操作辦法隨后添加。

注意,OpenGL ES 3.0頂點著色器中不允許指定統(tǒng)一變量和輸出變量的布局修飾符,下面的寫法將導致編譯失敗。

layout(location = 0) uniform sampler2D u_sampler;
layout(location = 0) out vec4 out_color;

然而,在片段著色器中,指定輸出變量的布局修飾符是合法的。

2、讀取GPU處理的結(jié)果圖像

讀取頂點著色器輸出的圖像數(shù)據(jù)的過程略為曲折,由于圖像RGB(A)數(shù)據(jù)一般是大端存儲,而iOS是小端,故最終輸出時得作些額外操作。

2.1、映射GPU內(nèi)存

圖像操作的結(jié)果數(shù)據(jù)在GPU內(nèi)存中,而生成UIImage得在CPU上運行,因此不得不進行內(nèi)存映射。根據(jù)老外的說法,iOS設(shè)備使用統(tǒng)一內(nèi)存模型(Uniform Memory Model),那么數(shù)據(jù)不像PC一樣在主存和顯存中拷貝,而是全部放置于主存中。

GLuint *mappedBuffer = glMapBufferRange(GL_ARRAY_BUFFER, 
    0, 
    imagePixels * sizeof(GLuint), 
    GL_MAP_READ_BIT);

這里映射的數(shù)據(jù)類型和著色器代碼中輸出的數(shù)據(jù)類型保持一致,避免讀寫越界錯誤。

2.2、RGBA原始數(shù)據(jù)創(chuàng)建UIImage

這里參考我另一個文檔iOS OpenGL ES 3.0 數(shù)據(jù)可視化 4:紋理映射實現(xiàn)2維圖像與視頻渲染簡介描述的RGBA祼數(shù)據(jù)創(chuàng)建UIImage的方法,區(qū)別是前面繪制時沒使用頂點數(shù)據(jù),所以紋理是完全按原圖像進行采樣,不存在結(jié)果圖像倒轉(zhuǎn)問題,因此刪除了翻轉(zhuǎn)代碼。

CGContextTranslateCTM(context, 0.0, renderTargetHeight);
CGContextScaleCTM(context, 1.0, -1.0);

完整實現(xiàn)如下所示。

int renderTargetSize = imagePixels * 4;
int renderTargetWidth = imageWidth;
int renderTargetHeight = imageHeight;
int rowSize = renderTargetWidth * 4;
CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, 
    mappedBuffer, 
    renderTargetSize, 
    NULL);
CGImageRef iref = CGImageCreate(renderTargetWidth,
    renderTargetHeight, 8, 32, rowSize,
    CGColorSpaceCreateDeviceRGB(),
    kCGImageAlphaLast | kCGBitmapByteOrderDefault, ref,
    NULL, true, kCGRenderingIntentDefault);

uint8_t* contextBuffer = (uint8_t*)malloc(renderTargetSize);
memset(contextBuffer, 0, renderTargetSize);
CGContextRef context = CGBitmapContextCreate(contextBuffer,
    renderTargetWidth, renderTargetHeight, 
    8, 
    rowSize,
    CGImageGetColorSpace(iref),
    kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, 
    CGRectMake(0.0, 0.0, renderTargetWidth, renderTargetHeight), 
    iref);
CGImageRef outputRef = CGBitmapContextCreateImage(context);
UIImage* image = [[UIImage alloc] initWithCGImage:outputRef];

CGImageRelease(outputRef);
CGContextRelease(context);
CGImageRelease(iref);
CGDataProviderRelease(ref);
free(contextBuffer);

3、生成紋理坐標

出于編程方便起見,定義紋理坐標結(jié)構(gòu)體。

typedef struct {
    GLushort s, t;
} TextureCoodinate;

根據(jù)圖像維度信息生成紋理坐標。

int imagePixels = (int) (image.size.width * image.size.height);
TextureCoodinate *texcoods = calloc(imagePixels, sizeof(TextureCoodinate));
int index = 0;
for (int line = 0; line < image.size.height; ++line) {
    for (int col = 0; col < image.size.width; ++col) {
        TextureCoodinate *t = &texcoods[index];
        t->t = (GLushort)line;
        t->s = (GLushort)col;
        ++index;
    }
}

生成坐標時,需注意紋理坐標系的方向。

4、坑

雖然樸素實現(xiàn)代碼達成了目標,但它有多余可優(yōu)化之處?,F(xiàn)在逐一介紹本人已實踐的優(yōu)化辦法。

4.1、使用整數(shù)采樣器isampler2D容易出現(xiàn)的精度問題

在樸素實現(xiàn)中,使用了浮點類型的采樣器sampler2D,它采得的是浮點數(shù)、范圍在[0, 1]內(nèi)。然而,多數(shù)情況下,我們加載和創(chuàng)建UIImage時使用的數(shù)據(jù)源往往是[0, 255]的整數(shù),最終輸出變量不得不乘以255.0作逆映射。為優(yōu)化這種多余的乘法,現(xiàn)嘗試使用整型采樣器isampler2D。

按之前的編程經(jīng)驗,自然寫出如下代碼。

// Vertex Shader
uniform isampler2D u_sampler;

但是,得到一個編譯錯誤:declaration must include a precision qualifier for type。

片段著色器需要聲明浮點數(shù)的精度,這在OpenGL ES 3.0的開發(fā)過程中大家熟知的步驟,然而,整型采樣器需要添加什么精度修飾符呢?語法類似于單個變量的浮點數(shù)精度聲明,直接添加精度修飾符在變量類別關(guān)鍵字之后、類型之前,示例如下。

// Vertex Shader
uniform lowp isampler2D u_sampler;

現(xiàn)在,通過u_sampler使用texture采樣,我們得到了[0, 255]之間的顏色值。

4.2、使用整數(shù)采樣器isampler2D容易出現(xiàn)的紋理坐標問題

雖然,前面的修改讓我們得到了整數(shù)顏色值,但是,對于紋理坐標歸一化,還得每次都計算一次,還是多了一次額外的操作。那么,是否可以使用ivec2替換當前的vec2浮點紋理坐標呢?經(jīng)嘗試,不可行。可能需要額外的設(shè)置步驟,基于本人有限的OpenGL ES了解,暫時放棄此方案。不過,前面的實現(xiàn)可進一步優(yōu)化為:

vec2 normalized_texcoord = in_texcoord / 
    vec2(u_image_width, u_image_height);

4.3、紋理坐標的數(shù)據(jù)類型

樸素實現(xiàn)代碼采用了逐點繪制方式進行每個像素點的操作,這要求生成的紋理坐標與glVertexAttribPointer函數(shù)指定數(shù)據(jù)解析格式相符,比如:

// Using Vertex Buffer Object
glVertexAttribPointer(0, 
    2,
    GL_UNSIGNED_SHORT, 
    GL_FALSE,
    0,
    NULL);

若生成的紋理坐標為浮點類型,則glVertexAttribPointer的參數(shù)需同步為GL_FLOAT,避免錯誤的數(shù)據(jù)格式讀取,導致坐標值錯誤,最終輸出錯誤的計算結(jié)果。

5、性能比較

目前,因工作任務較多,暫未用Accelerate框架實現(xiàn)相同的圖像處理并比較兩者性能差異。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容