如何在 Android 中有效地动态操作 YUV 相机帧?

Posted

技术标签:

【中文标题】如何在 Android 中有效地动态操作 YUV 相机帧?【英文标题】:How to manipulate on the fly YUV Camera frame efficiently in Android? 【发布时间】:2019-05-30 06:09:24 【问题描述】:

我在 NV21 帧的感兴趣区域(中心)周围添加一个黑色 (0) 填充,该帧是从线程中的 android CameraPreview 回调获得的。

为避免转换为 RGB/位图和反向的开销,我尝试直接操作 NV21 字节数组,但这涉及嵌套循环,这也使预览/处理速度变慢。

这是我的run()方法在调用blackNonROI方法后向检测器发送帧。

public void run() 
    Frame outputFrame;
    ByteBuffer data;
    while (true) 
        synchronized (mLock) 

            while (mActive && (mPendingFrameData == null))
                try mLock.wait(); catch(InterruptedException e) return; 

            if (!mActive)  return; 

            // Region of Interest
            mPendingFrameData = blackNonROI(mPendingFrameData.array(),mPreviewSize.getWidth(),mPreviewSize.getHeight(),300,300);

            outputFrame = new Frame.Builder().setImageData(mPendingFrameData, mPreviewSize.getWidth(),mPreviewSize.getHeight(), ImageFormat.NV21).setId(mPendingFrameId).setTimestampMillis(mPendingTimeMillis).setRotation(mRotation).build();

            data = mPendingFrameData;
            mPendingFrameData = null;

        

        try 
            mDetector.receiveFrame(outputFrame);
         catch (Throwable t) 
         finally 
            mCamera.addCallbackBuffer(data.array());
        
    

下面是方法blackNonROI

private ByteBuffer blackNonROI(byte[] yuvData, int width, int height, int roiWidth, int roiHeight)

    int hozMargin = (width - roiWidth) / 2;
    int verMargin = (height - roiHeight) / 2;

    // top/bottom of center
    for(int x=0; x<width; x++)
        for(int y=0; y<verMargin; y++)
            yuvData[y * width + x] = 0;
        for(int y=height-verMargin; y<height; y++)
            yuvData[y * width + x] = 0;
    

    // left/right of center
    for(int y=verMargin; y<height-verMargin; y++)
        for (int x = 0; x < hozMargin; x++)
            yuvData[y * width + x] = 0;
        for (int x = width-hozMargin; x < width; x++)
            yuvData[y * width + x] = 0;
    

    return ByteBuffer.wrap(yuvData);

Example output frame

请注意,我没有裁剪图像,只是在图像的指定中心周围填充黑色像素,以保持协调以进行进一步的活动。这可以正常工作,但速度不够快,会导致预览和帧处理出现延迟。

    能否进一步改进字节数组更新? 调用blackNonROI的时间/地点合适吗? 还有什么其他方式/lib 可以更有效地做到这一点? 我简单的像素迭代这么慢,YUV/Bitmap 库怎么做复杂的事情这么快?他们使用 GPU 吗?

编辑:

我已经用以下代码替换了两个for 循环,现在速度非常快(详情请参阅greeble31 的回答):

    // full top padding
    from = 0;
    to = (verMargin-1)*width + width;
    Arrays.fill(yuvData,from,to,(byte)1);

    // full bottom padding
    from = (height-verMargin)*width;
    to = (height-1)*width + width;
    Arrays.fill(yuvData,from,to,(byte)1);

    for(int y=verMargin; y<height-verMargin; y++) 
        // left-middle padding
        from = y*width;
        to = y*width + hozMargin;
        Arrays.fill(yuvData,from,to,(byte)1);

        // right-middle padding
        from = y*width + width-hozMargin;
        to = y*width + width;
        Arrays.fill(yuvData,from,to,(byte)1);
    

【问题讨论】:

【参考方案1】:

1. 是的。要了解原因,让我们看一下 Android Studio 为您的“中心左/右”嵌套循环生成的字节码:

(来自blackNonROI,AS 3.2.1 的发布版本的注释摘录):

:goto_27
sub-int v2, p2, p4         ;for(int y=verMargin; y<height-verMargin; y++)
if-ge v1, v2, :cond_45
const/4 v2, 0x0
:goto_2c
if-ge v2, p3, :cond_36     ;for (int x = 0; x < hozMargin; x++)
mul-int v3, v1, p1
add-int/2addr v3, v2
.line 759
aput-byte v0, p0, v3
add-int/lit8 v2, v2, 0x1
goto :goto_2c
:cond_36
sub-int v2, p1, p3 
:goto_38
if-ge v2, p1, :cond_42     ;for (int x = width-hozMargin; x < width; x++)
mul-int v3, v1, p1
add-int/2addr v3, v2
.line 761
aput-byte v0, p0, v3
add-int/lit8 v2, v2, 0x1
goto :goto_38
:cond_42
add-int/lit8 v1, v1, 0x1
goto :goto_27
.line 764
:cond_45                   ;all done with the for loops!

