camera2捕获的图片-从YUV_420_888到NV21的转换

时间:2018-10-09 17:01:23

标签: android android-camera2 yuv

通过camera2 API,我们收到的图像对象的格式为 YUV_420_888 。然后,我们使用以下函数转换为 NV21

private static byte[] YUV_420_888toNV21(Image image) {
    byte[] nv21;
    ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
    ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
    ByteBuffer vBuffer = image.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);

    return nv21;
}

虽然此功能在cameraCaptureSessions.setRepeatingRequest上可以正常使用,但在调用cameraCaptureSessions.capture时,在进一步处理中(在JNI端)会出现分段错误。两者都通过ImageReader请求YUV_420_888格式。

在请求的类型相同的情况下,两个函数调用的结果为何不同?

更新:如注释中所述,由于图像大小不同(捕获请求的尺寸要大得多),我会遇到这种情况。但是我们在JNI方面的进一步处理操作对于这两个请求都是相同的,并且不取决于图像尺寸(仅取决于纵横比,这两种情况都相同)。

3 个答案:

答案 0 :(得分:2)

基于@Alex Cohn answer,我已经在JNI部分中实现了它,试图从字节访问和性能优势中获利。我把它留在这里,也许就像@Alex回答对我有用。它几乎与C语言中的算法相同;基于YUV_420_888格式的图片:

uchar* yuvToNV21(jbyteArray yBuf, jbyteArray uBuf, jbyteArray vBuf, jbyte *fullArrayNV21,
    int width, int height, int yRowStride, int yPixelStride, int uRowStride,
    int uPixelStride, int vRowStride, int vPixelStride, JNIEnv *env) {

    /* Check that our frame has right format, as specified at android docs for
     * YUV_420_888 (https://developer.android.com/reference/android/graphics/ImageFormat?authuser=2#YUV_420_888):
     *      - Plane Y not overlaped with UV, and always with pixelStride = 1
     *      - Planes U and V have the same rowStride and pixelStride (overlaped or not)
     */
    if(yPixelStride != 1 || uPixelStride != vPixelStride || uRowStride != vRowStride) {
        jclass Exception = env->FindClass("java/lang/Exception");
        env->ThrowNew(Exception, "Invalid YUV_420_888 byte structure. Not agree with https://developer.android.com/reference/android/graphics/ImageFormat?authuser=2#YUV_420_888");
    }

    int ySize = width*height;
    int uSize = env->GetArrayLength(uBuf);
    int vSize = env->GetArrayLength(vBuf);
    int newArrayPosition = 0; //Posicion por la que vamos rellenando el array NV21
    if (fullArrayNV21 == nullptr) {
        fullArrayNV21 = new jbyte[ySize + uSize + vSize];
    }
    if(yRowStride == width) {
        //Best case. No padding, copy direct
        env->GetByteArrayRegion(yBuf, newArrayPosition, ySize, fullArrayNV21);
        newArrayPosition = ySize;
    }else {
        // Padding at plane Y. Copy Row by Row
        long yPlanePosition = 0;
        for(; newArrayPosition<ySize; newArrayPosition += width) {
            env->GetByteArrayRegion(yBuf, yPlanePosition, width, fullArrayNV21 + newArrayPosition);
            yPlanePosition += yRowStride;
        }
    }

    // Check UV channels in order to know if they are overlapped (best case)
    // If they are overlapped, U and B first bytes are consecutives and pixelStride = 2
    long uMemoryAdd = (long)&uBuf;
    long vMemoryAdd = (long)&vBuf;
    long diff = std::abs(uMemoryAdd - vMemoryAdd);
    if(vPixelStride == 2 && diff == 8) {
        if(width == vRowStride) {
            // Best Case: Valid NV21 representation (UV overlapped, no padding). Copy direct
            env->GetByteArrayRegion(uBuf, 0, uSize, fullArrayNV21 + ySize);
            env->GetByteArrayRegion(vBuf, 0, vSize, fullArrayNV21 + ySize + uSize);
        }else {
            // UV overlapped, but with padding. Copy row by row (too much performance improvement compared with copy byte-by-byte)
            int limit = height/2 - 1;
            for(int row = 0; row<limit; row++) {
                env->GetByteArrayRegion(uBuf, row * vRowStride, width, fullArrayNV21 + ySize + (row * width));
            }
        }
    }else {
        //WORST: not overlapped UV. Copy byte by byte
        for(int row = 0; row<height/2; row++) {
           for(int col = 0; col<width/2; col++) {
               int vuPos = col*uPixelStride + row*uRowStride;
               env->GetByteArrayRegion(vBuf, vuPos, 1, fullArrayNV21 + newArrayPosition);
               newArrayPosition++;
               env->GetByteArrayRegion(uBuf, vuPos, 1, fullArrayNV21 + newArrayPosition);
               newArrayPosition++;
           }
        }
    }
    return (uchar*)fullArrayNV21;
}

