Setup thư viện OpenCV trên Android studio cho người mới

Gần đây tôi đã bắt đầu một dự án liên quan đến việc làm việc với OpenCV trên Android. Hầu hết các hướng dẫn về cách thiết lập thư viện trên Android đã lỗi thời hoặc chưa hoàn tất. Vì vậy, sau khi nhận được nhiều yêu cầu từ mọi người, tôi quyết định viết một hướng dẫn đơn giản về điều này.

Bước 1: Download thư viện OpenCV

Truy cập trang OpenCV Android Sourceforge và tải xuống thư viện Android OpenCV mới nhất.

Khi quá trình tải xuống hoàn tất, bạn giải nén tệp zip vào một thư mục.

Bước 2: Setup project

Tạo một project mới trên Android Studio nếu bạn chưa có project nào trên máy tính của mình. Nếu bạn đã có một project nào đó muốn sử dụng thư viện OpenCV thì dùng luôn project đó.

Bước 3: Import OpenCV module

Sau khi tạo thành công project , bạn tiến hành import OpenCV module vào project như sau. Click vào File -> New -> Import Module…

Nó sẽ xuất hiện một popup như hình ảnh bên dưới, nơi bạn có thể chọn đường dẫn đến module bạn muốn nhập.

Tìm đến thư mục nơi bạn giải nén tệp zip thư viện OpenCV Android. Chọn thư mục java bên trong thư mục sdk.

Sau khi chọn đúng đường dẫn và nhấp vào OK, bạn sẽ nhận được một màn hình như hình ảnh bên dưới.

Nhấn vào Next để đến màn hình tiếp theo. Trên màn hình tiếp theo (hình ảnh bên dưới), bạn nên để các tùy chọn mặc định được chọn và nhấp vào Finish để hoàn tất quá trình import module.

Bước 4: Sửa lỗi Gradle Sync

Bạn sẽ gặp lỗi build Gradle sau khi import xong thư viện OpenCV. Điều này xảy ra vì thư viện đang sử dụng SDK Android cũ mà bạn có thể không cài đặt.

Để nhanh chóng khắc phục lỗi này, hãy chuyển từ khung nhìn Android sang khung nhìn Project ở phía bên trái của Android Studio.

Duyệt đến thư viện OpenCV module và mở tệp build.gradle.

Để khắc phục lỗi, bạn chỉ cần thay đổi compileSdkVersiontargetSdkVersion thành phiên bản SDK Android mới nhất hoặc phiên bản bạn đã cài đặt trên PC. Sau khi thay đổi phiên bản, bạn nên nhấp vào nút Sync để Gradle có thể đồng bộ hóa project.

Bước 5: Thêm OpenCV Dependency

Để làm việc với thư viện Android OpenCV, bạn phải thêm nó vào mô-đun ứng dụng của mình dưới dạng Dependency. Để dễ dàng thực hiện việc này trên Android Studio, nhấp vào File -> Project Structure.

Khi hộp thoại Project Structure hiện ra, Chọn app -> chọn tab Dependencies -> chọn thư viện OpenCV module rồi chọn OK

Bước 6: Thêm thư viện Native

Mở thư mục chứa file giải nén của OpenCV -> mở tư mục sdk -> mở thư mụcnative. Sau đó copy thư mục libs paste vào thư mục main của project.

Đổi tên thư mục libs thành jniLibs

Bước 7: Thêm Permissions

Như trong project này, ta cần thêm camera permisson trong file manifest.xml. Lưu ý bạn phải request permisson at runtime nếu project đang chạy Android >= 6.

<uses-permission android:name="android.permission.CAMERA"/>

    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>

Bước 8: Làm demo

Để xác nhận rằng bạn đã tích hợp thành công thư viện OpenCV Android, bạn có thể thử một trong các mẫu có trong tệp zip thư viện. Hãy cùng thử các mẫu phát hiện màu color-blob-detection nhé!

Cập nhật lại code class MainActivity như dưới:


public class MainActivity extends Activity implements OnTouchListener, CvCameraViewListener2 {
    private static final String  TAG              = "MainActivity";

    private boolean              mIsColorSelected = false;
    private Mat                  mRgba;
    private Scalar               mBlobColorRgba;
    private Scalar               mBlobColorHsv;
    private ColorBlobDetector    mDetector;
    private Mat                  mSpectrum;
    private Size                 SPECTRUM_SIZE;
    private Scalar               CONTOUR_COLOR;

    private CameraBridgeViewBase mOpenCvCameraView;

