12/08/2018, 13:38

New API Camera2 in Android (Part II) Take photo

Như ở phần 1 tôi đã giới thiệu cho các bạn cơ bản về Camera2 và cách hiển thị hình ảnh thu được lên màn hình View. Trong phần này, tôi sẽ hướng dẫn các bạn cách chụp ảnh từ Camera2. Ở phần này tôi vẫn sẽ sử dụng lại Project mà trong phần đầu tôi đã hướng dẫn các bạn. Bạn nào chưa tham khảo phần 1 ...

Như ở phần 1 tôi đã giới thiệu cho các bạn cơ bản về Camera2 và cách hiển thị hình ảnh thu được lên màn hình View. Trong phần này, tôi sẽ hướng dẫn các bạn cách chụp ảnh từ Camera2.

Ở phần này tôi vẫn sẽ sử dụng lại Project mà trong phần đầu tôi đã hướng dẫn các bạn. Bạn nào chưa tham khảo phần 1 thì có thể xem link hướng dẫn chi tiết tại đây.

Các bạn nhớ ở phần trước có 1 số đối số trong các hàm mình vẫn để là "null". Bây giờ mình sẽ khai báo 2 biến backgroundThread và backgroundHandler để điền vào các vị trí trống đấy, nhằm đẩy các tác vụ của camera xuống chạy nền hệ thống.

...
private HandlerThread backgroundThread;
private Handler backgroundHandler;
...
/**
* Starts a background thread.
*/
private void startBackgroundThread() {
    backgroundThread = new HandlerThread("CameraBackground");
    backgroundThread.start();
    backgroundHandler = new Handler(backgroundThread.getLooper());
}

/**
* Stops the background thread.
*/
private void stopBackgroundThread() {
    backgroundThread.quitSafely();
    try {
        backgroundThread.join();
        backgroundThread = null;
        backgroundHandler = null;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

Ở 2 hàm onResum và onPause các bạn thêm 2 dòng này này vào.

@Override
public void onResume() {
    super.onResume();
    startBackgroundThread();
    ...
}

@Override
public void onPause() {
    closeCamera();
    stopBackgroundThread();
    super.onPause();
}

Tiếp đó bạn thay thế các đối số null trong project bằng biến "backgroundHandler" VD như ở hàm gọi openCamera manager.openCamera(cameraId, stateCallback, backgroundHandler);

Tới đây coi như là các thiết lập cơ bản cho 1 camera đã gần như là hoàn tất.Phần tới mình sẽ hướng dẫn các bạn thiết lập các trạng thái cho camera (lockFocus, unlockFocus) vì dù sao thì chúng ta cũng phải lấy nét trước khi chụp ảnh mà.

Đầu tiên các bạn khai báo thêm 1 Button nằm dưới TextureView trong xml.

<Button
        android:text="Click capture"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="12dp"/>

Trong phần code

...
private static final int STATE_PREVIEW = 0;
private static final int STATE_WAIT_LOCK = 1;
private int state = STATE_PREVIEW;
...

private CameraCaptureSession.CaptureCallback cameraSessionCaptureCallback =
        new CameraCaptureSession.CaptureCallback() {
            private void process(CaptureResult result) {
                switch (state) {
                case STATE_PREVIEW:
                    // khi đang hiển thị preview thì ko làm gì cả
                    break;
                case STATE_WAIT_LOCK:
                    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
                    // nếu trạng thái camera đã khóa được nét thì hiển thị Toast thông báo cho ng dùng
                    if (afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED) {
                    unlockFocus();
                    Toast.makeText(getApplicationContext(), "Focus Lock", Toast.LENGTH_SHORT).show();
                    }
                    break;
                }
            }

            @Override
            public void onCaptureCompleted(CameraCaptureSession session,
                CaptureRequest request, TotalCaptureResult result) {
                super.onCaptureCompleted(session, request, result);
                process(result);
            }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_camera);
    textureView = (TextureView) findViewById(R.id.textureView);
    btnCaptureImage = (Button) findViewById(R.id.button);
    btnCaptureImage.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            lockFocus();
        }
    });
}
private void lockFocus() {
    try {
        state = STATE_WAIT_LOCK;
        previewCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
        CaptureRequest.CONTROL_AF_TRIGGER_START);

        cameraCaptureSession.capture(previewCaptureRequestBuilder.build(),
        cameraSessionCaptureCallback,
        backgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
...

private void unlockFocus() {
    try {
        state = STATE_PREVIEW;
        previewCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
        CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);

        cameraCaptureSession.capture(previewCaptureRequestBuilder.build(),
        cameraSessionCaptureCallback,
        backgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
...

Tới đây khi bạn chạy code và ấn vào nút "Click capture" nếu camera khóa nét thành công thì sẽ có dòng Toast thông báo lên màn hình (code trên mới là để camera lấy nét tự động, chứ mình chưa viết phần click vào màn hình để chọn điểm lấy nét).

Tiếp là chúng ta sẽ khai báo 1 biến kiểu ImageReader. Biến này sẽ có trách nhiệm nhận dữ liệu từ CameraDevice truyền về và lưu ảnh (ở đây mình chỉ mới lưu ảnh JPGE thôi. Còn Camera2 còn cho phép bạn lưu ảnh dưới định dạng ko nén nữa nhưng mà mình sẽ trình bày ở bài tiếp theo hoặc các bạn có thể lên developer để tìm hiểu thêm)

...
private HandlerThread backgroundThread;
private Handler backgroundHandler;

private ImageReader imageReader;
// Biến callback lắng nghe khi có 1 sự kiện gọi đến "imageReader"
private ImageReader.OnImageAvailableListener onImageAvailableListener = new
        ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                backgroundHandler.post(new ImageSave(reader));
            }
        };

private static File imageFile;

private class ImageSave implements Runnable {
    private final Image image;
    private final ImageReader imageReader;

    private ImageSave(ImageReader reader) {
        imageReader = reader;
        image = imageReader.acquireNextImage();
    }

    @Override
    public void run() {
    // trong phần này là phần chịu trách nhiệm lưu ảnh xuống thẻ nhớ.
    // có rất nhiều cách để làm, nhưng ở đây mình làm theo cách cơ bản nhất
        ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[byteBuffer.remaining()];
        byteBuffer.get(bytes);

        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(imageFile);
            fileOutputStream.write(bytes);
            Toast.makeText(CameraActivity.this, "save " + imageFile.getName(), Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
...
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    btnCaptureImage.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            imageFile = createImageFile();
            lockFocus();
        }
    });
}

private void setupCamera(int awidth, int height) {
    ...
    StreamConfigurationMap map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    Size imageSize = Collections.max(
         Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
         new Comparator<Size>() {
             @Override
             public int compare(Size lhs, Size rhs) {
             return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getWidth() * rhs.getHeight());
             }
         }
    );
    imageReader = ImageReader.newInstance(
        imageSize.getWidth(),
        imageSize.getHeight(),
        ImageFormat.JPEG,
        1);
    imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler);
    ...
}
private void createCameraPreviewSession() {
    ...
    // trong phần này ta thêm imageReader.getSurface()
    // vào danh sách các đích đến của hình ảnh thu về từ cameraDevice
    // Các bạn có thể xem lại part I, mình có giải thích khá rõ ràng ở chỗ này.
    cameraDevice.createCaptureSession(Arrays.asList(previewSurface, imageReader.getSurface()),
    ...
    );
}

