OpenCV實現簡易相機

Android筆記③--OpenCV實現簡易相機

原文:https://zhuanlan.zhihu.com/p/28422134

前言:項目需要,需要在開發板上實現視頻監控以及拍照的功能。由於android.hardware.camera已被Google棄用,而camera2又不能在開發板上愉快地玩耍(4.0.3系統),因此只能通過OpenCV實現。

在使用OpenCV實現的過程中,使用的是最簡單的方法,即通過OpenCV Manager進行動態庫的鏈接,且實現最簡單的幀預覽以及圖片保存(即拍照)的功能,以下爲完整步驟,開發平臺爲Android Studio 2.2.3,使用的OpenCV版本爲opencv4android 3.2.0。

  • 下載OpenCV並導入工程,下載得到的opencv-3.2.0-android-sdk.zip解壓,新建項目,左上角“File-New-Import Module...”,在彈出的窗口中打開解壓得到的文件中的“sdk\java”,然後next,finish,此時會提示build錯誤,打開“OpenCVLibrary310\build.gradle”修改與項目的build.gradle一致,並sync again即可導入成功。
  • 在手機上安裝下載的zip中apk的對應版本,此時還需要把opencv要用到的動態庫放到項目中,否則運行會出錯,具體如下:將下載解壓得到的文件中的“sdk\native\libs\”複製一份到項目的“app\src\main”中,並重命名爲jniLibs即可,以上步驟完成後項目目錄如下:

  • 此時OpenCV的開發環境已經搭建好,接下來開始我們主要內容的編寫。首先打開佈局文件activity_main(默認),佈局如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:opencv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <org.opencv.android.JavaCameraView
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        opencv:show_fps="true"
        opencv:camera_id="any" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="bottom|center_horizontal">
        <Button
            android:id="@+id/deal_btn"
            android:layout_width="100dp"
            android:layout_height="40dp"
            android:layout_marginBottom="20dp"
            android:text="拍照"/>
    </RelativeLayout>
</FrameLayout>
  • 這裏的JavaCameraView就是OpenCV裏的預覽部件,然後我們再添加了一個拍照的按鍵。
  • 然後我們打開AndroidManifest.xml,添加我們需要的權限,攝像頭和內部儲存,這個也十分重要:
   <uses-permission android:name="android.permission.CAMERA"/>  
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  • 完成後我們開始正式編寫MainActivity。
public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {

    private String TAG = "OpenCV_Test";
    //OpenCV的相機接口
    private CameraBridgeViewBase mCVCamera;
    //緩存相機每幀輸入的數據
    private Mat mRgba;
    private Button button;

    /**通過OpenCV管理Android服務,初始化OpenCV**/
    BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                    Log.i(TAG, "OpenCV loaded successfully");
                    mCVCamera.enableView();
                    break;
                default:break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化並設置預覽部件
        mCVCamera = (CameraBridgeViewBase) findViewById(R.id.camera_view);
        mCVCamera.setCvCameraViewListener(this);

        //拍照按鍵
        button = (Button) findViewById(R.id.deal_btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mRgba != null) {
                    if(!mRgba.empty()) {
                        Mat inter = new Mat(mRgba.width(), mRgba.height(), CvType.CV_8UC4);
                        //將四通道的RGBA轉爲三通道的BGR,重要!!
                        Imgproc.cvtColor(mRgba, inter, Imgproc.COLOR_RGBA2BGR);
                        File sdDir = null;
                        //判斷是否存在機身內存
                        boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
                        if(sdCardExist) {
                            //獲得機身儲存根目錄
                            sdDir = Environment.getExternalStorageDirectory();
                        }
                        //將拍攝準確時間作爲文件名
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
                        String filename = sdf.format(new Date());
                        String filePath = sdDir + "/Pictures/OpenCV/" + filename + ".png";
                        //將轉化後的BGR矩陣內容寫入到文件中
                        Imgcodecs.imwrite(filePath, inter);
                        Toast.makeText(MainActivity.this, "圖片保存到: "+ filePath, Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }

    @Override
    protected void onResume() {
        /***強制橫屏***/
        if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        } else {
            //橫屏後才加載部件,否則會FC
            if(!OpenCVLoader.initDebug()) {
                Log.d(TAG, "OpenCV library not found!");
            } else {
                Log.d(TAG, "OpenCV library found inside package. Using it!");
                mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
            }
        }
        super.onResume();
    }

    @Override
    protected void onDestroy() {
        if(mCVCamera != null) {
            mCVCamera.disableView();
        }
        super.onDestroy();
    }

    //對象實例化及基本屬性的設置,包括長度、寬度和圖像類型標誌
    public void onCameraViewStarted(int width, int height) {
        mRgba = new Mat(height, width, CvType.CV_8UC4);
    }

    /**圖像處理都寫在這裏!!!**/
    @Override
    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
        mRgba = inputFrame.rgba();  //一定要有!!!不然數據保存不進MAT中!!!
        //直接返回輸入視頻預覽圖的RGB數據並存放在Mat數據中
        return mRgba;
    }

    //結束時釋放
    @Override
    public void onCameraViewStopped() {
        mRgba.release();
        mTmp.release();
    }
}

