深入OpenCV Android应用开发 中文版 – 第一章代码更新

book

本书中文版链接:京东  当当

英文原版链接:亚马逊《Mastering OpenCV Android Application Programming》


        原书所给代码以章为单位,针对的Android版本从API 19到API 21不等,同时使用的OpenCV库版本也有2.4.9和2.4.10两种。本文给出的代码是在原书代码的基础上,针对Android 7.0(API 24)与OpenCV 3.2进行了修改,补充了一些注释,适当地增加了一些预处理操作,以使代码整体上更合理。

        原书的完整代码可以在这里获取:https://www.packtpub.com/lcode_download/22299

        更新后的代码托管在GitHub:https://github.com/johnhany/MOAAP/tree/master/MOAAP-Chp1-r3

        关于在Android Studio上配置OpenCV 3.1开发环境的方法,请参考《在Android Studio上进行OpenCV 3.1开发》。OpenCV 3.2的配置方法与之相似,不过在配置过程中要注意版本上的不同。

        本章主要利用OpenCV Java API对图像进行一些常见的滤波操作,包括均值模糊、高斯模糊、中值模糊、膨胀、腐蚀、阈值化以及自适应阈值化。本章不涉及NDK开发及多线程。


更新记录:

        2017.07.24更新:代码更新到Android Studio 2.3.3。

        2016.07.23更新:针对API 23增加了新的动态权限申请机制,以解决Android 6.0上运行时权限失败的错误。(这里这里。)


开发环境:

        Windows 10 x64专业版(Windows 7 x64旗舰版也验证通过)

        Android Studio 2.3.3(Gradle 3.3,Android Plugin 2.3.3)

        Android 7.0(API 24)

        JDK 8u141

        OpenCV 3.2.0 Android SDK


代码及简略解释:

        1.创建Android Studio项目,包命名为net.johnhany.moaap_chp1

        2.app\src\main\java目录中找到net.johnhany.moaap_chp1包,为MainActivity.java添加如下代码:

package net.johnhany.moaap_chp1;

import android.Manifest;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.annotation.NonNull;
import android.os.Bundle;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

