欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > 安卓学习笔记-unity调用原生opencv的sdk

安卓学习笔记-unity调用原生opencv的sdk

2024/10/24 11:26:11 来源:https://blog.csdn.net/pure81/article/details/142060476  浏览:    关键词:安卓学习笔记-unity调用原生opencv的sdk

unity调用原生opencv的sdk

  • 问题描述
  • 解决思路
  • 解决过程
    • 准备工作
      • opencv安卓原生sdk
      • 找到人脸检测的代码,检测成功后发送消息给unity
      • unity接收消息
    • 遇到的问题
      • 问题一
      • 问题二
      • 问题三
    • 如何解决遇到的问题
      • 问题一:opencvactivity遮挡unity的界面问题
      • 问题二:数据传输
      • 问题三:打包失败的一些解决方法
  • 其他注意事项

问题描述

情况描述:业务需求,在安卓系统上使用人脸检测功能,一开始使用的方案是在unity中直接调用unity资源商店中的插件“opencv-unity.unitypacage”,魔改一下关于人脸检测的脚本就可以用了。实际测试也没有问题
但是,在安卓广告机上使用发现会无缘无故闪退,经过一系列排查,最终确定是因为调用了webcamtexture之类的脚本,调取相机出现异常,导致的闪退
在网上找了两三天的帖子寻求帮助,发现这是安卓主板3588和3568不兼容unity的webcamtexture关于相机的api的脚本,因此在unity中直接使用插件进行人脸检测功能是没有什么可能了。找了技术支持也是提供不了什么解决方案的(想想也知道是个大工程…)

解决思路

那么怎么办呢?

我在排查的过程中发现,调用安卓原生的相机这些api时,这两个主板上是可以正常跑通的。(实际使用过·opencv安卓原生sdk·以及·facedetector·),调用原生相机没问题的话,那么只要将原生调用相机的插件接入到unity中不是就可以了吗?

解决过程

准备工作

为了印证该方案是否可行

  1. 需要确定opencv安卓原生的sdk能正常使用人脸检测功能,并且作为一个aar包被unity调用
  2. 找到人脸检测的代码,在识别到人脸的逻辑中发送消息给unity
  3. unity接收并处理信息(图片数据或图片路径)

opencv安卓原生sdk

在官网上下载安卓原生sdk包(SDK下载地址),然后导入到AndroidStudio中,由于它本身就是一个模块,所以很方便进行调用,在主模块中创建一个activity,继承sdk中的OpencvActivity,然后打包运行,会出现一个显示相机画面的界面,检测到人脸就会自动在人脸的部分绘制一个框框,这和unity的插件效果是一致的,然后打包到安卓主板(3588和3568)上进行测试,也是可以正常检测到的。这一步没问题
tips:需要自行处理gradle版本和compilesdk版本的问题,以及打包过程中可能出现的异常(需要在build.gradle中屏蔽掉一些代码,以及添加一些代码)时间问题就直接贴上来了

apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
//apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'def openCVersionName = "4.10.0"
def openCVersionCode = ((4 * 100 + 10) * 100 + 0) * 10 + 0println "OpenCV: " +openCVersionName + " " + project.buildscript.sourceFileandroid {
//    namespace 'org.opencv'compileSdkVersion 31defaultConfig {minSdkVersion 21targetSdkVersion 31versionCode openCVersionCodeversionName openCVersionNameexternalNativeBuild {cmake {arguments "-DANDROID_STL=c++_shared"targets "opencv_jni_shared"}}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}buildTypes {debug {packagingOptions {doNotStrip '**/*.so'  // controlled by OpenCV CMake scripts}}release {packagingOptions {doNotStrip '**/*.so'  // controlled by OpenCV CMake scripts}minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'}}lintOptions {checkReleaseBuilds falseabortOnError false}//    buildFeatures {
//        prefabPublishing true
//        buildConfig true
//    }
//    prefab {
//        opencv_jni_shared {
//            headers "native/jni/include"
//        }
//    }sourceSets {main {jniLibs.srcDirs = ['native/libs']java.srcDirs = ['java/src']res.srcDirs = ['java/res']manifest.srcFile 'java/AndroidManifest.xml'}}//    publishing {
//        singleVariant('release') {
//            withSourcesJar()
//            withJavadocJar()
//        }
//    }externalNativeBuild {cmake {path (project.projectDir.toString() + '/libcxx_helper/CMakeLists.txt')}}
}publishing {publications {release(MavenPublication) {groupId = 'org.opencv'artifactId = 'opencv'version = '4.10.0'afterEvaluate {from components.release}}}repositories {maven {name = 'myrepo'url = "${project.buildDir}/repo"}}
}dependencies {implementation files('libs\\unity-classes.jar')implementation 'com.android.support:appcompat-v7:28.0.0'
//implementation files('libs\\unity-classes.jar')
}task CopyPlugin(type: Copy) {dependsOn assemblefrom('build/outputs/aar')into('../../Assets/Plugins/Android')include(project.name + '-release.aar')
}