    private BaseLoaderCallback  mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i(TAG, "OpenCV loaded successfully");
                    mOpenCvCameraView.enableView();
                    mOpenCvCameraView.setOnTouchListener(MainActivity.this);
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    public MainActivity() {
        Log.i(TAG, "Instantiated new " + this.getClass());
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "called onCreate");
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        setContentView(R.layout.color_blob_detection_surface_view);

        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.color_blob_detection_activity_surface_view);
        mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
    }

    @Override
    public void onPause()
    {
        super.onPause();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override
    public void onResume()
    {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback);
        } else {
            Log.d(TAG, "OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }

    public void onDestroy() {
        super.onDestroy();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    public void onCameraViewStarted(int width, int height) {
        mRgba = new Mat(height, width, CvType.CV_8UC4);
        mDetector = new ColorBlobDetector();
        mSpectrum = new Mat();
        mBlobColorRgba = new Scalar(255);
        mBlobColorHsv = new Scalar(255);
        SPECTRUM_SIZE = new Size(200, 64);
        CONTOUR_COLOR = new Scalar(255,0,0,255);
    }

    public void onCameraViewStopped() {
        mRgba.release();
    }

    public boolean onTouch(View v, MotionEvent event) {
        int cols = mRgba.cols();
        int rows = mRgba.rows();

        int xOffset = (mOpenCvCameraView.getWidth() - cols) / 2;
        int yOffset = (mOpenCvCameraView.getHeight() - rows) / 2;

        int x = (int)event.getX() - xOffset;
        int y = (int)event.getY() - yOffset;

        Log.i(TAG, "Touch image coordinates: (" + x + ", " + y + ")");

        if ((x < 0) || (y < 0) || (x > cols) || (y > rows)) return false;

        Rect touchedRect = new Rect();

        touchedRect.x = (x>4) ? x-4 : 0;
        touchedRect.y = (y>4) ? y-4 : 0;

        touchedRect.width = (x+4 < cols) ? x + 4 - touchedRect.x : cols - touchedRect.x;
        touchedRect.height = (y+4 < rows) ? y + 4 - touchedRect.y : rows - touchedRect.y;

        Mat touchedRegionRgba = mRgba.submat(touchedRect);

        Mat touchedRegionHsv = new Mat();
        Imgproc.cvtColor(touchedRegionRgba, touchedRegionHsv, Imgproc.COLOR_RGB2HSV_FULL);

        // Calculate average color of touched region
        mBlobColorHsv = Core.sumElems(touchedRegionHsv);
        int pointCount = touchedRect.width*touchedRect.height;
        for (int i = 0; i < mBlobColorHsv.val.length; i++)
            mBlobColorHsv.val[i] /= pointCount;

        mBlobColorRgba = converScalarHsv2Rgba(mBlobColorHsv);

        Log.i(TAG, "Touched rgba color: (" + mBlobColorRgba.val[0] + ", " + mBlobColorRgba.val[1] +
                ", " + mBlobColorRgba.val[2] + ", " + mBlobColorRgba.val[3] + ")");

        mDetector.setHsvColor(mBlobColorHsv);

        Imgproc.resize(mDetector.getSpectrum(), mSpectrum, SPECTRUM_SIZE, 0, 0, Imgproc.INTER_LINEAR_EXACT);

        mIsColorSelected = true;

        touchedRegionRgba.release();
        touchedRegionHsv.release();

        return false; // don't need subsequent touch events
    }

    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        mRgba = inputFrame.rgba();

        if (mIsColorSelected) {
            mDetector.process(mRgba);
            List<MatOfPoint> contours = mDetector.getContours();
            Log.e(TAG, "Contours count: " + contours.size());
            Imgproc.drawContours(mRgba, contours, -1, CONTOUR_COLOR);

            Mat colorLabel = mRgba.submat(4, 68, 4, 68);
            colorLabel.setTo(mBlobColorRgba);

            Mat spectrumLabel = mRgba.submat(4, 4 + mSpectrum.rows(), 70, 70 + mSpectrum.cols());
            mSpectrum.copyTo(spectrumLabel);
        }

        return mRgba;
    }

    private Scalar converScalarHsv2Rgba(Scalar hsvColor) {
        Mat pointMatRgba = new Mat();
        Mat pointMatHsv = new Mat(1, 1, CvType.CV_8UC3, hsvColor);
        Imgproc.cvtColor(pointMatHsv, pointMatRgba, Imgproc.COLOR_HSV2RGB_FULL, 4);

        return new Scalar(pointMatRgba.get(0, 0));
    }
}

Tạo class ColorBlobDetector và code như sau:

public class ColorBlobDetector {
    // Lower and Upper bounds for range checking in HSV color space
    private Scalar mLowerBound = new Scalar(0);
    private Scalar mUpperBound = new Scalar(0);
    // Minimum contour area in percent for contours filtering
    private static double mMinContourArea = 0.1;
    // Color radius for range checking in HSV color space
    private Scalar mColorRadius = new Scalar(25,50,50,0);
    private Mat mSpectrum = new Mat();
    private List<MatOfPoint> mContours = new ArrayList<MatOfPoint>();

    // Cache
    Mat mPyrDownMat = new Mat();
    Mat mHsvMat = new Mat();
    Mat mMask = new Mat();
    Mat mDilatedMask = new Mat();
    Mat mHierarchy = new Mat();

    public void setColorRadius(Scalar radius) {
        mColorRadius = radius;
    }

    public void setHsvColor(Scalar hsvColor) {
        double minH = (hsvColor.val[0] >= mColorRadius.val[0]) ? hsvColor.val[0]-mColorRadius.val[0] : 0;
        double maxH = (hsvColor.val[0]+mColorRadius.val[0] <= 255) ? hsvColor.val[0]+mColorRadius.val[0] : 255;

        mLowerBound.val[0] = minH;
        mUpperBound.val[0] = maxH;

        mLowerBound.val[1] = hsvColor.val[1] - mColorRadius.val[1];
        mUpperBound.val[1] = hsvColor.val[1] + mColorRadius.val[1];

        mLowerBound.val[2] = hsvColor.val[2] - mColorRadius.val[2];
        mUpperBound.val[2] = hsvColor.val[2] + mColorRadius.val[2];

        mLowerBound.val[3] = 0;
        mUpperBound.val[3] = 255;

        Mat spectrumHsv = new Mat(1, (int)(maxH-minH), CvType.CV_8UC3);

        for (int j = 0; j < maxH-minH; j++) {
            byte[] tmp = {(byte)(minH+j), (byte)255, (byte)255};
            spectrumHsv.put(0, j, tmp);
        }

        Imgproc.cvtColor(spectrumHsv, mSpectrum, Imgproc.COLOR_HSV2RGB_FULL, 4);
    }

    public Mat getSpectrum() {
        return mSpectrum;
    }

    public void setMinContourArea(double area) {
        mMinContourArea = area;
    }

    public void process(Mat rgbaImage) {
        Imgproc.pyrDown(rgbaImage, mPyrDownMat);
        Imgproc.pyrDown(mPyrDownMat, mPyrDownMat);

        Imgproc.cvtColor(mPyrDownMat, mHsvMat, Imgproc.COLOR_RGB2HSV_FULL);

        Core.inRange(mHsvMat, mLowerBound, mUpperBound, mMask);
        Imgproc.dilate(mMask, mDilatedMask, new Mat());

        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();

        Imgproc.findContours(mDilatedMask, contours, mHierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

        // Find max contour area
        double maxArea = 0;
        Iterator<MatOfPoint> each = contours.iterator();
        while (each.hasNext()) {
            MatOfPoint wrapper = each.next();
            double area = Imgproc.contourArea(wrapper);
            if (area > maxArea)
                maxArea = area;
        }

        // Filter contours by area and resize to fit the original image size
        mContours.clear();
        each = contours.iterator();
        while (each.hasNext()) {
            MatOfPoint contour = each.next();
            if (Imgproc.contourArea(contour) > mMinContourArea*maxArea) {
                Core.multiply(contour, new Scalar(4,4), contour);
                mContours.add(contour);
            }
        }
    }

    public List<MatOfPoint> getContours() {
        return mContours;
    }
}

Cuối cùng, cập nhật code của file activity_main.xml của ứng dụng.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <org.opencv.android.JavaCameraView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/color_blob_detection_activity_surface_view" />

</FrameLayout>

Bước 9: Sử dụng OpenCV Manager

Vì bạn đang gói các thư viện gốc trong app APK của mình, nên bạn sẽ có kích thước file APK rất lớn.

Một cách để giải quyết vấn đề này là sử dụng ABI splits, do đó bạn chỉ cần các thư viện cần thiết cho mỗi thiết bị trong APK.

Một cách khác xung quanh vấn đề kích thước là sử dụng OpenCV Manager. Trong thư mục nơi bạn giải nén thư viện vào, có một thư mục có tên apk chứa OpenCV Manager cho các architectures khác nhau.

Chọn APK phù hợp với thiết bị thử nghiệm của bạn và sử dụng ADB thông qua command prompt/line để install nó vào trong thiết bị của bạn.

Bước 10: Test app

Run app để xem kết quả nhé!

Cảm ơn bạn đã quan tâm!

Nguồn: https://android.jlelse.eu/a-beginners-guide-to-setting-up-opencv-android-library-on-android-studio-19794e220f3c