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:オブジェクトではなく、単なるポインタ)(コードの長さのため、ByteBufferとNV21BufferPoolの実装コードは貼り付けられません。学生は、ポータルから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