☰
Unity AR图像识别实战:动态添加照片到图像库的完整方案
2026/10/23 3:53:25 网站建设 项目流程

如果你在Unity里做AR 2D图像识别,AR Tracked Image这套机制八成已经接触过了。最近连续有好几个人找我聊同一个需求:用户从相册里选一张照片,别重新打包APP,直接动态加进图像库,然后手机对着这张照片就能识别并显示AR内容。听起来需求不大,做起来牵扯到的坑却不少,尤其是“动态加库”这个点,和编辑器里拖一张图片进去完全不是一回事。

这篇内容适合两类人:一类是刚接触Unity AR Foundation、正想实现图像识别的开发者;另一类是做展览、互动营销、AIoT这类场景,需要在运行时接收用户图片并加入识别库的人。我会把AR Tracked Image的工作原理、动态图像库的可行实现方式、照片处理、参数计算、还有我实际调试中踩过的坑都理一遍,尽量给出一套可以直接改着用的流程。

1. 这类需求的实际样子:为什么要动态加图

1.1 静态图库解决不了的场景

大多数Unity开发者第一次接触AR图像识别时,流程非常简单:在Project窗口右键,Create一个XR Reference Image Library,把准备好的参考图片拖进去,设置物理宽,然后挂在ARTrackedImageManager上,运行后识别就生效了。

但这里藏着两个前提:第一,图片必须是开发阶段就确定好的;第二,一旦打包发出去,图片库就被焊死在APP里了。如果主办方第二天换了一张活动海报,你就得重新出包、重新审核、重新让用户下载。对于互动展览、商店陈列、直播玩法这类频繁更新内容的场景,这等于给自己挖了个运维无底洞。

所以“动态添加相片到图像库”本质上不是炫技,而是为了解决内容更新的成本问题。用户自己拍一张照片,APP把它变成可被AR跟踪的参考图,这种能力在很多项目里甚至是核心卖点。你不需要替用户准备素材,用户自己想识别什么就识别什么。

1.2 AR Tracked Image是怎么把照片“认出来”的

说实现之前,先把原理讲透。AR Tracked Image并不是像人脸识别那样去“看懂”照片内容,它做的是特征点匹配。底层平台会从相机画面里提取一组视觉特征点,比如角点、边缘、纹理块,然后拿这些特征点和你提供的参考图做比对。匹配上了,就告诉Unity:这里有一张已知图片,它的位置和姿态是多少。

对应到Unity的AR Foundation里,核心组件是ARTrackedImageManager。它负责驱动底层图像跟踪子系统,并通过trackedImagesChanged事件广播三批数据:新增的added、每帧更新的updated、消失的removed。每个结果都是一个ARTrackedImage对象,里面有trackableId、referenceImage引用、trackingState状态、还有代表图片中心位置和朝向的transform。

这里有个很实际的理解:AR Tracked Image靠的是“特征点分布”,不是像素级对比,所以参考图绝不能是一张白纸,也不能是纯色块或者纹理过于重复的图案。你需要的是高对比、边缘清晰、几何信息丰富的图片。这直接决定了用户随手拍的照片能不能被系统识别出来。这一点在后面的照片预处理环节会反复提到。

2. 动手前把三件事理清楚:版本、图像质量、物理尺寸

2.1 环境版本与包选型

动态图像库能不能做,很多时候不是写代码的问题,是底层包版本的问题。AR Foundation版本迭代非常快,4.x、5.x、6.x在图像库API上有明显差异,实际操作前先确认版本组合能省掉大量不必要的debug时间。

我在实际项目中常用的组合是Unity 2021.3 LTS或2022.3 LTS,搭配AR Foundation 5.x对应的ARCore XR Plugin和ARKit XR Plugin。用LTS不是迷信稳定版,而是这些版本在Package Manager里组件匹配最不容易出幺蛾子,网上能查到的资料也最多。如果你的项目用的是Unity 6或者AR Foundation 6.x,API会有些变化,重点关注对应版本的官方变更记录即可。

组件建议版本说明
Unity2021.3 LTS / 2022.3 LTS函数库与文档最稳
AR Foundation5.x支持MutableRuntimeReferenceImageLibrary
ARCore XR Plugin与ARF匹配Android平台实现
ARKit XR Plugin与ARF匹配iOS平台实现

还有一个很容易被忽略的准备工作:你应该在编辑器里创建一个空的XR Reference Image Library,什么都不放,先把它挂到ARTrackedImageManager上。这一步不是多余的,因为有些平台在manager拿到null库时不会正确进入可加库状态。你先给它一个空库,运行时再往里面动态添加照片,流程会顺很多。

