将Android camera2 api中的YUV_420_888转换为RGB

22

我正在编写一款应用程序,它使用相机捕获的数据流并将其转换为RGB格式以进行处理。

在旧的相机实现中,该应用程序可以正常运行,因为使用了NV21 YUV格式。但是在新的YUV格式YUV_420_888下,出现了问题。在使用新的 Camera2 API 时,由于该API发送的是YUV_420_888格式而不是NV21(YUV_420_SP)格式,所以图像无法正确转换为RGB。

请问有谁能告诉我如何将YUV_420_888转换为RGB呢?

谢谢。


3
那是针对旧相机实现的,对我没有帮助。不过还是谢谢你。 - Constantin Georgiu
有人将 YUV_420_888 转换为 NV21 (YUV_420_SP) 吗? - Demian Flavius
@ConstantinGeorgiu,你解决了上面的问题吗? - user1154390
我的解决方案以media.image作为输入并返回位图(https://dev59.com/P4vda4cB1Zd3GeqPb6JN#35994288)。 - Settembrini
请查看此链接:link。我使用了这种方法来解决我的问题。 - Chung Yau
6个回答

11

在Java中将Camera2 YUV_420_888转换为RGB Mat(使用opencv)

@Override
    public void onImageAvailable(ImageReader reader){
        Image image = null;

        try {
            image = reader.acquireLatestImage();
            if (image != null) {

                byte[] nv21;
                ByteBuffer yBuffer = mImage.getPlanes()[0].getBuffer();
                ByteBuffer uBuffer = mImage.getPlanes()[1].getBuffer();
                ByteBuffer vBuffer = mImage.getPlanes()[2].getBuffer();

                int ySize = yBuffer.remaining();
                int uSize = uBuffer.remaining();
                int vSize = vBuffer.remaining();

                nv21 = new byte[ySize + uSize + vSize];

                //U and V are swapped
                yBuffer.get(nv21, 0, ySize);
                vBuffer.get(nv21, ySize, vSize);
                uBuffer.get(nv21, ySize + vSize, uSize);

                Mat mRGB = getYUV2Mat(nv21);



            }
        } catch (Exception e) {
            Log.w(TAG, e.getMessage());
        }finally{
            image.close();// don't forget to close
        }
    }



    public Mat getYUV2Mat(byte[] data) {
    Mat mYuv = new Mat(image.getHeight() + image.getHeight() / 2, image.getWidth(), CV_8UC1);
    mYuv.put(0, 0, data);
    Mat mRGB = new Mat();
    cvtColor(mYuv, mRGB, Imgproc.COLOR_YUV2RGB_NV21, 3);
    return mRGB;
}

这是我找到的最快的方法。其他转换都有for循环,这会减慢实时处理场景的速度。 - chamoda
3
请注意,这仅适用于底层缓冲区为NV21时,即当uBuffer和vBuffer重叠时,参见https://dev59.com/za7la4cB1Zd3GeqPl_rA#52740776。 - Alex Cohn
很好地转换了Android CameraX图像分析帧(YUV)为RBG,谢谢。 - RobertPitt

9
在我的方法中,我使用OpenCV Mat和来自https://gist.github.com/camdenfullmer/dfd83dfb0973663a7974的脚本。首先,您需要使用上面链接中的代码将YUV_420_888图像转换为Mat。
*mImage是我在ImageReader.OnImageAvailableListener中获取的图像对象。
Mat mYuvMat = imageToMat(mImage);

public static Mat imageToMat(Image image) {
    ByteBuffer buffer;
    int rowStride;
    int pixelStride;
    int width = image.getWidth();
    int height = image.getHeight();
    int offset = 0;

    Image.Plane[] planes = image.getPlanes();
    byte[] data = new byte[image.getWidth() * image.getHeight() * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
    byte[] rowData = new byte[planes[0].getRowStride()];

    for (int i = 0; i < planes.length; i++) {
        buffer = planes[i].getBuffer();
        rowStride = planes[i].getRowStride();
        pixelStride = planes[i].getPixelStride();
        int w = (i == 0) ? width : width / 2;
        int h = (i == 0) ? height : height / 2;
        for (int row = 0; row < h; row++) {
            int bytesPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8;
            if (pixelStride == bytesPerPixel) {
                int length = w * bytesPerPixel;
                buffer.get(data, offset, length);

                if (h - row != 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
                offset += length;
            } else {


                if (h - row == 1) {
                    buffer.get(rowData, 0, width - pixelStride + 1);
                } else {
                    buffer.get(rowData, 0, rowStride);
                }

                for (int col = 0; col < w; col++) {
                    data[offset++] = rowData[col * pixelStride];
                }
            }
        }
    }

    Mat mat = new Mat(height + height / 2, width, CvType.CV_8UC1);
    mat.put(0, 0, data);

    return mat;
}

我们有一个YUV Mat通道。为BGR图像定义新的Mat(尚未是RGB):
Mat bgrMat = new Mat(mImage.getHeight(), mImage.getWidth(),CvType.CV_8UC4);

我刚开始学习OpenCV,所以这可能不需要是4通道的Mat,而可以是3通道的,但对我来说它能够工作。现在我使用convert color方法将我的yuv Mat转换为bgr Mat。

Imgproc.cvtColor(mYuvMat, bgrMat, Imgproc.COLOR_YUV2BGR_I420);

现在我们可以进行所有与图像处理相关的操作,如查找轮廓、颜色、圆等。为了将图像打印回屏幕上,我们需要将其转换为位图:

Mat rgbaMatOut = new Mat();
Imgproc.cvtColor(bgrMat, rgbaMatOut, Imgproc.COLOR_BGR2RGBA, 0);
final Bitmap bitmap = Bitmap.createBitmap(bgrMat.cols(), bgrMat.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(rgbaMatOut, bitmap);

我将所有的图像处理放在了一个单独的线程中,因此要设置我的ImageView,我需要在UI线程上执行此操作。

runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if(bitmap != null) {
                            mImageView.setImageBitmap(bitmap);
                        }
                    }
                });

1

Shyam Kumar的答案对我的手机不适用,但Daniel Więcek的答案是正确的。我进行了调试,发现planes[i].getRowStride()为1216,planes[i].getPixelStride()为2。而图像的宽度和高度都是1200。

由于我的声望只有3,所以我无法评论,但可以发布答案。


您可以在此处找到优化的Java转换器,将图像转换为NV21格式(可供OpenCV使用):https://dev59.com/za7la4cB1Zd3GeqPl_rA#52740776。请注意,在大多数情况下,[renderscript conversion](https://dev59.com/014c5IYBdhLWcg3wAWLv#28445255)比CPU更快,并且不需要OpenCV。 - Alex Cohn

1

0

我遇到了完全相同的问题,我的代码从OnPreviewFrame()获取旧的YUV_420_SP格式的byte[]数据并将其转换为RGB。

关键在于byte[]中的“旧”数据是YYYYYY ... CrCbCrCbCrCb,而来自Camera2 API的“新”数据分为3个平面:0=Y,1=Cb,2=Cr。从这里,您可以获得每个字节数组。因此,您需要做的就是重新排序新数据作为与“旧”格式匹配的单个数组,您可以将其传递给现有的toRGB()函数:

Image.Plane[] planes = image.getPlanes(); // in YUV220_888 format
int acc = 0, i;
ByteBuffer[] buff = new ByteBuffer[planes.length];

for (i = 0; i < planes.length; i++) {
   buff[i] = planes[i].getBuffer();
   acc += buff[i].capacity();
}
byte[] data = new byte[acc],
   tmpCb = new byte[buff[1].capacity()] , tmpCr = new byte[buff[2].capacity()];

buff[0].get(data, 0, buff[0].capacity()); // Y
acc = buff[0].capacity();

buff[2].get(tmpCr, 0, buff[2].capacity()); // Cr
buff[1].get(tmpCb, 0, buff[1].capacity()); // Cb

for (i=0; i<tmpCb.length; i++) {
    data[acc] = tmpCr[i];
    data[acc + 1] = tmpCb[i];
    acc++;
}

现在,data[] 的格式与旧的 YUV_420_SP 格式相同。

(希望这能帮助一些人,尽管已经过去了几年..)


你的回答看起来很有前途,确实让我从崩溃到了转换位图。不幸的是,生成的位图只有绿色和灰色条纹... - Sira Lam
请注意,此代码仅适用于底层缓冲区为YV12的情况,即uBuffer和vBuffer的pixelStride=1。 - Alex Cohn

0
大约比上面提到的“imageToMat”函数快10倍的是以下代码:
Image image = reader.acquireLatestImage();
...
Mat yuv = new Mat(image.getHeight() + image.getHeight() / 2, image.getWidth(), CvType.CV_8UC1);
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
final byte[] data = new byte[buffer.limit()];
buffer.get(data);
yuv.put(0, 0, data);
...
image.close();

我不认为你的答案是正确的...imageToMat和你的简化函数生成了两个不同的字节数组。如果OpenCV可以处理来自Camera2 api的新YUV格式,那就太好了。但是下一个转换:Imgproc.cvtColor(mYuvMat, bgrMat, mgproc.COLOR_YUV2BGR_I420);至关重要,根据我的所有测试,只有imageToMat产生了良好的结果。如果有YUV2BGR_CAMERA2_API,你的答案可能会更好。现在已经过去两个月了;) - Daniel Więcek
这可能适用于某些设备,在这些设备上yBuffer(即image.getPlanes(0))实际上是完整的NV21直接缓冲区。但这种情况很少见。 - Alex Cohn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接