1 package org.opencv.android;
2 
3 import java.util.List;
4 
5 import org.opencv.BuildConfig;
6 import org.opencv.R;
7 import org.opencv.core.Mat;
8 import org.opencv.core.Size;
9 
10 import android.app.Activity;
11 import android.app.AlertDialog;
12 import android.content.Context;
13 import android.content.DialogInterface;
14 import android.content.res.TypedArray;
15 import android.graphics.Bitmap;
16 import android.graphics.Canvas;
17 import android.graphics.Rect;
18 import android.util.AttributeSet;
19 import android.util.Log;
20 import android.view.SurfaceHolder;
21 import android.view.SurfaceView;
22 
23 /**
24  * This is a basic class, implementing the interaction with Camera and OpenCV library.
25  * The main responsibility of it - is to control when camera can be enabled, process the frame,
26  * call external listener to make any adjustments to the frame and then draw the resulting
27  * frame to the screen.
28  * The clients shall implement CvCameraViewListener.
29  */
30 public abstract class CameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback {
31 
32     private static final String TAG = "CameraBridge";
33     protected static final int MAX_UNSPECIFIED = -1;
34     private static final int STOPPED = 0;
35     private static final int STARTED = 1;
36 
37     private int mState = STOPPED;
38     private Bitmap mCacheBitmap;
39     private CvCameraViewListener2 mListener;
40     private boolean mSurfaceExist;
41     private final Object mSyncObject = new Object();
42 
43     protected int mFrameWidth;
44     protected int mFrameHeight;
45     protected int mMaxHeight;
46     protected int mMaxWidth;
47     protected float mScale = 0;
48     protected int mPreviewFormat = RGBA;
49     protected int mCameraIndex = CAMERA_ID_ANY;
50     protected boolean mEnabled;
51     protected boolean mCameraPermissionGranted = false;
52     protected FpsMeter mFpsMeter = null;
53 
54     public static final int CAMERA_ID_ANY   = -1;
55     public static final int CAMERA_ID_BACK  = 99;
56     public static final int CAMERA_ID_FRONT = 98;
57     public static final int RGBA = 1;
58     public static final int GRAY = 2;
59 
CameraBridgeViewBase(Context context, int cameraId)60     public CameraBridgeViewBase(Context context, int cameraId) {
61         super(context);
62         mCameraIndex = cameraId;
63         getHolder().addCallback(this);
64         mMaxWidth = MAX_UNSPECIFIED;
65         mMaxHeight = MAX_UNSPECIFIED;
66     }
67 
CameraBridgeViewBase(Context context, AttributeSet attrs)68     public CameraBridgeViewBase(Context context, AttributeSet attrs) {
69         super(context, attrs);
70 
71         int count = attrs.getAttributeCount();
72         Log.d(TAG, "Attr count: " + Integer.valueOf(count));
73 
74         TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase);
75         if (styledAttrs.getBoolean(R.styleable.CameraBridgeViewBase_show_fps, false))
76             enableFpsMeter();
77 
78         mCameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1);
79 
80         getHolder().addCallback(this);
81         mMaxWidth = MAX_UNSPECIFIED;
82         mMaxHeight = MAX_UNSPECIFIED;
83         styledAttrs.recycle();
84     }
85 
86     /**
87      * Sets the camera index
88      * @param cameraIndex new camera index
89      */
setCameraIndex(int cameraIndex)90     public void setCameraIndex(int cameraIndex) {
91         this.mCameraIndex = cameraIndex;
92     }
93 
94     public interface CvCameraViewListener {
95         /**
96          * This method is invoked when camera preview has started. After this method is invoked
97          * the frames will start to be delivered to client via the onCameraFrame() callback.
98          * @param width -  the width of the frames that will be delivered
99          * @param height - the height of the frames that will be delivered
100          */
onCameraViewStarted(int width, int height)101         public void onCameraViewStarted(int width, int height);
102 
103         /**
104          * This method is invoked when camera preview has been stopped for some reason.
105          * No frames will be delivered via onCameraFrame() callback after this method is called.
106          */
onCameraViewStopped()107         public void onCameraViewStopped();
108 
109         /**
110          * This method is invoked when delivery of the frame needs to be done.
111          * The returned values - is a modified frame which needs to be displayed on the screen.
112          * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
113          */
onCameraFrame(Mat inputFrame)114         public Mat onCameraFrame(Mat inputFrame);
115     }
116 
117     public interface CvCameraViewListener2 {
118         /**
119          * This method is invoked when camera preview has started. After this method is invoked
120          * the frames will start to be delivered to client via the onCameraFrame() callback.
121          * @param width -  the width of the frames that will be delivered
122          * @param height - the height of the frames that will be delivered
123          */
onCameraViewStarted(int width, int height)124         public void onCameraViewStarted(int width, int height);
125 
126         /**
127          * This method is invoked when camera preview has been stopped for some reason.
128          * No frames will be delivered via onCameraFrame() callback after this method is called.
129          */
onCameraViewStopped()130         public void onCameraViewStopped();
131 
132         /**
133          * This method is invoked when delivery of the frame needs to be done.
134          * The returned values - is a modified frame which needs to be displayed on the screen.
135          * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
136          */
onCameraFrame(CvCameraViewFrame inputFrame)137         public Mat onCameraFrame(CvCameraViewFrame inputFrame);
138     };
139 
140     protected class CvCameraViewListenerAdapter implements CvCameraViewListener2  {
CvCameraViewListenerAdapter(CvCameraViewListener oldStypeListener)141         public CvCameraViewListenerAdapter(CvCameraViewListener oldStypeListener) {
142             mOldStyleListener = oldStypeListener;
143         }
144 
onCameraViewStarted(int width, int height)145         public void onCameraViewStarted(int width, int height) {
146             mOldStyleListener.onCameraViewStarted(width, height);
147         }
148 
onCameraViewStopped()149         public void onCameraViewStopped() {
150             mOldStyleListener.onCameraViewStopped();
151         }
152 
onCameraFrame(CvCameraViewFrame inputFrame)153         public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
154              Mat result = null;
155              switch (mPreviewFormat) {
156                 case RGBA:
157                     result = mOldStyleListener.onCameraFrame(inputFrame.rgba());
158                     break;
159                 case GRAY:
160                     result = mOldStyleListener.onCameraFrame(inputFrame.gray());
161                     break;
162                 default:
163                     Log.e(TAG, "Invalid frame format! Only RGBA and Gray Scale are supported!");
164             };
165 
166             return result;
167         }
168 
setFrameFormat(int format)169         public void setFrameFormat(int format) {
170             mPreviewFormat = format;
171         }
172 
173         private int mPreviewFormat = RGBA;
174         private CvCameraViewListener mOldStyleListener;
175     };
176 
177     /**
178      * This class interface is abstract representation of single frame from camera for onCameraFrame callback
179      * Attention: Do not use objects, that represents this interface out of onCameraFrame callback!
180      */
181     public interface CvCameraViewFrame {
182 
183         /**
184          * This method returns RGBA Mat with frame
185          */
rgba()186         public Mat rgba();
187 
188         /**
189          * This method returns single channel gray scale Mat with frame
190          */
gray()191         public Mat gray();
192     };
193 
surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)194     public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
195         Log.d(TAG, "call surfaceChanged event");
196         synchronized(mSyncObject) {
197             if (!mSurfaceExist) {
198                 mSurfaceExist = true;
199                 checkCurrentState();
200             } else {
201                 /** Surface changed. We need to stop camera and restart with new parameters */
202                 /* Pretend that old surface has been destroyed */
203                 mSurfaceExist = false;
204                 checkCurrentState();
205                 /* Now use new surface. Say we have it now */
206                 mSurfaceExist = true;
207                 checkCurrentState();
208             }
209         }
210     }
211 
surfaceCreated(SurfaceHolder holder)212     public void surfaceCreated(SurfaceHolder holder) {
213         /* Do nothing. Wait until surfaceChanged delivered */
214     }
215 
surfaceDestroyed(SurfaceHolder holder)216     public void surfaceDestroyed(SurfaceHolder holder) {
217         synchronized(mSyncObject) {
218             mSurfaceExist = false;
219             checkCurrentState();
220         }
221     }
222 
223 
224     /**
225      * This method is provided for clients, so they can signal camera permission has been granted.
226      * The actual onCameraViewStarted callback will be delivered only after setCameraPermissionGranted
227      * and enableView have been called and surface is available
228      */
setCameraPermissionGranted()229     public void setCameraPermissionGranted() {
230         synchronized(mSyncObject) {
231             mCameraPermissionGranted = true;
232             checkCurrentState();
233         }
234     }
235 
236 
237     /**
238      * This method is provided for clients, so they can enable the camera connection.
239      * The actual onCameraViewStarted callback will be delivered only after setCameraPermissionGranted
240      * and enableView have been called and surface is available
241      */
enableView()242     public void enableView() {
243         synchronized(mSyncObject) {
244             mEnabled = true;
245             checkCurrentState();
246         }
247     }
248 
249     /**
250      * This method is provided for clients, so they can disable camera connection and stop
251      * the delivery of frames even though the surface view itself is not destroyed and still stays on the screen
252      */
disableView()253     public void disableView() {
254         synchronized(mSyncObject) {
255             mEnabled = false;
256             checkCurrentState();
257         }
258     }
259 
260     /**
261      * This method enables label with fps value on the screen
262      */
enableFpsMeter()263     public void enableFpsMeter() {
264         if (mFpsMeter == null) {
265             mFpsMeter = new FpsMeter();
266             mFpsMeter.setResolution(mFrameWidth, mFrameHeight);
267         }
268     }
269 
disableFpsMeter()270     public void disableFpsMeter() {
271             mFpsMeter = null;
272     }
273 
274     /**
275      *
276      * @param listener
277      */
278 
setCvCameraViewListener(CvCameraViewListener2 listener)279     public void setCvCameraViewListener(CvCameraViewListener2 listener) {
280         mListener = listener;
281     }
282 
setCvCameraViewListener(CvCameraViewListener listener)283     public void setCvCameraViewListener(CvCameraViewListener listener) {
284         CvCameraViewListenerAdapter adapter = new CvCameraViewListenerAdapter(listener);
285         adapter.setFrameFormat(mPreviewFormat);
286         mListener = adapter;
287     }
288 
289     /**
290      * This method sets the maximum size that camera frame is allowed to be. When selecting
291      * size - the biggest size which less or equal the size set will be selected.
292      * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The
293      * preview frame will be selected with 176x152 size.
294      * This method is useful when need to restrict the size of preview frame for some reason (for example for video recording)
295      * @param maxWidth - the maximum width allowed for camera frame.
296      * @param maxHeight - the maximum height allowed for camera frame
297      */
setMaxFrameSize(int maxWidth, int maxHeight)298     public void setMaxFrameSize(int maxWidth, int maxHeight) {
299         mMaxWidth = maxWidth;
300         mMaxHeight = maxHeight;
301     }
302 
SetCaptureFormat(int format)303     public void SetCaptureFormat(int format)
304     {
305         mPreviewFormat = format;
306         if (mListener instanceof CvCameraViewListenerAdapter) {
307             CvCameraViewListenerAdapter adapter = (CvCameraViewListenerAdapter) mListener;
308             adapter.setFrameFormat(mPreviewFormat);
309         }
310     }
311 
312     /**
313      * Called when mSyncObject lock is held
314      */
checkCurrentState()315     private void checkCurrentState() {
316         Log.d(TAG, "call checkCurrentState");
317         int targetState;
318 
319         if (mEnabled && mCameraPermissionGranted && mSurfaceExist && getVisibility() == VISIBLE) {
320             targetState = STARTED;
321         } else {
322             targetState = STOPPED;
323         }
324 
325         if (targetState != mState) {
326             /* The state change detected. Need to exit the current state and enter target state */
327             processExitState(mState);
328             mState = targetState;
329             processEnterState(mState);
330         }
331     }
332 
processEnterState(int state)333     private void processEnterState(int state) {
334         Log.d(TAG, "call processEnterState: " + state);
335         switch(state) {
336         case STARTED:
337             onEnterStartedState();
338             if (mListener != null) {
339                 mListener.onCameraViewStarted(mFrameWidth, mFrameHeight);
340             }
341             break;
342         case STOPPED:
343             onEnterStoppedState();
344             if (mListener != null) {
345                 mListener.onCameraViewStopped();
346             }
347             break;
348         };
349     }
350 
processExitState(int state)351     private void processExitState(int state) {
352         Log.d(TAG, "call processExitState: " + state);
353         switch(state) {
354         case STARTED:
355             onExitStartedState();
356             break;
357         case STOPPED:
358             onExitStoppedState();
359             break;
360         };
361     }
362 
onEnterStoppedState()363     private void onEnterStoppedState() {
364         /* nothing to do */
365     }
366 
onExitStoppedState()367     private void onExitStoppedState() {
368         /* nothing to do */
369     }
370 
371     // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x
372     // Bitmap must be constructed before surface
onEnterStartedState()373     private void onEnterStartedState() {
374         Log.d(TAG, "call onEnterStartedState");
375         /* Connect camera */
376         if (!connectCamera(getWidth(), getHeight())) {
377             AlertDialog ad = new AlertDialog.Builder(getContext()).create();
378             ad.setCancelable(false); // This blocks the 'BACK' button
379             ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed.");
380             ad.setButton(DialogInterface.BUTTON_NEUTRAL,  "OK", new DialogInterface.OnClickListener() {
381                 public void onClick(DialogInterface dialog, int which) {
382                     dialog.dismiss();
383                     ((Activity) getContext()).finish();
384                 }
385             });
386             ad.show();
387 
388         }
389     }
390 
onExitStartedState()391     private void onExitStartedState() {
392         disconnectCamera();
393         if (mCacheBitmap != null) {
394             mCacheBitmap.recycle();
395         }
396     }
397 
398     /**
399      * This method shall be called by the subclasses when they have valid
400      * object and want it to be delivered to external client (via callback) and
401      * then displayed on the screen.
402      * @param frame - the current frame to be delivered
403      */
deliverAndDrawFrame(CvCameraViewFrame frame)404     protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
405         Mat modified;
406 
407         if (mListener != null) {
408             modified = mListener.onCameraFrame(frame);
409         } else {
410             modified = frame.rgba();
411         }
412 
413         boolean bmpValid = true;
414         if (modified != null) {
415             try {
416                 Utils.matToBitmap(modified, mCacheBitmap);
417             } catch(Exception e) {
418                 Log.e(TAG, "Mat type: " + modified);
419                 Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
420                 Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());
421                 bmpValid = false;
422             }
423         }
424 
425         if (bmpValid && mCacheBitmap != null) {
426             Canvas canvas = getHolder().lockCanvas();
427             if (canvas != null) {
428                 canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
429                 if (BuildConfig.DEBUG)
430                     Log.d(TAG, "mStretch value: " + mScale);
431 
432                 if (mScale != 0) {
433                     canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
434                          new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
435                          (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
436                          (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
437                          (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
438                 } else {
439                      canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
440                          new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
441                          (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
442                          (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
443                          (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
444                 }
445 
446                 if (mFpsMeter != null) {
447                     mFpsMeter.measure();
448                     mFpsMeter.draw(canvas, 20, 30);
449                 }
450                 getHolder().unlockCanvasAndPost(canvas);
451             }
452         }
453     }
454 
455     /**
456      * This method is invoked shall perform concrete operation to initialize the camera.
457      * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be
458      * initialized with the size of the Camera frames that will be delivered to external processor.
459      * @param width - the width of this SurfaceView
460      * @param height - the height of this SurfaceView
461      */
connectCamera(int width, int height)462     protected abstract boolean connectCamera(int width, int height);
463 
464     /**
465      * Disconnects and release the particular camera object being connected to this surface view.
466      * Called when syncObject lock is held
467      */
disconnectCamera()468     protected abstract void disconnectCamera();
469 
470     // NOTE: On Android 4.1.x the function must be called before SurfaceTexture constructor!
AllocateCache()471     protected void AllocateCache()
472     {
473         mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888);
474     }
475 
476     public interface ListItemAccessor {
getWidth(Object obj)477         public int getWidth(Object obj);
getHeight(Object obj)478         public int getHeight(Object obj);
479     };
480 
481     /**
482      * This helper method can be called by subclasses to select camera preview size.
483      * It goes over the list of the supported preview sizes and selects the maximum one which
484      * fits both values set via setMaxFrameSize() and surface frame allocated for this view
485      * @param supportedSizes
486      * @param surfaceWidth
487      * @param surfaceHeight
488      * @return optimal frame size
489      */
calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight)490     protected Size calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) {
491         int calcWidth = 0;
492         int calcHeight = 0;
493 
494         int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth;
495         int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight;
496 
497         for (Object size : supportedSizes) {
498             int width = accessor.getWidth(size);
499             int height = accessor.getHeight(size);
500             Log.d(TAG, "trying size: " + width + "x" + height);
501 
502             if (width <= maxAllowedWidth && height <= maxAllowedHeight) {
503                 if (width >= calcWidth && height >= calcHeight) {
504                     calcWidth = (int) width;
505                     calcHeight = (int) height;
506                 }
507             }
508         }
509         if ((calcWidth == 0 || calcHeight == 0) && supportedSizes.size() > 0)
510         {
511             Log.i(TAG, "fallback to the first frame size");
512             Object size = supportedSizes.get(0);
513             calcWidth = accessor.getWidth(size);
514             calcHeight = accessor.getHeight(size);
515         }
516 
517         return new Size(calcWidth, calcHeight);
518     }
519 }
520