引言:当AR眼镜遇上年夜饭
每年春节,面对满桌的鸡鸭鱼肉、煎炸炖煮,无数人陷入"吃还是不吃"的两难------红烧肉到底多少卡?油炸春卷热量有多高?糖醋鱼会不会让血糖飙升?传统的解决方案要么掏出手机打开App手动搜索,要么靠记忆力估算,不仅繁琐,还常常因为"懒得查"而放弃。
基于Rokid CXR-M SDK,可以开发一款手机端识别分析 + 眼镜端AR显示的协同应用:用户只需用眼镜扫过餐桌,系统即可实时识别菜品并投射热量数据至视野中,让健康管理从"刻意为之"变成"自然发生"。
楼主将完整记录从SDK集成、设备连接、图像采集与识别,到AR投射与语音交互的全过程,希望能为其他开发者提供一份可复用的实操指南。
一、系统架构与核心能力
整体架构设计
本系统采用手机为控制中枢、眼镜为显示终端的端云协同架构。CXR-M SDK作为桥梁,封装了底层通信细节,让我们能专注于业务逻辑。
- 感知层:Rokid Glasses摄像头实时捕获餐桌画面
- 分析层:手机端运行AI模型进行菜品识别与热量计算
- 交互层:眼镜端通过AR叠加显示识别结果,支持语音控制
二、项目初始化与设备连接
2.1 环境搭建
首先在项目中配置Rokid Maven仓库和SDK依赖:
scss
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// 添加Rokid官方Maven仓库
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
}
}
// app/build.gradle.kts
android {
defaultConfig {
minSdk = 28 // 必须 ≥28
}
}
dependencies {
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
implementation("org.tensorflow:tensorflow-lite:2.9.0") // 端侧AI推理
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") // MVVM架构
}
2.2 权限声明与动态申请
在AndroidManifest.xml中声明必要权限:
ini
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
Android 12+需动态申请蓝牙权限:
kotlin
class PermissionHelper(private val activity: AppCompatActivity) {
fun requestRequiredPermissions(onGranted: () -> Unit) {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.CAMERA
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions += Manifest.permission.BLUETOOTH_SCAN
permissions += Manifest.permission.BLUETOOTH_CONNECT
}
if (permissions.all {
activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}) {
onGranted()
} else {
activity.requestPermissions(permissions.toTypedArray(), REQUEST_CODE)
}
}
}
2.3 双通道设备连接
Rokid CXR-M SDK支持蓝牙+Wi-Fi双通道连接:蓝牙通道用于控制指令传输,Wi-Fi通道用于大容量图像数据传输。
kotlin
class ConnectionManager(private val context: Context) {
private val tag = "ConnectionManager"
private var isBluetoothConnected = false
private var isWifiConnected = false
private var deviceAuthenticated = false
fun initDeviceConnection(device: BluetoothDevice) {
// 1. 初始化蓝牙连接
CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback {
override fun onConnectionInfo(socketUuid: String?, macAddress: String?,
rokidAccount: String?, glassesType: Int) {
socketUuid?.let { uuid ->
macAddress?.let { address ->
connectBluetooth(uuid, address)
}
}
}
override fun onConnected() {
Log.d(tag, "蓝牙连接成功")
isBluetoothConnected = true
initWifiConnection()
}
override fun onDisconnected() {
Log.w(tag, "蓝牙连接断开")
isBluetoothConnected = false
deviceAuthenticated = false
// 尝试重连
}
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
Log.e(tag, "蓝牙连接失败: ${errorCode?.name}")
}
})
}
private fun connectBluetooth(uuid: String, mac: String) {
CxrApi.getInstance().connectBluetooth(context, uuid, mac, bluetoothCallback)
}
// 2. 初始化Wi-Fi P2P
private fun initWifiConnection() {
if (!isBluetoothConnected) return
val status = CxrApi.getInstance().initWifiP2P(object : WifiP2PStatusCallback {
override fun onConnected() {
isWifiConnected = true
Log.d(tag, "Wi-Fi P2P连接成功")
// 等待设备认证
}
override fun onDisconnected() {
isWifiConnected = false
Log.e(tag, "Wi-Fi P2P断开")
}
override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) {
Log.e(tag, "Wi-Fi P2P连接失败: ${errorCode?.name}")
}
})
}
// 3. 关键:等待设备认证完成
fun setAuthenticationListener() {
CxrApi.getInstance().setDeviceAuthListener { token ->
if (token.isNotEmpty()) {
deviceAuthenticated = true
Log.d(tag, "设备认证成功,token: $token")
// 此时才可进行UI渲染等操作
onDeviceReady()
}
}
}
}
⚠️ 关键经验 :onConnected()只代表物理链路通了,但设备认证信息尚未同步。必须等待onDeviceAuthenticated()回调后,才能安全调用openCustomView()等UI接口。
三、核心功能实现
3.1 AI场景触发与图像采集
利用眼镜侧面的AI按键,用户只需长按即可唤醒识别功能:
kotlin
class FoodRecognitionManager {
private val tag = "FoodRecognitionManager"
// 设置AI按键监听
fun setupAiKeyListener() {
CxrApi.getInstance().setAiEventListener(object : AiEventListener {
override fun onAiKeyDown() {
Log.d(tag, "AI按键长按,启动识别流程")
captureAndRecognize()
}
override fun onAiKeyUp() {
// 短按可预留其他功能
}
override fun onAiKeyCancel() {
Log.d(tag, "AI按键取消")
}
})
}
// 采集图像并识别
private fun captureAndRecognize() {
// 1. 打开相机预览(如果未打开)
CxrApi.getInstance().openGlassCamera(
width = 1920,
height = 1080,
quality = 85,
object : CameraStatusCallback {
override fun onCameraOpened(status: ValueUtil.CxrStatus?) {
if (status == ValueUtil.CxrStatus.RESPONSE_SUCCEED) {
Log.d(tag, "相机已开启")
takePhoto()
} else {
Log.e(tag, "相机开启失败: ${status?.name}")
}
}
}
)
}
// 2. 拍照获取高清图像
private fun takePhoto() {
CxrApi.getInstance().takeGlassPhoto(object : PhotoResultCallback {
override fun onPhotoTaken(data: ByteArray?) {
data?.let { imageData ->
Log.d(tag, "拍照成功,图片大小: ${imageData.size} bytes")
// 发送到AI识别服务
recognizeFood(imageData)
} ?: run {
Log.e(tag, "拍照失败: data为空")
}
}
override fun onFailed(errorCode: ValueUtil.CxrCameraErrorCode?) {
Log.e(tag, "拍照失败: ${errorCode?.name}")
}
})
}
}
3.2 手机端AI菜品识别
拍照获得的图像数据通过ByteArray形式返回,我们可以在手机端运行TensorFlow Lite模型进行识别:
kotlin
class FoodRecognitionService {
private var tflite: Interpreter? = null
private val foodDatabase = mapOf(
"hongshao_rou" to FoodInfo("红烧肉", 480, "高脂肪", "#FF4444"),
"qingzheng_yu" to FoodInfo("清蒸鱼", 120, "优质蛋白", "#44FF44"),
"chun_juan" to FoodInfo("春卷", 350, "油炸食品", "#FFAA00"),
"liangban_huanggua" to FoodInfo("凉拌黄瓜", 45, "低卡推荐", "#44FF44"),
"niangao" to FoodInfo("年糕", 210, "主食", "#FFAA00")
)
data class FoodInfo(
val name: String,
val calories: Int, // 千卡/100g
val tag: String,
val color: String // 用于AR显示的颜色
)
fun recognizeFood(imageData: ByteArray, callback: (FoodInfo?) -> Unit) {
// 1. 图像预处理
val bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
val resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true)
// 2. 转换为模型输入格式
val input = convertBitmapToByteBuffer(resized)
// 3. 执行推理
val output = Array(1) { FloatArray(5) } // 假设5个分类
tflite?.run(input, output)
// 4. 解析结果
val maxIndex = output[0].indices.maxByOrNull { output[0][it] } ?: 0
val confidence = output[0][maxIndex]
if (confidence > 0.7) {
val foodKey = when (maxIndex) {
0 -> "hongshao_rou"
1 -> "qingzheng_yu"
2 -> "chun_juan"
3 -> "liangban_huanggua"
4 -> "niangao"
else -> null
}
callback(foodDatabase[foodKey])
} else {
callback(null) // 识别失败
}
}
private fun convertBitmapToByteBuffer(bitmap: Bitmap): ByteBuffer {
// TensorFlow Lite输入格式转换
// 具体实现略
}
}
3.3 AR界面渲染:JSON动态构建
识别完成后,需要在眼镜端显示热量信息。Rokid CXR-M SDK支持通过JSON动态构建自定义界面。
⚠️ 重要限制 :Rokid Glasses的光学显示模组对特定波长敏感,所有显示元素必须使用绿色通道(#00FF00) 才能被用户看到。
typescript
class ARDisplayManager {
// 构建热量显示界面
fun showCalorieInfo(foodInfo: FoodRecognitionService.FoodInfo) {
// 根据热量等级决定提示颜色(但最终只能渲染绿色,颜色用于语义区分)
val indicatorColor = when (foodInfo.color) {
"#FF4444" -> "#00FF00" // 警告(高热量)→ 亮绿闪烁
"#FFAA00" -> "#00AA00" // 中等 → 中绿
"#44FF44" -> "#008800" // 低卡 → 暗绿
else -> "#00FF00"
}
val caloriesJson = """
{
"type": "ConstraintLayout",
"props": {
"layout_width": "match_parent",
"layout_height": "match_parent",
"backgroundColor": "#00000000"
},
"children": [
{
"type": "LinearLayout",
"props": {
"id": "card_background",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"orientation": "vertical",
"gravity": "center",
"backgroundColor": "#BB000000",
"padding": "16dp",
"layout_alignParentBottom": "true",
"layout_centerHorizontal": "true",
"marginBottom": "40dp"
},
"children": [
{
"type": "TextView",
"props": {
"id": "food_name",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "${foodInfo.name}",
"textSize": "22sp",
"textColor": "$indicatorColor",
"textStyle": "bold",
"gravity": "center"
}
},
{
"type": "TextView",
"props": {
"id": "calorie_info",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "${foodInfo.calories} kcal/100g",
"textSize": "28sp",
"textColor": "$indicatorColor",
"textStyle": "bold",
"marginTop": "4dp",
"gravity": "center"
}
},
{
"type": "TextView",
"props": {
"id": "food_tag",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "${foodInfo.tag}",
"textSize": "16sp",
"textColor": "#AAAAAA",
"marginTop": "8dp",
"gravity": "center"
}
}
]
}
]
}
""".trimIndent()
// 打开自定义界面
val status = CxrApi.getInstance().openCustomView(caloriesJson)
if (status != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.e("ARDisplay", "打开自定义界面失败: $status")
}
}
// 更新已有界面(用于连续识别)
fun updateCalorieInfo(foodInfo: FoodRecognitionService.FoodInfo) {
val updateJson = """
[
{
"action": "update",
"id": "food_name",
"props": {
"text": "${foodInfo.name}"
}
},
{
"action": "update",
"id": "calorie_info",
"props": {
"text": "${foodInfo.calories} kcal/100g"
}
},
{
"action": "update",
"id": "food_tag",
"props": {
"text": "${foodInfo.tag}"
}
}
]
""".trimIndent()
CxrApi.getInstance().updateCustomView(updateJson)
}
// 关闭界面
fun hideCalorieInfo() {
CxrApi.getInstance().closeCustomView()
}
}
四、总结与展望
6.1 技术亮点回顾
本文基于Rokid CXR-M SDK,完整实现了春节饮食助手的核心功能:
- 端云协同架构:手机端负责AI计算,眼镜端专注AR显示,兼顾性能与功耗
- 双通道通信:蓝牙传指令、Wi-Fi传图像,保障流畅体验
- JSON动态UI:通过绿色通道渲染,实现轻量级AR界面
6.2 未来可扩展性
基于同样的技术架构,可以扩展更多场景:
- 糖尿病管理:识别菜品并显示碳水化合物含量
- 健身指导:根据用户目标推荐蛋白质摄入量
- 饮食日志:自动记录每餐摄入,生成周报
- 社交分享:将营养报告分享给家人或营养师
当科技真正"隐于无形",服务于生活时,它便拥有了最温暖的意义。这个春节,让Rokid眼镜成为你的"热量透视镜",吃明白,过个健康年。