前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android CameraX NDK OpenCV(一)--实时灰度图预览

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

作者头像
Vaccae
发布2020-12-17 14:21:30
2.6K0
发布2020-12-17 14:21:30
举报
文章被收录于专栏:微卡智享微卡智享

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 搭建环境
  • CMakeList代码
  • YUV_420_888转为byteArray
  • 预览图像旋转的问题
  • C++中将传入的byteArray转为Mat
  • 调用JNI返回并生成图像
  • 在drawbitmap前缩放图像
  • Demo地址
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档