学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为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
完