图像识别 - 虚幻引擎基于AR根据2D贴图生成3D物体

Viewed 11

目的

  • 有个需求是需要根据现实生活中的2D图片来在游戏里生成3D物体,首先想到的是AR,故把相关的技术和流程走一遍,记录一下,以备参考。
  • 本文对基于AR的虚幻引擎2D贴图生成3D物体方案进行实践、总结和记录。
  • 本文基于 UE4.27.2,主要在 Android 平台测试,相关功能可能受引擎版本、AR SDK、目标平台影响,但即便在这些差异环境下,也都有一定的参考价值。

效果


2D图片转3D物体https://www.zhihu.com/video/1684575175486541824
* 原始图片是彩色的,但是即便是打印出来的黑白图片,也能准确识别,响应速度在毫秒级(这个其实跟图片的复杂度有关,图片越复杂、特征越明显,越好识别)。

  • 相机扫描现实场景中的图片后,在游戏里的图片实物位置显示对应的3D物体(这里用简单的模型代替,以示差异)。
  • 界面右上角会显示识别出的图片和名字。

步骤

前置条件

  • 本文假设你会新建 UE4 工程、正确配置 Android 开发环境、且能正常打出 Android 包。
  • 这一步涉及 Android 环境的配置等,如有疑问,可参考:UE5 打包 Android 全图文流程附工具软件下载(虽然是基于 UE5,但是 UE4 基本适用)

启用插件

打开工程(或新建个空工程),在 Plugins 中启用 Google ARCore 。


资源准备

准备如下资源。

  • BP_ARTest:一个基于 Actor 创建的 Blueprint ,里边挂了个 StaticMeshComponent ,用以根据追踪到的图片来显示对应的模型。


  • Pawn_ARTest:一个基于 Pawn 创建的 Blueprint ,里边挂在了一个 Camera Component ,用以显示扫描相机内容。另外,AR追踪的大部分逻辑也在这里边写了(仅为了方便,不符合工业开发的规范)。

  • GM_ARTest:我们用来做AR测试的游戏模式,一个基于 GameModeBase 创建的 Blueprint Class,其 Default Pawn Class 设置为 Pawn_ARTest 。

  • L_ARTest:一个基于 Default Level 新建的 Level 地图。为了避免挡住现实场景,把里边的 Sky Sphere、Atmospheric Fog、Floor 删掉了。为了让模型有光照效果,保留了 Light Source 。另外,对应的 GameMode 改成了 GM_ARTest 。

  • T_1 - T_5:五张用来识别追踪的贴图,建议用内容复杂的彩色贴图,格式(模式,Format)为 RGBA8 ,以免无法识别(可用 PS 查看和修改格式)。图片的 CompressionSettings 选 UserInterface2D (RGBA) ,TextureGroup 设置为 UI 。


  • DA_T_1 - DA_T_5:五个 ARCandidateImage 资源,即”指向场景中要检测的图像并提供现实生活中物体大小的资源“,名字分别为 DA_T_1 、DA_T_2、...、DA_T_5 ,其中属性 CandidateTexture 依次从 T_1 - T_5 里选,FriendlyName 合理设置,这里直接用图片名,Width 和 Height 合理设置,这里设置为 100 。



  • DA_ARTest_Session:AR 特定描述配置文件。CandidateImages 里把刚才的 DA_T_1 - DA_T_5 添加进去。MaxNumSimultaneousImagesTracked : 同时跟踪的最大图像数量。 按需设置,这里采用默认值 1。


  • WBP_ARTest:一个 UserWidget ,用以显示当前追踪的图片信息,非必须。

