OpenGL.Shader:志哥教你写一个滤镜直播客户端(2)视频图像如何适配界面不变形?

OpenGL.Shader:志哥教你写一个滤镜直播客户端(2)

上一章简单介绍了代码的编码思路和整体结构,并基本完成了Java层面的逻辑。

接下来我们顺着GpuFilterRender.java->GpuFilterRender.cpp的JNI层过度接口,分析两个注意要点。

(1)倒转的 水平翻转 和 垂直翻转

JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_setRotationCamera(JNIEnv *env, jobject instance,
                                                             jint rotation, jboolean flipHorizontal,
                                                             jboolean flipVertical) {
    // 注意这里flipVertical对应render->setRotationCamera.flipHorizontal
    // 注意这里flipHorizontal对应render->setRotationCamera.flipVertical
    // 因为Android的预览帧数据是横着的,仿照GPUImage的处理方式。
    if (render == NULL) {
        render = new GpuFilterRender();
    }
    render->setRotationCamera(rotation, flipVertical, flipHorizontal);
}

注意setRotationCamera这个接口(调用栈是JClass Activity->JClass CFEScheduler->JMethod setUpCamera->JNIMethod setRotationCamera),这是仿照GPUImage开源工程的处理方式,因为在Android系统当中,onPreviewFrame回调出来的data默认是横向的,所以当横向的数据遇上垂直屏幕的开发需求的时候,水平和垂直的翻转刚好就要互换处理。

继续进入GpuFilterRender->setRotationCamera

void GpuFilterRender::setRotationCamera(int rotation, bool flipHorizontal, bool flipVertical)
{
    this->mRotation = rotation;
    this->mFlipHorizontal = flipHorizontal;
    this->mFlipVertical = flipVertical;
    adjustFrameScaling();
}

mViewWidth和mViewHeight是在GLThread 的三大生命周期回调当中传入进来的。由于篇幅的关系,代码就不粘贴上来了,有关自定义GLThread 和 GLRender的知识请查阅以前所写的文章。 (https://blog.csdn.net/a360940265a/article/details/88600962

接下来调用adjustFrameScaling,从方法名字可以了解这是 根据参数调整帧图缩放比例的,但现在放一下,稍后再仔细分析。

(2)帧数据缓存池

JNI层还有一个函数和比较重要的,就是feedVideoData(调用栈是JClass Activity->JClass CFEScheduler->JCallback Camera.onPreviewFrame->JNIMethod feedVideoData),其作用是缓存Camera预览回调接口的帧数据,用于视频渲染。

@Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if( mGpuFilterRender!=null){
            final Camera.Size previewSize = camera.getParameters().getPreviewSize();
            mGpuFilterRender.feedVideoData(data.clone(), previewSize.width, previewSize.height);
        }
    }

我们先来看看Java层的回调接口,参数传入previewSize.width,previewSize.height ,但是这里的previewSize是横屏的呢,还是竖屏的呢?data数据都是横向的,显然previewSize也是横向的!(即width>height)

JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_feedVideoData(JNIEnv *env, jobject instance,
                                                         jbyteArray array, jint width, jint height) {
    if (render == NULL) return;
    jbyte *nv21_buffer = env->GetByteArrayElements(array, NULL);
    jsize array_len = env->GetArrayLength(array);
    render->feedVideoData(nv21_buffer, array_len, width, height);
    env->ReleaseByteArrayElements(array, nv21_buffer, 0);
}

继续进入GpuFilterRender->feedVideoData内部

void GpuFilterRender::feedVideoData(int8_t *data, int data_len, int previewWidth, int previewHeight)
{
    if( mFrameWidth != previewWidth){
        mFrameWidth  = previewWidth;
        mFrameHeight = previewHeight;
        adjustFrameScaling();
    }
    int size = previewWidth * previewHeight;
    int y_len = size;   // mWidth*mHeight
    int u_len = size / 4;   // mWidth*mHeight / 4
    int v_len = size / 4;   // mWidth*mHeight / 4
    // nv21数据中 y占1个width*height,uv各占0.25个width*mHeight 共 1.5个width*height
    if(data_len < y_len+u_len+v_len)
        return;
    pthread_mutex_lock(&mutex);
    ByteBuffer* p = new ByteBuffer(data_len);
    p->param1 = y_len;
    p->param2 = u_len;
    p->param3 = v_len;
    p->wrap(data, data_len);
    mNV21Pool.put(p);
    pthread_mutex_unlock(&mutex);
}