對以上內容的一些說明:

  1. onCameraViewStarted方法對對象進行初始化,這裏使用Mat類的對象mRgba,默認是RGBA格式。
  2. onCameraFrame方法爲圖像處理的主要部分,今後如需添加圖像處理的內容就寫在此方法中,可用switch-case結構進入不同的處理方法。該方法的返回值爲一個Mat對象,我們這裏不做處理,即將傳入的Mat對象原樣返回。
  3. 對於圖像保存部分,首先我們通過File類的對象設置文件路徑,然後使用Imgproc.cvtColor方法,將之前的RGBA格式的Mat轉爲BGR格式,原因是我們保存的是一個三通道的8/16位的png格式的圖像,但是之前對於圖像處理所使用的Mat類是一個四通道的RGBA格式,因此需要進行轉換,即RGBA2BGR,否則保存的圖像文件顏色會出現問題。
  4. 在這裏我通過OpenCV裏面的Imgcodecs.imwrite將Mat類型的對象保存到本地文件中,省去了再創建Bitmap類對象這樣的一些步驟。另提供一段可行的通過使用Mat2Bitmap方法,創建Bitmap對象並由FileOutStream流保存文件的代碼:
                       Bitmap bmp = null;
                        try {
                            bmp = Bitmap.createBitmap(mRgba.cols(), mRgba.rows(), Bitmap.Config.ARGB_8888);
                            Utils.matToBitmap(mRgba, bmp); //將MAT對象轉換爲bmp對象
                            Log.d(TAG, "mat to bmp.");
                        } catch (CvException e) {
                            Log.d(TAG, e.getMessage());
                        }

                        FileOutputStream out = null;
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
                        String fileName = sdf.format(new Date()) + ".png";

                        File sd = new File(Environment.getExternalStorageDirectory() + "/Pictures/OpenCV");
                        boolean success = true;
                        if(!sd.exists()) {
                            success = sd.mkdir();
                        }
                        if(success) {
                            File dest = new File(sd, fileName);

                            try {
                                out = new FileOutputStream(dest);
                                bmp.compress(Bitmap.CompressFormat.PNG, 100, out);
                                //Imgcodecs.imwrite(dest.toString(), mRgba);
                                Toast.makeText(MainActivity.this, "Picture has saved in " + dest.toString(), Toast.LENGTH_SHORT).show();
                            } catch (Exception e) {
                                e.printStackTrace();
                                Log.d(TAG, e.getMessage());
                            } finally {
                                try {
                                    if(out != null) {
                                        out.close();
                                        Log.d(TAG, "success save");
                                    }
                                } catch (IOException e) {
                                    Log.d(TAG, e.getMessage() + "Error");
                                    e.printStackTrace();
                                }
                            }
                        }

將這段代碼替換到按鍵監聽事件中第二個判斷語句if(!mRgba.empty()) 之後,也是可以運行的。

 

以上,通過OpenCV實現的一個簡易相機已經實現了,效果如圖:

 

如果需要其他的一些處理,則將相關代碼寫在onCameraFrame方法中即可。我也是剛開始接觸OpenCV以及圖像處理方面的內容,因此各方面可能會有不足之處,敬請原諒。

 

參考資料:

  1. 在Android Studio上進行OpenCV 3.1開發
  2. OpenCV學習筆記(六)—— OpenCV for Android打開相機
  3. Reading and Writing Images - OpenCV
  4. cvtColor - OpenCV
發佈了102 篇原創文章 · 獲贊 94 · 訪問量 52萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章