找到人脸检测的代码,检测成功后发送消息给unity

这一步比较简单,找到opencvactivity中的visualize方法,其中的faces.rows()就是检测到的人脸数量。目前我只需要它告诉我有人脸就行了,所以就直接在这个方法中发送消息给unity

UnityPlayer.UnitySendMessage("receiveObj", "faceresult", "人脸数量为" + faces.rows());

unity接收消息

这一步就更简单了
在场景中创建receiveObj这个对象,然后把脚本挂载到这个对象中,添加下面的代码

public void faceresult(string path){//接收到消息之后的逻辑处理}

遇到的问题

那么实际上,在按照这个思路和方案执行的过程中,会遇到很多,很多,很多小问题

问题一

如果要调用人脸校测的脚本,就要使用到opencvactivity,而观察代码可发现它是一个activity,那么调用这个activity,势必会将unity 的activiy进行遮挡,导致无法点击和看到unity的界面,十分影响体验,可以说是十分致命的问题

问题二

传输数据。在opencvactivity中获取到的数据,需要转化成二进制数据或者base64或者图片路径然后再传给unity才能使用,实际上在使用过程中发现无法正确获取到二进制和base64这两个数据

问题三

原生的opencv插件打包成aar后,在unity中调用会出现些许问题,如无法正确找到opencvactivity,无法找到主题等异常
这几个是主要的问题,其他零散的可能一时间想不起来了

如何解决遇到的问题

问题一:opencvactivity遮挡unity的界面问题

这个问题在前面的方案思路中有提到,为了使用这个插件,就需要调用这个activity,但我本身unity就有一个activity了,如果调用这个activity的话,势必会暂停unity的界面,然后显示opencvactivity,从而导致体验感下降
解决方法:

  1. 隐藏这个activity
  2. 魔改这个activity,改成fragment或者dialog
    由于时间和能力有限,方法1是我的唯一选择
    那么这个方法在网上能找到很多解决方案
    我是这么解决的

1.1 在res的values文件夹中的style.xml和theme.xml文件中,添加关于透明主题和样式的信息
style.xml

<style name="TranslucentActivity" parent="Theme.AppCompat.Light.NoActionBar">//无标题<item name="android:windowIsTranslucent">true</item>//透明</style>

theme.xml

<resources><style name="TranslucentActivity.NoActionBar"><item name="windowActionBar">false</item><item name="windowNoTitle">true</item></style><style name="TranslucentActivity.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /><style name="TranslucentActivity.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

1.2 在app的AndroidManifest.xml的application节点中添加

android:theme="@style/TranslucentActivity"

1.3 在opencvactivity脚本中设置一些参数
需要将这个opencvactivity脚本对应的view进行隐藏,去掉点击事件和返回事件。也就是在onCreate方法的最后添加下面的代码

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);getWindow().addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);setContentView(R.layout.face_detect_surface_view);mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.fd_activity_surface_view);
//        mOpenCvCameraView.setVisibility(CameraBridgeViewBase.VISIBLE);mOpenCvCameraView.setCvCameraViewListener(this);// 设置透明沉浸状态栏if (Build.VERSION.SDK_INT >= 21) {View decorView = getWindow().getDecorView();decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); //使背景图与状态栏融合到一起,这里需要在setcontentview前执行getWindow().setStatusBarColor(Color.TRANSPARENT);}//设置1像素Window window = getWindow();window.setGravity(Gravity.LEFT | Gravity.TOP);WindowManager.LayoutParams params = window.getAttributes();params.x = 0;params.y = 0;params.height = 1;params.width = 1;window.setAttributes(params);

