Search code examples
androidandroid-camera2image-reader

Android Camera 2: ImageReader's Images Have no Stride Values


I've been trying to route the images from the camera to an ImageReader so that I can manipulate the images directly using the Camera2 API. When I have the capture session stream to a SurfaceView, the stream works just fine. When I then set the capture session stream to my ImageReader, I notice that the images are somehow invalid.

In my ImageReader's OnImageAvailable callback function, I pull the next available Image and try to read it. This is where I have the problem. The Image isn't null and the planes are there, but the planes' buffers are null at first. When I try to grab the buffers, they are suddenly not null, but trying to read from them crashes the app without a stack trace. Further, the pixel and row strides in the planes are set to 0. The width and height of the image are properly set, though.

Therefore, I think that I'm not setting my ImageReader up correctly. The question is then what am I not doing correctly?

Code:

public class CompatibleCamera {
    private static final int CAMERA2_API_LEVEL = 23;
    public static final int FORMAT_RAW = ImageFormat.RAW_SENSOR;
    public static final int FORMAT_JPEG = ImageFormat.JPEG;
    private static final int MAX_IMAGES = 2;
    // Interface for the user to use. User supplies the function to manipulate the image
    public interface ImageTransform
    {
        void doTransform(Image image);
    }

    //***********Camera 2 API Members***********

    // The camera2 API CameraManager. Used to access the camera device
    private CameraManager mCamera2Manager;
    // The information used by the device to reference the camera. Not a camera object itself
    private CameraDevice mCamera2Device;
    private String mCamera2DeviceID = "";
    // The class that allows us to get the camera's image

    private ImageReader mImageReader;
    // This listener is where we have the programmer deal with the image. Just edit the interface
    private ImageReader.OnImageAvailableListener mListener;
    // This is the thread for the handler. It keeps it off the UI thread so we don't block the GUI

    private HandlerThread mCameraCaptureHandlerThread;
    // This runs in the background and handles the camera feed, activating the OnImageAvailableListener
    private Handler mCameraCaptureHandler;

    private HandlerThread mImageAvailableHandlerThread;
    // This runs in the background and handles the camera feed, activating the OnImageAvailableListener
    private Handler mImageAvailableHandler;

    // This object is the camera feed, essentially. We store it so we can properly close it later
    private CameraCaptureSession cameraCaptureSession;

    // DEBUG
    private boolean TEST_SURFACE_VIEW = false;
    private Surface dbSurface;

    // Mutex lock. Locks and unlocks when the ImageReader is pulling and processing an image
    private Semaphore imageReaderLock = new Semaphore(1);

    //***********Common Members***********

    // The context of the activity holding this object
    private Context mContext;

    // Our ImageTransform implementation to alter the image as it comes in
    private ImageTransform mTransform;

    private int iImageFormat= FORMAT_RAW;

    //==========Methods==========

    public CompatibleCamera(Context context, ImageTransform transform, int imageFormat)
    {
        mContext = context;
        mTransform = transform;

        mListener = new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader imageReader) {
                try {
                    imageReaderLock.acquire();
                    Image image = imageReader.acquireNextImage();

                    //<--------------Problem With Image is Here-------------->
                    mTransform.doTransform(image);
                    image.close();
                    imageReaderLock.release();
                }
                catch(InterruptedException ex)
                {
                    ex.printStackTrace();
                }
            }
        };
    }

    private boolean camera2GetManager()
    {
        //----First, get the CameraManager and a Camera Device----
        mCamera2Manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
        if (mCamera2Manager == null) {
            System.out.println("    DEBUG: Manager is null");
            return false;
        }
        else {
            System.out.println("    DEBUG: Camera Manager obtained");

            try {
                String[] cameraIDs = mCamera2Manager.getCameraIdList();

                for (String cameraID : cameraIDs) {
                    CameraCharacteristics cameraCharacteristics = mCamera2Manager.getCameraCharacteristics(cameraID);
                    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) ==
                            CameraCharacteristics.LENS_FACING_BACK) {
                        mCamera2DeviceID = cameraID;
                        break;
                    }
                }
                if (mCamera2DeviceID.equals("")) {
                    System.out.println("No back camera, exiting");
                    return false;
                }
                System.out.println("    DEBUG: Camera Device obtained");

                // Open the Camera Device
            } catch (Exception ex) {
                ex.printStackTrace();
                return false;
            }
            return camera2OpenCamera();
        }
    }

    private boolean camera2SetupImageReader()
    {
        // Get the largest image size available

        CameraCharacteristics cameraCharacteristics;
        try {
            cameraCharacteristics= mCamera2Manager.getCameraCharacteristics(mCamera2DeviceID);
        } catch(Exception e) {
            e.printStackTrace();
            return false;
        }

        StreamConfigurationMap map = cameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        Size largestSize = Collections.max(
                Arrays.asList(map.getOutputSizes(iImageFormat)),
                new CompareSizesByArea());


        // Set up the handler
        mCameraCaptureHandlerThread = new HandlerThread("cameraCaptureHandlerThread");
        mCameraCaptureHandlerThread.start();
        mCameraCaptureHandler = new Handler(mCameraCaptureHandlerThread.getLooper());


        mImageAvailableHandlerThread = new HandlerThread("imageReaderHandlerThread");
        mImageAvailableHandlerThread.start();
        mImageAvailableHandler = new Handler(mImageAvailableHandlerThread.getLooper());

        mImageReader = ImageReader.newInstance( largestSize.getWidth(),
                largestSize.getHeight(),
                iImageFormat,
                MAX_IMAGES);
        mImageReader.setOnImageAvailableListener(mListener, mImageAvailableHandler);

        // This callback is used to asynchronously set up the capture session on our end
        final CameraCaptureSession.StateCallback captureStateCallback = new CameraCaptureSession.StateCallback() {
            // When configured, set the target surface
            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                try
                {
                    CaptureRequest.Builder requestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
                    if (TEST_SURFACE_VIEW)
                        requestBuilder.addTarget(dbSurface);
                    else
                        requestBuilder.addTarget(mImageReader.getSurface());
                    //set to null - image data will be produced but will not receive metadata
                    session.setRepeatingRequest(requestBuilder.build(), null, mCameraCaptureHandler);
                    cameraCaptureSession = session;
                }
                catch (Exception ex)
                {
                    ex.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                System.out.println("Failed to configure the capture session :(");
            }
        };

        ArrayList<Surface> surfaces = new ArrayList<>();
        if (TEST_SURFACE_VIEW)
            surfaces.add(dbSurface);
        else
            surfaces.add(mImageReader.getSurface());

        try
        {
            mCamera2Device.createCaptureSession(surfaces, captureStateCallback, mCameraCaptureHandler);
        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }
        return true;
    }
}

Solution

  • RAW_SENSOR is a special beast of formats.

    General raw camera sensor image format, usually representing a single-channel Bayer-mosaic image. Each pixel color sample is stored with 16 bits of precision.

    The layout of the color mosaic, the maximum and minimum encoding values of the raw pixel data, the color space of the image, and all other needed information to interpret a raw sensor image must be queried from the android.hardware.camera2.CameraDevice which produced the image.

    You should not attempt to use its stride info directly, as if it were a YUV frame.