private void closeCamera() {
    ...
    if (imageReader != null) {
        imageReader.close();
        imageReader = null;
    }
}
...

private File createImageFile() {
    String dirPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
                File.separator + ROOT_FOLDER +
                File.separator + FOLDER_NAME;
    File dir = new File(dirPath);
    if (!dir.exists())
        dir.mkdirs();

    String fileName = FILE_NAME + String.valueOf(System.currentTimeMillis()) + ".jpg";
    return new File(dir, fileName);
}

Sau khi đã chuẩn bị hoàn tất cho việc lấy ảnh, thì bây giờ là phần quan trọng nhất của bài viết. Tiếp theo mình sẽ hướng dẫn các bạn viết sự kiện để có thể lấy được ảnh khi ấn vào nút " Capture Picture " trên màn hình.

Lưu ý, chúng ta chỉ chụp ảnh và lưu sau khi camera đã khóa dc nét.

...
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 90);
    ORIENTATIONS.append(Surface.ROTATION_90, 0);
    ORIENTATIONS.append(Surface.ROTATION_180, 270);
    ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
...
private CameraCaptureSession.CaptureCallback cameraSessionCaptureCallback =
            new CameraCaptureSession.CaptureCallback() {
                private void process(CaptureResult result) {
                    switch (state) {
                        ...
                        case STATE_WAIT_LOCK:
                            Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
                            if (afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED) {
                                unlockFocus();
                                Toast.makeText(getApplicationContext(),
                                "Focus Lock", Toast.LENGTH_SHORT).show();
                                // gọi đến hàm để thực hiện capture
                                captureStillImage();
                            }
                            break;
                    }

                }
}
...

private void captureStillImage() {
        try {
            CaptureRequest.Builder captureStill =  cameraDevice.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE);

            // set các thuộc tính của CaptureRequest.Builder
            // ở đây mình chọn đích đến là imageReader.getSurface().
            // Đích đến ở đây phải nằm trong danh sách đích đến đã được
            // khai báo trước trong cameraDevice.createCaptureSession
            captureStill.addTarget(imageReader.getSurface());

            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            captureStill.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));

            CameraCaptureSession.CaptureCallback captureCallback = new
                    CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(CameraCaptureSession session,
                        CaptureRequest request,
                        TotalCaptureResult result) {
                            super.onCaptureCompleted(session, request, result);
                            Toast.makeText(getApplicationContext(),
                            "Image Capture", Toast.LENGTH_SHORT).show();
                            unlockFocus();
                        }
                    };
            // ở đây các bạn lưu ý. khi gọi hàm này thì chúng ta sẽ gọi đến sự kiện lắng nghe của
            // imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler);
            // và thực hiện lấy hình ảnh từ CameraDevice cũng như là lưu ảnh.
            cameraCaptureSession.capture(captureStill.build(), captureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

Tới đây là chúng ta đã hoàn thành cơ bản việc viết code để có thể chụp ảnh và lưu ảnh từ camera. Các bạn có thể chạy thử để kiểm tra thành quả.

Đây là link toàn bộ code của cả 2 phần. Trong này thì mình có lược bỏ đi 1 số comment.

Nhưng các bạn lưu ý, Camera2 này chỉ hỗ trợ từ API21 trở lên, các bản API cũ hơn thì các bạn vẫn phải sử API Camera.

0