1.4 可能会出现的异常(attr之类的)
需要在app的builder.gradle中添加

implementation 'com.android.support:appcompat-v7:28.0.0'implementation 'com.android.support.constraint:constraint-layout:1.1.3'

通过以上步骤应该就可以实现将opencvactivity对应的view进行隐藏掉的功能了

问题二:数据传输

因为没时间研究sdk的逻辑和代码,所以我只是大概的看了一下,发现将获取到的人脸信息转成byte[]或者base64再传给unity的话,他的值始终是不变的。原因还没有研究,但是这个路子行不通的话就只能在sdk检测到人脸之后存到本地,然后返回给unity一个文件名,unity接收到文件名,在相应的路径下读取文件然后获取字节流了

  1. sdk保存图片到本地(路径我设置为了私有路径,也即是包名下的路径)
    在opencvactivity的visualize中添加savealum方法,savealbum方法和相应的其他方法如下
 public static String saveAlbum(Context context, Mat rbga, Bitmap.CompressFormat format, int quality, boolean recycle) {Bitmap bitmap = null;bitmap = Bitmap.createBitmap(rbga.cols(), rbga.rows(), Bitmap.Config.ARGB_8888);ByteArrayOutputStream byStream = new ByteArrayOutputStream();bitmap.compress(Bitmap.CompressFormat.PNG, 100, byStream);byte[] byteArray = byStream.toByteArray();Utils.matToBitmap(rbga, bitmap);String suffix;if (Bitmap.CompressFormat.JPEG == format)suffix = "JPG";elsesuffix = format.name();String fileName = System.currentTimeMillis() + "_" + quality + "." + suffix;if (Build.VERSION.SDK_INT < 29) {if (!isGranted(context)) {Log.e("ImageUtils", "save to album need storage permission");return null;}File picDir = Environment.getExternalStoragePublicDirectory("");File destFile = new File(context.getFilesDir(), fileName);if (!save(bitmap, destFile, format, quality, recycle))return null;Uri uri = null;if (destFile.exists()) {uri = Uri.parse("file://" + destFile.getAbsolutePath());Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);intent.setData(uri);context.sendBroadcast(intent);}return fileName;} else {// 获取内部存储的目录File dir = context.getExternalFilesDir(null);// 创建文件对象File file = new File(dir, fileName);// 创建一个用于写入文件的FileOutputStreamtry (FileOutputStream fos = new FileOutputStream(file)) {// 压缩图片到文件输出流中(这里以PNG格式为例)// 注意:你也可以选择其他格式,如JPEG,但需要使用不同的compress方法bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);return fileName;} catch (IOException e) {e.printStackTrace();return "fail";}}//        return Base64.encodeToString(byteArray, Base64.DEFAULT);}private static boolean save(Bitmap bitmap, File file, Bitmap.CompressFormat format, int quality, boolean recycle) {if (isEmptyBitmap(bitmap)) {Log.e("ImageUtils", "bitmap is empty.");return false;}if (bitmap.isRecycled()) {Log.e("ImageUtils", "bitmap is recycled.");return false;}if (!createFile(file, true)) {Log.e("ImageUtils", "create or delete file <$file> failed.");return false;}OutputStream os = null;boolean ret = false;try {os = new BufferedOutputStream(new FileOutputStream(file));ret = bitmap.compress(format, quality, os);if (recycle && !bitmap.isRecycled()) bitmap.recycle();} catch (IOException e) {e.printStackTrace();} finally {try {if (os != null)os.close();} catch (IOException e) {// ignore}}return ret;}private static boolean isEmptyBitmap(Bitmap bitmap) {return bitmap == null || bitmap.isRecycled() || bitmap.getWidth() == 0 || bitmap.getHeight() == 0;}private static boolean createFile(File file, boolean isDeleteOldFile) {if (file == null) return false;if (file.exists()) {if (isDeleteOldFile) {if (!file.delete()) return false;} elsereturn file.isFile();}if (!createDir(file.getParentFile())) return false;try {return file.createNewFile();} catch (IOException e) {return false;}}private static boolean createDir(File file) {if (file == null) return false;if (file.exists())return file.isDirectory();elsereturn file.mkdirs();}private static boolean isGranted(Context context) {return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE));}

保存本地图片的方法我是在网上扒的,网上在安卓10以后都可以访问到DCIM文件夹了,他们也大多都是使用的这个方法,但unity是不支持访问这些路径的,所以我还是改成了保存到报名下的私有路径中,这样unity就可以直接通过Application.persistdatapath这个api去访问文件了
然后只需要visulize这个方法中保存图片成功后,将图片名称返回给unity就可以了

问题三:打包失败的一些解决方法

问题三关于打包过程的问题在问题1中已经解决的差不多了,其他的可能就是一些版本问题,可以查看一下和我下面的信息是否有出入
需要实现一些样式的插件,否则在打包的时候会提示无法找到对应的UI属性的问题
因为compilesdkversion版本导致闪退的问题
在unity中可能会出现的无法正常拉取opencvactivity导致闪退的异常问题

其他注意事项

  1. 如果按照我的实现方法,就不能只在unity中打包,因为unity原生默认的主题是黑色的(涉及到unityplayeractivity这个类,而这个类里的方法都是在unityclass.jar中的,超出了我的能力范围),这个我尝试过改成透明的,这样会导致整个unityactivity都变成透明度的,这样是不合理的现象,所以你需要
    1.1 unity中打包成安卓工程,而不是apk。
    1.2 然后导入到androidstudio项目中,最好新建一个
    1.3 新建一个项目,然后在app的src中创建一个activity,这个activity的作用很简单,就是调用unityplayer这个脚本,如下
public class StartActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);Intent intent = new Intent(getApplicationContext(), UnityPlayerActivity.class);startActivity(intent);}
}
  1. opencv插件如何打包成aar包
    2.1 在插件模块中的build.gradle最后添加

