导盲项目:什么场景非得用相机预览?按杖上按钮或定时拍照,识别后语音提醒

« 返回首页

有用户问:啥场景非得要预览相机?我们在做一个导盲的项目:一个手持的导盲杖,胸前戴一个手机识别障碍物,两者通过蓝牙连接。按杖上的按钮就拍照,然后把结果反馈出来;也要能定时自动拍。

答案:凡是「不靠人按快门、由程序决定什么时候拍」的场景,都要用实时预览录制的预览模式拍照。 导盲正是这种:按的是杖上的按钮、或者计时器到点,没有人去点手机屏幕上的快门。

三种拍照方式,导盲该用哪种

方式 怎么拍 导盲能不能用
照相机 打开系统相机界面,人按快门才拍 不能:使用者看不到快门,也没法定时
实时预览录制·预览模式 先「初始化预览」把画面显示在一个布局里,之后积木随时调用「拍照」 能:蓝牙按钮、计时器都能触发
实时预览录制·后台录制模式 不显示画面,但只能在录像进行中拍照 勉强:要一直录像,费电又占空间

所以「非得要预览」,是因为程序自己拍照需要相机一直开着,而预览模式开相机的方式就是把画面显示在布局里。预览画面对视障使用者没有用,但:

  • 布局可以做小(比如高 120 像素放在屏幕顶端),只要可见且有尺寸;
  • 调试时很有用:一眼看出手机有没有对着前方、衣服有没有挡住镜头。

整个系统的数据流:

导盲杖 → 蓝牙 胸前手机      
按一下按钮 发 1 个字节 P(0x50) 收到后拍照 目标检测 算方位和远近 语音播报 + 震动

准备

组件 命名 关键属性
垂直布局 垂直布局_预览 宽「充满」、高 120 像素;里面不要放别的组件(初始化预览会清空它)
实时预览录制 实时预览录制1 「后台录制」不勾、「使用前置摄像头」不勾
按钮 按钮_拍照、按钮_定时 文本「拍一张」「开始定时」
图像 图像_照片 宽「充满」、高 160 像素
标签 标签_状态 —
计时器 计时器_拍照 间隔 3000,不勾「启用计时」
计时器 计时器_蓝牙 间隔 100,不勾「启用计时」
蓝牙客户端 蓝牙客户端1 用于经典蓝牙(HC-05 等)
列表选择器 列表选择器_连接 文本「连接导盲杖」
文本朗读器、音效 文本朗读器1、音效1 实验三用

杖上的单片机只需要做一件事:按一下按钮,通过蓝牙发 1 个字节 0x50(字符 P),不加回车换行。蓝牙模块先在手机系统设置里配对。

实验一:在 App 里拍一张

申请相机权限后初始化预览,收到「预览已启动」才允许拍照。拍照是异步的,「拍照完成」事件才给出照片路径。 用一个「忙碌」标记保证上一张没处理完不拍下一张:

global 就绪 = false
global 忙碌 = false

procedure 请求拍照() {
  if (就绪 and not 忙碌) {
    忙碌 = true
    标签_状态.Text = "拍照中…"
    实时预览录制1.CapturePhoto()
  }
}

when Screen1.Initialize() {
  Screen1.AskForPermission("android.permission.CAMERA")
}

when Screen1.PermissionGranted(权限名称) {
  实时预览录制1.InitializeCamera(垂直布局_预览)
}

when 实时预览录制1.CameraPreviewStarted() {
  就绪 = true
  标签_状态.Text = "预览已就绪"
}

when 按钮_拍照.Click() {
  请求拍照()
}

when 实时预览录制1.PhotoCaptured(文件路径) {
  图像_照片.Picture = 文件路径
  标签_状态.Text = "已拍照"
  忙碌 = false
}

when 实时预览录制1.PhotoCaptureError(消息) {
  标签_状态.Text = join("拍照失败:", 消息)
  忙碌 = false
}

跑起来看:第一次打开允许相机权限,屏幕上方出现实时画面,标签显示「预览已就绪」;点「拍一张」,下方显示刚拍的照片。 预览一直黑着的话,检查布局是不是被隐藏或高度为 0。

实验二:杖上按钮和定时都能拍

把实验一的积木换成下面这一整段。新增的入口(蓝牙按钮、定时器)都只调用同一个「请求拍照」,所以不会两边同时拍:

global 就绪 = false
global 忙碌 = false

procedure 请求拍照() {
  if (就绪 and not 忙碌) {
    忙碌 = true
    标签_状态.Text = "拍照中…"
    实时预览录制1.CapturePhoto()
  }
}

