日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

OpenCV4Android JavaCameraView实现

發布時間:2024/4/11 Android 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 OpenCV4Android JavaCameraView实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

OpenCV4Android中,主要用 org.opencv.android.JavaCameraView(后面用JavaCameraView 指代)、org.opencv.android.NativeCameraView(后面用NativeCameraView 指代)及 org.opencv.android.CameraBridgeViewBase(后面用CameraBridgeViewBase 指代)這幾個類將應用程序的邏輯與 Camera 的圖像捕捉及處理后的圖像顯示邏輯聯系起來的。這幾個類的結構大致如下圖所示的這樣:

大體上可以認為,CameraBridgeViewBase 主要是借助于 SurfaceView 的功能,來完成圖像顯示相關的功能。而 JavaCameraView 則主要用于與 Camera 硬件設備進行交互。而從線程的角度來分析,則這里也是一種生產者消費這模型。生產者線程在
onPreviewFrame() 回調方法中將 camera preview 的一幀幀數據壓縮進類型為 Mat 的緩沖區中。而消費者線程則取出這些 Mat 圖像,先是傳遞給 app 注冊的 listener 做一些特定于應用的圖像處理操作,然后把處理后的圖像顯示出來。在此處,我們也主要從生產者-消費者模型的角度來分析相關這些類的實現。

生產者、消費者

對于生產者線程,我們可以簡單的將它理解為,當 Camera preview 的一幀數據準備好了的時候,它會去調用 onPreviewFrame() 回調方法將一幀圖像壓縮進 Mat 的緩沖區。具體 onPreviewFrame() 回調方法的調用流程,則大致如下面的這個 backtrace 所示的那樣:

由此我們不難理解,生產者這一角色其實是由應用程序的主線程來擔任的。我們還可以看一下在 JavaCameraView.onPreviewFrame(byte[], Camera) 里面具體都做了些什么:

private byte mBuffer[];private Mat[] mFrameChain;private int mChainIdx = 0;protected Camera mCamera;protected JavaCameraFrame[] mCameraFrame;public void onPreviewFrame(byte[] frame, Camera arg1) {Log.d(TAG, "Preview Frame received. Frame size: " + frame.length);synchronized (this) {mFrameChain[1 - mChainIdx].put(0, 0, frame);this.notify();}if (mCamera != null)mCamera.addCallbackBuffer(mBuffer);}

非常直接,將一幀 camera preview 數據壓入 Mat 緩沖區中;通知在當前對象的等待隊列上等待的線程;然后便是調用了 mCamera.addCallbackBuffer(mBuffer) (后續會進一步解釋這一行)。

接著我們來看消費者線程的前世今生。首先是消費者線程的創建及啟動過程,在JavaCameraView.connectCamera() 方法中:

private Thread mThread;private boolean mStopThread;@Overrideprotected boolean connectCamera(int width, int height) {/* 1. We need to instantiate camera* 2. We need to start thread which will be getting frames*//* First step - initialize camera connection */Log.d(TAG, "Connecting to camera");if (!initializeCamera(width, height))return false;/* now we can start update thread */Log.d(TAG, "Starting processing thread");mStopThread = false;mThread = new Thread(new CameraWorker());mThread.start();return true;}

整個過程可以看作兩步,首先是完成對 Camera 硬件的初始化,為下一步消費者線程的創建及啟動創造條件;然后便是創建一個線程,即消費者線程,來執行整個的圖像處理及顯示循環。那這個方法又是如何一步步被調到的呢?或者換句話說,camera圖像的處理及顯示循環會在什么樣的情況下被啟動呢?搜一下這個函數的callers,只有 CameraBridgeViewBase.onEnterStartedState() 的一處:

// NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x// Bitmap must be constructed before surfaceprivate void onEnterStartedState() {/* Connect camera */if (!connectCamera(getWidth(), getHeight())) {AlertDialog ad = new AlertDialog.Builder(getContext()).create();ad.setCancelable(false); // This blocks the 'BACK' buttonad.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();}}

