OpenGL ES 3.0 Transform Feedback實現圖像對比度調整

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

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

本人已編寫的Transform Feedback相關文檔:

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

原圖
改變對比度

1、(頂點著色器)實現圖像對比度調整

樸素實現如下所示。

#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表示不對結果進行插值,從而保持main函數的處理結果。
  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函數實現了對比度調整。mix函數的作用對contrast和rgb兩個參數,根據u_contrast_adjustment的值(表示為百分比)進行線性插值,最終將contrast與rgb所表示的兩個顏色混合到一起。如果mix函數的第三個參數為第二個參數的alpha值,此時,計算結果相當于調用glBlendFunc函數。
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  1. 將計算結果映射回[0, 255]范圍,后續在CPU上創建UIImage。不像Fragment Shader那樣使用vec4的原因是,CGImage需要32位(4分量、每分量一字節)的數據格式,而vec4是4個浮點數據,還得做數據截斷,多出了工作量。補充:根據對圖像數據的進一步了解,圖像的RGB值也可定義為浮點數,具體操作辦法隨后添加。

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

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

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

2、讀取GPU處理的結果圖像

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

2.1、映射GPU內存

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

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

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

2.2、RGBA原始數據創建UIImage

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

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

完整實現如下所示。

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、生成紋理坐標

出于編程方便起見,定義紋理坐標結構體。

typedef struct {
    GLushort s, t;
} TextureCoodinate;

根據圖像維度信息生成紋理坐標。

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、坑

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

4.1、使用整數采樣器isampler2D容易出現的精度問題

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

按之前的編程經驗,自然寫出如下代碼。

// Vertex Shader
uniform isampler2D u_sampler;

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

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

// Vertex Shader
uniform lowp isampler2D u_sampler;

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

4.2、使用整數采樣器isampler2D容易出現的紋理坐標問題

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

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

4.3、紋理坐標的數據類型

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

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

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

5、性能比較

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容