代码不复杂,思路很清晰,根据YUV的NV21格式,计算其数据长度,然后把数据和长度分别寄存到ByteBuffer对象当中,然后把ByteBuffer的指针压栈到NV21的缓存池当中。(PS:只是指针,并不是对象)(由于篇幅关系ByteBuffer 和 NV21BufferPool 的实现代码就不粘贴上来,同学可以通过传送门到github查看);还有需要提醒的就是,有put就有get,生产者消费者模式,所以要利用线程锁做同步操作

回头再看看:当第一帧图数据输入时,利用mFrameWidth!=previewWidth作为初始化条件,记录当前的previewWidth和previewHeight,并监测其previewFrameSize有改变的情况下,都将触发一次adjustFrameScaling方法;

(3)adjustFrameScaling

那么这个adjustFrameScaling究竟是干啥的呢?从方法名字可以了解这是 根据参数调整帧图缩放比例的。显然与预览图的大小,方向等有关,那么现在就看看这方法的内容实现。

void GpuFilterRender::adjustFrameScaling()
{
    //第一步、获取surfaceview的宽高,一般是竖屏的,所以width < height,例如720:1280
    float outputWidth = mViewWidth;
    float outputHeight = mViewHeight;
    //第二步、根据摄像头角度,调整横竖屏的参数值
    //默认情况下都会执行width/height互换的代码,如果调用Camera.setDisplayOrientation方法那就看情况而定了
    if (mRotation == ROTATION_270 || mRotation == ROTATION_90) {
    outputWidth = mViewHeight;
    outputHeight = mViewWidth;
    }
    //互换之后,output变成1280:720,呈现的是一张横屏的画布
    //FrameSize = previewSize,默认是横向,例如1024:768
    float ratio1 = outputWidth / mFrameWidth;
    float ratio2 = outputHeight / mFrameHeight;
    float ratioMax = std::max(ratio1, ratio2);
    //第三步、根据变换比值,求出“能适配输出载体的”预览图像尺寸
    //imageSizeNew相等于outputSize*ratioMax
    int imageWidthNew = static_cast<int>(mFrameWidth * ratioMax);
    int imageHeightNew = static_cast<int>(mFrameHeight * ratioMax);
    //第四步、重新计算图像比例值。新的预览图像尺寸/输出载体(有一项肯定是ratioMax,另外一项非ratioMax)
    float ratioWidth = imageWidthNew / outputWidth;
    float ratioHeight = imageHeightNew / outputHeight;
    //第五步、生成对应的顶点坐标数据 和 纹理坐标数据(关键点)
    generateFramePositionCords();
    generateFrameTextureCords(mRotation, mFlipHorizontal, mFlipVertical);
    //第六步、根据效果调整位置坐标or纹理坐标(难点)
    float distHorizontal = (1 - 1 / ratioWidth) / 2;
    float distVertical = (1 - 1 / ratioHeight) / 2;
    textureCords[0] = addDistance(textureCords[0], distHorizontal); // x
    textureCords[1] = addDistance(textureCords[1], distVertical); // y
    textureCords[2] = addDistance(textureCords[2], distHorizontal);
    textureCords[3] = addDistance(textureCords[3], distVertical);
    textureCords[4] = addDistance(textureCords[4], distHorizontal);
    textureCords[5] = addDistance(textureCords[5], distVertical);
    textureCords[6] = addDistance(textureCords[6], distHorizontal);
    textureCords[7] = addDistance(textureCords[7], distVertical);
}

函数的内容分6个步骤,每一步骤都写有关键的注释,这里直接挑重点来说明。第一第二步的处理是要把数据调整一致默认横向。第三第四步是根据输出屏幕和预览图的宽高,找到合适的适配比例,尽量满足一项变换另一项。之后是生成顶点坐标数据和纹理坐标数据,最后一步就是调整这些坐标点,我们先看看generateFramePositionCords 和 generateFrameTextureCords

