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