基于Rokid CXR-M SDK的AI饮食健康助手开发实战

引言:当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,完整实现了春节饮食助手的核心功能:

  1. 端云协同架构:手机端负责AI计算,眼镜端专注AR显示,兼顾性能与功耗
  2. 双通道通信:蓝牙传指令、Wi-Fi传图像,保障流畅体验
  3. JSON动态UI:通过绿色通道渲染,实现轻量级AR界面

6.2 未来可扩展性

基于同样的技术架构,可以扩展更多场景:

  • 糖尿病管理:识别菜品并显示碳水化合物含量
  • 健身指导:根据用户目标推荐蛋白质摄入量
  • 饮食日志:自动记录每餐摄入,生成周报
  • 社交分享:将营养报告分享给家人或营养师

当科技真正"隐于无形",服务于生活时,它便拥有了最温暖的意义。这个春节,让Rokid眼镜成为你的"热量透视镜",吃明白,过个健康年。

相关推荐
一枚前端小姐姐2 小时前
低代码平台表单设计系统技术分析(实战三)
前端·vue.js·低代码
牛奶2 小时前
ts随笔:面向对象与高级类型
前端·面试·typescript
牛奶2 小时前
React 基础理论 & API 使用
前端·react.js·面试
大漠_w3cpluscom2 小时前
别再死记CSS属性了!真正能让你少走半年弯路的,是这套思维
前端
兆子龙2 小时前
用 React + Remotion 做视频:入门与 AI 驱动生成
前端·架构
SuperEugene2 小时前
从 Vue2 到 Vue3:语法差异与迁移时最容易懵的点
前端·vue.js·面试
鼓浪屿2 小时前
vue3:组件中,v-model的区别(新版)
前端
Leon3 小时前
新手引导 intro.js 的使用
前端·javascript·vue.js
Zeros3 小时前
Claude Code 使用心得 - 从尝鲜到日常的进阶之路
前端