void GpuFilterRender::generateFramePositionCords()
{
    float cube[8] = {
            // position   x, y
            -1.0f, -1.0f,   //左下
            1.0f, -1.0f,    //右下
            -1.0f, 1.0f,    //左上
            1.0f, 1.0f,     //右上
    };
    memset(positionCords, 0, sizeof(positionCords));
    memcpy(positionCords, cube, sizeof(cube));
}
void GpuFilterRender::generateFrameTextureCords(int rotation, bool flipHorizontal, bool flipVertical)
{
    float tempTex[8]={0};
    switch (rotation)
    {
        case ROTATION_90:{
            float rotatedTex[8] = {
                    1.0f, 1.0f,
                    1.0f, 0.0f,
                    0.0f, 1.0f,
                    0.0f, 0.0f,
            };
            memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
        }break;
        case ROTATION_180:{
            float rotatedTex[8] = {
                    1.0f, 0.0f,
                    0.0f, 0.0f,
                    1.0f, 1.0f,
                    0.0f, 1.0f,
            };
            memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
        }break;
        case ROTATION_270:{
            float rotatedTex[8] = {
                    0.0f, 0.0f,
                    0.0f, 1.0f,
                    1.0f, 0.0f,
                    1.0f, 1.0f,
            };
            memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
        }break;
        default:
        case ROTATION_0:{
            float rotatedTex[8] = {
                    0.0f, 1.0f,
                    1.0f, 1.0f,
                    0.0f, 0.0f,
                    1.0f, 0.0f,
            };
            memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
        }break;
    }
    if (flipHorizontal) {
        tempTex[0] = flip(tempTex[0]);
        tempTex[2] = flip(tempTex[2]);
        tempTex[4] = flip(tempTex[4]);
        tempTex[6] = flip(tempTex[6]);
    }
    if (flipVertical) {
        tempTex[1] = flip(tempTex[1]);
        tempTex[3] = flip(tempTex[3]);
        tempTex[5] = flip(tempTex[5]);
        tempTex[7] = flip(tempTex[7]);
    }
    memset(textureCords, 0, sizeof(textureCords));
    memcpy(textureCords, tempTex, sizeof(tempTex));
}

positionCords和textureCords是GpuFilterRender类的私有变量,是一组float的数组。

位置坐标比较简单,就是四个位置点的x,y值;但要注意一点的是,这是竖屏的位置坐标哦。

纹理坐标可能就搞懵很多人了,首先我们看ROTATION_0的数据,这组数据就是标准的Android纹理坐标系顶点。(传统OpenGL纹理坐标系和Android中的纹理坐标系的区别,有疑问的同学请查看以前的文章,https://blog.csdn.net/a360940265a/article/details/79169497)但这组数据是没有旋转角度的纹理坐标,实际情况我们是有偏转角度的,(因为预览图像数据默认是横向的!)这下回头再看看ROTATION_270 / ROTATION_90对应的数据,把头顺时针 / 逆时针各转90°,再看看纹理坐标是否对齐位置了? o(* ̄▽ ̄*)ブ 

到这里还没结束哦,生成关键的数据之后,要做适配处理。对应的关键代码如下:

    int imageWidthNew = static_cast<int>(mFrameWidth * ratioMax);
    int imageHeightNew = static_cast<int>(mFrameHeight * ratioMax);

    float ratioWidth = imageWidthNew / outputWidth;
    float ratioHeight = imageHeightNew / outputHeight;

    float distHorizontal = (1 - 1 / ratioWidth) / 2;
    float distVertical = (1 - 1 / ratioHeight) / 2;
    textureCords[0] = addDistance(textureCords[0], distHorizontal); // x
    textureCords[1] = addDistance(textureCords[1], distVertical);   // y
    textureCords[2] = addDistance(textureCords[2], distHorizontal);
    textureCords[3] = addDistance(textureCords[3], distVertical);
    textureCords[4] = addDistance(textureCords[4], distHorizontal);
    textureCords[5] = addDistance(textureCords[5], distVertical);
    textureCords[6] = addDistance(textureCords[6], distHorizontal);
    textureCords[7] = addDistance(textureCords[7], distVertical);
/
这里写下dist的推演过程,我们可以反推:
    float distHorizontal * 2 = 1 - 1 / ratioWidth; --------->distHorizontal*2可以理解为整个水平间距
ratioWidth其实等于imageWidthNew / outputWidth,等价替换以上公式:
    float distHorizontal*2 = (imageWidthNew-outputWidth)/imageWidthNew; ---->右边通分一下
    float distHorizontal*2*imageWidthNew = (imageWidthNew-outputWidth); ---->把分母imageWidthNew移至左方
推算到这其实应该能看出个眉目了,imageSizeNew-outputSize,显然就是计算预览帧图与输出载体的偏差值,左方*2是对半平分的意义,imageSizeNew放回右方其实就是归一化处理。最终distHorizontal其实就是归一化后的预览帧图与输出载体的偏差值,把这个偏差值计算到纹理坐标上,就可以把预览帧图不变形的贴到输出载体上。(但会裁剪掉部分内容)

以上的推算过程,我感觉已经写得很明白了。addDistance是GpuFilterRender的内联函数,是针对纹理坐标进行差值计算的,内容如下:

__inline float addDistance(float coordinate, float distance)
    {
        return coordinate == 0.0f ? distance : 1 - distance;
    };

经过 adjustFrameScaling 之后,顶点坐标和纹理坐标就已经准备就绪,下一章介绍如何利用NV21的视频数据进行高效的渲染。

工程地址:https://github.com/MrZhaozhirong/NativeCppApp   入口文件CameraFilterEncoderActivity

猜你喜欢

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