1

I'm using Camera 2 API to save JPEG images on disk. I currently have 3-4 fps on my Nexus 5X, I'd like to improve it to 20-30. Is it possible?

Changing the image format to YUV I manage to generate 30 fps. Is it possible to save them at this frame-rate, or should I give up and live with my 3-4 fps?

Obviously I can share code if needed, but if everyone agree that it's not possible, I'll just give up. Using the NDK (with libjpeg for instance) is an option (but obviously I'd prefer to avoid it...).

Thanks

EDIT: here is how I convert the YUV android.media.Image to a single byte[]:

private byte[] toByteArray(Image image, File destination) {

    ByteBuffer buffer0 = image.getPlanes()[0].getBuffer();
    ByteBuffer buffer2 = image.getPlanes()[2].getBuffer();
    int buffer0_size = buffer0.remaining();
    int buffer2_size = buffer2.remaining();

    byte[] bytes = new byte[buffer0_size + buffer2_size];

    buffer0.get(bytes, 0, buffer0_size);
    buffer2.get(bytes, buffer0_size, buffer2_size);

    return bytes;
}

EDIT 2: another method I found to convert the YUV image into a byte[]:

private byte[] toByteArray(Image image, File destination) {

    Image.Plane yPlane = image.getPlanes()[0];
    Image.Plane uPlane = image.getPlanes()[1];
    Image.Plane vPlane = image.getPlanes()[2];

    int ySize = yPlane.getBuffer().remaining();

    // be aware that this size does not include the padding at the end, if there is any
    // (e.g. if pixel stride is 2 the size is ySize / 2 - 1)
    int uSize = uPlane.getBuffer().remaining();
    int vSize = vPlane.getBuffer().remaining();

    byte[] data = new byte[ySize + (ySize/2)];

    yPlane.getBuffer().get(data, 0, ySize);

    ByteBuffer ub = uPlane.getBuffer();
    ByteBuffer vb = vPlane.getBuffer();

    int uvPixelStride = uPlane.getPixelStride(); //stride guaranteed to be the same for u and v planes

    if (uvPixelStride == 1) {

        uPlane.getBuffer().get(data, ySize, uSize);
        vPlane.getBuffer().get(data, ySize + uSize, vSize);
    }
    else {

        // if pixel stride is 2 there is padding between each pixel
        // converting it to NV21 by filling the gaps of the v plane with the u values
        vb.get(data, ySize, vSize);
        for (int i = 0; i < uSize; i += 2) {
            data[ySize + i + 1] = ub.get(i);
        }
    }

    return data;
}
Tim Autin
  • 6,043
  • 5
  • 46
  • 76
  • Have you used method tracing or other techniques to determine precisely where your time is being spent? What resolution are you using? Are you doing your disk I/O on a separate thread? – CommonsWare Dec 14 '16 at 23:35
  • Yes, the time is spent in the YUV -> JPEG conversion, and in disk I/O. I'm using the max resolution (4000*3000 or so). And yes, disk I/O is threaded. But if I multi-thread the image saving, and if it takes more time than image creation, Ill likely run into OOME (or no disk space), right? – Tim Autin Dec 14 '16 at 23:40
  • "the time is spent in the YUV -> JPEG conversion" -- what YUV->JPEG conversion? Why isn't Camera2 giving you JPEG directly, taking advantage of any device hardware dedicated for that conversion? – CommonsWare Dec 14 '16 at 23:46
  • Well using camera 2 JPEGs, it falls to 4-5 fps before saving it on disk. I tried with YUV to try to improve this. – Tim Autin Dec 15 '16 at 00:03
  • I can't imagine that going from the camera chips to YUV to JPEG is somehow going to be faster than going from the camera chips to JPEG. At best, it would be a wash (e.g., camera chips always give YUV and Camera2 is doing the JPEG conversion on the CPU). If you are looking for doing this for a short stretch, you might look into burst capture modes. – CommonsWare Dec 15 '16 at 00:09
  • Well I don't know, I'm trying every possibility. Using YUV allowed to convert asynchronously, which helped to keep the preview smooth. The whole capture can lasts something like 1mn, so I guess that the burst mode won't help, right? – Tim Autin Dec 15 '16 at 00:15
  • I don't think you can burst that long. Forgetting the actual capture for the moment, I don't see any way that you are going to write 1800 (30 fps x 60 seconds) 12-megapixel photos to disk. Even if you shunted images over to the NDK space right away, so you could use all system RAM instead of being stuck with the heap limit, each of those photos should be clocking in at ~18MB of RAM if I'm doing the YUV math right, and even on top-end devices, you aren't going to have system RAM for more than 150 of them, and you won't be able to get 1650 JPEGs written that fast. – CommonsWare Dec 15 '16 at 00:21
  • Well keeping 150 images in RAM may be enough, if the others are saved fast enough. The iOS version of our app does 20 fps on an iPhone 7, I'd like to get close to that (not with the 5X, but with the Google Pixel for instanced). An iPhone 5S does 10 fps, the Nexus 5X should be able to do more. I'm not sure if these numbers are for full res, but it is at least 1920*1080. Is it possible to pass an YUV image to the jni? I'm not familiar with YUV, is there a way to convert it as a byte array? – Tim Autin Dec 15 '16 at 00:29
  • "if the others are saved fast enough" -- 1650 in 60 seconds? That seems unrealistic. "I'm not familiar with YUV, is there a way to convert it as a byte array?" -- presumably, though I have only used Camera2 with JPEG. – CommonsWare Dec 15 '16 at 00:39
  • OK thanks. I just tested, the iPhone 5S can save 7.5 fps in 1920*1080, and 2.5 in 3264*2448. I will test tomorrow the Nexus 5x in 1920*1080. I'm not sure but I think I did not get more fps when reducing the resolution (so, still 3-4 fps for 1920*1080). I'd expect the 5X something like two time faster than the iPhone 5S (twice the RAM, 6 core against 2). – Tim Autin Dec 15 '16 at 00:52
  • There are other variables (flash storage speed, camera speed) that may be more important than CPU (and definitely would be more important than system RAM). – CommonsWare Dec 15 '16 at 00:54
  • Sure, but still, I'd expect the 5X to be more powerful. I can confirm that lowering the resolution does not improve things a lot. It improves the writing time, but not the time needed to Camera2 to generate the image (between mCaptureSession.capture and onImageAvailable). Can you confirm this behavior? – Tim Autin Dec 15 '16 at 11:20

1 Answers1

1

The dedicated JPEG encoder units on mobile phones are efficient, but not generally optimized for throughput. (Historically, users took one photo every second or two). At full resolution, the 5X's camera pipeline will not generate JPEGs at faster than a few FPS.

If you need higher rates, you need to capture in uncompressed YUV. As mentioned by CommonsWare, there's not enough disk bandwidth to stream full-resolution uncompressed YUV to disk, so you can only hold on to some number of frames before you run out of memory.

You can use libjpeg-turbo or some other high-efficiency JPEG encoder and see how many frames per second you can compress yourself - this may be higher than the hardware JPEG unit. The simplest way to maximize the rate is to capture YUV at 30fps, and run some number of JPEG encoding threads in parallel. For maximum speed, you'll want to hand-write the code talking to the JPEG encoder, because your source data is YUV, not RGB, which most JPEG encoding interfaces tend to accept (even though typically the colorspace of an encoded JPEG is actually YUV as well).

Whenever an encoder thread finishes the previous frame, it can grab the next frame that comes from the camera (you can maintain a small circular buffer of the latest YUV Images to make this simpler).

Eddy Talvala
  • 17,243
  • 2
  • 42
  • 47
  • Thanks for the detailed answer! Do you know if OpenCV can encode JPEGs efficiently enough (I already have OpenCV set up on my project)? The YUV image comes as an android.media.Image, which stores the data in 3 planes. Do you know how to feed OpenCV (or libjpeg) with this data? I updated my question with a sample code to convert it as a byte[], but it does not work with all resolutions (1920*1080 is working, 2592*1944 is not), and I'm not sure that it is reliable. Have you some experience with this format? – Tim Autin Dec 15 '16 at 20:18
  • Your sample code will only work if the underlying data is NV21; this is not guaranteed at all in general (other Android devices may have different layouts, which is why you need to pay attention to the Image row and pixel strides, etc), though it is the case on a Nexus 5X. And your sample code probably doesn't work if rowStride != width, since you'll be copying garbage values (though you don't specify what "doesn't work" means specifically). – Eddy Talvala Dec 15 '16 at 22:05
  • I have no idea how fast OpenCV's JPEG encode is, unfortunately, and I don't really recall what kinds of input YUV formats OpenCV supports. If you're feeding OpenCV NV21, you may need to relayout the pixel data from planar to semiplanar, if the input Image has pixelStride = 1, or a different plane ordering. – Eddy Talvala Dec 15 '16 at 22:05
  • Thanks! I added another method I found to convert the YUV data, but the result is the same (does not work for all resolutions)? By "it does not work", it meant that the output looks like this: http://img11.hostingpics.net/pics/669579test8.jpg . Do you know what I need to change (what I need to do if rowStride != width)? – Tim Autin Dec 16 '16 at 09:23
  • If rowStride != width, and OpenCV expects unpadded data (it does not allow you specify a stride separate from width), you need to copy the image row-by-row, skipping (rowStride-width) bytes at the end of each one. Microsoft has a convenient diagram of what stride means here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa473780(v=vs.85).aspx – Eddy Talvala Dec 16 '16 at 19:09