OpenCV for Android (6)——通过使用JNI实现骨架化

OpenCV在Android上虽然有自己的开源库,能够处理很多的图像问题,但是一旦涉及到一些需要使用算法方面的问题比如骨架化或者像素点操作的问题时,其处理速度会变得很满,且处理效果并不是十分完美。

例如我最近需要实现书法字的骨架化问题,对于使用导入的OpenCV库,如果使用像素点的逐个操作,要是再放在主线程肯定会导致ANR,毕竟这样的操作太耗时了。而改用其他的骨架化算法效果不佳:

public static void skeletonProcess(Bitmap bitmap, int value) {
        org.opencv.android.Utils.bitmapToMat(bitmap, sSrc);
        Imgproc.cvtColor(sSrc, sSrc, Imgproc.COLOR_BGRA2GRAY);
        Imgproc.threshold(sSrc, sSrc, 0, 255,
                Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);

        Mat ske = new Mat(sSrc.size(), CvType.CV_8UC1, new Scalar(0, 0, 0));
        Mat temp = new Mat(sSrc.size(), CvType.CV_8UC1);
        Mat erode = new Mat();

        sStrElement = Imgproc.getStructuringElement(Imgproc.MORPH_CROSS, new Size(3, 3));

        boolean done;
        do {
            Imgproc.erode(sSrc, erode, sStrElement);
            Imgproc.dilate(erode, temp, sStrElement);
            Core.subtract(sSrc, temp, temp);
            Core.bitwise_or(ske, temp, ske);
            erode.copyTo(sSrc);
            done = (Core.countNonZero(sSrc) == 0);
        } while (!done);

        Imgproc.GaussianBlur(ske, ske, new Size(5, 5), 0, 0, 4);
        Imgproc.threshold(ske, ske, 0, 255,
                Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
        org.opencv.android.Utils.matToBitmap(ske, bitmap);

        ske.release();
        temp.release();
        erode.release();
        sStrElement.release();
        sSrc.release();
    }

先腐蚀,再膨胀;后减操作,最后与操作,这样的算法相比使用像素化其速度还是可以保证的,但是细化效果却不是最好的。

Java层OpenCV细化

如图所示,骨架化的图片存在一定是细节缺失,其次存在大量的噪声点,让整个细化后的图片显得不是最好的,对后续其他的操作也会带来不好的影响。

因此,还是需要使用像素点的操作方式。实现像素点方式的骨架化有很多,但都是基于C++的。好在Android拥有JNI方式,可以通过实现native方法来实现。

要实现如此的方法,具体有如下的一些方法:

1. 导入OpenCV库,这里不再赘述。

OpenCV for Android(1)——环境搭建

2. 通过Cmake将OpenCV的so库导入到工程中

这里我的实现其实并不好,使用的绝对路径,这样的操作对以后的重新下载工程是不好的,未来会改进

如下方法实现是基于已经在工程中添加了C++支持。

1. CMakeLists.txt

这个是在app目录下的CMakeLists.txt

#工程目录
set(pathToProject D:\\Android\\workplace\\calligraphyRecognize)
#OpenCV目录
set(pathToOpenCv D:\\Android\\OpenCV-android-sdk)

#CMake版本信息
cmake_minimum_required(VERSION 3.4.1)
#支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

#配置加载native依赖
include_directories(${pathToOpenCv}/sdk/native/jni/include)

#CPP文件夹下带编译的cpp文件
add_library( native-lib SHARED src/main/cpp/native-lib.cpp )

#动态方式加载
add_library( lib_opencv SHARED IMPORTED )

#引入libopencv_java3.so文件
set_target_properties(lib_opencv
                      PROPERTIES IMPORTED_LOCATION
                      ${pathToProject}/app/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

target_link_libraries( native-lib
                       ${log-lib}
                       lib_opencv)

上述的地址有些是绝对地址,如果在自己电脑上实现需要修改。

2. Build.Gradle

需要在Android的根目录下修改或添加两处:

 externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -frtti -fexceptions"
                abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'mips64'
            }
        }
 sourceSets {
        main {
            jni.srcDirs = ['D:\\Android\\workplace\\calligraphyRecognize\\app\\src\\main\\jniLibs']
        }
    }

3. 将OpenCV-Android-SDK中的lib都复制到jniLibs下

其实我认为放在libs下也是可以的,不过这样放区分度会好一些。

点编译按钮,基本上能够实现项目的构建了。接下来就是实现native方法了。

在此实现的方式是在Java层上将Mat传入jni,操作后将其返回到Java层。

native是无法将Mat传过去的,其实现实际传入地址,通过指针指向该区域,对该区域进行操作,再返回其地址从而完成了整个操作。

3. 实现

首先Java层书写Native方法:

public static native void gThin(long matSrcAddr, long matDstAddr);

转向jni在native上实现:

/*
 * 实现图像骨架化的Native方法:Rosenfeld细化算法
 * @param src:原图片
 * @return dst:细化后图片
 *
 * Rosenfeld细化算法描述如下:
 * 1. 扫描所有像素,如果像素是北部边界点,且是8simple,但不是孤立点和端点,删除该像素。
 * 2. 扫描所有像素,如果像素是南部边界点,且是8simple,但不是孤立点和端点,删除该像素。
 * 3. 扫描所有像素,如果像素是东部边界点,且是8simple,但不是孤立点和端点,删除该像素。
 * 4. 扫描所有像素,如果像素是西部边界点,且是8simple,但不是孤立点和端点,删除该像素。
 *
 * 执行完上面4个步骤后,就完成了一次迭代,我们重复执行上面的迭代过程,
 * 直到图像中再也没有可以删除的点后,退出迭代循环。
 *
 */
