优化OpenGLSL.texelFetch锯齿问题,让我想起一道阿里面试题。

一、关于MediaCodec解码10位,能不能输出离屏SurfaceTexture

关于最近原创的HDR系列文章,有同学问到能不能设置离屏的EGLSurface,硬解10bit的hdr流,输出到SurfaceTexture这种常规易用的方式。当时我从理论思考,持否定的态度。这次结合代码测试验证,记录结果。

先说结果:只要MediaCodec能识别解码10bit HDR码流,不崩溃,那是可以输出SurfaceTexture。换句话说,可以理解位由系统内部帮我们做tonemap色调映射处理。但效果质量不保证。

以上结论主要做了以下几组比对测试:

1、横向比较。使用支持硬解10位P010视频码流的设备(如三星GalaxyS9、小米12至尊版),分辨率设定为368*640,然后比较MediaCodec+SurfaceView直接渲染(简称模式1) 和 MediaCodec+EGLSurface输出到纹理格式是GL_TEXTURE_EXTERNAL_OES的SurfaceTexture(简称模式2),全屏观看作主观比较,发现模式1画质更细腻,屏幕亮度值色值都符合预期,系统屏幕会自动进入观看hdr的模式;模式2能明显感觉到模糊边缘,系统屏幕也会自动进入观看hdr的模式,但是屏幕亮度值不符合预期。

2、纵向版本比较。使用较为低端的系统版本在6~7的设备,MediaCodec解码10bit P010视频,会存在崩溃、一直输出MediaCodec.INFO_TRY_AGAIN_LATER等异常现象;中高端设备(华为P30)能解码输出,但是系统屏幕没有自动进入观看hdr模式,屏幕亮度值不符合预期,需要代码调节activity.window的亮度。

3、效果比较。利用软解解出双字节YUV码流+OpenGL.tonemap渲染(模式3) 与 输出到SurfaceTexture(模式2)作比较:模式2效果不稳定,主要是看设备系统支持的力度,说白了就是靠小米OV系统的图像算法工程师调整,肯定是各家有各自的效果,同厂的不同系列低中高端手机效果也完全不一样;模式3效果更稳定统一,毕竟是自己控制的嘛~

以上就是测试结论。

二、OpenGLSL.texelFetch锯齿问题。

说完第一个内容点,接着第二个。

MediaCodec解码P010,OpenGLSL.texelFetch读取非归一化纹素。_Mr_Zzr的博客-CSDN博客_glsl texelfetch

在之前的文章介绍到利用texelFetch读取非归一化纹理像素,纹理参数需要设置GL_NEAREST(如下),但是这样会导致标清360P、540P的码流,在一些高dpi屏幕,全屏view显示下会出现明显的锯齿现象。这原因很简单,就是没有经过线性插值的直接放大显示。但是用texelFetch就不能用线性插值放大啊。那要如何解决?

