OpenGL.Shader: Zhige lehrt Sie, einen Live-Filter-Client zu schreiben. (2) Wie kann man das Videobild ohne Verzerrung an die Benutzeroberfläche anpassen?

OpenGL.Shader: Zhige lehrt Sie, einen Live-Filter-Client zu schreiben (2)

Das letzte Kapitel stellte kurz die Codierungsideen und die Gesamtstruktur des Codes vor und vervollständigte im Wesentlichen die Logik der Java-Ebene.

Als nächstes folgen wir der JNI-Layer-Übergangsschnittstelle von GpuFilterRender.java-> GpuFilterRender.cpp, um zwei Aufmerksamkeitspunkte zu analysieren.

(1) Invertierter horizontaler und vertikaler Flip

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

Beachten Sie die setRotationCamera-Schnittstelle (der Aufrufstapel lautet JClass Activity-> JClass CFEScheduler-> JMethod setUpCamera-> JNIMethod setRotationCamera) , die der Open Source-Projektverarbeitungsmethode GPUImage nachempfunden ist, da der Datenrückruf von onPreviewFrame im Android-System standardmäßig horizontal ist. Wenn die horizontalen Daten die Entwicklungsanforderungen des vertikalen Bildschirms erfüllen, müssen daher nur die horizontalen und vertikalen Flips ausgetauscht werden.

Geben Sie weiterhin GpuFilterRender-> setRotationCamera ein

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

mViewWidth und mViewHeight werden in den drei Lebenszyklus-Rückrufen von GLThread übergeben. Aufgrund der Länge des Codes wird der Code nicht eingefügt. Informationen zum Anpassen von GLThread und GLRender finden Sie in dem zuvor beschriebenen Artikel. ( Https://blog.csdn.net/a360940265a/article/details/88600962 )

Rufen Sie als Nächstes adjustFrameScaling auf. Aus dem Namen der Methode können Sie ersehen, dass hierdurch die Skalierung des Rahmenbilds gemäß den Parametern angepasst wird. Lassen Sie sie jetzt los und analysieren Sie sie später.

(2) Rahmendatenpufferpool

Die JNI-Ebene hat auch eine und eine wichtigere Funktion, feedVideoData (der Aufrufstapel lautet JClass Activity-> JClass CFEScheduler-> JCallback Camera.onPreviewFrame-> JNIMethod feedVideoData) , mit der die Rahmendaten der Camera Preview Callback-Schnittstelle für Videos zwischengespeichert werden Rendern.

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

Schauen wir uns zuerst die Callback-Oberfläche der Java-Ebene an. Die Parameter werden in PreviewSize.width und PreviewSize.height übergeben. Befindet sich PreviewSize hier jedoch im Quer- oder Hochformat? Datendaten sind horizontal, VorschauSize ist natürlich auch horizontal! (Dh Breite> Höhe)

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

Geben Sie weiterhin GpuFilterRender-> feedVideoData ein

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

Der Code ist nicht kompliziert und die Idee ist sehr klar. Gemäß dem NV21-Format von YUV wird die Datenlänge berechnet, und dann werden die Daten und die Länge jeweils im ByteBuffer-Objekt gespeichert, und dann wird der Zeiger des ByteBuffer auf den NV21-Pufferpool verschoben. (PS: nur ein Zeiger, kein Objekt) (Aufgrund der Länge des Codes wird der Implementierungscode von ByteBuffer und  NV21BufferPool nicht eingefügt, die Schüler können ihn über das Portal zu Github überprüfen.) Verbrauchermodus. Verwenden Sie daher Thread-Sperren für Synchronisierungsvorgänge .

Rückblickend: Wenn der erste Frame von Bilddaten eingegeben wird, verwenden Sie mFrameWidth! = PreviewWidth als Initialisierungsbedingung, zeichnen Sie die aktuelle Vorschaubreite und Vorschauhöhe auf und überwachen Sie die VorschauFrameSize, um sie zu ändern. Dies löst eine adjustFrameScaling-Methode aus.

(3) adjustFrameScaling

Was genau macht diese adjustFrameScaling? Aus dem Methodennamen ist ersichtlich, dass dies dazu dient, das Zoomverhältnis des Rahmenbildes gemäß den Parametern anzupassen. Offensichtlich hängt es mit der Größe und Richtung des Vorschaubilds zusammen. Schauen wir uns nun die inhaltliche Implementierung dieser Methode an.

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

Der Inhalt der Funktion ist in 6 Schritte unterteilt. Jeder Schritt wird mit den wichtigsten Anmerkungen geschrieben. Hier ist der wichtigste zu erläuternde Punkt. Bei der Verarbeitung des ersten und zweiten Schritts werden die Daten an eine einheitliche horizontale Standardausrichtung angepasst. Der dritte und vierte Schritt besteht darin, ein geeignetes Anpassungsverhältnis entsprechend der Breite und Höhe des Ausgabebildschirms und des Vorschaubildes zu finden und zu versuchen, ein Element zu befriedigen und das andere zu transformieren. Nachdem Sie Scheitelpunktkoordinatendaten und Texturkoordinatendaten generiert haben, müssen Sie im letzten Schritt diese Koordinatenpunkte anpassen. Schauen wir uns das angenerateFramePositionCords 和 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 und textureCords sind private Variablen der GpuFilterRender-Klasse und ein Array von Floats.

Die Positionskoordinaten sind relativ einfach, dh die x- und y-Werte der vier Positionspunkte. Beachten Sie jedoch, dass dies die Positionskoordinaten des vertikalen Bildschirms sind.

Texturkoordinaten können viele Menschen verwirren. Schauen wir uns zunächst die Daten von ROTATION_0 an. Dieser Datensatz ist der Eckpunkt des Standard-Android-Texturkoordinatensystems. (Der Unterschied zwischen dem herkömmlichen OpenGL-Texturkoordinatensystem und dem Texturkoordinatensystem in Android. Wenn Sie Fragen haben, lesen Sie bitte den vorherigen Artikel unter https://blog.csdn.net/a360940265a/article/details/79169497. ) Es gibt jedoch keinen solchen Datensatz Die Texturkoordinaten des Drehwinkels. Tatsächlich haben wir einen Ablenkwinkel (da die Vorschaubilddaten standardmäßig horizontal sind!). Schauen Sie nun auf die Daten zurück, die ROTATION_270 / ROTATION_90 entsprechen, und drehen Sie den Kopf im Uhrzeigersinn / gegen den Uhrzeigersinn um 90 °. Mal sehen, ob die Texturkoordinaten ausgerichtet sind? o (*  ̄ ▽  ̄ *) ブ 

Es ist noch nicht vorbei, nachdem wir die Schlüsseldaten generiert haben, müssen wir uns anpassen. Der entsprechende Schlüsselcode lautet wie folgt:

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

Ich bin der Meinung, dass der obige Berechnungsprozess sehr klar geschrieben wurde. addDistance ist eine Inline-Funktion von GpuFilterRender, die die Differenz zwischen Texturkoordinaten berechnet. Der Inhalt lautet wie folgt:

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

Nach dem Anpassen von FrameScaling sind die Scheitelpunktkoordinaten und Texturkoordinaten bereit. Im nächsten Kapitel wird die Verwendung von NV21-Videodaten für ein effizientes Rendern erläutert.

Projektadresse: https://github.com/MrZhaozhirong/NativeCppApp    Eintragsdatei CameraFilterEncoderActivity

 

Ich denke du magst

Origin blog.csdn.net/a360940265a/article/details/104246229
Empfohlen
Rangfolge