這個函數的功能只是調用 connectCamera(getWidth(), getHeight()) + 對于調用執行出錯時的處理,即顯示一個 Dialog 給用戶一些提示,并在用戶點擊了 Dialog 上的 Button 時退出應用。繼續向上追,onEnterStartedState() 也只有一個 caller,為CameraBridgeViewBase.processEnterState(int state):

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;};}

這個函數處理狀態的進入,進入啟動狀態,或者進入退出狀態。將要啟動 camera 圖像的處理及顯示循環的是其中的進入啟動狀態部分。由此我們也不難發現,在camera圖像的處理及顯示循環被啟動之后,OpenCV4Android 的JavaCameraView(CameraBridgeViewBase) 還會通過app注冊的
Listener(CvCameraViewListener2.onCameraViewStarted(int width, int height)) 給app一個通知。這個方法也還是一個 private 方法,似乎依然沒有追到那種讓我們可以很容易理解的地步。繼續向上追,在 CameraBridgeViewBase.checkCurrentState():

/*** 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);}}

在這個方法中,會檢查這個 View 當前的狀態,然后退出老的狀態,更新當前狀態,并進入新的狀態。當狀態由 STOPPED 轉入 STARTED 狀態時,會去啟動 camera 圖像的處理及顯示循環。可是究竟什么樣的動作會改變這個 View 的狀態,并觸發這個狀態的切換呢?可以查一下這個方法的 callers:

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();}}/*** 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();}}

總結一下,系統在 SurfaceView(CameraBridgeViewBase/JavaCameraView) 的 surface狀態發生改變時可能會去更新這個 View 的當前狀態;同時也提供給 app 兩個接口enableView() 和 disableView() 來主動的改變 View 的狀態。當一個 View 進入了STARTED 狀態時,camera 圖像的處理及顯示循環會被啟動。

消費者線程的啟動過程大體如此。那么在消費線程的處理循環中又都會做些什么樣的事情呢?來看 JavaCameraView.CameraWorker 類的實現:

private class CameraWorker implements Runnable {public void run() {do {synchronized (JavaCameraView.this) {try {JavaCameraView.this.wait();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}if (!mStopThread) {if (!mFrameChain[mChainIdx].empty())deliverAndDrawFrame(mCameraFrame[mChainIdx]);mChainIdx = 1 - mChainIdx;}} while (!mStopThread);Log.d(TAG, "Finish processing thread");}}

可以看到,一個執行周期大體上為:先是等待被另外一個線程喚醒,然后在線程沒有被終止的情況下,由 Mat 緩沖中取出一個 Mat,交由 app 進行圖像處理,并顯示處理之后圖像,最后更新 mChainIdx 的值,以使得生產者線程能夠了解那塊緩沖是已經被使用過了的。配合前面 JavaCameraView.onPreviewFrame(byte[], Camera) 一起來看,消費者線程總是會先去等待生產者線程的通知,onPreviewFrame() 壓入一幀圖像之后,會去喚醒消費者線程去處理并顯示圖像。mFrameChain 是一個有著兩個元素的數組,mChainIdx 為將要被處理的那一幀圖像的 index。粗略的來看 code,似乎是當 onPreviewFrame() 向 index 為 0 的 frame 中壓入一幀數據后,它會通知消費者線程去取出 index 為 1 的 frame 來處理,而不是它剛剛壓入了數據的 index 為 0 的 frame;而當它向 index 為 1 的 frame 中壓入數據時,則它實際上是通知消費者線程去處理它上一次壓入圖像的 index 為 0 的數據。因而,盡管這里使用了 synchronized-block,但這實際上似乎是一種無鎖的并發。

這種設計似乎還是有一些問題。比如,剛啟動的時候,mChainIdx 為 0,消費者線程會先等待,生產者線程向 index 為 1 的一幀中壓入數據,然后消費這線程被喚醒,去處理沒有實際內容的 index 為 0 的那一幀,消費者線程可能還是要花一些時間,于是生產者線程可能又多次向 index 為 1 的一幀中壓入數據。消費者線程在處理完之后,則會實際上去等待處理 index 為1的那一幀。但消費者線程具體是被生產者線程向 index 為 1 還是為 0 的那一幀中壓入圖像之后所通知的,還是有不同的可能。假設 onPreviewFrame() 壓入一幀圖像可能會花一些時間,消費者線程會在生產者線程在更新 index 為 1 的那一幀時,迅速將 mChainIdx 更新為 1,并進入等待狀態,于是乎,消費者線程將要處理的那一幀將會是剛剛被 onPreviewFrame() 更新的那一幀;另外的可能是,onPreviewFrame() 先是發出了 notify,消費者線程丟失了這一次通知,onPreviewFrame() 會需要一段時間才會有下一幀數據到來,但消費者線程會一直執行,于是消費者線程會繼續更新 mChainIdx 為 1,并進入等待狀態,而當 onPreviewFrame() 下一次被調到時,則它將會去更新 index為 0 的那一幀,于是發生的 case 將是,onPreviewFrame() 更新 index 為 0 的那一幀之后通知消費者線程去處理 index 為 1 的那一幀。

總結一下,此處是一種無鎖并發的設計,使用了生產者-消費者模型,生產者生產的速度快于消費者消費速度時的丟棄策略為,丟掉之前的數據。消費者線程總是會輪流去處理緩沖區中的那兩幀,而具體生產者是在更新完哪一幀時去通知的消費者線程則具有頗多的不確定性。比如在上一段的分析中,消費者線程要處理 index 為 1 的那一幀時,既有可能是onPreviewFrame() 剛剛第二(N) 次更新 index 為 1 的那一幀之后通知的,也可能是剛剛更新了一次或多次 index 為 0 的那一幀。這種設計總還是會丟棄掉很多幀不去處理及顯示,當然這也是這樣的應用場景下,非常 reasonable 的一種丟棄策略,即丟掉那些不能及時處理的數據,而只處理最新的那些。

消費的執行周期中,最主要的還是對于 deliverAndDrawFrame() 的調用,都是在這個方法中對圖像進行處理并繪制的嘛。接著我們就來看一下這個方法的實現:

/*** 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(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) {Canvas canvas = getHolder().lockCanvas();if (canvas != null) {canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);Log.d(TAG, "mStretch value: " + mScale);if (mScale != 0) {canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),(int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),(int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),(int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);} else {canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,(canvas.getHeight() - mCacheBitmap.getHeight()) / 2,(canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),(canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);}if (mFpsMeter != null) {mFpsMeter.measure();mFpsMeter.draw(canvas, 20, 30);}getHolder().unlockCanvasAndPost(canvas);}}}

前面嘮叨了半天“圖像的處理及繪制”,那“圖像的處理”究竟指的是什么呢?實際上指的就是那個對于 listener 的回調方法 mListener.onCameraFrame(frame) 的調用。圖像處理是應用程序的邏輯,因而這一職責也完全的是在應用程序端。圖像處理完了之后呢?將 Mat 這種 android 不能直接處理的結構轉化為 android 能直接處理的 Bitmap 結構嘍。接下來便是繪制,將 Bitmap 繪制在 surface 的正中心位置,可能會放縮,也可能不會,具體要看mScale 的值。此處繪制的部分,也是充分體現了 SurfaceView 可以在非主線程中繪制的巨大優勢。

那消費者線程又是在什么時候終止的呢?在回到這個問題之前,當然是要先回答另一個問題,就是消費者線程是如何被終止的?由 JavaCameraView.CameraWorker.run() 還是不難看出,是通過標記 mStopThread 的設置來終止的。稍作搜索,不難發現,是在JavaCameraView.disconnectCamera():

protected void releaseCamera() {synchronized (this) {if (mCamera != null) {mCamera.stopPreview();mCamera.setPreviewCallback(null);mCamera.release();}mCamera = null;if (mFrameChain != null) {mFrameChain[0].release();mFrameChain[1].release();}if (mCameraFrame != null) {mCameraFrame[0].release();mCameraFrame[1].release();}}}protected void disconnectCamera() {/* 1. We need to stop thread which updating the frames* 2. Stop camera and release it*/Log.d(TAG, "Disconnecting from camera");try {mStopThread = true;Log.d(TAG, "Notify thread");synchronized (this) {this.notify();}Log.d(TAG, "Wating for thread");if (mThread != null)mThread.join();} catch (InterruptedException e) {e.printStackTrace();} finally {mThread = null;}/* Now release camera */releaseCamera();}