glGenTextures(1, textureHandles);
glBindTexture(target, mTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

解决思路很简单,就是要做线性放大呗。解决方案我这有两:

1、利用CPU运算进行放大操作:libyuv

2、利用GPU运算进行放大操作:compute shader

// Supported filtering.
typedef enum FilterMode {
  kFilterNone = 0,  // Point sample; Fastest.
  kFilterLinear = 1,  // Filter horizontally only.
  kFilterBilinear = 2,  // Faster than box, but lower quality scaling down.
  kFilterBox = 3  // Highest quality.
} FilterModeEnum;

int ret = libyuv::I420Scale_16(src_y, src_stride,
        src_u, src_stride >> 1,
        src_v, src_stride >> 1,
        src_width, src_height,
        dst_y, dst_stride,
        dst_u, dst_stride >> 1,
        dst_v, dst_stride >> 1,
        dst_width, dst_height,
        libyuv::FilterModeEnum::kFilterLinear);

我现在用的是方案1,libyuv::I420Scale_16,双字节版本的缩放API,支持4种缩放模式。

方案2我暂时没有空改造代码进行测试,但依我这种性格大概也会抽空完善好进行测试。并专门出一篇文章讲述。

这就说完完了吗?还真没有,代码没成型,但理论要先行啊。同学你就不好奇I420Scale_16是怎么Scale的吗?4种FilterModeEnum又有什么区别?你就不好奇阿里达摩院面试的实操代码题吗?

三、阿里面试题:请实现图像的放大和旋转的方法。

是的你没看错,看着就是这么简单,不是什么leetCode的求无重复字符的最长子串,也不是什么实现红黑树,跳转表等等内卷到不要不要的基础题。4年前,也就2018年,阿里达摩院面试的实操代码题:请简单实现图像的放大旋转方法。限时30分钟。放在现在的你能完成吗?

今天我们先来聊聊图像的缩放基本原理:就是将原图像的已有位置像素经过加权运算得到目标图像的位置像素。

比如说把720P的图像放大到1080P,宽高放大比例是1080 / 720 = 1.5;则目标图像中的(x,y)位置映射到原图像的(x/1.5,y/1.5)位置的像素。(x/1.5,y/1.5)是整数位置自然好办,直接提取像素值就好了,不是整数的就需要通过原有图像的已有像素值插值才能得到对应的像素值。

介绍完思路原理,接着介绍三种插值算法。

1、最近邻插值(GL_NEAREST、libyuv的kFilterNone)

  • 首先将目标图像映射原图像位置。
  • 然后找到原图像中映射位置周边的4个像素值
  • 最后取映射位置最近的像素点的像素值作为目标像素值

还是以720P放大到1080P为例,目标像素点 t(1,0)对应原像素点 s(0.67,0),取周边4个位置分别是s0(0,0)s1(1,0)s2(0,1)s3(1,1),其中距离t(0.67,0)最近的位置很明显是s1(1,0)位置的像素值,因此,我们将原图像中的s1(1,0)位置的像素值赋值給目标图像 t(1,0)位置的像素点。

最近临近插值有个明显的缺点,就是得到的放大图像插值点会和相邻两个像素值相同,放大后的图像会出现锯齿现象,优点就是不需要太多计算量,速度非常快。

2、线性插值(GL_LINEAR、libyuv的kFilterLinear)

  • 将目标图像映射到原图像位置,找出最靠近的2个像素点
  • 求出映射位置点到两个像素点的距离作为权重
  • 根据权重取这两个像素值的和。

还是上面的例子,目标像素点 t(1,0)对应原像素点 s(0.67,0),最靠近s的两个像素点是s0(0,0)和s1(1,0),则t(1,0)=(1-0.67)*s0(0,0)+0.67*s1(1,0);或者以更通用的公式表示如下数学关系:

 (自己变通x1=x2,y1=y2的情况)线性插值比最近临近插值运算要多了一个复杂度,速度肯定比不上最近临近插值,但可以明显感觉到锯齿效果被优化了。

3、双线性插值(GL_LINEAR_MIPMAP_LINEAR、libyuv的kFilterBilinear)

  • 将目标图像映射到原图像位置,找出最靠近的4个像素点
  • 把映射位置点,投影到上下或者左右方向上的,分别求出的线程插值
  • 再对两个线程插值结果,再做一次线程插值

 双线程插值本质上其实就是三次线性插值的过程。双线程插值相比线性插值要多了2次运算,速度上肯定还要再降,换来的是抗锯齿效果更上一层楼了。

还有些插值算法没有介绍到,譬如GL_NEAREST_MIPMAP_NEAREST(先在上下进行两次邻近插值,然后再一次邻近)、GL_LINEAR_MIPMAP_NEAREST(先在上下进行两次线性插值,然后再临近)、kFilterBox三线性插值(周围16个点线程插值)这些其实都是在上面的基础上演变,各位同学应该能举一反三、触类旁通。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/125874903