逻辑编写

  • BP_ARTest:新建一个名为 PicMeshes 的 Map,Key 是字符串,表示图片名字,Value StaticMesh ,表示对应的网格体。新建 SetMesh 函数,调用 StaticMesh 的 Set Static Mesh 方法,去根据识别的图片更新网格体。


  • WBP_ARTest:新建 UpdateImageUI 函数,根据传入的图片,调用 Set Brush from Texture 修改显示的图片,根据传入的字符串,修改图片的名字文本。

  • Pawn_ARTest
  • 概览
  • 我们在这里边写 AR 识别的主要逻辑。UI_ARTest 是 WBP_ARTest 类型的参数,主要用以显示当前识别的图片的信息。CurrentSpawnedActor 是 BP_ARTest 类型的参数,用以记录当前已生成的 Mesh Actor,以展示根据当前识别的图片来变化的物体。Camera 是 CameraComponent ,有相机才能看到东西。

  • 说明
  • Event BeginPlay 的 StartARSession 用以开启 AR 任务,打开相机。参数类型选 DA_ARTest_Session 。Control Screensaver ,控制屏幕是否自动进入休眠模式,非必须。顺便打印一个日志,免得屏幕上啥都没有不知道到底启动了没有。

  • Event Tick 调用了几个自定义的函数(等下解释),依次“获取识别出的图片”(Get ARDetected Image)、“更新展示网格体”(Update Static Mesh)、“更新当前识别出的图片信息UI”(Update Image in Screen)。

  • GetARDetectedImage 主要是调用 Get All AR Geometries By Class 去获取 ARTrackedImage 类型的 AR 几何体。获取到之后需要通过 Get Tracking State 判断一下状态,只有 Tracking 状态的几何体才是我们的目标(否则追踪过的也会返回)。

  • UpdateStaticMesh 主要用于根据当前识别的图片更新用于展示的网格体。如果 CurrentSpawnedActor 不存在,生成一个 BP_ARTest 并设置为 CurrentSpawnedActor,否则(或者生成之后),调用 CurrentSpawnedActor 的 Set Mesh 去根据当前识别的图片更新网格体。最后调用 SetActorTransform 去设置 CurrentSpawnedActor 的 Transform(位置、旋转和缩放等),把它跟识别的图片的信息关联起来。注意它的缩放我们设置为 0.2 了,以免太大遮住了视野。

  • UpdateImageInScreen 主要根据当前识别的图片去创建并更新 WBP_ARTest 上的图片信息,如图片和图片名字等。

打包

  • 没什么特殊的,直接调用 UE 的原始的打包方式即可打包(UE4和UE5的打包入口略有差别)

  • 也可以点击 Launch 里对应的手机,来直接在手机上启动游戏(前提是手机开了开发者模式,并且通过 USB 文件传输连接上了)。

测试

运行后(安装后首次运行)会弹出相机权限,允许之后,相机对着目标图片,会自动生成对应的模型(参见效果)。

答疑

  • AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: ".../libarcore_sdk_c.so" is for EM_386 (3) instead of EM_ARM (40)
  • 详细报错
09-07 17:45:27.979  1095  1095 D MI-SF   : isSceneExist: module = 7 (Unknown), value = 0
09-07 17:45:27.980 26850 26850 E AndroidRuntime: FATAL EXCEPTION: main
09-07 17:45:27.980 26850 26850 E AndroidRuntime: Process: com.YourCompany.ARTest, PID: 26850
09-07 17:45:27.980 26850 26850 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/~~S-UrOCyyz53B36pDtMp6KA==/com.YourCompany.ARTest-cvTh8jOKgSM9PbCH7q_MEQ==/lib/arm/libarcore_sdk_c.so" is for EM_386 (3) instead of EM_ARM (40)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at java.lang.Runtime.loadLibrary0(Runtime.java:998)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at java.lang.System.loadLibrary(System.java:1656)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at com.epicgames.ue4.GameActivity.<clinit>(GameActivity.java:6687)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at java.lang.Class.newInstance(Native Method)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:43)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.Instrumentation.newActivity(Instrumentation.java:1273)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3660)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3937)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2288)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:106)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.os.Looper.loopOnce(Looper.java:210)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:299)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:8293)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
09-07 17:45:27.980 26850 26850 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)09-07 17:45:27.982  2795  2836 D HomeWallpaperRenderer: OFFSET  x: 1.0  y: 0.0
09-07 17:45:27.982  2795  3357 W OpenGLRenderer: Surface doesn't have any previously queued frames, nothing to readback from
09-07 17:45:27.983  4003  4111 E OOMEventManager: oom event not support this pkg, dismiss this!
09-07 17:45:27.983  1963  3257 W MiuiFreeFormGesturePointerEventListener: setMiuiFreeFormTouchExcludeRegion mTouchExcludeRegion = SkRegion((0,0,1080,90)(0,2296,1080,2340))
09-07 17:45:27.985  2795  2836 D HomeWallpaperRenderer: OFFSET  x: 1.0  y: 0.0
 
  • 可能原因
  • Android设备不支持 armv7 [aka armeabi-v7a]
  • 解决方案
  • 修改 Android 打包设置,取消 Support armv7 [aka armeabi-v7a],勾选 Support arm64 [aka arm64-v8a] 。

  • java.lang.UnsatisfiedLinkError: dlopen failed: ".../libarcore_sdk_c.so" is 32-bit instead of 64-bit
  • 详细报错
