7

I am using the camera2 API. I need to take a photo in the service without a preview. It works, but the photos have a bad exposure. The pictures are very dark or sometimes very light. How can I fix my code so that the photos are of high quality? I'm using the front camera.

public class Camera2Service extends Service
{

    protected static final String TAG = "myLog";
    protected static final int CAMERACHOICE = CameraCharacteristics.LENS_FACING_BACK;
    protected CameraDevice cameraDevice;
    protected CameraCaptureSession session;
    protected ImageReader imageReader;

    protected CameraDevice.StateCallback cameraStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            Log.d(TAG, "CameraDevice.StateCallback onOpened");
            cameraDevice = camera;
            actOnReadyCameraDevice();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            Log.w(TAG, "CameraDevice.StateCallback onDisconnected");
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.e(TAG, "CameraDevice.StateCallback onError " + error);
        }
    };

    protected CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() {

        @Override
        public void onReady(CameraCaptureSession session) {
            Camera2Service.this.session = session;
            try {
                session.setRepeatingRequest(createCaptureRequest(), null, null);
            } catch (CameraAccessException e) {
                Log.e(TAG, e.getMessage());
            }
        }


        @Override
        public void onConfigured(CameraCaptureSession session) {

        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        }
    };

    protected ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.d(TAG, "onImageAvailable");
            Image img = reader.acquireLatestImage();
            if (img != null) {
                processImage(img);
                img.close();
            }
        }
    };

    public void readyCamera() {
        CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
        try {
            String pickedCamera = getCamera(manager);
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            manager.openCamera(pickedCamera, cameraStateCallback, null);
            imageReader = ImageReader.newInstance(1920, 1088, ImageFormat.JPEG, 2 /* images buffered */);
            imageReader.setOnImageAvailableListener(onImageAvailableListener, null);
            Log.d(TAG, "imageReader created");
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
    }

    public String getCamera(CameraManager manager){
        try {
            for (String cameraId : manager.getCameraIdList()) {
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
                int cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (cOrientation != CAMERACHOICE) {
                    return cameraId;
                }
            }
        } catch (CameraAccessException e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand flags " + flags + " startId " + startId);

        readyCamera();

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onCreate() {
        Log.d(TAG,"onCreate service");
        super.onCreate();
    }

    public void actOnReadyCameraDevice()
    {
        try {
            cameraDevice.createCaptureSession(Arrays.asList(imageReader.getSurface()), sessionStateCallback, null);
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
    }

    @Override
    public void onDestroy() {
        try {
            session.abortCaptures();
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
        session.close();
    }


    private void processImage(Image image){
        //Process image data
        ByteBuffer buffer;
        byte[] bytes;
        boolean success = false;
        File file = new File(Environment.getExternalStorageDirectory() + "/Pictures/image.jpg");
        FileOutputStream output = null;

        if(image.getFormat() == ImageFormat.JPEG) {
            buffer = image.getPlanes()[0].getBuffer();
            bytes = new byte[buffer.remaining()]; // makes byte array large enough to hold image
            buffer.get(bytes); // copies image from buffer to byte array
            try {
                output = new FileOutputStream(file);
                output.write(bytes);    // write the byte array to file
                j++;
                success = true;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                image.close(); // close this to free up buffer for other images
                if (null != output) {
                    try {
                        output.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

        }


    }

    protected CaptureRequest createCaptureRequest() {
        try {
            CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
            builder.addTarget(imageReader.getSurface());
            return builder.build();
        } catch (CameraAccessException e) {
            Log.e(TAG, e.getMessage());
            return null;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
Daniel
  • 2,355
  • 9
  • 23
  • 30
Sergey Unk
  • 161
  • 1
  • 1
  • 11

1 Answers1

6

Sergey, I copied your code and indeed I was able to reproduce the issue. I got totally black pictures out of Google Pixel 2 (Android 8.1).

However, I have successfully resolved the black-pic issue as follows:

First, in case anyone is wondering, you actually do NOT need any Activity, or any preview UI element as many other threads about the Camera API claim! That used to be true for the deprecated Camera v1 API. Now, with the new Camera v2 API, all I needed was a foreground service.

To start the capturing process, I used this code:

CaptureRequest.Builder builder = cameraDevice.createCaptureRequest (CameraDevice.TEMPLATE_VIDEO_SNAPSHOT);
builder.set (CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
builder.set (CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
builder.addTarget (imageReader.getSurface ());
captureRequest = builder.build ();

Then, in ImageReader.onImageAvailable, I skipped the first N pictures (meaning I did not save them). I let the session run, and capture more pics without saving them.

That gave the camera enough time to automatically gradually adjust the exposition parameters. Then, after N ignored photos, I saved a photo, which was normally exposed, not black at all.

The value of the N constant will depend on characteristics of your hardware. So you will need to determine the ideal value of N experimentally for your hardware. You can also use histogram-based heuristic automation. At the beginning of experiments, don't be afraid to start saving only after hundreds of milliseconds of calibration have passed.

Finally, in a lot of similar threads people suggest to just wait e.g. 500 ms after creating the session and only then taking a single picture. That does not help. One really has to let the camera run and let it take many pictures rapidly (at the fastest rate possible). For that, simply use the setRepeatingRequest method (as in your original code).

Hope this helps. :)

EDITED TO ADD: When skipping the initial N pictures, you need to call the acquireLatestImage method of ImageReader for each of those skipped pictures too. Otherwise, it won't work.

Full original code with my changes incorporated that resolved the issue, tested and confirmed as working on Google Pixel 2, Android 8.1:

public class Camera2Service extends Service
{
    protected static final int CAMERA_CALIBRATION_DELAY = 500;
    protected static final String TAG = "myLog";
    protected static final int CAMERACHOICE = CameraCharacteristics.LENS_FACING_BACK;
    protected static long cameraCaptureStartTime;
    protected CameraDevice cameraDevice;
    protected CameraCaptureSession session;
    protected ImageReader imageReader;

    protected CameraDevice.StateCallback cameraStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            Log.d(TAG, "CameraDevice.StateCallback onOpened");
            cameraDevice = camera;
            actOnReadyCameraDevice();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            Log.w(TAG, "CameraDevice.StateCallback onDisconnected");
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.e(TAG, "CameraDevice.StateCallback onError " + error);
        }
    };

    protected CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() {

        @Override
        public void onReady(CameraCaptureSession session) {
            Camera2Service.this.session = session;
            try {
                session.setRepeatingRequest(createCaptureRequest(), null, null);
                cameraCaptureStartTime = System.currentTimeMillis ();
            } catch (CameraAccessException e) {
                Log.e(TAG, e.getMessage());
            }
        }


        @Override
        public void onConfigured(CameraCaptureSession session) {

        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        }
    };

    protected ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.d(TAG, "onImageAvailable");
            Image img = reader.acquireLatestImage();
            if (img != null) {
                if (System.currentTimeMillis () > cameraCaptureStartTime + CAMERA_CALIBRATION_DELAY) {
                    processImage(img);
                }
                img.close();
            }
        }
    };

    public void readyCamera() {
        CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
        try {
            String pickedCamera = getCamera(manager);
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            manager.openCamera(pickedCamera, cameraStateCallback, null);
            imageReader = ImageReader.newInstance(1920, 1088, ImageFormat.JPEG, 2 /* images buffered */);
            imageReader.setOnImageAvailableListener(onImageAvailableListener, null);
            Log.d(TAG, "imageReader created");
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
    }

    public String getCamera(CameraManager manager){
        try {
            for (String cameraId : manager.getCameraIdList()) {
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
                int cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (cOrientation == CAMERACHOICE) {
                    return cameraId;
                }
            }
        } catch (CameraAccessException e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand flags " + flags + " startId " + startId);

        readyCamera();

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onCreate() {
        Log.d(TAG,"onCreate service");
        super.onCreate();
    }

    public void actOnReadyCameraDevice()
    {
        try {
            cameraDevice.createCaptureSession(Arrays.asList(imageReader.getSurface()), sessionStateCallback, null);
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
    }

    @Override
    public void onDestroy() {
        try {
            session.abortCaptures();
        } catch (CameraAccessException e){
            Log.e(TAG, e.getMessage());
        }
        session.close();
    }


     private void processImage(Image image){
    //Process image data
    ByteBuffer buffer;
    byte[] bytes;
    boolean success = false;
    File file = new File(Environment.getExternalStorageDirectory() + "/Pictures/image.jpg");
    FileOutputStream output = null;

    if(image.getFormat() == ImageFormat.JPEG) {
        buffer = image.getPlanes()[0].getBuffer();
        bytes = new byte[buffer.remaining()]; // makes byte array large enough to hold image
        buffer.get(bytes); // copies image from buffer to byte array
        try {
            output = new FileOutputStream(file);
            output.write(bytes);    // write the byte array to file
            j++;
            success = true;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            image.close(); // close this to free up buffer for other images
            if (null != output) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }


}

protected CaptureRequest createCaptureRequest() {
    try {
        CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
        builder.addTarget(imageReader.getSurface());
        return builder.build();
    } catch (CameraAccessException e) {
        Log.e(TAG, e.getMessage());
        return null;
    }
}

@Override
public IBinder onBind(Intent intent) {
    return null;
}

}

deLock
  • 762
  • 8
  • 16
  • Original solution! Can I ask you to look at the resulting source code? I'm afraid I will reproduce your decision for a long time) – Sergey Unk Apr 13 '18 at 17:33
  • My source code is exactly the same as yours, except the changed code for the CaptureRequest, which I posted in my answer, and also in your onImageAvailable I test the condition if enough time has passed since I started the capturing process (to do that, you can simply use this, for example: "if (System.currentTimeMillis () > capturingStartTime + EXPERIMENTALLY_DETERMINED_INTERVAL)". Otherwise, I did not make any other changes to your source code. – deLock Apr 13 '18 at 17:41
  • Also, I suppose TEMPLATE_VIDEO_SNAPSHOT might not be the only one that works. It's just the one I chose as the first to test, and it worked for me. I did not try any other template after that. – deLock Apr 13 '18 at 17:43
  • after calling `CaptureRequest.Builder builder = cameraDevice.createCaptureRequest (CameraDevice.TEMPLATE_VIDEO_SNAPSHOT);` an error occurs `IllegalArgumentException while invoking public void android.hardware.camera2.CameraCaptureSession$StateCallback.onReady(android.hardware.camera2.CameraCaptureSession) java.lang.IllegalArgumentException: createDefaultRequest - invalid templateId specified` – Sergey Unk Apr 13 '18 at 18:09
  • It's very strange that the code does not work. Could you show your code to Gist? I want to compare two classes – Sergey Unk Apr 13 '18 at 18:29
  • I'm sorry, I don't know what Gist is. It might be the case that your hardware does not support the TEMPLATE_VIDEO_SNAPSHOT template. But in principle, you could try any other template and it should work too. Try for example one of these: TEMPLATE_STILL_CAPTURE or TEMPLATE_RECORD or TEMPLATE_PREVIEW – deLock Apr 13 '18 at 18:44
  • The photos again turn out to be dark ((Please publish the source code here https://gist.github.com. This will help me a lot. – Sergey Unk Apr 13 '18 at 19:21
  • I've added the full source code to my answer above. You will really need to try different templates. They might be the culprit. – deLock Apr 13 '18 at 19:45
  • I tried all the templates. The result does not change. It seems to me that CAMERA_CALIBRATION_DELAY does not affect the result when using these templates. I used 500, 1000,3000 - the photos are equally dark. Have you tried using other templates besides TEMPLATE_VIDEO_SNAPSHOT? – Sergey Unk Apr 13 '18 at 20:13
  • I've tested the following templates just now: TEMPLATE_STILL_CAPTURE -- did not work. But TEMPLATE_RECORD works well! Also, I use builder.set (CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT); Maybe that helps me too. If none of the other templates work for you, note that there is a bug in your code: (cOrientation != CAMERACHOICE) should actually be: (cOrientation == CAMERACHOICE). Which leads me to a question: Are you sure you are using the right camera and not physically covering the lens with anything? – deLock Apr 13 '18 at 21:42
  • I updated the code I posted -- fixed two bugs: 1) img wasn't closed when it was skipped, 2) camera choice (front/back) was incorrect. – deLock Apr 14 '18 at 06:29
  • I need to take a photo from the front camera. Unfortunately, on the phone xiaomi redmi 4a, android 7.1.1 the pictures again turn out to be dark (((( – Sergey Unk Apr 14 '18 at 10:42
  • I'm really sorry to hear that, man. :( If you tried all of the templates, the last thing that I can think of for you to try is to copy the official Camera2 example and see if it works -- good luck. https://github.com/googlesamples/android-Camera2Video/blob/master/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java – deLock Apr 14 '18 at 11:23
  • 2
    What is the j++ for? I can't see any other mention of it? – stuart194 Apr 17 '19 at 00:57
  • 1
    @stuart194 you are asking the wrong poster. If you read the very first line of my post, you will find out that I "copied the code" from the OP, from the first post, and enhanced it, so that it works for me. And if you search the original code, you will find j++ there too. So ask him, why he had j++ there. In any case, in this excerpt from his code, it is obviously not used for anything. :) – deLock Apr 19 '19 at 07:48
  • When I start the service it works fine but when I try to stop the service it gives error " java.lang.IllegalStateException: Session has been closed; further changes are illegal" .... Please help – Osama Shakeel Jan 27 '21 at 10:42
  • How can I use this service to take both front and back side of picture at the same time – Osama Shakeel Jan 29 '21 at 05:17
  • @PakistanBeauty If you mean both front and back _camera_ (both cameras) at the same time, then you need a phone that supports that (few do). Software-wise, there might be some Camera API stuff for that, I don't know. If there isn't, you could try using my code in two separate threads, one for each camera. If that doesn't work, then obviously do a loop with two iterations per frame, each iteration using a different camera. – deLock Jan 31 '21 at 10:11