OpenGL.Shader: Zhige vous apprend à écrire un filtre client en direct (2) Comment adapter l'image vidéo à l'interface sans distorsion?

OpenGL.Shader: Zhige vous apprend à écrire un client de filtre en direct (2)

Le dernier chapitre a brièvement présenté les idées de codage et la structure globale du code, et a essentiellement complété la logique du niveau Java.

Ensuite, nous suivons l'interface de transition de couche JNI de GpuFilterRender.java-> GpuFilterRender.cpp pour analyser deux points d'attention.

(1) Flip horizontal inversé et retournement vertical

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

Faites attention à l'interface setRotationCamera (la pile d'appels est JClass Activity-> JClass CFEScheduler-> JMethod setUpCamera-> JNIMethod setRotationCamera) , qui est modélisée d'après la méthode de traitement de projet open source GPUImage, car dans le système Android, le rappel des données de onPreviewFrame est horizontal par défaut. Par conséquent, lorsque les données horizontales répondent aux exigences de développement de l'écran vertical, les inversions horizontales et verticales doivent simplement être échangées.

Continuez à entrer GpuFilterRender-> setRotationCamera

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

mViewWidth et mViewHeight sont transmis dans les trois rappels de cycle de vie de GLThread. En raison de la longueur du code, le code ne sera pas collé. Pour savoir comment personnaliser GLThread et GLRender, reportez-vous à l'article précédent. ( Https://blog.csdn.net/a360940265a/article/details/88600962 )

Ensuite, appelez AdjustFrameScaling. À partir du nom de la méthode, vous pouvez voir qu'il s'agit d'ajuster le rapport de zoom de l'image du cadre en fonction des paramètres.

(2) Pool de tampons de données de trame

La couche JNI a également une fonction et une plus importante, feedVideoData (la pile d'appels est JClass Activity-> JClass CFEScheduler-> JCallback Camera.onPreviewFrame-> JNIMethod feedVideoData) , qui est utilisée pour mettre en cache les données d'image de l'interface de rappel d'aperçu de la caméra pour la vidéo Le rendu.

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

Regardons d'abord l'interface de rappel de la couche Java. Les paramètres sont passés dans previewSize.width et previewSize.height, mais est-ce que previewSize est ici en mode paysage ou portrait? Les données de données sont horizontales, évidemment la taille de l'aperçu est également horizontale! (Ie largeur> hauteur)

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

Continuez à entrer 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);
}

Le code n'est pas compliqué et l'idée est très claire. Selon le format NV21 de YUV, la longueur des données est calculée, puis les données et la longueur sont respectivement stockées dans l'objet ByteBuffer, puis le pointeur du ByteBuffer est poussé sur le pool de tampons NV21. (PS: juste un pointeur, pas un objet) (en raison de la longueur du code, le code d' implémentation de ByteBuffer et  NV21BufferPool ne sera pas collé, les étudiants peuvent le vérifier via le portail vers github); il faut également se rappeler que s'il est mis, il y a get, producteur Mode consommateur, utilisez donc des verrous de thread pour les opérations de synchronisation ;

Rétrospective: lorsque la première image des données d'image est entrée, utilisez mFrameWidth! = PreviewWidth comme condition d'initialisation, enregistrez l'aperçu actuel et previewHeight, et surveillez le previewFrameSize à changer, déclenchera une méthode AdjustFrameScaling;

(3) AdjustFrameScaling

Alors, que fait exactement ce AdjustFrameScaling? Il peut être compris d'après le nom de la méthode qu'il s'agit d'ajuster le rapport de zoom de l'image du cadre en fonction des paramètres. Évidemment, cela est lié à la taille et à la direction de l'image de prévisualisation, alors regardons maintenant l'implémentation du contenu de cette méthode.

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

Le contenu de la fonction est divisé en 6 étapes, chaque étape est écrite avec des notes clés, voici le point clé à expliquer. Le traitement des première et deuxième étapes consiste à ajuster les données à une orientation horizontale par défaut cohérente. Les troisième et quatrième étapes consistent à trouver un rapport d'adaptation approprié en fonction de la largeur et de la hauteur de l'écran de sortie et de l'image de prévisualisation, et d'essayer de satisfaire un élément et de transformer l'autre. Ensuite, pour générer des données de coordonnées de sommet et des données de coordonnées de texture, la dernière étape consiste à ajuster ces points de coordonnées, jetons un coup d'œilgenerateFramePositionCords 和 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 et textureCords sont des variables privées de la classe GpuFilterRender et sont un tableau de flottants.

Les coordonnées de position sont relativement simples, c'est-à-dire les valeurs x et y des quatre points de position; mais une chose à noter est qu'il s'agit des coordonnées de position de l'écran vertical.

Les coordonnées de texture peuvent dérouter de nombreuses personnes. Tout d'abord, examinons les données de ROTATION_0. Cet ensemble de données est les sommets du système de coordonnées de texture standard Android. (La différence entre le système de coordonnées de texture traditionnel OpenGL et le système de coordonnées de texture dans Android. Si vous avez des questions, veuillez consulter l'article précédent, https://blog.csdn.net/a360940265a/article/details/79169497 ) Mais il n'y a pas un tel ensemble de données Les coordonnées de texture de l'angle de rotation. En fait, nous avons un angle de déflexion (car les données de l'image de prévisualisation sont horizontales par défaut!) Maintenant, regardez les données correspondant à ROTATION_270 / ROTATION_90, tournez la tête dans le sens horaire / antihoraire de 90 °, Voyons si les coordonnées de la texture sont alignées? o (*  ̄ ▽  ̄ *) ブ 

Ce n'est pas encore fini, après avoir généré les données clés, nous devons nous adapter. Le code clé correspondant est le suivant:

    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其实就是归一化后的预览帧图与输出载体的偏差值,把这个偏差值计算到纹理坐标上,就可以把预览帧图不变形的贴到输出载体上。(但会裁剪掉部分内容)

Je pense que le processus de calcul ci-dessus a été écrit très clairement. addDistance est une fonction en ligne de GpuFilterRender, qui calcule la différence entre les coordonnées de texture. Le contenu est le suivant:

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

Après AdjustFrameScaling, les coordonnées des sommets et des textures sont prêtes. Le chapitre suivant présente comment utiliser les données vidéo NV21 pour un rendu efficace.

Adresse du projet: https://github.com/MrZhaozhirong/NativeCppApp    fichier d'entrée CameraFilterEncoderActivity

 

Je suppose que tu aimes

Origine blog.csdn.net/a360940265a/article/details/104246229
conseillé
Classement