OpenGL.Shader:Zhigeは、ライブフィルタークライアントの作成方法を教えています(2)ビデオ画像を歪みなしでインターフェイスに適合させる方法は?

OpenGL.Shader:Zhigeは、ライブフィルタークライアントの作成方法を教えています(2)

最後の章では、コーディングのアイデアとコードの全体的な構造を簡単に紹介し、基本的にJavaレベルのロジックを完成させました。

次に、GpuFilterRender.java-> GpuFilterRender.cppのJNIレイヤー遷移インターフェイスに従って、2つの注意点を分析します。

(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);
}

GPUImageオープンソースプロジェクト処理メソッドをモデルにしたsetRotationCameraインターフェイス(呼び出しスタックはJClass Activity-> JClass CFEScheduler-> JMethod setUpCamera-> JNIMethod setRotationCamera)注意してください。これは、Androidシステムでは、onPreviewFrameからのデータコールバックがデフォルトで水平であるためです。したがって、水平方向のデータが垂直方向の画面の開発要件を満たしている場合は、水平方向と垂直方向のフリップを交換するだけで済みます。

引き続きGpuFilterRender-> setRotationCameraと入力します

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

mViewWidthとmViewHeightは、GLThreadの3つのライフサイクルコールバックで渡されます。コードの長さのため、コードは貼り付けられません。GLThreadとGLRenderのカスタマイズについては、前に書いた記事を参照してください。Https://blog.csdn.net/a360940265a/article/details/88600962

次に、adjustFrameScalingを呼び出します。メソッドの名前から、これはパラメータに従ってフレームイメージのスケーリングを調整することであることが理解できますが、ここで放して後で分析します。

(2)フレームデータバッファプール

JNIレイヤーには、機能と、より重要な機能であるfeedVideoData (呼び出しスタックはJClass Activity-> JClass CFEScheduler-> JCallback Camera.onPreviewFrame-> JNIMethod feedVideoData)もあります。これは、ビデオのカメラプレビューコールバックインターフェイスのフレームデータをキャッシュするために使用されます。レンダリング。

@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は横向きモードですか、縦向きモードですか。データデータは水平です、明らかにpreviewSizeも水平です!(つまり、幅>高さ)

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:オブジェクトではなく、単なるポインタ)コードの長さのため、ByteBufferNV21BufferPoolの実装コードは貼り付けられません。学生は、ポータルからgithubにチェックできます)。また、配置されている場合は、get、producerがあることを 忘れないでください。コンシューマーモードなので、同期操作にはスレッドロックを使用します

振り返ってみると、画像データの最初のフレームが入力されたら、初期化条件として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つのステップに分かれており、各ステップにはキーノートが書かれています。ここで説明する重要なポイントを示します。最初と2番目のステップの処理は、データを一貫したデフォルトの水平方向に調整することです。3番目と4番目のステップは、出力画面とプレビュー画像の幅と高さに応じて適切な適応率を見つけ、一方の項目を満たし、もう一方の項目を変換することです。その後、頂点座標データとテクスチャ座標データを生成します。最後のステップは、これらの座標点を調整することです。見てみましょう。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の配列です。

位置座標は比較的単純です。つまり、4つの位置ポイントの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