1 package org.opencv.android; 2 3 import java.util.List; 4 5 import org.opencv.R; 6 import org.opencv.android.Utils; 7 import org.opencv.core.Mat; 8 import org.opencv.core.Size; 9 import org.opencv.videoio.Videoio; 10 11 import android.app.Activity; 12 import android.app.AlertDialog; 13 import android.content.Context; 14 import android.content.DialogInterface; 15 import android.content.res.TypedArray; 16 import android.graphics.Bitmap; 17 import android.graphics.Canvas; 18 import android.graphics.Rect; 19 import android.util.AttributeSet; 20 import android.util.Log; 21 import android.view.SurfaceHolder; 22 import 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 */ 31 public 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 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 * 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 */ enableView()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 */ disableView()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 */ enableFpsMeter()248 public void enableFpsMeter() { 249 if (mFpsMeter == null) { 250 mFpsMeter = new FpsMeter(); 251 mFpsMeter.setResolution(mFrameWidth, mFrameHeight); 252 } 253 } 254 disableFpsMeter()255 public void disableFpsMeter() { 256 mFpsMeter = null; 257 } 258 259 /** 260 * 261 * @param listener 262 */ 263 setCvCameraViewListener(CvCameraViewListener2 listener)264 public void setCvCameraViewListener(CvCameraViewListener2 listener) { 265 mListener = listener; 266 } 267 setCvCameraViewListener(CvCameraViewListener listener)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 */ setMaxFrameSize(int maxWidth, int maxHeight)283 public void setMaxFrameSize(int maxWidth, int maxHeight) { 284 mMaxWidth = maxWidth; 285 mMaxHeight = maxHeight; 286 } 287 SetCaptureFormat(int format)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 */ checkCurrentState()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 processEnterState(int state)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 processExitState(int state)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 onEnterStoppedState()348 private void onEnterStoppedState() { 349 /* nothing to do */ 350 } 351 onExitStoppedState()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 onEnterStartedState()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 onExitStartedState()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 */ deliverAndDrawFrame(CvCameraViewFrame frame)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 */ connectCamera(int width, int height)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 */ disconnectCamera()452 protected abstract void disconnectCamera(); 453 454 // NOTE: On Android 4.1.x the function must be called before SurfaceTextre constructor! AllocateCache()455 protected void AllocateCache() 456 { 457 mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888); 458 } 459 460 public interface ListItemAccessor { getWidth(Object obj)461 public int getWidth(Object obj); getHeight(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 */ calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight)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