import java.io.FileNotFoundException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    private final int SELECT_PHOTO = 1;
    private ImageView ivImage, ivImageProcessed;
    Mat src, src_gray;
    static int ACTION_MODE = 0;
    static int REQUEST_READ_EXTERNAL_STORAGE = 0;
    static boolean read_external_storage_granted = false;

    private BaseLoaderCallback mOpenCVCallBack = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                    //DO YOUR WORK/STUFF HERE
                    break;
                default:
                    super.onManagerConnected(status);
                    break;
            }
        }
    };

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

        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, mOpenCVCallBack);

        //Add a left arrow on Action Bar to return to the previous Activity
        assert getSupportActionBar() != null;
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        ivImage = (ImageView)findViewById(R.id.ivImage);
        ivImageProcessed = (ImageView)findViewById(R.id.ivImageProcessed);
        Intent intent = getIntent();

        if(intent.hasExtra("ACTION_MODE")){
            ACTION_MODE = intent.getIntExtra("ACTION_MODE", 0);
        }

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            Log.i("permission", "request READ_EXTERNAL_STORAGE");
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_READ_EXTERNAL_STORAGE);
        }else {
            Log.i("permission", "READ_EXTERNAL_STORAGE already granted");
            read_external_storage_granted = true;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_load_image && read_external_storage_granted) {
            Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
            photoPickerIntent.setType("image/*");
            startActivityForResult(photoPickerIntent, SELECT_PHOTO);

            return true;
        }else if(!read_external_storage_granted) {
            Log.i("permission", "READ_EXTERNAL_STORAGE denied");
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) {
        //Put it there, just in case:)
        super.onActivityResult(requestCode, resultCode, imageReturnedIntent);

        switch(requestCode) {
            case SELECT_PHOTO:
                if(resultCode == RESULT_OK && read_external_storage_granted){
                    try {
                        final Uri imageUri = imageReturnedIntent.getData();
                        final InputStream imageStream = getContentResolver().openInputStream(imageUri);
                        final Bitmap selectedImage = BitmapFactory.decodeStream(imageStream);
                        src = new Mat(selectedImage.getHeight(), selectedImage.getWidth(), CvType.CV_8UC4);
                        Utils.bitmapToMat(selectedImage, src);
                        src_gray = new Mat(selectedImage.getHeight(), selectedImage.getWidth(), CvType.CV_8UC1);
                        switch (ACTION_MODE) {
                            case HomeActivity.GAUSSIAN_BLUR:
                                Imgproc.GaussianBlur(src, src, new Size(9, 9), 0);
                                break;
                            case HomeActivity.MEAN_BLUR:
                                Imgproc.blur(src, src, new Size(9, 9));
                                break;
                            case HomeActivity.MEDIAN_BLUR:
                                Imgproc.medianBlur(src, src, 9);
                                break;
                            case HomeActivity.SHARPEN:
                                Mat kernel = new Mat(3, 3, CvType.CV_16SC1);
                                //int[] values = {0, -1, 0, -1, 5, -1, 0, -1, 0};
                                Log.d("imageType", CvType.typeToString(src.type()) + "");
                                kernel.put(0, 0, 0, -1, 0, -1, 5, -1, 0, -1, 0);
                                Imgproc.filter2D(src, src, src_gray.depth(), kernel);
                                break;
                            case HomeActivity.DILATE:
                                Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_BGR2GRAY);
                                Imgproc.threshold(src_gray, src_gray, 100, 255, Imgproc.THRESH_BINARY);
                                Mat kernelDilate = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
                                Imgproc.dilate(src_gray, src_gray, kernelDilate);
                                Imgproc.cvtColor(src_gray, src, Imgproc.COLOR_GRAY2RGBA, 4);
                                break;
                            case HomeActivity.ERODE:
                                Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_BGR2GRAY);
                                Imgproc.threshold(src_gray, src_gray, 100, 255, Imgproc.THRESH_BINARY);
                                Mat kernelErode = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(5, 5));
                                Imgproc.erode(src_gray, src_gray, kernelErode);
                                Imgproc.cvtColor(src_gray, src, Imgproc.COLOR_GRAY2RGBA, 4);
                                break;
                            case HomeActivity.THRESHOLD:
                                Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_BGR2GRAY);
                                Imgproc.threshold(src_gray, src_gray, 100, 255, Imgproc.THRESH_BINARY);
                                Imgproc.cvtColor(src_gray, src, Imgproc.COLOR_GRAY2RGBA, 4);
                                break;
                            case HomeActivity.ADAPTIVE_THRESHOLD:
                                Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_BGR2GRAY);
                                Imgproc.adaptiveThreshold(src_gray, src_gray, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 3, 0);
                                Imgproc.cvtColor(src_gray, src, Imgproc.COLOR_GRAY2RGBA, 4);
                                break;
                        }
                        Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
                        Log.i("imageType", CvType.typeToString(src.type()) + "");
                        Utils.matToBitmap(src, processedImage);
                        ivImage.setImageBitmap(selectedImage);
                        ivImageProcessed.setImageBitmap(processedImage);
                        Log.i("process", "process done");
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
        if (requestCode == REQUEST_READ_EXTERNAL_STORAGE) {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission was granted
                Log.i("permission", "READ_EXTERNAL_STORAGE granted");
                read_external_storage_granted = true;
            } else {
                // permission denied
                Log.e("permission", "READ_EXTERNAL_STORAGE refused");
                read_external_storage_granted = false;
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
    }
}

        与原来的代码产生的变化有:

        (1)去掉了一些无用的import

        (2)增加了一个Mat src_gray用来保存灰度图像(因为阈值化和自适应阈值化只能应用在灰度图像上,而膨胀和腐蚀操作只能应用在二值图像上),相应地为阈值化和自适应阈值化增加了转换成灰度的步骤,为膨胀和腐蚀增加了转换成灰度并阈值化的步骤;

        (3)把Log.d()中的tag长度减小(因为编译器会有超过最大长度的warning);

        (4)增加了高斯滤波、均值滤波和中值滤波的核的尺寸;

        (5)在动作栏添加返回按钮的代码由