再來看一下這個方法被調用到時的調用棧:

對照前面調用 connectCamera() 的過程的分析,大概也不需要做過多的說明。

Camera

在 JavaCameraView 中,與 Camera 硬件的交互方式還是有一些獨特性的。首先來看camera 的打開及初始化:

protected boolean initializeCamera(int width, int height) {Log.d(TAG, "Initialize java camera");boolean result = true;synchronized (this) {mCamera = null;if (mCameraIndex == CAMERA_ID_ANY) {Log.d(TAG, "Trying to open camera with old open()");try {mCamera = Camera.open();}catch (Exception e){Log.e(TAG, "Camera is not available (in use or does not exist): " + e.getLocalizedMessage());}if(mCamera == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {boolean connected = false;for (int camIdx = 0; camIdx < Camera.getNumberOfCameras(); ++camIdx) {Log.d(TAG, "Trying to open camera with new open(" + Integer.valueOf(camIdx) + ")");try {mCamera = Camera.open(camIdx);connected = true;} catch (RuntimeException e) {Log.e(TAG, "Camera #" + camIdx + "failed to open: " + e.getLocalizedMessage());}if (connected) break;}}} else {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {int localCameraIndex = mCameraIndex;if (mCameraIndex == CAMERA_ID_BACK) {Log.i(TAG, "Trying to open back camera");Camera.CameraInfo cameraInfo = new Camera.CameraInfo();for (int camIdx = 0; camIdx < Camera.getNumberOfCameras(); ++camIdx) {Camera.getCameraInfo( camIdx, cameraInfo );if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {localCameraIndex = camIdx;break;}}} else if (mCameraIndex == CAMERA_ID_FRONT) {Log.i(TAG, "Trying to open front camera");Camera.CameraInfo cameraInfo = new Camera.CameraInfo();for (int camIdx = 0; camIdx < Camera.getNumberOfCameras(); ++camIdx) {Camera.getCameraInfo( camIdx, cameraInfo );if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {localCameraIndex = camIdx;break;}}}if (localCameraIndex == CAMERA_ID_BACK) {Log.e(TAG, "Back camera not found!");} else if (localCameraIndex == CAMERA_ID_FRONT) {Log.e(TAG, "Front camera not found!");} else {Log.d(TAG, "Trying to open camera with new open(" + Integer.valueOf(localCameraIndex) + ")");try {mCamera = Camera.open(localCameraIndex);} catch (RuntimeException e) {Log.e(TAG, "Camera #" + localCameraIndex + "failed to open: " + e.getLocalizedMessage());}}}}if (mCamera == null)return false;/* Now set camera parameters */try {Camera.Parameters params = mCamera.getParameters();Log.d(TAG, "getSupportedPreviewSizes()");List<android.hardware.Camera.Size> sizes = params.getSupportedPreviewSizes();if (sizes != null) {/* Select the size that fits surface considering maximum size allowed */Size frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), width, height);params.setPreviewFormat(ImageFormat.NV21);Log.d(TAG, "Set preview size to " + Integer.valueOf((int)frameSize.width) + "x" + Integer.valueOf((int)frameSize.height));params.setPreviewSize((int)frameSize.width, (int)frameSize.height);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)params.setRecordingHint(true);List<String> FocusModes = params.getSupportedFocusModes();if (FocusModes != null && FocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)){params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);}mCamera.setParameters(params);params = mCamera.getParameters();mFrameWidth = params.getPreviewSize().width;mFrameHeight = params.getPreviewSize().height;if ((getLayoutParams().width == LayoutParams.MATCH_PARENT) && (getLayoutParams().height == LayoutParams.MATCH_PARENT))mScale = Math.min(((float)height)/mFrameHeight, ((float)width)/mFrameWidth);elsemScale = 0;if (mFpsMeter != null) {mFpsMeter.setResolution(mFrameWidth, mFrameHeight);}int size = mFrameWidth * mFrameHeight;size = size * ImageFormat.getBitsPerPixel(params.getPreviewFormat()) / 8;mBuffer = new byte[size];mCamera.addCallbackBuffer(mBuffer);mCamera.setPreviewCallbackWithBuffer(this);mFrameChain = new Mat[2];mFrameChain[0] = new Mat(mFrameHeight + (mFrameHeight/2), mFrameWidth, CvType.CV_8UC1);mFrameChain[1] = new Mat(mFrameHeight + (mFrameHeight/2), mFrameWidth, CvType.CV_8UC1);AllocateCache();mCameraFrame = new JavaCameraFrame[2];mCameraFrame[0] = new JavaCameraFrame(mFrameChain[0], mFrameWidth, mFrameHeight);mCameraFrame[1] = new JavaCameraFrame(mFrameChain[1], mFrameWidth, mFrameHeight);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {mSurfaceTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);mCamera.setPreviewTexture(mSurfaceTexture);} elsemCamera.setPreviewDisplay(null);/* Finally we are ready to start the preview */Log.d(TAG, "startPreview");mCamera.startPreview();}elseresult = false;} catch (Exception e) {result = false;e.printStackTrace();}}return result;}

