OpenCV自学笔记31. Android 上使用jni和opencv 实现边缘检测和直线检测

39 篇文章 75 订阅
6 篇文章 0 订阅

Android 上使用jni和opencv 实现边缘检测和直线检测

项目的github地址:https://github.com/lijialinneu/MyApplication

首先来看实验效果

这里写图片描述

这是在android上运行的一个小demo,使用真机调试,运行在红米Note3上。上面是原始图片,下面是边缘检测的结果图。这里的边缘检测使用OpenCV中的canny算法。


下面说明如何创建这样一个项目

分为以下几步:

  • 1、配置ndk开发环境
  • 2、写好make文件:Android.mk和Application.mk
  • 3、写好canny.h、canny.cpp(c++)和java封装类
  • 4、写好界面,包括MainActivity,activity_main.xml

名词解释:

sdk,即software development kit,Android的软件开发工具包。

ndk,即native develop kit,是google为了方便在android上使用原生代码语言而推出的开发工具。简单地说,ndk作用之一就是帮助开发者在android上使用c++开发程序。

jni,即java native interface,提供了API实现java和C/C++的交互。可以看做java程序和C/C++程序间的桥梁。


1、配置ndk开发环境

**(1)首先需要安装AndroidStudio,并新建一个空项目:MyApplication。**这一步可以参考:http://blog.csdn.net/liranke/article/details/49636927/

**(2)然后下载android-ndk-r10并解压,搭建ndk环境。**在Project Structure设置项中,可以配置sdk和ndk的位置。这里面的ndk就是刚刚下载好的android-ndk-r10。

这里写图片描述

(3)修改app的build.gradle:

这部分代码,是根据网上资料修改而来,某些配置项可以按照开发时的实际情况修改。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "com.example.lijialin.myapplication"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {
            moduleName "OpenCV"          //生成的so名字
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    /**
     *  禁止自带的ndk功能
     *  @author 10405
     *  add on 2017-10-20
     */
    sourceSets.main.jni.srcDirs = []
    sourceSets.main.jniLibs.srcDirs = ['src/main/libs', 'src/main/jniLibs']
    //重定向so目录为src/main/libs和src/main/jniLibs,原来为src/main/jniLibs

    task ndkBuild(type: Exec, description: 'Compile JNI source with NDK') {
        Properties properties = new Properties()
        properties.load(project.rootProject.file('local.properties').newDataInputStream())
        def ndkDir = properties.getProperty('ndk.dir')

        if (org.apache.tools.ant.taskdefs.condition.Os.isFamily(org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)) {
            commandLine "$ndkDir/ndk-build.cmd", '-C', file('src/main/jni').absolutePath
        } else {
            commandLine "$ndkDir/ndk-build", '-C', file('src/main/jni').absolutePath
        }
    }

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

    task ndkClean(type: Exec, description: 'Clean NDK Binaries') {
        Properties properties = new Properties()
        properties.load(project.rootProject.file('local.properties').newDataInputStream())
        def ndkDir = properties.getProperty('ndk.dir')

        if (org.apache.tools.ant.taskdefs.condition.Os.isFamily(org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)) {
            commandLine "$ndkDir/ndk-build.cmd  ", 'clean', '-C', file('src/main/jni').absolutePath
        } else {
            commandLine "$ndkDir/ndk-build  ", 'clean', '-C', file('src/main/jni').absolutePath
        }
    }

    clean.dependsOn 'ndkClean'
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    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:25.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9'
    testCompile 'junit:junit:4.12'
}

(4)从opencv官网下载opencv为android平台准备的开发工具:OpenCV-3.1.0-android-sdk,当然使用其他版本也可以。下载后解压,在路径OpenCV-3.1.0-android-sdk\OpenCV-android-sdk\sdk\下,有一个native文件。将native文件复制到MyApplication中。复制后的目录结构如下图所示:

这里写图片描述

(5)在app\src\main目录下,新建一个jni目录,在jni目录中,添加三个文件:
Android.mkApplication.mk,canny.cpp

其中,Android.mk的内容如下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)


OpenCV_INSTALL_MODULES := on
OpenCV_CAMERA_MODULES := off