getActionBar().setDisplayHomeAsUpEnabled(true);

修改为

assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        (6)onResume()中的OPENCV_VERSION_2_4_10改为OPENCV_VERSION_3_2_0

        (7)类定义中由extends Activity改为extends AppCompatActivity

        (8)添加了动态权限申请代码,参考:https://developer.android.com/training/permissions/requesting.htmlhttp://www.jianshu.com/p/b4a8b3d4f587

        一些补充:

        (1)在onOptionsItemSelected()中,如果对MenuItem item执行了一些处理,则需要返回true;否则返回super.onOptionsItemSelected(item)

        (2)在onActivityResult()的第一行添加的

super.onActivityResult(requestCode, resultCode, imageReturnedIntent);

一般是不会执行任何操作的,但是这样写是为了防止一些特殊情况。比如当使用的是FragmentActivitySherlockFragmentActivity时,这一行就很有必要了。(参考:http://stackoverflow.com/questions/11745366/is-there-a-need-to-use-super-onactivityresult-in-onactivityresult。)

        3.net.johnhany.moaap_chp1包内创建一个HomeActivity.java文件,添加以下代码:

package net.johnhany.moaap_chp1;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class HomeActivity extends Activity {
    public static final int MEAN_BLUR = 1;
    public static final int MEDIAN_BLUR = 2;
    public static final int GAUSSIAN_BLUR = 3;
    public static final int SHARPEN = 4;
    public static final int DILATE = 5;
    public static final int ERODE = 6;
    public static final int THRESHOLD = 7;
    public static final int ADAPTIVE_THRESHOLD = 8;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        Button bMean, bGaussian, bMedian, bSharpen, bDilate, bErode, bThreshold, bAdaptiveThreshold;
        bMean = (Button)findViewById(R.id.bMean);
        bMedian = (Button)findViewById(R.id.bMedian);
        bGaussian = (Button)findViewById(R.id.bGaussian);
        bSharpen = (Button)findViewById(R.id.bSharpen);
        bDilate = (Button)findViewById(R.id.bDilate);
        bErode = (Button)findViewById(R.id.bErode);
        bThreshold = (Button)findViewById(R.id.bThreshold);
        bAdaptiveThreshold = (Button)findViewById(R.id.bAdaptiveThreshold);

        bMean.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", MEAN_BLUR);
                startActivity(i);
            }
        });

        bMedian.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", MEDIAN_BLUR);
                startActivity(i);
            }
        });

        bGaussian.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", GAUSSIAN_BLUR);
                startActivity(i);
            }
        });

        bSharpen.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", SHARPEN);
                startActivity(i);
            }
        });

        bDilate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", DILATE);
                startActivity(i);
            }
        });

        bErode.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", ERODE);
                startActivity(i);
            }
        });

        bThreshold.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", THRESHOLD);
                startActivity(i);
            }
        });

        bAdaptiveThreshold.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(getApplicationContext(), MainActivity.class);
                i.putExtra("ACTION_MODE", ADAPTIVE_THRESHOLD);
                startActivity(i);
            }
        });
    }
}

        与原来的代码产生的变化有:

        (1)去掉了一些无用的import

        4.修改res\layout\activity_main.xml文件,添加以下内容:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="net.johnhany.moaap_chp1.MainActivity">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.5"
        android:id="@+id/ivImage" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.5"
        android:id="@+id/ivImageProcessed" />

