1package org.opencv.android;
2
3import java.util.List;
4
5import org.opencv.R;
6import org.opencv.android.Utils;
7import org.opencv.core.Mat;
8import org.opencv.core.Size;
9import org.opencv.videoio.Videoio;
10
11import android.app.Activity;
12import android.app.AlertDialog;
13import android.content.Context;
14import android.content.DialogInterface;
15import android.content.res.TypedArray;
16import android.graphics.Bitmap;
17import android.graphics.Canvas;
18import android.graphics.Rect;
19import android.util.AttributeSet;
20import android.util.Log;
21import android.view.SurfaceHolder;
22import android.view.SurfaceView;
23
24/**
25 * This is a basic class, implementing the interaction with Camera and OpenCV library.
26 * The main responsibility of it - is to control when camera can be enabled, process the frame,
27 * call external listener to make any adjustments to the frame and then draw the resulting
28 * frame to the screen.
29 * The clients shall implement CvCameraViewListener.
30 */
31public abstract class CameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback {
32
33    private static final String TAG = "CameraBridge";
34    private static final int MAX_UNSPECIFIED = -1;
35    private static final int STOPPED = 0;
36    private static final int STARTED = 1;
37
38    private int mState = STOPPED;
39    private Bitmap mCacheBitmap;
40    private CvCameraViewListener2 mListener;
41    private boolean mSurfaceExist;
42    private Object mSyncObject = new Object();
43
44    protected int mFrameWidth;
45    protected int mFrameHeight;
46    protected int mMaxHeight;
47    protected int mMaxWidth;
48    protected float mScale = 0;
49    protected int mPreviewFormat = RGBA;
50    protected int mCameraIndex = CAMERA_ID_ANY;
51    protected boolean mEnabled;
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
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
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     */
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         */
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         */
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         */
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         */
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         */
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         */
137        public Mat onCameraFrame(CvCameraViewFrame inputFrame);
138    };
139
140    protected class CvCameraViewListenerAdapter implements CvCameraViewListener2  {
141        public CvCameraViewListenerAdapter(CvCameraViewListener oldStypeListener) {
142            mOldStyleListener = oldStypeListener;
143        }
144
145        public void onCameraViewStarted(int width, int height) {
146            mOldStyleListener.onCameraViewStarted(width, height);
147        }
148
149        public void onCameraViewStopped() {
150            mOldStyleListener.onCameraViewStopped();
151        }
152
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
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         */
186        public Mat rgba();
187
188        /**
189         * This method returns single channel gray scale Mat with frame
190         */
191        public Mat gray();
192    };
193
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
212    public void surfaceCreated(SurfaceHolder holder) {
213        /* Do nothing. Wait until surfaceChanged delivered */
214    }
215
216    public void surfaceDestroyed(SurfaceHolder holder) {
217        synchronized(mSyncObject) {
218            mSurfaceExist = false;
219            checkCurrentState();
220        }
221    }
222
223    /**
224     * This method is provided for clients, so they can enable the camera connection.
225     * The actual onCameraViewStarted callback will be delivered only after both this method is called and surface is available
226     */
227    public void enableView() {
228        synchronized(mSyncObject) {
229            mEnabled = true;
230            checkCurrentState();
231        }
232    }
233
234    /**
235     * This method is provided for clients, so they can disable camera connection and stop
236     * the delivery of frames even though the surface view itself is not destroyed and still stays on the scren
237     */
238    public void disableView() {
239        synchronized(mSyncObject) {
240            mEnabled = false;
241            checkCurrentState();
242        }
243    }
244
245    /**
246     * This method enables label with fps value on the screen
247     */
248    public void enableFpsMeter() {
249        if (mFpsMeter == null) {
250            mFpsMeter = new FpsMeter();
251            mFpsMeter.setResolution(mFrameWidth, mFrameHeight);
252        }
253    }
254
255    public void disableFpsMeter() {
256            mFpsMeter = null;
257    }
258
259    /**
260     *
261     * @param listener
262     */
263
264    public void setCvCameraViewListener(CvCameraViewListener2 listener) {
265        mListener = listener;
266    }
267
268    public void setCvCameraViewListener(CvCameraViewListener listener) {
269        CvCameraViewListenerAdapter adapter = new CvCameraViewListenerAdapter(listener);
270        adapter.setFrameFormat(mPreviewFormat);
271        mListener = adapter;
272    }
273
274    /**
275     * This method sets the maximum size that camera frame is allowed to be. When selecting
276     * size - the biggest size which less or equal the size set will be selected.
277     * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The
278     * preview frame will be selected with 176x152 size.
279     * This method is useful when need to restrict the size of preview frame for some reason (for example for video recording)
280     * @param maxWidth - the maximum width allowed for camera frame.
281     * @param maxHeight - the maximum height allowed for camera frame
282     */
283    public void setMaxFrameSize(int maxWidth, int maxHeight) {
284        mMaxWidth = maxWidth;
285        mMaxHeight = maxHeight;
286    }
287
288    public void SetCaptureFormat(int format)
289    {
290        mPreviewFormat = format;
291        if (mListener instanceof CvCameraViewListenerAdapter) {
292            CvCameraViewListenerAdapter adapter = (CvCameraViewListenerAdapter) mListener;
293            adapter.setFrameFormat(mPreviewFormat);
294        }
295    }
296
297    /**
298     * Called when mSyncObject lock is held
299     */
300    private void checkCurrentState() {
301        Log.d(TAG, "call checkCurrentState");
302        int targetState;
303
304        if (mEnabled && mSurfaceExist && getVisibility() == VISIBLE) {
305            targetState = STARTED;
306        } else {
307            targetState = STOPPED;
308        }
309
310        if (targetState != mState) {
311            /* The state change detected. Need to exit the current state and enter target state */
312            processExitState(mState);
313            mState = targetState;
314            processEnterState(mState);
315        }
316    }
317
318    private void processEnterState(int state) {
319        Log.d(TAG, "call processEnterState: " + state);
320        switch(state) {
321        case STARTED:
322            onEnterStartedState();
323            if (mListener != null) {
324                mListener.onCameraViewStarted(mFrameWidth, mFrameHeight);
325            }
326            break;
327        case STOPPED:
328            onEnterStoppedState();
329            if (mListener != null) {
330                mListener.onCameraViewStopped();
331            }
332            break;
333        };
334    }
335
336    private void processExitState(int state) {
337        Log.d(TAG, "call processExitState: " + state);
338        switch(state) {
339        case STARTED:
340            onExitStartedState();
341            break;
342        case STOPPED:
343            onExitStoppedState();
344            break;
345        };
346    }
347
348    private void onEnterStoppedState() {
349        /* nothing to do */
350    }
351
352    private void onExitStoppedState() {
353        /* nothing to do */
354    }
355
356    // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x
357    // Bitmap must be constructed before surface
358    private void onEnterStartedState() {
359        Log.d(TAG, "call onEnterStartedState");
360        /* Connect camera */
361        if (!connectCamera(getWidth(), getHeight())) {
362            AlertDialog ad = new AlertDialog.Builder(getContext()).create();
363            ad.setCancelable(false); // This blocks the 'BACK' button
364            ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed.");
365            ad.setButton(DialogInterface.BUTTON_NEUTRAL,  "OK", new DialogInterface.OnClickListener() {
366                public void onClick(DialogInterface dialog, int which) {
367                    dialog.dismiss();
368                    ((Activity) getContext()).finish();
369                }
370            });
371            ad.show();
372
373        }
374    }
375
376    private void onExitStartedState() {
377        disconnectCamera();
378        if (mCacheBitmap != null) {
379            mCacheBitmap.recycle();
380        }
381    }
382
383    /**
384     * This method shall be called by the subclasses when they have valid
385     * object and want it to be delivered to external client (via callback) and
386     * then displayed on the screen.
387     * @param frame - the current frame to be delivered
388     */
389    protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
390        Mat modified;
391
392        if (mListener != null) {
393            modified = mListener.onCameraFrame(frame);
394        } else {
395            modified = frame.rgba();
396        }
397
398        boolean bmpValid = true;
399        if (modified != null) {
400            try {
401                Utils.matToBitmap(modified, mCacheBitmap);
402            } catch(Exception e) {
403                Log.e(TAG, "Mat type: " + modified);
404                Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
405                Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());
406                bmpValid = false;
407            }
408        }
409
410        if (bmpValid && mCacheBitmap != null) {
411            Canvas canvas = getHolder().lockCanvas();
412            if (canvas != null) {
413                canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
414                Log.d(TAG, "mStretch value: " + mScale);
415
416                if (mScale != 0) {
417                    canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
418                         new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
419                         (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
420                         (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
421                         (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
422                } else {
423                     canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
424                         new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
425                         (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
426                         (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
427                         (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
428                }
429
430                if (mFpsMeter != null) {
431                    mFpsMeter.measure();
432                    mFpsMeter.draw(canvas, 20, 30);
433                }
434                getHolder().unlockCanvasAndPost(canvas);
435            }
436        }
437    }
438
439    /**
440     * This method is invoked shall perform concrete operation to initialize the camera.
441     * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be
442     * initialized with the size of the Camera frames that will be delivered to external processor.
443     * @param width - the width of this SurfaceView
444     * @param height - the height of this SurfaceView
445     */
446    protected abstract boolean connectCamera(int width, int height);
447
448    /**
449     * Disconnects and release the particular camera object being connected to this surface view.
450     * Called when syncObject lock is held
451     */
452    protected abstract void disconnectCamera();
453
454    // NOTE: On Android 4.1.x the function must be called before SurfaceTextre constructor!
455    protected void AllocateCache()
456    {
457        mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888);
458    }
459
460    public interface ListItemAccessor {
461        public int getWidth(Object obj);
462        public int getHeight(Object obj);
463    };
464
465    /**
466     * This helper method can be called by subclasses to select camera preview size.
467     * It goes over the list of the supported preview sizes and selects the maximum one which
468     * fits both values set via setMaxFrameSize() and surface frame allocated for this view
469     * @param supportedSizes
470     * @param surfaceWidth
471     * @param surfaceHeight
472     * @return optimal frame size
473     */
474    protected Size calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) {
475        int calcWidth = 0;
476        int calcHeight = 0;
477
478        int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth;
479        int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight;
480
481        for (Object size : supportedSizes) {
482            int width = accessor.getWidth(size);
483            int height = accessor.getHeight(size);
484
485            if (width <= maxAllowedWidth && height <= maxAllowedHeight) {
486                if (width >= calcWidth && height >= calcHeight) {
487                    calcWidth = (int) width;
488                    calcHeight = (int) height;
489                }
490            }
491        }
492
493        return new Size(calcWidth, calcHeight);
494    }
495}
496