2.2 用户照片有没有资格成为参考图

用户从相册里选出来的照片,和我们在编辑器里精心准备的参考图差别很大。照片可能过曝、模糊、分辨率过大、方向旋转、甚至有大量杂乱背景。这些图像如果直接塞进动态库,识别率会让你怀疑人生。所以我处理用户照片时有一套固定的准备流程:

第一步,检查纹理是否可读。从相册插件之类渠道拿到的Texture2D,很多默认isReadable是false,而动态加库必须读取像素数据,所以需要先用RenderTexture中转一次。

第二步,缩放图像。我一般把最长边缩到1024像素以内。图像太大并不会提高识别率,反而让特征提取和内存占用变高。太小的也不行,特征点不够密集,识别会飘。

第三步,转成RGBA32格式。有些平台返回的纹理格式很怪,比如YV12、NV12这种相机格式,直接传给动态加库接口会报格式不支持,统一转成RGBA32最稳妥。如果你的参考图本身没颜色需求,用灰度图也可以,但RGBA32通用性最高。

第四步,尽量裁剪掉多余的背景。参考图里的有效主体区域越大,特征点越集中,匹配越准确。如果整张照片里主体只占一小块,后面全是虚化的杂景,系统会把杂景也当成特征去匹配,误识别率直接上升。

第五步,提醒用户把照片拍得“像一张海报”。不要严重反光、不要弯折、不要在暗光环境下拍。这些不是玄学,AR特征点提取依赖稳定的灰度梯度,反光和阴影会把梯度搅乱。

2.3 widthInMeters怎么算

动态加图和其他参数不同,物理尺寸不是可以随便填的数。参考图的widthInMeters是这张图在现实世界中的实际宽度,它的含义是:当相机离图片有一定距离时,系统需要根据这个尺寸来推算图片在空间里的大小和姿态。

比如一张A4纸,实际宽度大约是0.21米。如果用户选的是一张打印好的A4海报,这个值就应该填0.21。如果用户想识别的是手机屏幕上的图片,那就得填手机屏幕的实际可视宽度,比如iPhone标准款的显示区域宽度大约是0.067米。

布景实践时常见的错误是:把照片的分辨率像素值当成物理宽度来填。像素和米是两个维度的东西,图像的像素尺寸除以实际物理尺寸,得到的数值是分辨密度,这会影响系统对图片“清晰度”的判断,但系统本身的定位输出依赖的是物理尺寸。这个参数填错了,模型就会贴不到图片上,或者出现在距离图片很远的地方。

如果没有精确尺子,也可以用手机里常见的A4纸、信用卡作为参照物去估算。记住一条经验法则:在能看清图片细节的前提下,参考图的物理尺寸越大,跟踪距离可以越远;尺寸越小,越需要贴近看,保持稳定跟踪的难度也越大。

3. 动态加库的代码落地:一个可直接改的流程

3.1 加图前的纹理准备函数

先把照片预处理封装成一个函数,这样后面不管从相册拿图还是从摄像头拍图,都可以复用。这个函数解决两个核心问题:纹理不可读和尺寸过大。