</android.support.constraint.ConstraintLayout>

        5.res\layout下创建一个activity_home.xml文件,添加以下内容:

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

    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:orientation="vertical" >

        <Button
            android:id="@+id/bMean"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_mean_blur" />

        <Button
            android:id="@+id/bGaussian"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_gaussain_blur" />

        <Button
            android:id="@+id/bMedian"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_median_blur" />

        <Button
            android:id="@+id/bSharpen"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_sharpen_image" />

        <Button
            android:id="@+id/bDilate"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_dilate_image" />

        <Button
            android:id="@+id/bErode"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_erode_image" />

        <Button
            android:id="@+id/bThreshold"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_threshold_image" />

        <Button
            android:id="@+id/bAdaptiveThreshold"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="@string/menu_adaptive_threshold" />
    </LinearLayout>
</ScrollView>

        与原来的代码产生的变化有:

        LinearLayout标签中的

android:layout_height="match_parent"

改为

android:layout_height="wrap_content"

        6.res目录下创建一个名为menuRes Folder,再在res\menu中创建一个如下的menu_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_load_image"
        android:title="@string/action_load_image"
        android:orderInCategory="1"
        app:showAsAction="ifRoom" />
</menu>

        与原来的代码产生的变化有:

        (1)去掉了menu标签中的tools:context一行;

        (2)为menu标签添加一行

xmlns:app="http://schemas.android.com/apk/res-auto"

        (3)将item标签中的android:showAsAction改为app:showAsAction

        7.打开res\values\strings.xml文件,修改为以下内容:

<resources>
    <string name="app_name">第一章 - 深入OpenCV Android应用开发</string>

    <string name="action_load_image">载入图像</string>

    <string name="menu_mean_blur">Mean Blur</string>
    <string name="menu_gaussain_blur">Gaussian Blur</string>
    <string name="menu_median_blur">Median Blur</string>
    <string name="menu_sharpen_image">Sharpen Image</string>
    <string name="menu_dilate_image">Dilate Image</string>
    <string name="menu_erode_image">Erode Image</string>
    <string name="menu_threshold_image">Threshold Image</string>
    <string name="menu_adaptive_threshold">Adaptive Threshold</string>
</resources>

        8.修改res\AndroidManifest.xml文件为如下内容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.johnhany.moaap_chp1">

    <supports-screens
        android:anyDensity="true"
        android:largeScreens="true"
        android:normalScreens="true"
        android:resizeable="true"
        android:smallScreens="true" />

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:parentActivityName=".HomeActivity" >
        </activity>

        <activity android:name=".HomeActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

        与原来的代码产生的变化有:

        (1)去掉了uses-sdk标签,因为该属性总是会被build.gradle中设置的值所覆盖;

        (2)去掉了设置PARENT_ACTIVITYmeta-data标签。

        (3)删除了多余的权限申请。

        9.检查一下app\build.gradle文件是否为以下内容:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "26.0.0"
    defaultConfig {
        applicationId "net.johnhany.moaap_chp1"
        minSdkVersion 16
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile project(':openCVLibrary320')
}

        其中,compileSdkVersionbuildToolsVersiontargetSdkVersion三个版本可以自定,com.android.support:appcompat的版本最好与targetSdkVersion相一致,比如同为23或24。

        10.检查openCVLibrary320\build.gradle文件是否为以下内容:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 24
    buildToolsVersion "26.0.0"

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 24
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

运行效果:

moaap_chapter1

3

avatar
2 Comment threads
1 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
福尔魔龙John Hanyfilter Recent comment authors
  Subscribe  
最新 最旧
订阅评论
福尔魔龙
福尔魔龙

请问一下这个用来进行多张(三四张)的拼接吗,这些图片有重叠的部分

filter
filter

询问博主一个题外问题,opencvforandroid支持打开视频文件吗?我正在实现一个处理视频的功能,使用Android自带的getFrameAtTime取帧好慢啊,间隔短的话还会乱序,楼主能不能给点建议