OPENCV_LIB_TYPE :=STATIC

ifeq ("$(wildcard $(OPENCV_MK_PATH))","")
#include ..\..\..\..\native\jni\OpenCV.mk
# 根据自己的路径修改
include C:\Users\lijialin\AndroidStudioProjects\MyApplication\native\jni\OpenCV.mk

else
include $(OPENCV_MK_PATH)
endif

LOCAL_MODULE := OpenCV

LOCAL_SRC_FILES := canny.cpp
#LOCAL_SRC_FILES += lsd.cpp #在这里可以添加其他cpp文件

#LOCAL_LDLIBS +=  -lm -llog
LOCAL_LDLIBS += -llog

include $(BUILD_SHARED_LIBRARY)

Application.mk的内容如下:

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi
APP_PLATFORM := android-8
APP_OPIM := debug

(6)在app\src\main目录下,新建一个libs目录,我们编译后生成的libOpenCV.so会生存储在这个目录下。


2、编写canny.h,canny.cpp和Java封装类

canny.h是canny.cpp的头文件,功能只有一个,就是声明了native函数:
Java_com_example_lijialin_myapplication_OpenCVCanny_canny

  • com_example_lijialin是包名
  • myapplication是项目名
  • OpenCVCanny是java封装类的类名
  • canny是函数名
//
// Created by lijialin on 2017/10/20.
//

/* Header for class canny */

#include<jni.h>


#ifndef MYAPPLICATION_CANNY_H
#define MYAPPLICATION_CANNY_H

#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     canny
 * Method:    canny
 * Signature: ([III)[I
 */
JNIEXPORT jintArray JNICALL Java_com_example_lijialin_myapplication_OpenCVCanny_canny (JNIEnv *, jclass, jintArray, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

canny.cpp是我们实现边缘检测的核心程序,在这个程序中,首先将图像转为灰度图,然后调用高斯滤波对图像进行平滑去噪,最后使用canny边缘检测算法,提取出图像中的边缘。

canny.cpp

//
// Created by lijialin on 2017/10/20.
//

#include "canny.h"
#include <stdio.h>
#include <stdlib.h>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <android/log.h>
#include <iostream>


#define LOG_TAG    "asdf"
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, __VA_ARGS__) // 定义LOGD类型,便于调试

using namespace cv;
using namespace std;

extern  "C" {

// JNIEnv 代表java环境,通过这个参数可以调用java中的方法

JNIEXPORT jintArray JNICALL Java_com_example_lijialin_myapplication_OpenCVCanny_canny(JNIEnv *env, jclass obj, jintArray buf, int w, int h);

JNIEXPORT jintArray JNICALL Java_com_example_lijialin_myapplication_OpenCVCanny_canny(JNIEnv *env, jclass obj, jintArray buf, int w, int h) {

    jint *cbuf;
    cbuf = env->GetIntArrayElements(buf, false); // 读取输入参数
    if (cbuf == NULL) {
        return 0;
    }

    Mat image(h, w, CV_8UC4, (unsigned char*) cbuf); // 初始化一个矩阵(图像)4通道的图像
    cvtColor(image, image, COLOR_BGR2GRAY); // 转为灰度图
    GaussianBlur(image, image, Size(5,5), 0, 0); // 高斯滤波
    Canny(image, image, 50, 150, 3); // 边缘检测

    // LSD 直线检测
    Mat image2(image.size(), image.type()); // 用于绘制直线
    Ptr<LineSegmentDetector> ls = createLineSegmentDetector(LSD_REFINE_STD, 0.80);
    vector<Vec4f> lines_std;
    ls->detect(image, lines_std);

    // LOGD("channels = %d", image.channels());
    ls->drawSegments(image2, lines_std);
    cvtColor(image2, image2, COLOR_BGR2GRAY); // 此处要转为灰度图

    // TODO 这里还可以添加其他的功能

    // 构造返回结果
    int* outImage = new int[w * h];
    int n = 0;
    for(int i = 0; i < h; i++) {
        uchar* data = image.ptr<uchar>(i);
        // uchar* data = image2.ptr<uchar>(i); // 如果是直线检测,就用image2
        for(int j = 0; j < w; j++) {
            if(data[j] == 255) {
                outImage[n++] = 0;
            }else {
                outImage[n++] = -1;
            }
        }
    }

    int size = w * h;
    jintArray result = env->NewIntArray(size);
    env->SetIntArrayRegion(result, 0, size, outImage);
    env->ReleaseIntArrayElements(buf, cbuf, 0);
    return result;

}


}

在写好canny.cpp后,还需要在外面封装一层java代码,定义native方法以便调用本地的C/C++代码。

OpenCVCanny.java:

package com.example.lijialin.myapplication;

/**
 * Created by lijialin on 2017/10/20.
 */

public class OpenCVCanny {
    static {
        System.loadLibrary("OpenCV"); // 加载编译好的.so动态库
    }

    /**
     * 声明native方法,调用OpenCV的边缘检测
     *
     * @param buf 图像
     * @param w 宽
     * @param h 高
     * @return 边缘图
     */
    public static native int[] canny(int[] buf, int w, int h);
}

如果我们此时编译,也就是make,可以看到在libs目录下生成了libOpenCV.so

这里写图片描述


如果make报错,需要修改好错误。修改make时的错误是一个痛苦的过程,我一般借助android studio上的message和Gradle Console工具,同时通过自己打Log来分析代码中的错误。

这里写图片描述


这样核心的部分就写好了,下面开始编写android的界面。


3、编写android界面

app只有一个界面,只需把MainActivity写好就可以了。在MainActivity中,程序首先从资源文件中获取了building图像,然后调用上一步封装好的OpenCVCanny.canny()方法,并传递相应的参数进去。canny()方法会使用native方法,也就是canny.cpp中的Java_com_example_lijialin_myapplication_OpenCVCanny_canny函数,实现边缘检测的功能。

MainActivity.java

package com.example.lijialin.myapplication;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

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

        // 获取图片,使用边缘检测提取图像的边缘
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.building);
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int[] pix = new int[width * height];
        bitmap.getPixels(pix, 0, width, 0, 0, width, height);
        int[] resultPixels = OpenCVCanny.canny(pix, width, height); // 调用canny方法

        Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); // 创建一个4通道的图像
        resultBitmap.setPixels(resultPixels, 0, width, 0, 0, width, height);

        // 将边缘图显示出来
        ImageView view = (ImageView) findViewById(R.id.resultView);
        view.setImageBitmap(resultBitmap);
        view.setVisibility(View.VISIBLE);
    }
}