private Texture2D PrepareTexture(Texture2D source, int maxSide = 1024) { if (source == null) return null; // 如果原图不可读,用RenderTexture中转 if (!source.isReadable) { RenderTexture rt = RenderTexture.GetTemporary( source.width, source.height, 0, RenderTextureFormat.Default); Graphics.Blit(source, rt); Texture2D temp = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false); temp.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); temp.Apply(); RenderTexture.ReleaseTemporary(rt); source = temp; } // 按最长边缩放,控制特征提取压力 float scale = 1f; if (source.width >= source.height) scale = maxSide / (float)source.width; else scale = maxSide / (float)source.height; int newW = Mathf.RoundToInt(source.width * scale); int newH = Mathf.RoundToInt(source.height * scale); RenderTexture rt2 = RenderTexture.GetTemporary(newW, newH, 0, RenderTextureFormat.Default); Graphics.Blit(source, rt2); Texture2D result = new Texture2D(newW, newH, TextureFormat.RGBA32, false); result.ReadPixels(new Rect(0, 0, newW, newH), 0, 0); result.Apply(); RenderTexture.ReleaseTemporary(rt2); return result; }

这个函数有几个细节要说明一下。Graphics.Blit配合ReadPixels是处理“不可读纹理”最稳定的路,比起直接改纹理本身的isReadable属性,它不需要重新导入,也不依赖相册插件的实现方式。缩放这一步别看简单,它对动态识别的稳定性影响很大。原图太大时,底层特征提取会花更多时间,而且大图里包含的杂点干扰反而更多。

3.2 核心:把Texture调度进可变库

图片准备好之后,下一步是把这张纹理真正加入运行时图像库。AR Foundation 5.x里,ARTrackedImageManager.referenceLibrary如果是一个可变库,会被转换为MutableRuntimeReferenceImageLibrary,我们调用它的ScheduleAddImageWithValidationJob方法即可。

public IEnumerator AddPhotoToLibrary(Texture2D source, string photoName, float physicalWidthMeters) { Texture2D readyTex = PrepareTexture(source); if (readyTex == null) { Debug.LogError("图像转换失败,无法加入识别库"); yield break; } if (trackImageManager.referenceLibrary is MutableRuntimeReferenceImageLibrary mutableLibrary) { var jobHandle = mutableLibrary.ScheduleAddImageWithValidationJob( readyTex, photoName, physicalWidthMeters ); // 不要在这里立刻 Complete(),会卡顿;用协程等待Job真正完成 while (!jobHandle.IsCompleted) { yield return null; } jobHandle.Complete(); Debug.Log($"已把 {photoName} 加入动态图像库,接下来可以用相机对准它"); } else { Debug.LogError("当前Unity版本或平台不支持可变运行时图像库"); } }

这段代码里有几个点值得解释。

ScheduleAddImageWithValidationJob这个方法本身是异步的,会在底层启动一个Job,底层平台会对图片做校验、提取特征、构建平台识别的描述数据。这整个过程并不是马上结束的,可能需要几十毫秒甚至几百毫秒。如果我在调用方法后立刻执行Complete(),主线程会被这个Job卡住,轻则掉帧,重则让你怀疑是不是死机了。用协程每帧去检查IsCompleted,等它真的完成后才执行Complete(),体验上要平滑很多。

还有一个问题必须提:同一个镜头下,如果你在同一帧里连续调用好几次ScheduleAddImageWithValidationJob,底层平台不一定能正确处理并发添加。稳妥起见,我通常会在脚本里加一个开关,保证同一时间只有一个协程在执行加图任务,其他图片进入队列排队等待。

3.3 绑定跟踪事件与显示

图片加进库之后,剩下的事情就交给ARTrackedImageManager了。你需要在场景里摆一个trackedImagePrefab,这个预制体会在图片被识别到时自动出现,并跟随图片的位置移动。如果只是想验证流程,预制体可以是一个简单的Cube或一个带名字的TextMesh。

监听事件的部分很简单:

private void OnEnable() { trackImageManager.trackedImagesChanged += OnTrackedImagesChanged; } private void OnDisable() { trackImageManager.trackedImagesChanged -= OnTrackedImagesChanged; } private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs args) { foreach (ARTrackedImage image in args.added) { Debug.Log($"新增识别目标: {image.referenceImage.name}"); // 在这里把AR内容挂到image.transform下 } foreach (ARTrackedImage image in args.updated) { // 图片还在画面中,更新内容的坐标与旋转 // 完整代码中会做平滑插值,避免内容抖动 } foreach (ARTrackedImage image in args.removed) { // 图片离开了视野,可以选择隐藏内容、暂停播放 } }

你可能会问,added、updated、removed三个事件分别什么时候触发。added是第一次被识别到时触发一次;updated是每一帧持续触发的;removed是彻底丢失、连续若干帧没有找到时触发。理解这个节奏很重要,比如不要把模型生成逻辑放在updated里,否则每帧都会重复创建。

3.4 从相册到AR识别的完整顺序

把所有环节串起来,完整的主流程是:

  1. 用户点击按钮,用相册插件打开系统相册,选择一张照片。
  2. 插件把照片转成Texture2D返回。
  3. 调用PrepareTexture做可读性修复和缩放。
  4. 用户在UI上输入一个可选的物理宽度,或者选择预设选项,比如“A4纸”“卡牌”“手机屏幕”。
  5. 调用AddPhotoToLibrary协程,把照片加入动态图像库。
  6. 等待Job完成后,提示用户“把相机对准这张照片”。
  7. trackedImagesChanged触发,AR内容出现在照片上。

整个流程最关键的地方在于:图片是从运行时进来的,库在运行时被修改,而不是预先设置好的。这也是“动态添加相片到图像库”的本质。有些同学会把步骤简化成“直接修改manager.referenceLibrary字段,把整个新库塞进去”,这样也能工作,但代价是图像跟踪子系统会被重置,之前已经识别到的图片会全部丢失,体验很差。使用MutableRuntimeReferenceImageLibrary则是在原有库基础之上做增量添加,更符合实际需求。

4. 经验复盘:动态加库的坑与性能控制

4.1 ARCore与ARKit在运行时变更上的表现

我调试这类功能时最大的感受是:Android和iOS在动态图像库体验上的差异非常明显,你不能只在一台iPhone上测完就说功能完成。

iOS上的ARKit对运行时参考图变更的支持相对成熟,你可以在会话进行中向库添加新图像,系统会逐步把新图纳入识别。Android上的ARCore在持续跟踪状态下修改参考图时,某些版本会要求你在添加后重新初始化图像跟踪子系统,甚至可能出现图像库更新后之前的跟踪状态全部失效的情况。这里的根源是底层SDK的设计差异,不是Unity这层能完全掩盖掉的。

所以我的建议是:Android和iOS务必分开测试,不要互相替代。Android端如果动态添加后一直识别不了,优先排查是不是AR会话需要重启。iOS端如果添加后识别延迟变长,大概率是设备正在对新图重新做特征提取,给一两秒缓冲时间再让用户对准目标,体验会正常不少。

另外,ARCore对参考图的像素尺寸和物理尺寸有更明显的要求,图片分辨率过低时可能直接拒绝加入,或者加入后完全不触发跟踪。处理照片时尽量维持在512到2048像素之间,不要拿一个8000像素的合影原图直接塞进去。

4.2 现场常见问题速查表

我把实际调试过程中容易遇到的问题整理成了一份速查表,排查时可以按这个顺序对照。

现象可能原因解决思路
日志报Texture is not readable相册返回的纹理默认不可读用RenderTexture中转,ReadPixels生成新纹理
调用动态加库后没有任何反应AR Foundation版本太老或平台不支持切换到ARF 5.x,确认referenceLibrary是MutableRuntime
Job很快就Completed,但识别目标始终不出现图片特征太少或物理尺寸错误提高对比度、缩小背景干扰、重新估算widthInMeters
添加第二张图后第一张图开始丢失并发任务或同时跟踪数量达到硬件限制排队添加,尽量控制动态库总图数
相机一靠近图片,跟踪就飘图片纹理重复、反光或光线不稳提示用户避免强光直射,选择纹理更清晰的图
动态库添加成功但重启APP后失效动态运行时状态不会持久化需要自己在本地存储图片和尺寸,启动时重新加入

这里特别说明最后一行。很多人以为动态加库之后,图片数据就永久存在于APP里了。不是的,MutableRuntimeReferenceImageLibrary是运行时的临时状态,APP重启后回到最初的空库。如果你希望用户再次打开APP还能识别上一张照片,你必须把原图、物理宽度、名称这些信息保存在本地或者服务端,启动时重新调用加库流程。这个逻辑不难,但容易被忽略,等用户第二天打开APP发现图片全没了才知道问题就晚了。

4.3 真正想做到可靠,有几个个人建议

动态图像库适合用户自选小批量的图片,比如几张开到几十张。如果你的业务需求是每天上传几百张新图片,那动态加库这种方案就不合适了,你应该考虑在服务端提前把图片处理成参考图数据,然后下发到客户端。动态加库的特征提取是在设备上现场完成的,成本和耗时都有上限。

在实际项目里,我做的最多的组合方式是“静态预置+动态补充”。主办方必须识别的核心物料,打包时写成静态库;用户自己的照片、临时替换的物料,走动态加库。这种搭配既保证了核心体验的稳定,又给了内容运营足够的灵活性。

还有一点,动态加图死不承认。AR图像识别不是OCR,它不认识文字,不解析二维码。它只能根据视觉特征确定“这里有一张和参考图很像的图片”。所以你没法期望用户拍一张纯文字的便签纸就能稳定识别,除非这张便签有足够丰富的背景纹理。这个预期管理最好放到产品文档或用户引导里,否则验收时会被当成bug提上来。

最后再分享一个小技巧:给动态加库的画面加一个调试模式,把每张参考图的“特征状态”可视化出来。比如用户选中照片后,你可以先缓存并在界面上提示“图片对比度太低,建议重新选一张”。这个调试开关在交付现场特别有用,能让对方直观感受到不是功能坏了,而是图片本身达不到识别条件。省去大量解释成本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询