09-08 10:32:37.857 14597 14597 D AndroidRuntime: Shutting down VM
09-08 10:32:37.857 14597 14597 E AndroidRuntime: FATAL EXCEPTION: main
09-08 10:32:37.857 14597 14597 E AndroidRuntime: Process: com.YourCompany.ARTest, PID: 14597
09-08 10:32:37.857 14597 14597 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/~~Bh3coASS6vm3uKzSO2cOJw==/com.YourCompany.ARTest-LCGsCTUB5P3mqup1HwsFIQ==/lib/arm64/libarcore_sdk_c.so" is 32-bit instead of 64-bit
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at java.lang.Runtime.loadLibrary0(Runtime.java:998)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at java.lang.System.loadLibrary(System.java:1656)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at com.epicgames.ue4.GameActivity.<clinit>(GameActivity.java:6687)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at java.lang.Class.newInstance(Native Method)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:43)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.Instrumentation.newActivity(Instrumentation.java:1273)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3660)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3937)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2288)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:106)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.os.Looper.loopOnce(Looper.java:210)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:299)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:8293)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
09-08 10:32:37.857 14597 14597 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)
09-08 10:32:37.858  2795  2836 D HomeWallpaperRenderer: OFFSET  x: 1.0  y: 0.0
09-08 10:32:37.859  4003  4111 E OOMEventManager: oom event not support this pkg, dismiss this!
09-08 10:32:37.860  4003 27583 W MQSEventManager: jeoccur
09-08 10:32:37.861 14600 14600 W android.browse: ClassLoaderContext parent mismatch.  (PCL[] | PCL[];PCL[])
 
  • 可能原因
  • libarcore_sdk_c.so 估计是拷贝错了,需要 64 位的结果拷贝的 32 位。
  • 解决方案
  • 可以考虑用 Engine\Source\ThirdParty\GoogleARCore\lib\arm64-v8a 下的 so (如 libarcore_sdk_c.so、libarcore_sdk_jni.so) 替换 Engine\Source\ThirdParty\GoogleARCore\lib\x86 下的。

  • 获取几何体失败或识别时间太长。
  • 可能原因
  • 图片特征不够明显
  • 解决方案
  • 用复杂点的图片,比如大点的彩色图片。
  • PackagingResults: Error: Texture T_3 is not RGBA8 or BGRA8 and cannot be used as a tracking target.
  • 可能原因
  • 图片格式(模式)不对,必须为 RGBA8 或 BGRA8 。
  • 解决方案
  • 如果是黑白图片,可能自动就是 gray 的。可以考虑直接用彩色图片,可能默认就是 RGBA8 的。
  • 用 PS 打开图片可以查看和修改图片格式
  • Failed to get enough keypoints from target image.
  • 可能原因
  • 图片(里的像素点)太简单了,特征不够,不利于识别。
  • 解决方案
  • 图片搞复杂点,比如用彩色大图。
  • 生成的 Mesh 看不到
  • 可能原因
  • 可能是太大了。
  • 解决方案
  • 把 Mesh 缩放搞小点试试
  • 注意本示例中,生成的物体的缩放是基于目标的大小的(Get Local to World Transform),直接调整 Actor 资源的缩放不会生效(蓝图里覆盖了),需要在生成时设置缩放(见步骤)。
  • 不显示现实场景
  • 可能原因
  • 现实场景被游戏场景挡住了
  • 解决方案
  • 把游戏场景中的天空盒、大气等删掉。
  • 不过即便挡住了现实场景,但是还是能正常扫描和识别物体的,可以利用这个特性来做特殊需求。
  • GetAllARGeometriesByClass 把当前没有关注但之前扫描过的物体也返回了
  • 可能原因
  • GetAllARGeometriesByClass 这个接口会把之前追踪过的以及当前正在追踪的都返回的。
  • 解决方案
  • 可对 GetAllARGeometriesByClass 的结果进一步过滤,使用 GetTrackingState 接口判断追踪状态。对于追踪过的,其值为 NotTracking ,对于正在追踪的,其值为 Tracking 。
  • 如果原图为彩色,其黑白版本能否被追踪识别?
  • 可以

链接

**声明:**本文来自公众号:GameDevLearning,转载请附上原文链接及本声明。

0 Answers