简介:面向Unity开发者的Rokid AR眼镜扫码识别功能源码包,基于Unity 2022.3.56f1c1与UXR3.0.3 SDK,适配Rokid Max Pro与Station Pro,适合在工业、物流、医疗等专业场景中做快速无接触信息读取。实现思路是抓取眼镜端摄像头画面,通过CameraPreview类获得预览纹理,再借助ZXing插件解析二维码或条形码,识别特定码值后触发对应业务逻辑,可覆盖资产追踪、设备点检、仓库管理等常见AR应用。压缩包为7z格式,共728个文件,以C#脚本、Unity场景与Prefab、材质与Shader、纹理图片和配置文件为主,整体15.87MB,结构清晰,便于按模块修改或调试。已有196人学习下载。资源内含可集成的完整C#源码、示例场景、配套说明文档及相关依赖,同时保留贴图、动画、字体等素材,既能快速验证扫码效果,也适合在此基础上扩展完整Rokid AR业务。导入后可从CameraPreview与ZXing的调用关系入手快速上手。
1. 在 Rokid 眼镜上做扫码识别:先解决「视频流怎么进 Unity」,再谈二维码解码
标题里「Unity3d C# 基于 RokidAR 眼镜扫码识别功能源码」这个组合,真正卡住新手的反直觉结论是:二维码解码本身只占一小部分,最难的是把 Rokid 眼镜(或它连接的主机/手机)摄像头画面,稳定地变成 Unity 里一张 Texture2D,再喂给 ZXing 解码库。如果你现在打算在 Rokid AR 眼镜上做扫码开锁、设备绑定、展厅打卡这类功能,这篇文章就是照着落地用的。读者最好有 Unity 基础,会打包 Android 工程;不要求懂 Rokid 私有 SDK,因为常见做法是用标准 Android 相机能力就能完成采集,这套方案同样能复用到其他一体式 AR 设备上。
2. 方案选型与工程骨架:为什么自己搭 C# 视频帧管线而不是用现成插件
2.1 三条路线对比:官方 SDK、商店插件、自建 ZXing 管线
做 Rokid 眼镜扫码,大部分团队会先走三条路,我按踩坑成本从高到低排一下,你对照自己的工期选。
第一条是 Rokid 官方 SDK。部分 Rokid 眼镜是分体式设计,眼镜本体负责显示和传感器,算力在 Rokid Station 或手机端。官方 SDK 确实提供了视频透传、SLAM 等能力,但扫码识别这个需求通常在业务应用层,官方不一定开放裸摄像头帧给三方应用,即便开放,不同型号的接口也有差异。如果只是「扫个码触发一个事件」,为了接私有的视频流接口去啃文档,交付风险偏大。
第二条是 Unity 商店里的现成扫码插件。这些插件本质上是「ZXing 的 C# 移植 + Unity 相机封装」打包出售,看起来省事,但典型问题有三个:内部实现是个黑匣子,横竖屏旋转、像素格式、纹理方向这些参数没法调;Rokid 这类分体式设备上摄像头枚举顺序跟手机不一样,插件往往写死了devices[0],结果扫的是错误摄像头;最要命的是遇到识别率不达标,你连改哪里都不知道。我自己用过两次,后面全推倒重写了。
第三条是自建管线:用 Unity 的WebCamTexture采集摄像头画面,每帧取像素字节,直接传给 ZXing.Net 的RGBLuminanceSource,解码结果通过线程安全队列抛回主线程更新 UGUI。这条路线代码量不大,每一层都是你控制的,出问题能看到是采集慢、字节布局错、还是解码超时。对扫码这种明确需求,我一般直接选第三条。
2.2 工程目录拆分:采集、解码、UI 互相解耦
自建管线最怕写成一个大类:摄像头初始化、解码逻辑、UI 刷新全堆在MonoBehaviour里,参数想调一个就得动别的。我常用的工程结构是这样:
Assets/ Scripts/ Capture/ RokidCameraController.cs // 摄像头初始化、暂停恢复、帧采样 Decoder/ CodeDecoder.cs // ZXing 封装,输入字节数组,输出结果 ScanStrategy.cs // ROI、冷却时间、降级策略 UI/ ScanOverlay.cs // 扫码框、结果文本、状态提示 Core/ ScanResultQueue.cs // 跨线程结果队列 Plugins/ Android/ // 如果后续要接原生 AAR 放在这里 ThirdParty/ ZXing/ // ZXing.Net 源码拷贝或 DLL这个拆法的核心是:采集层不引用 UI 层,解码层不知道摄像头长什么样。RokidCameraController只管把每一帧的Color32[]或原始字节交给CodeDecoder,CodeDecoder返回Result放进队列,ScanOverlay只在Update里排空队列。这样后期从WebCamTexture换成 Android 原生 Camera2 API 时,只需要改RokidCameraController一个文件。
ScanResultQueue用ConcurrentQueue<string>而不是直接回调事件,原因在 3.3 节详细说,这里先记住:Unity 的 API 不能跨线程调用,队列是默认的解耦手段。
2.3 Android 构建设置:权限声明、IL2CPP 裁剪和 ARM64
工程结构定了,先别急着写代码,构建配置有 3 个地方必须在动手前确认,否则代码对了也会在真机上翻车。
第一是权限声明。WebCamTexture在 Android 上需要相机权限,Unity 打包时会在AndroidManifest.xml里自动合并,但我建议你主动在Plugins/Android/AndroidManifest.xml里显式声明,避免某些 Rokid 主机系统对权限合并结果敏感:
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera.any" android:required="false" />注意required="false",这表示没有摄像头也能安装,防止某些纯显示模式的 Rokid 设备在应用市场里被过滤掉。
第二是 IL2CPP 裁剪。ZXing 底层对条码格式的判断依赖反射,Unity 在 IL2CPP 下默认的 Managed Stripping Level 如果设成 High/Medium,会把 ZXing 里按字符串类型查找BarcodeFormat的代码剪掉,真机上表现就是「扫码永远抛ReaderException,或者直接提示TypeLoadException」。常见做法是把 Stripping Level 设为 Low,同时在link.xml里保留 ZXing 程序集:
<linker> <assembly fullname="ZXing" preserve="all" /> </linker>第三是 ARM64。现在 Rokid 主机基本都是 arm64 系统,Player Settings > Other Settings > Target Architectures里勾上 ARM64。如果你在工程里引用了任何只有 armeabi-v7a 的原生库,尽早换成带 arm64 的版本,不要等真机跑起来出现DllNotFoundException再回头查。
3. 从摄像头纹理到 ZXing 解码:一套可直接复制的 C# 核心链路
3.1 运行时权限与摄像头初始化(兼容 Rokid 分体式主机)
WebCamTexture的初始化看起来只有几行,但放到 Rokid 分体式设备上有两个坑:一是主机可能挂了多个摄像头设备(眼镜上的双目摄像头 + 主机自带摄像头),枚举顺序不稳定;二是 Android 6.0 以后必须运行时申请权限。
先处理权限:
using UnityEngine; using UnityEngine.Android; public class RokidCameraController : MonoBehaviour { private WebCamTexture camTexture; private IEnumerator Start() { // 权限弹窗是异步的,这里必须等待用户操作完成再启动相机 if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Permission.RequestUserPermission(Permission.Camera); // 轮询等待,避免立刻调用 WebCamTexture 导致黑屏 while (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { yield return null; } } StartCamera(); } private void StartCamera() { WebCamDevice[] devices = WebCamTexture.devices; if (devices.Length == 0) { Debug.LogError("Rokid: 未检测到摄像头设备"); return; } // 优先选非前置摄像头;分体式主机上眼镜端设备名通常含 Camera 字样 int targetIndex = -1; for (int i = 0; i < devices.Length; i++) { if (devices[i].isFrontFacing) continue; targetIndex = i; Debug.Log($"Rokid: 选择摄像头 {i} = {devices[i].name}, 前置={devices[i].isFrontFacing}"); break; } if (targetIndex < 0) targetIndex = 0; camTexture = new WebCamTexture(devices[targetIndex].name, 640, 480, 15); camTexture.Play(); } }这段代码有两个设计意图。第一个意图是「轮询等待权限」,Permission.RequestUserPermission在 Android 上会弹系统对话框,但 Unity 的异步回调比较隐晦,直接用while轮询标志位最直观,注意要放在协程里,不能阻塞主线程。第二个意图是「摄像头选择尽量不硬编码」,Rokid 设备上WebCamTexture.devices可能返回两三个设备,isFrontFacing可以帮助排除前置,但有些眼镜双目的两个设备都标记为非前置,这就需要在 3.2 节的调试信息里确认设备名,然后在代码里加白名单:优先匹配"CAM"、"Camera"这类关键字。
分辨率这里先写 640x480,为什么不用 1080p,第 4 章细讲。
3.2 每帧采样:用 BGRA32 字节直接喂 ZXing,避免 GetPixels 拖垮帧率
摄像头转起来之后,每帧要做三件事:取像素、转亮度、解码。网上的旧教程会写camTexture.GetPixels32()拿Color32[],再转成byte[]数组,这个写法在 640x480 下勉强能跑,但到了 1280x720 就会把主线程的帧耗时拉到 20 毫秒左右,配合解码耗时,整机帧率直降 10 帧。我改用Texture2D.GetRawTextureData<byte>()直接读底层字节:
using ZXing; using ZXing.Common; using UnityEngine; public class CodeDecoder : MonoBehaviour { [Header("解码参数")] public float decodeInterval = 0.2f; // 相邻两次解码最小间隔(秒) public bool useRoi = true; public Rect roiRect = new Rect(0.2f, 0.2f, 0.6f, 0.6f); // 默认取画面中心 60% private MultiFormatReader reader; private Texture2D frameTexture; private byte[] frameBytes; private float lastDecodeTime; private void Awake() { var hints = new System.Collections.Generic.Dictionary<DecodeHintType, object> { { DecodeHintType.POSSIBLE_FORMATS, new System.Collections.Generic.List<BarcodeFormat> { BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128, BarcodeFormat.EAN_13 } }, { DecodeHintType.TRY_HARDER, true }, { DecodeHintType.CHARACTER_SET, "UTF-8" } }; reader = new MultiFormatReader(); reader.decode(Array.Empty<byte>() == null ? null : new BinaryBitmap(new HybridBinarizer(new RGBLuminanceSource(new byte[0], 1, 1))), hints); } }等等,上面Awake里那段是我为了演示hints的构造写出来的,不能直接这么用。正确的做法是每次解码时把hints传给decode(bitmap, hints),而且不要在Awake里用一个假亮度源去「预热」,这是多余的。
重新把解码封装写清楚:
private void DecodeFrame(byte[] rawPixels, int width, int height) { if (rawPixels == null || rawPixels.Length < width * height * 4) return; // Unity 在 Android 上 WebCamTexture 内部像素常见为 BGRA32 // ZXing 的 RGBLuminanceSource 支持直接按 BGRA32 读,不需要手动转 RGB var luminance = new RGBLuminanceSource(rawPixels, width, height, RGBLuminanceSource.BitmapFormat.BGRA32); LuminanceSource source = luminance; if (useRoi) { int left = Mathf.FloorToInt(width * roiRect.x); int top = Mathf.FloorToInt(height * roiRect.y); int cropW = Mathf.FloorToInt(width * roiRect.width); int cropH = Mathf.FloorToInt(height * roiRect.height); // 防止越界 if (left + cropW > width) cropW = width - left; if (top + cropH > height) cropH = height - top; if (cropW > 0 && cropH > 0) { source = luminance.crop(left, top, cropW, cropH); } } var bitmap = new BinaryBitmap(new HybridBinarizer(source)); try { var result = reader.decode(bitmap, hintsDict); if (result != null && !string.IsNullOrEmpty(result.Text)) { // 结果进队列,不在解码线程里直接操作 UI ScanResultQueue.Instance.Push(result.Text); } } catch (ReaderException) { // 没有识别出码,正常现象;解码失败不要抛异常,影响 GC } }逻辑说明:RGBLuminanceSource构造函数的第三个参数是像素格式枚举,我这里用的是BGRA32,与 Unity Android 端GetRawTextureData的常见布局对应。如果你在部分设备上发现识别率很低,可以把枚举换成RGBA32对比测试,这是因为部分 Rokid 主机系统对视频帧做了格式转换。
参数说明:useRoi默认开,裁剪出的区域越小,解码越快,但注意crop的结果坐标是相对裁剪区域的,最终拿到条码位置时要换算回全图坐标。decodeInterval是帧间隔,下一节讲怎么调。
3.3 解码结果回调:跨线程投递与主线程刷新 UGUI
ZXing 的decode是 CPU 密集型同步操作,把它放在Update里直接跑会卡 UI,常见做法是丢到子线程或协程里,但 ZXing 解密时需要读 Unity 纹理数据,必须保证纹理不被销毁。我自己更稳妥的方案是:在Update里取好像素字节,丢到线程池解码,结果进ConcurrentQueue,UI 在下一帧排空队列。
using System.Collections.Concurrent; using UnityEngine; using UnityEngine.UI; public class ScanResultQueue : MonoBehaviour { public static ScanResultQueue Instance { get; private set; } private readonly ConcurrentQueue<string> codes = new ConcurrentQueue<string>(); [SerializeField] private Text resultText; [SerializeField] private GameObject scanDonePanel; private void Awake() { Instance = this; } public void Push(string code) { codes.Enqueue(code); } private void Update() { // 每帧最多处理一条,避免连续弹窗;跟过 UGUI 源码就知道, // 频繁改 Text.text 会触发 Canvas 标记 dirty,下一帧重建几何,批量处理更稳 while (codes.TryDequeue(out string code)) { resultText.text = code; scanDonePanel.SetActive(true); break; } } }这里有个细节:ConcurrentQueue的TryDequeue出队之后,如果 UI 还没消费完,消息就丢了。所以我每帧只取一条并且立即刷新 UI,如果扫到长串 JSON 文本,Text组件有自动换行,不用额外处理。另一个注意点是ScanResultQueue用单例模式,但Awake里不能在类加载时就访问 UI 组件,必须保证ScanOverlay等 UI 脚本先于它执行,否则resultText是空的。解法是把resultText的赋值放到ScanOverlay的Start里,或者用[DefaultExecutionOrder(-100)]控制脚本执行顺序。
4. 解码参数与性能调节:ROI、分辨率和帧间隔怎么配合
4.1 分辨率不是越高越好:720p 和 15fps 的默认组合
很多新手第一反应是把摄像头分辨率拉满,觉得 1080p 看得清楚就能扫得更快。实际在 Rokid 这类分体式设备上,分辨率和帧率不是越高越好。扫码识别关注的是条码在图像中的像素高度,一个 50x50 像素的二维码,在 640x480 下占 1/9 面积,在 1920x1080 下占 1/36 面积,分辨率翻倍,条码反而变小,ZXing 的HybridBinarizer二值化更容易受噪声干扰。
我从几个项目里沉淀出的默认值是:640x480 + 15fps。这个组合下,GetRawTextureData每帧数据量只有 1.2MB,解码一次耗时大约 30~60 毫秒(取决于 ROI 大小和条码密度),帧间隔 200 毫秒留给解码充足时间,不至于拖垮主线程。如果一定要用 720p,那解码间隔必须拉到 300 毫秒以上,否则 CPU 满载后摄像头帧率自己会掉下来,反而更慢。
各参数组合的适用场景我整理成表:
| 分辨率 | 帧率 | 推荐场景 | 注意点 |
|---|---|---|---|
| 320x240 | 10~15 | 快速原型、性能压测 | 条码过小时识别率低,只用于功能验证 |
| 640x480 | 15 | 默认首选 | 平衡解码耗时与识别率,大多数单码场景够用 |
| 1280x720 | 15 | 条码很小/带环境框 | 解码间隔至少 0.3s,注意帧数据拷贝耗时 |
| 1920x1080 | 30 | 基本不推荐 | 每帧 CPU 负载过高,Rokid 主机容易发热降频 |
4.2 用「先粗扫后细扫」替代每一帧全力解码
decodeInterval用来控制解码频率,但更聪明的做法是分两级策略。第一级:用关闭TRY_HARDER的低成本模式扫全图,每秒扫 3 次;如果连续 1.5 秒没结果,进入第二级:打开TRY_HARDER,同时启用 ROI 裁剪,扫描画面中心 60% 区域,把解码频率降到每秒 2 次。这样环境里没有码时 CPU 开销很小,而码出现在 ROI 边缘时也能通过粗扫兜底。
实现上,我在CodeDecoder里加了状态切换:
private enum ScanMode { Coarse, Fine } private ScanMode currentMode = ScanMode.Coarse; private float modeSwitchTimer; private bool UpdateScanMode(float deltaTime) { modeSwitchTimer -= deltaTime; if (currentMode == ScanMode.Coarse && modeSwitchTimer <= 0f) { currentMode = ScanMode.Fine; modeSwitchTimer = 1.5f; // 细扫维持 1.5 秒,仍无结果则回到粗扫 return true; } if (currentMode == ScanMode.Fine && modeSwitchTimer <= 0f) { currentMode = ScanMode.Coarse; modeSwitchTimer = 1.0f; return true; } return false; }切换时重新构造hints,Coarse 模式下不加TRY_HARDER;Fine 模式下加上TRY_HARDER并启用 ROI。这个策略对「手机屏幕上的二维码」这类需要快速响应的场景特别有效:粗扫先捕获,Fine 模式用来确认,减少误报。
4.3 触发判定的 3 个参数:消抖次数、冷却时间、最大重试
解码出结果之后,严谨的扫码交互还需要三个参数来防止「扫到一个码就触发十次事件」。
第一个是消抖次数:同一个文本内容连续识别 N 次后才向外抛一次事件。N 一般设 3,N=1 容易误触发,N=5 以上对快速扫码不友好。第二个是冷却时间:抛完一次事件后,同一内容在 2~3 秒内不再重复上报,防止人眼还在看码时后台疯狂触发。第三个是最大重试次数:如果扫码框已经对准目标,但 5 秒内连续失败,建议把 ROI 临时扩大到全屏扫一次,并降低TRY_HARDER,因为有可能条码在 ROI 边缘被截断。
下面这个片段是消抖和冷却的实现骨架:
public class ScanEventDispatcher : MonoBehaviour { private string lastCode; private int sameCodeCount; private float cooldownRemain; public void Submit(string code) { if (cooldownRemain > 0f && code == lastCode) return; if (code == lastCode) { sameCodeCount++; } else { lastCode = code; sameCodeCount = 1; } if (sameCodeCount >= 3) { cooldownRemain = 2.5f; sameCodeCount = 0; Dispatch(code); // 这里才真正调用业务逻辑,比如设备绑定 API } } private void Update() { cooldownRemain -= Time.deltaTime; } }这段逻辑可以独立于 UI 层存在,方便后面接 WebSocket 或 REST 接口。实际项目中,扫码结果常常是 JSON 字符串,比如{"deviceId":"RK-001","token":"abc"},你在Dispatch里用JsonUtility解析时要注意:Unity 的JsonUtility不直接支持 JSON 数组最外层,需要先包一层对象。这一点在扫码场景里非常容易踩,我放在第 5 章一起说。
5. 避坑记录:Rokid 眼镜扫码开发里的 5 个常见问题
5.1 IL2CPP 裁剪把 ZXing 裁掉:现象、原因与编译期规避
现象:电脑上编辑器里扫码一切正常,打包成 APK 装到 Rokid 主机上,一运行就抛TypeLoadException或NotSupportedException,日志里指向ZXing.QrCode.Internal.QRCodeReader。
原因:IL2CPP 的托管代码裁剪器认为 ZXing 里大量通过字符串映射BarcodeFormat的代码没有被直接引用,把它们从最终包里去掉了。ZXing 的源码里有个DecodeHintType.POSSIBLE_FORMATS分支,运行期根据 List 里的类型去实例化对应 Reader,裁剪器看不到这条动态调用链。
解决:Player Settings > Managed Stripping Level改成 Low,同时加link.xml保留 ZXing 程序集。如果不想保留整个程序集,至少要保留ZXing.MultiFormatReader、ZXing.Common.HybridBinarizer和ZXing.QrCode.QRCodeReader这几个类型。
5.2 选错摄像头:分体式主机多摄设备的设备名过滤
现象:扫码画面黑屏,或者画面显示的是主机前置镜头拍的地板,而不是眼镜朝前看到的现实场景;更有意思的画面是「左眼和右眼画面同时并排出现」。
原因:Rokid 眼镜如果带双目摄像头,会在系统里注册两个摄像头节点,Unity 的WebCamTexture.devices会列出它们,主机自身通常还有一颗前摄。而devices[0]不稳定,由驱动发现顺序决定。
解决:在初始化时把每个设备名打印出来,真机跑一次后做白名单过滤。我在代码里用Debug.Log输出设备名和isFrontFacing,然后按优先级匹配:先找名字含"CAM"或"Camera"的,再排除前置,最后如果还有两个候选,用WebCamTexture.GetDeviceName保存到 PlayerPrefs,下一次启动直接按上次成功设备初始化。需要注意,WebCamTexture.devices必须在Play()之前读取,而且要在权限授予之后,否则可能返回空数组。
5.3 旋转角没处理:横竖屏混合时的识别率暴跌
现象:在 Rokid 主机上应用是竖屏,但眼镜光学显示是横向 16:9,扫码框对准了二维码,ZXing 却一直报失败;偶尔成功了,扫出来的内容带乱码或前面多了几个字符。
原因:摄像头 CMOS 的方向和屏幕方向不一致,WebCamTexture有videoRotationAngle属性,常见值是 0 或 90。当 angle 为 90 时,原始图像是旋转过的,条码在 ZXing 看到的像素矩阵里其实被拉伸成了斜向条,二值化后阈值误判。
解决:在解码前读取camTexture.videoRotationAngle,不为 0 时对原始字节做一次旋转拷贝,或者直接使用 ZXing 的LuminanceSource.rotateCounterClockwise()。ZXing 的RGBLuminanceSource继承自带rotateCounterClockwise()方法,不需要你手写像素旋转。代码这样处理:
int angle = webCamTexture.videoRotationAngle; RGBLuminanceSource lumin = new RGBLuminanceSource(rawPixels, w, h, BitmapFormat.BGRA32); if (angle == 90) { lumin = (RGBLuminanceSource)lumin.rotateCounterClockwise(); }这里要注意:旋转后lumin的宽高会互换,后面crop的 ROI 坐标也必须跟着变,否则会裁剪到图像边界外。
5.4 后台恢复黑屏:OnApplicationPause 的摄像头状态机
现象:应用在 Rokid 主机上切换到后台(比如弹出系统权限对话框、通知栏下拉),再回前台,画面黑屏,扫码没反应;或者画面冻结在切出去之前的那一帧。
原因:Android 系统在应用进入后台后回收了摄像头资源,WebCamTexture没有自动恢复。Unity 不会替你Play(),需要自己监听生命周期。
解决:实现一个状态机,收到OnApplicationPause(true)就camTexture.Stop(),清零解码结果;收到OnApplicationPause(false)重新Play(),并且重置lastDecodeTime,防止恢复后第一帧就解码导致纹理还没准备好。
private void OnApplicationPause(bool pause) { if (pause) { camTexture.Stop(); reader.reset(); } else { camTexture.Play(); lastDecodeTime = 0f; } }注意reader.reset()一定要调用。ZXing 的MultiFormatReader内部缓存了上一次解码状态,不重置的话,恢复后台后第一次decode可能直接失败。
5.5 眼镜光学反光导致误识别:ROI、闪光灯和阈值
现象:在室内灯光下扫手机屏幕上的二维码,扫码框在白屏反光区域反复触发;或者扫码结果里偶发乱码。
原因:Rokid 眼镜的光学方案是波导/半透膜,环境光会在眼镜前方形成一层反光,手机屏幕本身也有摩尔纹干扰。ZXing 的HybridBinarizer对这类「局部过曝」很敏感,会把亮斑误判成条码边界。
解决:先缩小 ROI 到画面中心 50% 区域,避开边缘反光带;然后把条码格式白名单收紧,比如只保留QR_CODE,能显著降误报。如果目标条码在深色材质上,还可以临时把RGBLuminanceSource的亮度算法从默认改为只取绿色通道——ZXing 的灰度公式是0.3R+0.59G+0.11B,但对某些偏蓝屏的手机屏幕,直接取 G 通道反而更稳定。具体做法是给RGBLuminanceSource传一个只有 G 通道的byte[]:
byte[] gray = new byte[rawPixels.Length / 4]; for (int i = 0, j = 0; i < rawPixels.Length; i += 4, j++) { gray[j] = rawPixels[i + 1]; // BGRA 的 G 在索引 1 } var lumin = new RGBLuminanceSource(gray, w, h, RGBLuminanceSource.BitmapFormat.Gray8);这个是血泪经验:纯白手机屏在眼镜里看是带彩色边缘的,G 通道比 RGB 加权灰度更抗反光。代价是彩色条码识别率下降,但正常二维码是黑白两色,不受影响。
6. 进阶技巧:多码识别、连续扫码防抖和场景验证方法
多码识别和连续扫码是扫码功能从「能用」走向「好用」的分水岭。ZXing 的MultiFormatReader每帧只返回一个结果,要一次识别画面里的多个二维码,得用GenericMultipleBarcodeReader包装一下,它会把视图按不同裁剪区域重扫多次,结果是一个Result[]:
var multiReader = new GenericMultipleBarcodeReader(reader); Result[] results = multiReader.decodeMultiple(bitmap, hints); for (int i = 0; i < results.Length; i++) { ScanResultQueue.Instance.Push(results[i].Text); }这里注意:decodeMultiple比普通decode慢 3~5 倍,真机上不要每帧都调,建议decodeInterval提升到 0.5 秒,并且只在你明确需要同时绑定多个设备时才开启。单个二维码识别场景,用MultiFormatReader就够了。
连续扫码还有一个隐藏需求:同一个码在镜头前停留 2 秒,不能触发 5 次绑定请求。我在 4.3 节写的ScanEventDispatcher就是这个作用,实际项目里再加一步:把结果里的 JSON 用JsonUtility解析之后跟本地deviceList.json做匹配。但要注意JsonUtility直接从字符串解析会出现「顶层数组不支持」的报错,这是我踩过最隐蔽的坑之一。正确写法是先把 JSON 包到对象里再解析:
[Serializable] public class DeviceResponse { public string code; public string message; } var wrapper = JsonUtility.FromJson<DeviceResponse>("{\"code\":\"OK\",\"message\":\"success\"}");如果扫码得到的是原生 JSON 数组,就在内存里拼一个{"list":[...]}再解析,或者改用Newtonsoft.Json。不过引入一个 JSON 库之前,先评估有没有必要——只有 3~5 个字段的扫码结果,JsonUtility完全够用。
验证扫码延迟和稳定性的方法,我会在工程里保留一个调试面板,显示三组数据:当前摄像头分辨率、最近一次解码耗时、连续失败次数。把 Rokid 眼镜戴好,把手机屏幕调成最强亮度,分别测试「1 米外扫码」「50 厘米内扫码」「屏幕倾斜 30 度扫码」三种距离,记录从出现到触发事件的时间。如果近距离识别不稳定,优先查旋转角;如果远距离扫不到,先查 ROI 是否把条码截断了。编码上还有一个习惯我一直保留:解码抛ReaderException时不打印Exception堆栈,打印一次Debug.LogWarning作为计数即可,因为每帧失败都打堆栈,日志系统本身会成为性能瓶颈,也会把 Rokid 主机上的 logcat 撑爆。这个习惯让我在多个 AR 扫码项目里少走了不少弯路,希望帮到你。
本文还有配套的精品资源,点击获取