when Screen1.Initialize() {
  Screen1.AskForPermission("android.permission.CAMERA")
}

when Screen1.PermissionGranted(权限名称) {
  实时预览录制1.InitializeCamera(垂直布局_预览)
}

when 实时预览录制1.CameraPreviewStarted() {
  就绪 = true
  标签_状态.Text = "预览已就绪"
}

when 按钮_拍照.Click() {
  请求拍照()
}

when 列表选择器_连接.BeforePicking() {
  列表选择器_连接.Elements = 蓝牙客户端1.AddressesAndNames
}

when 列表选择器_连接.AfterPicking() {
  if (蓝牙客户端1.Connect(列表选择器_连接.Selection)) {
    计时器_蓝牙.TimerEnabled = true
    标签_状态.Text = "导盲杖已连接"
  }
}

when 计时器_蓝牙.Timer() {
  if (蓝牙客户端1.IsConnected) {
    if (蓝牙客户端1.BytesAvailableToReceive() > 0) {
      var 收到 = 蓝牙客户端1.ReceiveUnsignedBytes(蓝牙客户端1.BytesAvailableToReceive())
      if (contains(收到, 80)) {
        请求拍照()
      }
    }
  }
}

when 按钮_定时.Click() {
  计时器_拍照.TimerEnabled = not 计时器_拍照.TimerEnabled
  按钮_定时.Text = choose(计时器_拍照.TimerEnabled, "停止定时", "开始定时")
}

when 计时器_拍照.Timer() {
  请求拍照()
}

when 实时预览录制1.PhotoCaptured(文件路径) {
  图像_照片.Picture = 文件路径
  标签_状态.Text = "已拍照"
  忙碌 = false
}

when 实时预览录制1.PhotoCaptureError(消息) {
  标签_状态.Text = join("拍照失败:", 消息)
  忙碌 = false
}

跑起来看:

  • 点「连接导盲杖」选配对好的模块,标签显示「导盲杖已连接」;按一下杖上的按钮,大约 0.1 秒内拍一张;
  • 点「开始定时」,每 3 秒自动拍一张;再点一次停止;
  • 定时和按钮同时触发也只会拍一张:「忙碌」时的请求直接忽略。

80 就是 0x50(字符 P)。蓝牙收到的是字节列表,「包含」判断里面有没有这个字节;按钮抖动连发几个 P 也只拍一次。 还没有导盲杖时,可以用手机上的蓝牙串口助手连另一台手机发 P 来测试。

实验三:识别后语音提醒方位和远近

导入 MLKitObjectDetector 离线目标检测拓展,再拖一个文本朗读器和一个音效。 照片拍好后交给检测器;检测器对每个物体给出位置框,取最大的那个(通常离得最近),按框的中心在左、中、右三段判断方位, 框宽超过画面一半就算「很近」,加一下震动。下面是完整程序,替换实验二:

global 就绪 = false
global 忙碌 = false
global 物体数 = 0
global 图宽 = 1
global 最大宽 = 0
global 方位 = ""

procedure 请求拍照() {
  if (就绪 and not 忙碌) {
    忙碌 = true
    标签_状态.Text = "拍照中…"
    实时预览录制1.CapturePhoto()
  }
}

when Screen1.Initialize() {
  Screen1.AskForPermission("android.permission.CAMERA")
}

when Screen1.PermissionGranted(权限名称) {
  实时预览录制1.InitializeCamera(垂直布局_预览)
}

when 实时预览录制1.CameraPreviewStarted() {
  就绪 = true
  标签_状态.Text = "预览已就绪"
}

when 按钮_拍照.Click() {
  请求拍照()
}

when 列表选择器_连接.BeforePicking() {
  列表选择器_连接.Elements = 蓝牙客户端1.AddressesAndNames
}

when 列表选择器_连接.AfterPicking() {
  if (蓝牙客户端1.Connect(列表选择器_连接.Selection)) {
    计时器_蓝牙.TimerEnabled = true
    标签_状态.Text = "导盲杖已连接"
  }
}

when 计时器_蓝牙.Timer() {
  if (蓝牙客户端1.IsConnected) {
    if (蓝牙客户端1.BytesAvailableToReceive() > 0) {
      var 收到 = 蓝牙客户端1.ReceiveUnsignedBytes(蓝牙客户端1.BytesAvailableToReceive())
      if (contains(收到, 80)) {
        请求拍照()
      }
    }
  }
}

when 按钮_定时.Click() {
  计时器_拍照.TimerEnabled = not 计时器_拍照.TimerEnabled
  按钮_定时.Text = choose(计时器_拍照.TimerEnabled, "停止定时", "开始定时")
}