界面: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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context="com.example.lijialin.myapplication.MainActivity">

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

    <TextView
        android:id="@+id/title1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:text="原始图"/>

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="259dp"
        app:srcCompat="@mipmap/building" />

    <TextView
        android:id="@+id/title2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:text="边缘图" />

    <ImageView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    </LinearLayout>

</android.support.constraint.ConstraintLayout>


4、真机调试

编写完上述的代码后,可以进行真机调试了。如果程序没有其他报错,就能看到下面的运行结果。

这里写图片描述

总结:

如果我们要在android上使用opencv,除了jni外,还可以直接使用opencv提供的java api,但是对于实时性要求较高的系统,不建议直接使用java api。

大致思路就是这样,项目的github地址:https://github.com/lijialinneu/MyApplication,欢迎下载。(因为android版本的关系,不保证适配所有机型)




补充:DreameraJni小项目

求学时期写过一个小项目,主要用到了上问提及的jni+opencv技术,不过功能更加强大,还可以实现图片的水印效果。代码年久失修,已经不能正常运行了,但是一些解决问题的思路,还是可以参考借鉴的。

项目地址:
https://gitee.com/lijialin/DreameraJni


主要有以下几个功能:

1、使用百度地图api,实现地图聚合、选点,展示文字介绍(实际图片有出入)
在这里插入图片描述

2、调用系统相机,添加图层拍照
在这里插入图片描述

3、实现一个简单的水印叠加效果,包含简单的滤镜
在这里插入图片描述

4、分享图片至微信等

拜拜~

这里写图片描述

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值