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中不是就可以了吗?
解决过程
准备工作
为了印证该方案是否可行
- 需要确定opencv安卓原生的sdk能正常使用人脸检测功能,并且作为一个aar包被unity调用
- 找到人脸检测的代码,在识别到人脸的逻辑中发送消息给unity
- 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 + 0
println "OpenCV: " +openCVersionName + " " + project.buildscript.sourceFile
android {
// namespace 'org.opencv'
compileSdkVersion 31
defaultConfig {
minSdkVersion 21
targetSdkVersion 31
versionCode openCVersionCode
versionName openCVersionName
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
targets "opencv_jni_shared"
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
packagingOptions {
doNotStrip '**/*.so' // controlled by OpenCV CMake scripts
}
}
release {
packagingOptions {
doNotStrip '**/*.so' // controlled by OpenCV CMake scripts
}
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
checkReleaseBuilds false
abortOnError 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 assemble
from('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,从而导致体验感下降
解决方法:
- 隐藏这个activity
- 魔改这个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接收到文件名,在相应的路径下读取文件然后获取字节流了
- 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";
else
suffix = 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);
// 创建一个用于写入文件的FileOutputStream
try (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;
} else
return 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();
else
return 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属性的问题
其他注意事项
- 如果按照我的实现方法,就不能只在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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(getApplicationContext(), UnityPlayerActivity.class);
startActivity(intent);
}
}
- opencv插件如何打包成aar包
2.1 在插件模块中的build.gradle最后添加
task CopyPlugin(type: Copy) {
dependsOn assemble
from('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的方式打开安卓工程,然后以我们比较熟悉的界面去进行配置的修改和打包测试
- 在unity调用opencvactivity过程中,会出现unity界面内容停止的情况
这个原因很简单,因为是从一个activity打开另一个activity,上一个activity是肯定会暂停的。那么为了体验感,就需要unity的activity保持运行。我的解决方法比较粗暴,不太优美。直接修改unity的unityplayeractivity这个脚本的生命周期。如下图,将onPause方法中的mUnityPlayer.onPause()这一行代码注释掉即可
总结,以上就是大致实现通过opencv安卓原生的sdk插件实现人脸检测的同时不影响unity本身的activity运行,然后将检测到的信息保存到本地,通知unity,unity将信息读取出来进行处理的功能了。
这个流程走通大概花了4、5天左右,中间试错成本也不低。不过总算是能勉强解决这个因为安卓主板无法调用webcam相机然后闪退从而导致无法进行人脸检测的问题了。
太痛苦了…