Android CameraX NDK OpenCV(一)--实时灰度图预览

2020-12-17 14:21:30 浏览数 (1)

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为5350,预计阅读11分钟

前言

上一篇《Android JetPack组件CameraX使用及修改显示图像》已经实现了CameraX的相机预览使用,所以要结合OpenCV(android ndk方式)准备做点小东西,所以就先按最简单的实时灰度图显示来验证效果。

搭建环境

摄像机预览:JetPack CameraX

OpenCV版本:4.5

NDK版本:21.1.6352462

CMake版本:3.10.2

开发语言:kotlin

实现效果

关于项目搭建与NDK配置

微卡智享

关于NDK的相关配置在我以前的文章《OpenCV4Android中NDK开发(一)--- OpenCV4.1.0环境搭建》中有详细说过,有兴趣的可以看看这里面说的,本次改变主要是以后放出源码后,大家下载不用再重新修改CmakeList的文件,能直接用,所以本篇主要就是讲讲这次配置的一些区别

01

OpenCV动态库位置

下载了OpenCV4.5 Android的SDK后,在Libs动态库里我们只取了arm64-v8a和armeabi-v7a这两个架构的,主要是也让安装的包小一点,只用了这两个。

直接将两个文件夹拷贝到了创建的android项目默认生成的libs的文件夹下。

02

OpenCV头文件

在OpenCV的SDK目录sdk/native/jni/include中的opencv2整个文件夹是调用的头文件

拷贝到项目创建后默认的Cmakelists对应的目录下

03

Cmakelist设置

指定我们刚才拷贝的OpenCV动态库对应的目录,将其定义为opencvlibs的变量

设置调用头文件的目录,因为是我们拷到opencv2的文件夹和Cmakelists.txt是同一目录,所以这里获取的也是当前目录

建立了libopencv_java45的动态库,连接了上面定义的库目录下对应的CPU架构中的libopencv_java4.so的文件

CMakeList代码

代码语言:javascript复制
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

project("opencv")

#该变量为真时会创建完整版本的Makefile
set(CMAKE_VERBOSE_MAKEFILE on)

#定义变量ocvlibs使后面的命令可以使用定位具体的库文件
set(opencvlibs ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)

#调用头文件的具体路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR})

#增加我们的动态库
add_library(libopencv_java45 SHARED IMPORTED)

#建立链接
set_target_properties(libopencv_java45 PROPERTIES IMPORTED_LOCATION
        "${opencvlibs}/${ANDROID_ABI}/libopencv_java4.so")


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

file(GLOB native_srcs "*.cpp")

add_library( # Sets the name of the library.
        opencv-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        ${native_srcs})

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        opencv-lib
        -ljnigraphics
        libopencv_java45

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

04

build.gradle配置

引入CameraX的相关包

代码语言:javascript复制
dependencies {
    implementation "androidx.camera:camera-camera2:1.0.0-beta12"
    implementation "androidx.camera:camera-view:1.0.0-alpha19"
    implementation "androidx.camera:camera-extensions:1.0.0-alpha19"
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta12"
}

Cmake对应的设置

abifilters这里面就是只使用我们包中的两个CPU架构

arguments这一句是将我们拷贝到libs文件夹下的opencv的动态库一起打包进安装包中,省去了以前还要加入SourceSets的配置了

下图是以前AndroidNDKOpenCV的项目中build.gradle加入SourceSets的配置项截图

到这里,基本配置上比较重要的都说完了,接下来就要说一下在写代码过程遇到的坑及怎么填的。

开发过程中填坑记录

微卡智享

01

预览图像传入OpenCV转为Mat问题

YUV_420_888转为byteArray

上篇使用CameraX中提到过,在图像分析里面通过ImageAnalysis.Analyzer中analyze事件中进行处理。

从上图中可以看到analyze事件中传入的参数为ImageProxy,在CameraX中生成的图片格式为YUV_420_888,如果要传到OpenCV中要先进行数据的处理,这问题在网上找了好久,代码也用了好几个,可以在调用NDK过程中生成处理返回的数据就会直接崩溃。主要还是将YUV_420_888转为byteArray时出现的问题。

后来是无意中看到了有人分析OpenCV4Android的源码时里面有一块处理的,照着那个改了一个YUV_420_888转byteArray后解决。

代码语言:javascript复制
        //将ImageProxy图片YUV_420_888转换为位图的byte数组
        fun imageProxyToByteArray(image: ImageProxy): ByteArray {
            val yuvBytes = ByteArray(image.width * (image.height   image.height / 2))
            val yPlane = image.planes[0].buffer
            val uPlane = image.planes[1].buffer
            val vPlane = image.planes[2].buffer

            yPlane.get(yuvBytes, 0, image.width * image.height)

            val chromaRowStride = image.planes[1].rowStride
            val chromaRowPadding = chromaRowStride - image.width / 2

            var offset = image.width * image.height
            if (chromaRowPadding == 0) {

                uPlane.get(yuvBytes, offset, image.width * image.height / 4)
                offset  = image.width * image.height / 4
                vPlane.get(yuvBytes, offset, image.width * image.height / 4)
            } else {
                for (i in 0 until image.height / 2) {
                    uPlane.get(yuvBytes, offset, image.width / 2)
                    offset  = image.width / 2
                    if (i < image.height / 2 - 2) {
                        uPlane.position(uPlane.position()   chromaRowPadding)
                    }
                }
                for (i in 0 until image.height / 2) {
                    vPlane.get(yuvBytes, offset, image.width / 2)
                    offset  = image.width / 2
                    if (i < image.height / 2 - 1) {
                        vPlane.position(vPlane.position()   chromaRowPadding)
                    }
                }
            }

            return yuvBytes
        }