我敢肯定,可以添加一些改进,但是我已经在很多设备上进行了测试,并且它具有非常好的性能和稳定性。

答案 1 :(得分:1)

如果根本没有填充,并且U和V平原重叠并且实际上表示隔行扫描的VU值,则您的代码将仅返回正确的NV21。这种情况在预览时经常发生,但是在这种情况下,您会为数组分配额外的w * h / 4字节(这大概不是问题)。也许对于捕获的图像,您需要更强大的实现,例如

private static byte[] YUV_420_888toNV21(Image image) {

    int width = image.getWidth();
    int height = image.getHeight(); 
    int ySize = width*height;
    int uvSize = width*height/4;

    byte[] nv21 = new byte[ySize + uvSize*2];

    ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
    ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
    ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V

    int rowStride = image.getPlanes()[0].getRowStride();
    assert(image.getPlanes()[0].getPixelStride() == 1);

    int pos = 0;

    if (rowStride == width) { // likely
        yBuffer.get(nv21, 0, ySize);
        pos += ySize;
    }
    else {
        for (; pos<ySize; pos+=width) {
            yBuffer.get(nv21, pos, width);
            yBuffer.position(yBuffer.position() + rowStride - width); // skip
        }
    }

    rowStride = image.getPlanes()[2].getRowStride();
    int pixelStride = image.getPlanes()[2].getPixelStride();

    assert(rowStride == image.getPlanes()[1].getRowStride());
    assert(pixelStride == image.getPlanes()[1].getPixelStride());

    if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) {
        // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
        byte savePixel = vBuffer.get(1);
        vBuffer.put(1, (byte)0);
        if (uBuffer.get(0) == 0) {
            vBuffer.put(1, (byte)255);
            if (uBuffer.get(0) == 255) {
                vBuffer.put(1, savePixel);
                vBuffer.get(nv21, ySize, uvSize);

                return nv21; // shortcut
            }
        }

        // unfortunately, the check failed. We must save U and V pixel by pixel
        vBuffer.put(1, savePixel);
    }

    // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), 
    // but performance gain would be less significant

    for (int row=0; row<height/2; row++) {
        for (int col=0; col<width/2; col++) {
            int vuPos = col*pixelStride + row*rowStride;
            nv21[pos++] = vBuffer.get(vuPos);
            nv21[pos++] = uBuffer.get(vuPos);
        }
    }

    return nv21;
}

如果您打算将结果数组传递给C ++,则可以利用fact

  

返回的缓冲区将始终具有isDirect返回true,因此可以在JNI中将基础数据映射为指针,而无需使用GetDirectBufferAddress进行任何复制。

这意味着可以使用最少的开销在C ++中完成相同的转换。在C ++中,您甚至可能发现实际像素排列已经是NV21!

PS 实际上,这可以用Java来完成,而开销却可以忽略不计,请参见上面的行if (pixelStride == 2 && …

答案 2 :(得分:0)

    public static byte[] YUV420toNV21(Image image) {
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        int width = crop.width();
        int height = crop.height();
        Image.Plane[] planes = image.getPlanes();
        byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
        byte[] rowData = new byte[planes[0].getRowStride()];

        int channelOffset = 0;
        int outputStride = 1;
        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    channelOffset = width * height + 1;
                    outputStride = 2;
                    break;
                case 2:
                    channelOffset = width * height;
                    outputStride = 2;
                    break;
            }

            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();

            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(data, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        data[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
        }
        return data;
    }