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

虽然朴素实现代码达成了目标,但它有多余可优化之处。现在逐一介绍本人已实践的优化办法。

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框架实现相同的图像处理并比较两者性能差异。

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

推荐阅读更多精彩内容