package org.opencv.android; import java.util.List; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.highgui.Highgui; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import static android.view.View.VISIBLE; import pt.chambino.p.pulse.R; /** * This is a basic class, implementing the interaction with Camera and OpenCV library. * The main responsibility of it - is to control when camera can be enabled, process the frame, * call external listener to make any adjustments to the frame and then draw the resulting * frame to the screen. * The clients shall implement CvCameraViewListener. */ public abstract class MyCameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "MyCameraBridge"; private static final int MAX_UNSPECIFIED = -1; private static final int STOPPED = 0; private static final int STARTED = 1; private int mState = STOPPED; private Bitmap mCacheBitmap; private MyCameraBridgeViewBase.CvCameraViewListener2 mListener; private boolean mSurfaceExist; private Object mSyncObject = new Object(); protected int mFrameWidth; protected int mFrameHeight; protected int mMaxHeight; protected int mMaxWidth; protected float mScale = 1; protected int mPreviewFormat = Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA; protected int mCameraIndex = -1; protected boolean mEnabled; protected FpsMeter mFpsMeter = null; public MyCameraBridgeViewBase(Context context, int cameraId) { super(context); mCameraIndex = cameraId; getHolder().addCallback(this); mMaxWidth = MAX_UNSPECIFIED; mMaxHeight = MAX_UNSPECIFIED; } public MyCameraBridgeViewBase(Context context, AttributeSet attrs) { super(context, attrs); int count = attrs.getAttributeCount(); Log.d(TAG, "Attr count: " + Integer.valueOf(count)); TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase); if (styledAttrs.getBoolean(R.styleable.CameraBridgeViewBase_show_fps, false)) enableFpsMeter(); mCameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1); getHolder().addCallback(this); mMaxWidth = MAX_UNSPECIFIED; mMaxHeight = MAX_UNSPECIFIED; } public interface CvCameraViewListener { /** * This method is invoked when camera preview has started. After this method is invoked * the frames will start to be delivered to client via the onCameraFrame() callback. * @param width - the width of the frames that will be delivered * @param height - the height of the frames that will be delivered */ public void onCameraViewStarted(int width, int height); /** * This method is invoked when camera preview has been stopped for some reason. * No frames will be delivered via onCameraFrame() callback after this method is called. */ public void onCameraViewStopped(); /** * This method is invoked when delivery of the frame needs to be done. * The returned values - is a modified frame which needs to be displayed on the screen. * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc) */ public Mat onCameraFrame(Mat inputFrame); public void onCameraFrame(Canvas canvas); } public interface CvCameraViewListener2 { /** * This method is invoked when camera preview has started. After this method is invoked * the frames will start to be delivered to client via the onCameraFrame() callback. * @param width - the width of the frames that will be delivered * @param height - the height of the frames that will be delivered */ public void onCameraViewStarted(int width, int height); /** * This method is invoked when camera preview has been stopped for some reason. * No frames will be delivered via onCameraFrame() callback after this method is called. */ public void onCameraViewStopped(); /** * This method is invoked when delivery of the frame needs to be done. * The returned values - is a modified frame which needs to be displayed on the screen. * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc) */ public Mat onCameraFrame(MyCameraBridgeViewBase.CvCameraViewFrame inputFrame); public void onCameraFrame(Canvas canvas); }; protected class CvCameraViewListenerAdapter implements MyCameraBridgeViewBase.CvCameraViewListener2 { public CvCameraViewListenerAdapter(MyCameraBridgeViewBase.CvCameraViewListener oldStypeListener) { mOldStyleListener = oldStypeListener; } public void onCameraViewStarted(int width, int height) { mOldStyleListener.onCameraViewStarted(width, height); } public void onCameraViewStopped() { mOldStyleListener.onCameraViewStopped(); } public Mat onCameraFrame(MyCameraBridgeViewBase.CvCameraViewFrame inputFrame) { Mat result = null; switch (mPreviewFormat) { case Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGB: result = mOldStyleListener.onCameraFrame(inputFrame.rgb()); break; case Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA: result = mOldStyleListener.onCameraFrame(inputFrame.rgba()); break; case Highgui.CV_CAP_ANDROID_GREY_FRAME: result = mOldStyleListener.onCameraFrame(inputFrame.gray()); break; default: Log.e(TAG, "Invalid frame format! Only RGBA and Gray Scale are supported!"); }; return result; } @Override public void onCameraFrame(Canvas canvas) { mOldStyleListener.onCameraFrame(canvas); } public void setFrameFormat(int format) { mPreviewFormat = format; } private CvCameraViewListenerAdapter() {} private int mPreviewFormat = Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA; private MyCameraBridgeViewBase.CvCameraViewListener mOldStyleListener; }; /** * This class interface is abstract representation of single frame from camera for onCameraFrame callback * Attention: Do not use objects, that represents this interface out of onCameraFrame callback! */ public interface CvCameraViewFrame { /** * This method returns RGB Mat with frame */ public Mat rgb(); /** * This method returns RGBA Mat with frame */ public Mat rgba(); /** * This method returns single channel gray scale Mat with frame */ public Mat gray(); }; public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { Log.d(TAG, "call surfaceChanged event"); synchronized(mSyncObject) { if (!mSurfaceExist) { mSurfaceExist = true; checkCurrentState(); } else { /** Surface changed. We need to stop camera and restart with new parameters */ /* Pretend that old surface has been destroyed */ mSurfaceExist = false; checkCurrentState(); /* Now use new surface. Say we have it now */ mSurfaceExist = true; checkCurrentState(); } } } public void surfaceCreated(SurfaceHolder holder) { /* Do nothing. Wait until surfaceChanged delivered */ } public void surfaceDestroyed(SurfaceHolder holder) { synchronized(mSyncObject) { mSurfaceExist = false; checkCurrentState(); } } public void switchCamera() { setCameraId((mCameraIndex + 1) % 2); } public int getCameraId() { return mCameraIndex; } public void setCameraId(int cameraId) { if (mCameraIndex != cameraId) { mCameraIndex = cameraId; if (mEnabled) { disableView(); enableView(); } } } /** * This method is provided for clients, so they can enable the camera connection. * The actual onCameraViewStarted callback will be delivered only after both this method is called and surface is available */ public void enableView() { synchronized(mSyncObject) { mEnabled = true; checkCurrentState(); } } /** * This method is provided for clients, so they can disable camera connection and stop * the delivery of frames even though the surface view itself is not destroyed and still stays on the scren */ public void disableView() { synchronized(mSyncObject) { mEnabled = false; checkCurrentState(); } } /** * This method enables label with fps value on the screen */ public void enableFpsMeter() { if (mFpsMeter == null) { mFpsMeter = new FpsMeter(); mFpsMeter.setResolution(mFrameWidth, mFrameHeight); } } public void disableFpsMeter() { mFpsMeter = null; } public boolean isFpsMeterEnabled() { return mFpsMeter != null; } public void setFpsMeter(boolean enable) { if (enable) enableFpsMeter(); else disableFpsMeter(); } /** * * @param listener */ public void setCvCameraViewListener(MyCameraBridgeViewBase.CvCameraViewListener2 listener) { mListener = listener; } public void setCvCameraViewListener(MyCameraBridgeViewBase.CvCameraViewListener listener) { MyCameraBridgeViewBase.CvCameraViewListenerAdapter adapter = new MyCameraBridgeViewBase.CvCameraViewListenerAdapter(listener); adapter.setFrameFormat(mPreviewFormat); mListener = adapter; } /** * This method sets the maximum size that camera frame is allowed to be. When selecting * size - the biggest size which less or equal the size set will be selected. * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The * preview frame will be selected with 176x152 size. * This method is useful when need to restrict the size of preview frame for some reason (for example for video recording) * @param maxWidth - the maximum width allowed for camera frame. * @param maxHeight - the maximum height allowed for camera frame */ public void setMaxFrameSize(int maxWidth, int maxHeight) { mMaxWidth = maxWidth; mMaxHeight = maxHeight; } public void SetCaptureFormat(int format) { mPreviewFormat = format; if (mListener instanceof MyCameraBridgeViewBase.CvCameraViewListenerAdapter) { MyCameraBridgeViewBase.CvCameraViewListenerAdapter adapter = (MyCameraBridgeViewBase.CvCameraViewListenerAdapter) mListener; adapter.setFrameFormat(mPreviewFormat); } } /** * Called when mSyncObject lock is held */ private void checkCurrentState() { int targetState; if (mEnabled && mSurfaceExist && getVisibility() == VISIBLE) { targetState = STARTED; } else { targetState = STOPPED; } if (targetState != mState) { /* The state change detected. Need to exit the current state and enter target state */ processExitState(mState); mState = targetState; processEnterState(mState); } } private void processEnterState(int state) { switch(state) { case STARTED: onEnterStartedState(); if (mListener != null) { mListener.onCameraViewStarted(mFrameWidth, mFrameHeight); } break; case STOPPED: onEnterStoppedState(); if (mListener != null) { mListener.onCameraViewStopped(); } break; }; } private void processExitState(int state) { switch(state) { case STARTED: onExitStartedState(); break; case STOPPED: onExitStoppedState(); break; }; } private void onEnterStoppedState() { /* nothing to do */ } private void onExitStoppedState() { /* nothing to do */ } // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x // Bitmap must be constructed before surface private void onEnterStartedState() { /* Connect camera */ if (!connectCamera(getWidth(), getHeight())) { AlertDialog ad = new AlertDialog.Builder(getContext()).create(); ad.setCancelable(false); // This blocks the 'BACK' button ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed."); ad.setButton(DialogInterface.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); ((Activity) getContext()).finish(); } }); ad.show(); } } private void onExitStartedState() { disconnectCamera(); if (mCacheBitmap != null) { mCacheBitmap.recycle(); } } /** * This method shall be called by the subclasses when they have valid * object and want it to be delivered to external client (via callback) and * then displayed on the screen. * @param frame - the current frame to be delivered */ protected void deliverAndDrawFrame(MyCameraBridgeViewBase.CvCameraViewFrame frame) { Mat modified; if (mListener != null) { modified = mListener.onCameraFrame(frame); } else { modified = frame.rgba(); } boolean bmpValid = true; if (modified != null) { try { Utils.matToBitmap(modified, mCacheBitmap); } catch(Exception e) { Log.e(TAG, "Mat type: " + modified); Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight()); Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage()); bmpValid = false; } } if (bmpValid && mCacheBitmap != null) { if (mListener != null) { mListener.onCameraFrame(new Canvas(mCacheBitmap)); } Canvas canvas = getHolder().lockCanvas(); if (canvas != null) { canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR); Log.d(TAG, "mStretch value: " + mScale); Matrix matrix = new Matrix(); matrix.preTranslate((canvas.getWidth() - mCacheBitmap.getWidth()) / 2, (canvas.getHeight() - mCacheBitmap.getHeight()) / 2); matrix.postScale(mScale, mScale, canvas.getWidth() / 2, canvas.getHeight() / 2); canvas.drawBitmap(mCacheBitmap, matrix, null); if (mFpsMeter != null) { mFpsMeter.measure(); mFpsMeter.draw(canvas, 20, 30); } getHolder().unlockCanvasAndPost(canvas); } } } /** * This method is invoked shall perform concrete operation to initialize the camera. * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be * initialized with the size of the Camera frames that will be delivered to external processor. * @param width - the width of this SurfaceView * @param height - the height of this SurfaceView */ protected abstract boolean connectCamera(int width, int height); /** * Disconnects and release the particular camera object being connected to this surface view. * Called when syncObject lock is held */ protected abstract void disconnectCamera(); // NOTE: On Android 4.1.x the function must be called before SurfaceTextre constructor! protected void AllocateCache() { mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888); } public interface ListItemAccessor { public int getWidth(Object obj); public int getHeight(Object obj); }; /** * This helper method can be called by subclasses to select camera preview size. * It goes over the list of the supported preview sizes and selects the maximum one which * fits both values set via setMaxFrameSize() and surface frame allocated for this view * @param supportedSizes * @param surfaceWidth * @param surfaceHeight * @return optimal frame size */ protected Size calculateCameraFrameSize(List<?> supportedSizes, MyCameraBridgeViewBase.ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) { int calcWidth = 0; int calcHeight = 0; int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth; int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight; for (Object size : supportedSizes) { int width = accessor.getWidth(size); int height = accessor.getHeight(size); if (width <= maxAllowedWidth && height <= maxAllowedHeight) { if (width >= calcWidth && height >= calcHeight) { calcWidth = (int) width; calcHeight = (int) height; } } } return new Size(calcWidth, calcHeight); } }