预览图像旋转的问题

刚才是解决了怎么将图片转为byteArray传入OpenCV,在处理的过程中发现预览的是竖屏图像,但是传入的图像是90度旋转过去的,所以在OpenCV中处理完后回传显示的时候也是旋转后的图像。所以考虑传入OpenCV之前就把图像先旋转过来。

以前的AndroidNDKOpenCV的Demo中,因为是Camera的预览,所以生成的图像NV21先转为了BitMap,然后做的旋转后再传入的OpenCV,当然用以前的方式也可以,不过已经在Native中接口都写好了用byteArray方式处理,如果按这个接口写法,需要先转为bitmap,再旋转,然后再把bitmap转为bytearray,因为Demo做的是实时预览,这样比较影响效率,后来也是找到一个别人写的旋转的处理的算法解决这个问题。

代码语言:javascript复制
        //后置摄像头旋转90度
        fun rotateYUVDegree90(
            data: ByteArray,
            imageWidth: Int,
            imageHeight: Int
        ): ByteArray? {
            val yuv = ByteArray(imageWidth * imageHeight * 3 / 2)
            // Rotate the Y luma
            var i = 0
            for (x in 0 until imageWidth) {
                for (y in imageHeight - 1 downTo 0) {
                    yuv[i] = data[y * imageWidth   x]
                    i  
                }
            }
            // Rotate the U and V color components
            i = imageWidth * imageHeight * 3 / 2 - 1
            var x = imageWidth - 1
            while (x > 0) {
                for (y in 0 until imageHeight / 2) {
                    yuv[i] = data[imageWidth * imageHeight   y * imageWidth   x]
                    i--
                    yuv[i] = data[imageWidth * imageHeight   y * imageWidth   (x - 1)]
                    i--
                }
                x -= 2
            }
            return yuv
        }

当然还有包括前置摄像头旋转270度等函数,我都写到了ImageUtils里面,文章最后会有Demo的下载链接。

C 中将传入的byteArray转为Mat

因为传输入的是YUV的byteArray所以生成Mat时是8UC1格式,我们还要通过cvt_color将YUA的转为BGRA。

代码语言:javascript复制
 //传入的图像转为Mat
Mat byteArrayToMat(JNIEnv *env, jbyteArray bytes, jint width, jint height) {
    try {
        Mat mBgr;
        //读取Yuv的图片数据
        jbyte *_yuv = env->GetByteArrayElements(bytes, 0);
        //加载为Mat
        Mat mYuv(height   height / 2, width, CV_8UC1, (uchar *) _yuv);

        //将Yuv420转为BGR的Mat
        cvtColor(mYuv, mBgr, COLOR_YUV2BGRA_I420);

        env->ReleaseByteArrayElements(bytes, _yuv, 0);
        mYuv.release();

        return mBgr;
    } catch (cv::Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
    }
}

02

实时显示的问题

上篇说过图像的预览窗口我们不修改数据,所以在上层又加了一个View进行绘制,生成的图片直接在View中进行绘制后发现和预览的图片大小不一致,如下图

调试中发现,ImageProxy中生成的图像默认是720*1280,上图中左上角的文字也显示了出来,而CameraX的预览里面Android内部已经把图像的缩放显示都集中进去了,所以我们如果直接按原图画上后,大小是不一样的,想要覆盖只要把生成的Bitmap图片进行缩放后再Canavs.drawbitbmp即可。

调用JNI返回并生成图像

代码语言:javascript复制
        try {
            //将ImageProxy图像转为ByteArray
            val buffer = ImageUtils.imageProxyToByteArray(imgProxy)
            //根据宽度和高度将图像旋转90度
            val bytes =ImageUtils.rotateYUVDegree90(buffer, image.width, image.height)

            if(mTypeId == 0){
                //调用Jni实现灰度图并返回图像的Pixels
                val grayPixels = grayShow(bytes!!, image.height, image.width)
                //将Pixels转换为Bitmap然后画图
                grayPixels?.let {
                    val bmp = Bitmap.createBitmap(image.height, image.width, Bitmap.Config.ARGB_8888)
                    bmp.setPixels(it, 0, image.height, 0, 0, image.height, image.width)
                    val str = "width:${image.width}" " height:${image.height}"

                    mView.post {
                        mView.drawBitmap(bmp)
                        mView.drawText(str)
                    }
                }
            }
        } catch (e: Exception) {
            Log.d("except", e.message.toString())
            mView.post { mView.drawText(e.message) }
        } finally {
            imgProxy.close()
        }

在drawbitmap前缩放图像

代码语言:javascript复制
    fun drawBitmap(bmp: Bitmap?) {
        bmp?.let {
            mBmp = Bitmap.createScaledBitmap(bmp, width,height,true)
        }
        invalidate()
    }

03

drawText文字换行

如果用原有的要drawtext实现,那当传入的字符串很长时,后面的就显示不全了,所以这里改为用StaticLayout实现,设置宽度后会自动换行

代码语言:javascript复制
    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        mBmp?.let {
            canvas?.drawBitmap(it, x, y, Paint())
        }
        mText?.let {
            val builder = StaticLayout.Builder.obtain(it, 0, it.length, textpaint, width)
            val myStaticLayout = builder.build()
            canvas?.let { t ->
                t.translate(x, y)
                myStaticLayout.draw(t)
            }
        }
    }

Demo地址

https://github.com/Vaccae/AndroidCameraXNDKOpenCV.git

0 人点赞