JavaCameraView 允許 app 指定是使用前置攝像頭還是后置攝像頭。當 app 指定了要使用哪一種(前置或后置)攝像頭時,此處會遍歷所有的攝像頭,找到符合要求的, cameraIndex 最小的那個來打開;沒有指定時,則會先嘗試打開設備上的第一個 back-facing 的 camera,如果失敗,就找能用的 camera 中 cameraIndex 最小的那個來打開。然后就是設置 camera 的 parameters。比較值得關注的,一是獲取 preview frame 的方式:此處是先調用 mCamera.addCallbackBuffer(mBuffer),然后調用mCamera.setPreviewCallbackWithBuffer(this) 注冊 preview 的 callback,這也就意味著camera 會把 preview 的數據填入我們傳遞給它的一個緩沖區中;同時我們注意到,在onPreviewFrame() 方法的最后會再次調用 mCamera.addCallbackBuffer(mBuffer),如果沒有這個調用的話,camera 傳回 preview 數據的過程將因沒有數據緩沖區可用而中止。另一個值得我們關注的是 camera preview 所用 SurfaceTexture 的來源問題:在此處是手動創建了一個 SurfaceTexture 并設置給 camera 以用于 preview。但此處這個手動創建的 SurfaceTexture 是不能直接交給一個 TextureView 來顯示的。當我們像下面這樣直接將這個 SurfaceTexture 交給 TextureView 來顯示時:

mTextureView = (TextureView)findViewById(R.id.texture);mSurfaceTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);mCamera = Camera.open();try {mCamera.setPreviewTexture(mSurfaceTexture);} catch (IOException ioe) {}mTextureView.setSurfaceTexture(mSurfaceTexture);

就會出現一個像下面這樣的Exception:

在把 SurfaceTexture 交給 TextureView 顯示之前,我們還需要先將SurfaceTexture detach from GL context,像下面這樣:

mTextureView = (TextureView)findViewById(R.id.texture);mSurfaceTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);mCamera = Camera.open();try {mCamera.setPreviewTexture(mSurfaceTexture);} catch (IOException ioe) {}mSurfaceTexture.detachFromGLContext();mTextureView.setSurfaceTexture(mSurfaceTexture);mCamera.startPreview();

基本上就是這樣。

Done.

總結

以上是生活随笔為你收集整理的OpenCV4Android JavaCameraView实现的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。