when 计时器_拍照.Timer() {
  请求拍照()
}

when 实时预览录制1.PhotoCaptured(文件路径) {
  图像_照片.Picture = 文件路径
  标签_状态.Text = "识别中…"
  MLKitObjectDetector1.DetectFromFile(文件路径)
}

when 实时预览录制1.PhotoCaptureError(消息) {
  标签_状态.Text = join("拍照失败:", 消息)
  忙碌 = false
}

when MLKitObjectDetector1.GotObjects(数量, imageWidth, imageHeight, resultJson) {
  物体数 = 数量
  图宽 = imageWidth
  最大宽 = 0
}

when MLKitObjectDetector1.GotObject(索引, x坐标, y坐标, 宽度, 高度, 标签文字, confidence, trackingId) {
  if (宽度 > 最大宽) {
    最大宽 = 宽度
    var 中心 = x坐标 + 宽度 / 2
    方位 = choose(中心 < 图宽 / 3, "左前方", choose(中心 > 图宽 * 2 / 3, "右前方", "正前方"))
  }
  if (索引 == 物体数) {
    var 很近 = 最大宽 > 图宽 / 2
    标签_状态.Text = join(方位, "有物体", choose(很近, ",很近", ""), "(共 ", 物体数, " 个)")
    文本朗读器1.Speak(join(方位, "有物体", choose(很近, ",很近", "")))
    if (很近) {
      音效1.Vibrate(500)
    }
    忙碌 = false
  }
}

when MLKitObjectDetector1.NoObjectFound(操作) {
  标签_状态.Text = "未检出物体(不代表前方安全)"
  忙碌 = false
}

when MLKitObjectDetector1.DetectFailed(操作, 错误信息) {
  标签_状态.Text = join("识别失败:", 错误信息)
  忙碌 = false
}

跑起来看:手机对着桌上的水杯,把杯子放在画面左边,按一下杖上的按钮(或点「拍一张」),听到「左前方有物体」; 把杯子挪到镜头跟前,听到「正前方有物体,很近」并震动一下。

要点:

  • 检测器先触发一次「检测到物体」汇总,再对每个物体触发一次「检测到一个物体」,所以在汇总里记下总数和图宽,在最后一个物体(序号等于总数)时才播报;
  • 「忙碌」在每条结束分支都要放开:播报完、未检出、识别失败、拍照失败,漏一个就再也不拍了;
  • 每 3 秒最多拍一张、处理完才拍下一张,语音不会堆积。

识别的边界(务必看)

这套程序验证的是「按键/定时 → 拍照 → 识别 → 反馈」这条链路能跑通,不是可以直接上路的导盲系统:

  • MLKit 的基础模型只有 5 个粗类(时尚商品、食物、家居用品、地点、植物),认不出台阶、坑洞、电线杆、车辆,也给不出距离;「很近」只是按框的大小估计;
  • 未检出不代表前方安全,绝不能据此播报「可以通行」;
  • 真正上路需要专门训练的障碍物模型(可换自定义模型版拓展)或测距传感器(超声波、激光测距放在杖上,用同一条蓝牙传距离更可靠),并在有人陪同的环境里反复测试。

常见问题

现象 原因 解决
预览一直黑、标签不显示就绪 没给相机权限,或预览布局被隐藏、高度为 0 允许权限;布局保持可见、给出高度
预览布局里原来的按钮不见了 初始化预览会清空这个布局 预览布局专用,其它组件放在外面
点拍照没反应 还没收到「预览已启动」,或上一张还没处理完 等就绪;检查每个结束分支都放开了「忙碌」
按杖上按钮没反应 没连上、固件发的不是 0x50,或用的是低功耗蓝牙 先用串口助手发 P 测试;BLE 设备改用低功耗蓝牙订阅
识别一次后再也不拍 某个分支没放开「忙碌」 对照实验三的四个结束分支
锁屏后不拍了 预览模式只在 App 前台工作 使用时保持亮屏,见让 App 不休眠
用久了占空间 每次拍照都生成新文件 识别完用文件管理器删除旧照片,或只保留最近几张

平台说明

  • 安卓:本文积木按安卓编写,实时预览录制、蓝牙客户端、文本朗读器都内置;MLKit 检测是安卓拓展。
  • 鸿蒙:实时预览录制的预览与拍照接口已实现,相机权限名、蓝牙类型请按鸿蒙设备实测;MLKit 拓展不能在鸿蒙上使用。

相关文档

文档反馈