extern "C"
JNIEXPORT void JNICALL
Java_yangchengyu_shmtu_edu_cn_calligraphyrecognize_utils_ImageProcessUtils_gThin(JNIEnv *env,
                                                                                 jclass type,
                                                                                 jlong matSrcAddr,
                                                                                 jlong matDstAddr) {

    Mat &src = *(Mat *) matSrcAddr;//通过指针获取Java层对应空间的原始图片mat
    Mat &dst = *(Mat *) matDstAddr;//通过指针返回Java层的处理后图片mat

    if (dst.data != src.data) {
        src.copyTo(dst);
    }

    int i, j, n;
    int width, height;
    //方便处理8邻域,防止越界
    width = src.cols - 1;
    height = src.rows - 1;
    int step = src.step;
    int p2, p3, p4, p5, p6, p7, p8, p9;
    uchar *img;
    bool ifEnd;
    cv::Mat tmpimg;
    int dir[4] = {-step, step, 1, -1};

    while (1) {
        //分四个子迭代过程,分别对应北,南,东,西四个边界点的情况
        ifEnd = false;
        for (n = 0; n < 4; n++) {
            dst.copyTo(tmpimg);
            img = tmpimg.data;
            for (i = 1; i < height; i++) {
                img += step;
                for (j = 1; j < width; j++) {
                    uchar *p = img + j;
                    //如果p点是背景点或者且为方向边界点,依次为北南东西,继续循环
                    if (p[0] == 0 || p[dir[n]] > 0) continue;
                    p2 = p[-step] > 0 ? 1 : 0;
                    p3 = p[-step + 1] > 0 ? 1 : 0;
                    p4 = p[1] > 0 ? 1 : 0;
                    p5 = p[step + 1] > 0 ? 1 : 0;
                    p6 = p[step] > 0 ? 1 : 0;
                    p7 = p[step - 1] > 0 ? 1 : 0;
                    p8 = p[-1] > 0 ? 1 : 0;
                    p9 = p[-step - 1] > 0 ? 1 : 0;
                    //8 simple判定
                    int is8simple = 1;
                    if (p2 == 0 && p6 == 0) {
                        if ((p9 == 1 || p8 == 1 || p7 == 1) && (p3 == 1 || p4 == 1 || p5 == 1))
                            is8simple = 0;
                    }
                    if (p4 == 0 && p8 == 0) {
                        if ((p9 == 1 || p2 == 1 || p3 == 1) && (p5 == 1 || p6 == 1 || p7 == 1))
                            is8simple = 0;
                    }
                    if (p8 == 0 && p2 == 0) {
                        if (p9 == 1 && (p3 == 1 || p4 == 1 || p5 == 1 || p6 == 1 || p7 == 1))
                            is8simple = 0;
                    }
                    if (p4 == 0 && p2 == 0) {
                        if (p3 == 1 && (p5 == 1 || p6 == 1 || p7 == 1 || p8 == 1 || p9 == 1))
                            is8simple = 0;
                    }
                    if (p8 == 0 && p6 == 0) {
                        if (p7 == 1 && (p3 == 9 || p2 == 1 || p3 == 1 || p4 == 1 || p5 == 1))
                            is8simple = 0;
                    }
                    if (p4 == 0 && p6 == 0) {
                        if (p5 == 1 && (p7 == 1 || p8 == 1 || p9 == 1 || p2 == 1 || p3 == 1))
                            is8simple = 0;
                    }
                    int adjsum;
                    adjsum = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;
                    //判断是否是邻接点或孤立点,0,1分别对于那个孤立点和端点
                    if (adjsum != 1 && adjsum != 0 && is8simple == 1) {
                        //满足删除条件,设置当前像素为0
                        dst.at<uchar>(i, j) = 0;
                        ifEnd = true;
                    }

                }
            }
        }

        //已经没有可以细化的像素了,则退出迭代
        if (!ifEnd) break;
    }

}

最后编写完整代码实现

//Native层方法骨架化
    public static Bitmap skeletonFromJNI(Bitmap bitmap) {
        org.opencv.android.Utils.bitmapToMat(bitmap, sSrc);
        Imgproc.cvtColor(sSrc, sSrc, Imgproc.COLOR_BGRA2GRAY);
        Imgproc.threshold(sSrc, sSrc, 0, 255,
                Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
        gThin(sSrc.getNativeObjAddr(), sDst.getNativeObjAddr());
        Imgproc.threshold(sDst, sDst, 0, 255,
                Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
        org.opencv.android.Utils.matToBitmap(sDst, bitmap);
        sSrc.release();
        sDst.release();
        return bitmap;
    }
这里写图片描述
Native骨架化

可以看到这样处理后实现效果很不错,从而达到了目标。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,594评论 25 707
  • _ 声明: 对原文格式以及内容做了细微的修改和美化, 主要为了方便阅读和理解 _ 一. 基础 Java Nativ...
    元亨利贞o阅读 5,803评论 0 34
  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全称 Java Native Interface,Java...
    cfanr阅读 57,238评论 9 132
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,110评论 2 44
  • 感赏儿子放学回来很高兴,谈了一些学校的情况,没什么报怨。虽然儿子仍然不学习,不写作业,但他不再对学习恨之入...
    高金伟阅读 107评论 0 0