无需费心逐行解读整个事情,很明显,您的每个小的内部循环都在执行:

1 个比较 1 个整数乘法 1 次添加 1 家商店 1 转到

这很多,当你考虑到你真正需要这个内部循环做的只是将一定数量的连续数组元素设置为 0 时。

此外,其中一些字节码需要多个机器指令来实现,所以如果您查看超过 20 个周期,我不会感到惊讶,只是为了对其中一个内部循环进行一次迭代。 (我还没有测试过 Dalvik VM 编译这段代码后的样子,但我真诚地怀疑它是否足够聪明,无法优化这些循环中的乘法。)

可能的修复

您可以通过消除一些冗余计算来提高性能。例如,每个内部循环都在重新计算y * width每次。相反,您可以预先计算该偏移量,将其存储在局部变量中(在外循环中),并在计算索引时使用它。

当性能绝对关键时,我有时会在本机代码中进行这种缓冲区操作。如果您可以合理地确定mPendingFrameDataDirectByteBuffer,那么这是一个更具吸引力的选择。缺点是 1.) 更高的复杂性,以及 2.) 如果出现问题/崩溃,“安全网”就会减少。

最合适的修复

在您的情况下,最合适的解决方案可能只是使用Arrays.fill(),这更有可能以优化的方式实现。

请注意,顶部和底部块是大的、连续的内存块,可以由一个Arrays.fill() 处理:

Arrays.fill(yuvData, 0, verMargin * width, 0);   //top
Arrays.fill(yuvData, width * height - verMargin * width, width * height, 0);    //bottom

然后双方可以这样处理:

for(int y=verMargin; y<height-verMargin; y++)
    int offset = y * width;
    Arrays.fill(yuvData, offset, offset + hozMargin, 0);  //left
    Arrays.fill(yuvData, offset + width, offset + width - hozMargin, 0);   //right

这里有更多的优化机会,但我们已经处于收益递减的地步。例如,由于每一行的结尾与下一行的开头相邻(在内存中),您实际上可以将两个较小的 fill() 调用组合成一个较大的调用,同时覆盖 N 行的右侧和左侧第 N + 1 行的一侧。以此类推。

2. 不确定。如果您的预览显示没有任何损坏/撕裂,那么它可能是一个调用函数的安全地方(从线程安全的角度来看),因此可能与任何地方一样好。 p>

3 和 4。 可能有库可以完成这项任务;对于基于 Java 的 NV21 框架,我不知道有什么副手。您必须进行一些格式转换,我认为这不值得。在我看来,使用 GPU 来完成这项工作是过度的过度优化,但它可能适用于某些专门的应用程序。在考虑使用 GPU 之前,我会考虑使用 JNI(本机代码)。

我认为您选择直接对 NV21 进行操作,而不是转换为位图,这是一个不错的选择(考虑到您的需求以及该任务足够简单以避免需要图形库的事实)。

【讨论】:

你成功了 greeble31 ! 同时,我为优化而苦苦挣扎,并试图使用 Arrays.fill()。令人惊讶的是,我得出了与您提出的几乎相同的解决方案。您的回答证明了我的方法并清除了许多事情。你应该感谢这样的回答。【参考方案2】:

显然,传递图像进行检测的最有效方法是将 ROI 矩形传递给检测器。我们所有的图像处理函数都接受边界框作为参数。

如果黑色边距用于显示,请考虑使用黑色覆盖蒙版进行预览布局,而不是像素操作。

如果像素操作不可避免,请检查是否可以将其限制为 Y 好的,您已经这样做了!

如果您的检测器适用于缩小的图像(就像我的面部识别引擎所做的那样),将黑色应用于调整大小的帧可能是明智之举。

无论如何,请保持循环干净整洁,删除所有重复计算。使用 Arrays.fill() 操作可能会有很大帮助,但不是很显着。

【讨论】:

黑色填充不显示在预览中,它被添加到图像中,因此当检测器返回结果矩形时,其坐标对应于实际预览大小。是的,我只是将 Y 变黑,希望检测器也只考虑 Y 帧。 通过裁剪框并补偿边界矩形的检测结果可能效率更高。我建议比较性能(即使是模拟),看看增益对您来说是否重要。 顺便说一句,如果您的检测器在缩小的图像上工作(就像我的面部识别引擎一样),在裁剪和/或涂黑像素时调整框架大小可能是明智的。 谢谢,我会试试你的建议。

以上是关于如何在 Android 中有效地动态操作 YUV 相机帧?的主要内容,如果未能解决你的问题,请参考以下文章

Java Swing:如何更改 Grid 布局中一行的大小并允许它仅水平而不是垂直地动态调整大小?

将 YUV_420_888 中的图像从 Android 发送到 OpenCV Mat 中的 JNI 的最有效方法

在YUV_420_888中将图像从Android发送到OpenCV Mat中的JNI的最有效方式

我如何从 android 相机 onPreviewFrame 中知道 YUV 格式?

YUV图像处理

如何在android上调整Yuv图像的大小