Android相机2 API YUV_420_888转换为JPEG

12

我正在使用OnImageAvailableListener获取预览帧:

我使用OnImageAvailableListener来获取相机的预览帧。

@Override
public void onImageAvailable(ImageReader reader) {
    Image image = null;
    try {
        image = reader.acquireLatestImage();
        Image.Plane[] planes = image.getPlanes();
        ByteBuffer buffer = planes[0].getBuffer();
        byte[] data = new byte[buffer.capacity()];
        buffer.get(data);
        //data.length=332803; width=3264; height=2448
        Log.e(TAG, "data.length=" + data.length + "; width=" + image.getWidth() + "; height=" + image.getHeight());
        //TODO data processing
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (image != null) {
        image.close();
    }
}

每次数据长度不同,但图像的宽度和高度相同。
主要问题:data.length 对于3264x2448这样的分辨率来说太小了。
数据数组的大小应该是3264*2448=7,990,272,而不是300,000 - 600,000。
出了什么问题?


imageReader = ImageReader.newInstance(3264, 2448, ImageFormat.JPEG, 5);
3个回答

28

我使用YUV_420_888图像格式解决了这个问题,并将其手动转换为JPEG图像格式。

imageReader = ImageReader.newInstance(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT, 
                                      ImageFormat.YUV_420_888, 5);
imageReader.setOnImageAvailableListener(this, null);

Surface imageSurface = imageReader.getSurface();
List<Surface> surfaceList = new ArrayList<>();
//...add other surfaces
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(imageSurface);
            surfaceList.add(imageSurface);
cameraDevice.createCaptureSession(surfaceList,
                    new CameraCaptureSession.StateCallback() {
//...implement onConfigured, onConfigureFailed for StateCallback
}, null);

@Override
public void onImageAvailable(ImageReader reader) {
    Image image = reader.acquireLatestImage();
    if (image != null) {
        //converting to JPEG
        byte[] jpegData = ImageUtils.imageToByteArray(image);
        //write to file (for example ..some_path/frame.jpg)
        FileManager.writeFrame(FILE_NAME, jpegData);
        image.close();
    }
}

public final class ImageUtil {

    public static byte[] imageToByteArray(Image image) {
        byte[] data = null;
        if (image.getFormat() == ImageFormat.JPEG) {
            Image.Plane[] planes = image.getPlanes();
            ByteBuffer buffer = planes[0].getBuffer();
            data = new byte[buffer.capacity()];
            buffer.get(data);
            return data;
        } else if (image.getFormat() == ImageFormat.YUV_420_888) {
            data = NV21toJPEG(
                    YUV_420_888toNV21(image),
                    image.getWidth(), image.getHeight());
        }
        return data;
    }

    private static byte[] YUV_420_888toNV21(Image image) {
        byte[] nv21;
        ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
        ByteBuffer vuBuffer = image.getPlanes()[2].getBuffer();

        int ySize = yBuffer.remaining();
        int vuSize = vuBuffer.remaining();

        nv21 = new byte[ySize + vuSize];

        yBuffer.get(nv21, 0, ySize);
        vuBuffer.get(nv21, ySize, vuSize);

        return nv21;
    }

    private static byte[] NV21toJPEG(byte[] nv21, int width, int height) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
        yuv.compressToJpeg(new Rect(0, 0, width, height), 100, out);
        return out.toByteArray();
    }
}

public final class FileManager {
    public static void writeFrame(String fileName, byte[] data) {
        try {
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
            bos.write(data);
            bos.flush();
            bos.close();
//            Log.e(TAG, "" + data.length + " bytes have been written to " + filesDir + fileName + ".jpg");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

data = NV21toJPEG(YUV_420_888toNV21(image), image.getWidth(), image.getHeight()); 会导致失真吗? - Volodymyr Kulyk
是的,我已经按原样放置了类,并实现了与您类似的写文件方式,但是使用FileOutputStream,最终图像失真。我可以通过git repo向您展示完整的实现,稍后我会推送。 - ahasbini
@ahasbini,你应该创建一个独立的问题,将你的代码贴出来。在评论区看不到你做了什么的情况下,很难解决你的问题。 - Volodymyr Kulyk
1
这个 YUV_420_888toNV21() 不可靠,请参见 https://dev59.com/za7la4cB1Zd3GeqPl_rA#52740776 - Alex Cohn
1
@AlexCohn 谢谢,我同意你的观点。我会测试你的解决方案。 - Volodymyr Kulyk
显示剩余3条评论

4

我不确定,但我认为你只取了YUV_420_888格式(亮度部分)中的一个平面。

在我的情况下,我通常以以下方式将我的图像转换为byte[]。

            Image m_img;
            Log.v(LOG_TAG,"Format -> "+m_img.getFormat());
            Image.Plane Y = m_img.getPlanes()[0];
            Image.Plane U = m_img.getPlanes()[1];
            Image.Plane V = m_img.getPlanes()[2];

            int Yb = Y.getBuffer().remaining();
            int Ub = U.getBuffer().remaining();
            int Vb = V.getBuffer().remaining();

            data = new byte[Yb + Ub + Vb];
            //your data length should be this byte array length.

            Y.getBuffer().get(data, 0, Yb);
            U.getBuffer().get(data, Yb, Ub);
            V.getBuffer().get(data, Yb+ Ub, Vb);
            final int width = m_img.getWidth();
            final int height = m_img.getHeight();

我使用这个字节缓冲区来转换为RGB。

希望对你有所帮助。

祝好,Unai。


谢谢您的回复!目前我正在使用 ImageFormat.JPEG,因此 m_img.getPlanes().length 等于 1。 - Volodymyr Kulyk
好的,如果我使用 ImageFormat.YUV_420_888,如何将 rgb 缓冲区压缩为 JPEG - Volodymyr Kulyk
太好了,不用谢。我不知道你使用这个 byteArray 的目的是什么。抱歉回复晚了。 - uelordi

2
您的代码正在请求JPEG格式的图像,这些图像是经过压缩的。它们会随着每一帧而改变大小,并且比未压缩的图像要小得多。如果您只想保存JPEG图像而不做其他操作,那么您可以将byte[]数据保存到磁盘中即可。
如果您想实际处理JPEG图像,例如将其转换为位图,则可以使用BitmapFactory.decodeByteArray()方法,但效率相对较低。
或者您可以切换到YUV格式,这样更高效,但需要更多的工作才能从中获取位图。

每帧都转换为位图再解码是不可行的。 - Volodymyr Kulyk
您目前的解决方案似乎是将YUV数据重新排列为NV21,然后将其压缩为JPEG;不清楚您需要YUV做什么,但如果您只想保存数据,那么只请求JPEG肯定更便宜。您可以根据分辨率要求JPEG和YUV两者,这可能是最有效的选择,尽管JPEG输出的帧率可能低于30fps。 - Eddy Talvala
我正在使用NV21进行QR码识别/解码。JPEG用于将帧发送到服务器。 - Volodymyr Kulyk

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