package org.opencv.android; import java.nio.ByteBuffer; import java.util.Arrays; import android.annotation.TargetApi; import android.content.Context; import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; import android.os.Handler; import android.os.HandlerThread; import android.util.AttributeSet; import android.util.Log; import android.view.Surface; import android.view.ViewGroup.LayoutParams; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.imgproc.Imgproc; /** * This class is an implementation of the Bridge View between OpenCV and Java Camera. * This class relays on the functionality available in base class and only implements * required functions: * connectCamera - opens Java camera and sets the PreviewCallback to be delivered. * disconnectCamera - closes the camera and stops preview. * When frame is delivered via callback from Camera - it processed via OpenCV to be * converted to RGBA32 and then passed to the external callback for modifications if required. */ @TargetApi(21) public class JavaCamera2View extends CameraBridgeViewBase { private static final String LOGTAG = "JavaCamera2View"; private ImageReader mImageReader; private int mPreviewFormat = ImageFormat.YUV_420_888; private CameraDevice mCameraDevice; private CameraCaptureSession mCaptureSession; private CaptureRequest.Builder mPreviewRequestBuilder; private String mCameraID; private android.util.Size mPreviewSize = new android.util.Size(-1, -1); private HandlerThread mBackgroundThread; private Handler mBackgroundHandler; public JavaCamera2View(Context context, int cameraId) { super(context, cameraId); } public JavaCamera2View(Context context, AttributeSet attrs) { super(context, attrs); } private void startBackgroundThread() { Log.i(LOGTAG, "startBackgroundThread"); stopBackgroundThread(); mBackgroundThread = new HandlerThread("OpenCVCameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } private void stopBackgroundThread() { Log.i(LOGTAG, "stopBackgroundThread"); if (mBackgroundThread == null) return; mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { Log.e(LOGTAG, "stopBackgroundThread", e); } } protected boolean initializeCamera() { Log.i(LOGTAG, "initializeCamera"); CameraManager manager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); try { String camList[] = manager.getCameraIdList(); if (camList.length == 0) { Log.e(LOGTAG, "Error: camera isn't detected."); return false; } if (mCameraIndex == CameraBridgeViewBase.CAMERA_ID_ANY) { mCameraID = camList[0]; } else { for (String cameraID : camList) { CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraID); if ((mCameraIndex == CameraBridgeViewBase.CAMERA_ID_BACK && characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) || (mCameraIndex == CameraBridgeViewBase.CAMERA_ID_FRONT && characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) ) { mCameraID = cameraID; break; } } } if (mCameraID != null) { Log.i(LOGTAG, "Opening camera: " + mCameraID); manager.openCamera(mCameraID, mStateCallback, mBackgroundHandler); } else { // make JavaCamera2View behaves in the same way as JavaCameraView Log.i(LOGTAG, "Trying to open camera with the value (" + mCameraIndex + ")"); if (mCameraIndex < camList.length) { mCameraID = camList[mCameraIndex]; manager.openCamera(mCameraID, mStateCallback, mBackgroundHandler); } else { // CAMERA_DISCONNECTED is used when the camera id is no longer valid throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED); } } return true; } catch (CameraAccessException e) { Log.e(LOGTAG, "OpenCamera - Camera Access Exception", e); } catch (IllegalArgumentException e) { Log.e(LOGTAG, "OpenCamera - Illegal Argument Exception", e); } catch (SecurityException e) { Log.e(LOGTAG, "OpenCamera - Security Exception", e); } return false; } private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice cameraDevice) { mCameraDevice = cameraDevice; createCameraPreviewSession(); } @Override public void onDisconnected(CameraDevice cameraDevice) { cameraDevice.close(); mCameraDevice = null; } @Override public void onError(CameraDevice cameraDevice, int error) { cameraDevice.close(); mCameraDevice = null; } }; private void createCameraPreviewSession() { final int w = mPreviewSize.getWidth(), h = mPreviewSize.getHeight(); Log.i(LOGTAG, "createCameraPreviewSession(" + w + "x" + h + ")"); if (w < 0 || h < 0) return; try { if (null == mCameraDevice) { Log.e(LOGTAG, "createCameraPreviewSession: camera isn't opened"); return; } if (null != mCaptureSession) { Log.e(LOGTAG, "createCameraPreviewSession: mCaptureSession is already started"); return; } mImageReader = ImageReader.newInstance(w, h, mPreviewFormat, 2); mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if (image == null) return; // sanity checks - 3 planes Image.Plane[] planes = image.getPlanes(); assert (planes.length == 3); assert (image.getFormat() == mPreviewFormat); // see also https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 // Y plane (0) non-interleaved => stride == 1; U/V plane interleaved => stride == 2 assert (planes[0].getPixelStride() == 1); assert (planes[1].getPixelStride() == 2); assert (planes[2].getPixelStride() == 2); ByteBuffer y_plane = planes[0].getBuffer(); ByteBuffer uv_plane = planes[1].getBuffer(); Mat y_mat = new Mat(h, w, CvType.CV_8UC1, y_plane); Mat uv_mat = new Mat(h / 2, w / 2, CvType.CV_8UC2, uv_plane); JavaCamera2Frame tempFrame = new JavaCamera2Frame(y_mat, uv_mat, w, h); deliverAndDrawFrame(tempFrame); tempFrame.release(); image.close(); } }, mBackgroundHandler); Surface surface = mImageReader.getSurface(); mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession cameraCaptureSession) { Log.i(LOGTAG, "createCaptureSession::onConfigured"); if (null == mCameraDevice) { return; // camera is already closed } mCaptureSession = cameraCaptureSession; try { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); Log.i(LOGTAG, "CameraPreviewSession has been started"); } catch (Exception e) { Log.e(LOGTAG, "createCaptureSession failed", e); } } @Override public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { Log.e(LOGTAG, "createCameraPreviewSession failed"); } }, null ); } catch (CameraAccessException e) { Log.e(LOGTAG, "createCameraPreviewSession", e); } } @Override protected void disconnectCamera() { Log.i(LOGTAG, "closeCamera"); try { CameraDevice c = mCameraDevice; mCameraDevice = null; if (null != mCaptureSession) { mCaptureSession.close(); mCaptureSession = null; } if (null != c) { c.close(); } if (null != mImageReader) { mImageReader.close(); mImageReader = null; } } finally { stopBackgroundThread(); } } boolean calcPreviewSize(final int width, final int height) { Log.i(LOGTAG, "calcPreviewSize: " + width + "x" + height); if (mCameraID == null) { Log.e(LOGTAG, "Camera isn't initialized!"); return false; } CameraManager manager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); try { CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraID); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); int bestWidth = 0, bestHeight = 0; float aspect = (float) width / height; android.util.Size[] sizes = map.getOutputSizes(ImageReader.class); bestWidth = sizes[0].getWidth(); bestHeight = sizes[0].getHeight(); for (android.util.Size sz : sizes) { int w = sz.getWidth(), h = sz.getHeight(); Log.d(LOGTAG, "trying size: " + w + "x" + h); if (width >= w && height >= h && bestWidth <= w && bestHeight <= h && Math.abs(aspect - (float) w / h) < 0.2) { bestWidth = w; bestHeight = h; } } Log.i(LOGTAG, "best size: " + bestWidth + "x" + bestHeight); assert(!(bestWidth == 0 || bestHeight == 0)); if (mPreviewSize.getWidth() == bestWidth && mPreviewSize.getHeight() == bestHeight) return false; else { mPreviewSize = new android.util.Size(bestWidth, bestHeight); return true; } } catch (CameraAccessException e) { Log.e(LOGTAG, "calcPreviewSize - Camera Access Exception", e); } catch (IllegalArgumentException e) { Log.e(LOGTAG, "calcPreviewSize - Illegal Argument Exception", e); } catch (SecurityException e) { Log.e(LOGTAG, "calcPreviewSize - Security Exception", e); } return false; } @Override protected boolean connectCamera(int width, int height) { Log.i(LOGTAG, "setCameraPreviewSize(" + width + "x" + height + ")"); startBackgroundThread(); initializeCamera(); try { boolean needReconfig = calcPreviewSize(width, height); mFrameWidth = mPreviewSize.getWidth(); mFrameHeight = mPreviewSize.getHeight(); if ((getLayoutParams().width == LayoutParams.MATCH_PARENT) && (getLayoutParams().height == LayoutParams.MATCH_PARENT)) mScale = Math.min(((float)height)/mFrameHeight, ((float)width)/mFrameWidth); else mScale = 0; AllocateCache(); if (needReconfig) { if (null != mCaptureSession) { Log.d(LOGTAG, "closing existing previewSession"); mCaptureSession.close(); mCaptureSession = null; } createCameraPreviewSession(); } } catch (RuntimeException e) { throw new RuntimeException("Interrupted while setCameraPreviewSize.", e); } return true; } private class JavaCamera2Frame implements CvCameraViewFrame { @Override public Mat gray() { return mYuvFrameData.submat(0, mHeight, 0, mWidth); } @Override public Mat rgba() { if (mPreviewFormat == ImageFormat.NV21) Imgproc.cvtColor(mYuvFrameData, mRgba, Imgproc.COLOR_YUV2RGBA_NV21, 4); else if (mPreviewFormat == ImageFormat.YV12) Imgproc.cvtColor(mYuvFrameData, mRgba, Imgproc.COLOR_YUV2RGB_I420, 4); // COLOR_YUV2RGBA_YV12 produces inverted colors else if (mPreviewFormat == ImageFormat.YUV_420_888) { assert (mUVFrameData != null); Imgproc.cvtColorTwoPlane(mYuvFrameData, mUVFrameData, mRgba, Imgproc.COLOR_YUV2RGBA_NV21); } else throw new IllegalArgumentException("Preview Format can be NV21 or YV12"); return mRgba; } public JavaCamera2Frame(Mat Yuv420sp, int width, int height) { super(); mWidth = width; mHeight = height; mYuvFrameData = Yuv420sp; mUVFrameData = null; mRgba = new Mat(); } public JavaCamera2Frame(Mat Y, Mat UV, int width, int height) { super(); mWidth = width; mHeight = height; mYuvFrameData = Y; mUVFrameData = UV; mRgba = new Mat(); } public void release() { mRgba.release(); } private Mat mYuvFrameData; private Mat mUVFrameData; private Mat mRgba; private int mWidth; private int mHeight; }; }