Merge pull request #10081 from floe:java-camera2-view
* add (untested) JavaCamera2View class * initial fixes * minor cleanup * exclude JavaCamera2View from build for older SDK version * fix method name typo * add asserts + sanity checks * fix preview format checks * fix the memory leak * export cvtTwoPlaneYUVtoBGR for Java usage * add handling for two-plane YUV frames (C wrapper still missing) * add two-plane cvtColor helper function * fix warnings * actually use the new cvtColorTwoPlane helper func * fix wrong output matrix size * fix wrong conversion type * simplify method signature, add error condition * minor fixes to Mat types * remove leftover semaphore from camera api 1 * android: JavaCamera2View minor refactoring - re-apply Java code style - imports cleanup - dump exceptions information * android: JavaCamera2View: pause/resume fixes * android: JavaCamera2View: fix mScalepull/10893/head
parent
eb94dfb442
commit
6a8f57e5d7
3 changed files with 412 additions and 0 deletions
@ -0,0 +1,374 @@ |
||||
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); |
||||
} |
||||
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; |
||||
}; |
||||
} |
Loading…
Reference in new issue