task CopyPlugin(type: Copy) {dependsOn assemblefrom('build/outputs/aar')into('../../Assets/Plugins/Android')include(project.name + '-release.aar')
}

然后在androidstudio如下图点击对应的copyplguins然后运行即可
在这里插入图片描述
2.2 直接在如下图中点击生成
在这里插入图片描述
如果找不到上图的gradle的命令选项的话,需要在settings中打开,如下图
在这里插入图片描述
3. 小技巧
可以直接使用unity的library中的路径在androidstudio中直接打开unity打包的项目,然后使用androidstudio进行打包,不用unity的打包。如下图
在这里插入图片描述

找到对应的路径,然后在androidstudio中打开,就可以用androidstudio进行打包了
这一个的目的是因为unity中修改一些配置类的文件比较繁琐,所以可以直接在library中以androidstudio的方式打开安卓工程,然后以我们比较熟悉的界面去进行配置的修改和打包测试

  1. 在unity调用opencvactivity过程中,会出现unity界面内容停止的情况
    这个原因很简单,因为是从一个activity打开另一个activity,上一个activity是肯定会暂停的。那么为了体验感,就需要unity的activity保持运行。我的解决方法比较粗暴,不太优美。直接修改unity的unityplayeractivity这个脚本的生命周期。如下图,将onPause方法中的mUnityPlayer.onPause()这一行代码注释掉即可
    在这里插入图片描述

总结,以上就是大致实现通过opencv安卓原生的sdk插件实现人脸检测的同时不影响unity本身的activity运行,然后将检测到的信息保存到本地,通知unity,unity将信息读取出来进行处理的功能了。
这个流程走通大概花了4、5天左右,中间试错成本也不低。不过总算是能勉强解决这个因为安卓主板无法调用webcam相机然后闪退从而导致无法进行人脸检测的问题了。

太痛苦了…

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com