系统使用 FR3C 机械臂、四指夹爪和 DaBai DCW2 深度相机,运行在 ROS 2 Humble 与 MoveIt 2 环境中。语音模块接收操作要求,视觉模块确定骰子位置和顶部点数,任务执行器完成运动规划、抓取、投放及回位。各模块通过明确的动作与状态接口衔接,形成从口头指令到机械臂执行、再到结果播报的完整流程。

系统架构与任务流程

语音、视觉和运动控制分别承担不同职责。语音识别先将音频转换成文字,业务解析再生成动作与参数;视觉输出目标位置、顶面点数及其确认状态;任务执行器根据这些信息选择观察、搜索、抓取、投放或结束。界面按钮与语音调用同一组业务入口,开闭爪、识别和回位等操作能够共用执行状态与反馈。

掷骰分为目标点数和随机投掷两种模式。目标模式按“观察、规划、抓取、投放、重新读数”循环执行,目标点数作为结束条件,并由轮次和超时约束控制整个任务。随机模式完成一次投放后进入结果读取阶段,确认落定后的顶部点数,再通过语音反馈“当前是几点”。

视觉定位贯穿任务过程。每次抓取前重新采集图像和深度,确认目标仍在原位置;发现骰子移动时,旧坐标和旧规划随之失效。骰子移出画面后进入搜索,重新出现在画面内时按新位置定位和规划,因此任务能够响应人工调整后的目标位置。

语音模型接入与指令解析

语音端由关键词唤醒、流式识别和语音合成组成。KWS 负责双重唤醒,Sherpa-ONNX 的预训练 Zipformer 模型负责识别 16 kHz 单声道音频,Matcha 与 Vocos 负责离线播报。骰子业务使用独立的词表、配置和会话,声学模型的输出继续由骰子指令解析器处理,业务适配集中在唤醒词、转写归一化和动作语义上。

用户连续说“小鸿小鸿”,或使用同音的“小红小红”,即可进入唤醒流程。系统先回复“我在,请问需要我做什么?”,问候播报结束后再采集操作指令。识别结果中的“色子”“筛子”统一为“骰子”,数字同音纠错限定在点数等明确上下文中,随后提取动作、目标点数、方向、距离和角度。

解析器将文字映射到受限动作,包括识别、开爪、闭爪、停止、回位、目标投掷、随机投掷、第六轴转动和机械臂平移。例如“向前移动五厘米”生成方向与距离,“第六轴逆时针转三十度”生成旋转方向与角度。参数在进入执行入口前检查单位和范围,点数存在歧义或指令含义不完整时返回澄清信息。

语音面板在完成转写后解析整句,确认动作含义,再交给流程调度器:

from fr3c_voice.dice_voice_intent import fast_dice_intent
# 先解析整句,生成有序动作;此时尚未下发机械臂命令
intent = fast_dice_intent("打开爪子然后关闭爪子最后随机投掷一次骰子")
if intent.action == "clarify":
    reply = intent.reply  # 任一步不明确,整句等待补充
else:
    reply = self.sequence.start(intent)  # 根据完成反馈逐步调度

音频采集通过短帧音量、语音活动和信噪比进行筛选,并参考唤醒声与指令声的强度关系,减少背景交谈带来的无效输入。系统播报期间暂停采集,播报完成后恢复监听,避免把扬声器输出再次识别为指令。

多步骤调度与语音会话

连续指令先解析成有序步骤,再启动第一个动作。例如“打开爪子,然后关闭爪子,接着识别骰子,最后随机投掷一次骰子”,对应四个独立业务步骤。解析阶段检查整句话,任何一步无法明确时先等待补充,避免前半句已经执行、后半句才发现无法理解。

调度器为当前步骤生成独立任务标识,并将该标识与业务反馈关联。只有收到相同标识的成功完成事件,才从队列取出下一步。步骤被拒绝、执行失败或用户要求停止时,队列随即取消。流程推进依赖完成回调,机械臂实际耗时发生变化时,后面的动作仍能保持正确顺序。

空闲会话在唤醒后持续接收指令,连续 10 秒没有有效指令便返回等待唤醒状态。任务运行期间的新操作需要重新唤醒,通过停止接管流程等待原任务退出;存在待完成回位或运动应答时,接管动作继续受恢复状态约束。

识别定稿后,消息携带会话标识与时间信息进入界面队列。界面丢弃旧会话、重复处理和超过时效的指令;定稿到窗口接收使用 2 秒时效限制。识别动作完成后播报点数,随机投掷完成后播报本次结果,执行状态与最终结果分别反馈。

骰子顶面模型训练

视觉定位采用 YOLO11 nano 迁移学习,将检测类别统一为 die_top。标注使用 X-AnyLabeling,矩形框只覆盖朝上的骰面,保留原图及同名 JSON。顶面、侧面和阴影在标注中保持明确边界,让定位模型学习同一种目标区域。

数据整理阶段把矩形标注转换为 YOLO 检测标签,每行依次保存类别编号、中心横坐标、中心纵坐标、宽和高,坐标按原图尺寸归一化。单类别编号统一为 0,空场景使用空标签文件。数据按 data.yaml 组织图像和标签,训练入口检查类别、坐标范围及图像标签对应关系。

train_trial.py 加载已有骰子顶面权重继续微调。核心训练调用如下,路径相对项目根目录,训练产物写入独立实验目录:

from ultralytics import YOLO
root = "evidence/dice/20261016/tabletop_trial"
# 在已有骰子顶面权重上微调,类别由data.yaml定义
model = YOLO("evidence/dice/20261014/models/die_top.pt")
model.train(
    data=f"{root}/trial_top_locator/data.yaml",
    epochs=15, imgsz=960, batch=2, device="cpu",  # CPU训练参数
    workers=0, seed=42, amp=False, cache=False,
    project="runs/dice_article", name="top_locator",  # 独立输出目录
    exist_ok=False,
)

部署进程加载顶面权重,输出候选框和置信度,深度与图像处理模块继续判断顶部平面及圆点。检测模型承担定位,几何与颜色处理承担读数,定位偏差和点数偏差可以在各自环节处理,更新权重也通过明确的模型路径完成。

顶面读数与三维定位

相机斜视骰子时,一个画面可能同时出现顶部和多个侧面。顶面筛选先利用配准深度拟合桌面斜平面,计算候选点相对桌面的高度,再结合区域尺寸、平面朝向和拟合残差确定顶部区域。点数统计使用该区域生成的掩膜,从空间几何上排除侧面圆点。

颜色处理在顶面内分离红蓝圆点,并检查连通区域、形状和粘连情况。深度存在小范围缺失时,掩膜可以保留相应的彩色信息用于数点;缺失深度本身仍保持缺失状态。多个检测框对应同一物理顶面时合并重复候选,存在不同物理目标时保留目标数量的区别。

horizontal_die_top 接收同步彩色图、基坐标点云和候选框,返回顶面位置与点数。调用时分别保留两项结果:

from top_surface import horizontal_die_top
# 输入同步彩色图、基坐标点云和检测候选框
top = horizontal_die_top(image_bgr, cloud_base, detections)
# 位置与点数独立保留;点数未知时仍可保留可靠位置
die_xy = top["center_xy_m"] if top is not None else None
face = top["top_face"] if top is not None else None

在线识别结合至少三个新画面确认位置稳定。位置可靠但点数未知时,保留定位结果并继续检查抓取条件;没有可靠位置时进入搜索。投掷后的结果读取则需要确认具体点数,最终播报只接受 1 到 6。

三维定位使用与彩色图配准的深度和相机内参,将有效顶面像素还原为相机坐标点,再通过手眼外参与该帧时间对应的机械臂变换映射到基坐标。相机随腕部移动时,每个视角都采用对应的变换,图像、深度和姿态作为同一组观察数据参与计算。

四爪居中与抓取深度

视觉输出的是骰子位置,机械臂规划需要的是 TCP 目标,两者通过夹爪几何关系连接。四指夹爪在抓取状态下的操作中心与闭合指尖 TCP 参考分别维护,骰子任务使用独立的平面补偿参数,将视觉中心换算为适合四爪夹持的位置。

操作中心标定使用同一静止骰子的两组数据:记录手动对中时的 TCP 位置和工具姿态,再移开夹爪读取视觉位置。由两者在基坐标下的差值反算工具坐标系中的平面偏移。执行时随工具姿态旋转该偏移,补偿方向能够跟随第六轴与抓取朝向变化。

标定数据与公共 TCP、手眼参数分别保存,加载时校验所依赖的标定信息。每次抓取都基于新识别位置叠加补偿,并重新检查桌面边界,使操作中心修正能够应用到不同落点。

from grasp_alignment import corrected_grasp_xy, grasp_z_at_surface
from tabletop_geometry import table_height_at
# 按工具姿态旋转操作中心偏移,并检查修正后的桌面边界
x, y = corrected_grasp_xy(die_xy, tool_rotation, safe_bounds_xy)
# 使用抓取点处的斜平面高度,长度单位统一为米
surface_z = float(table_height_at((x, y), table_z, table_plane))
target_xyz = (x, y, grasp_z_at_surface(surface_z))  # 组合最终TCP目标

抓取深度按修正后的平面位置求局部桌高,再叠加 4 毫米余量,并保留基坐标下 15 毫米的最低 TCP 高度。这里的下限约束 TCP 坐标,爪指与桌面的间隙由夹爪几何和深度标定确定。桌面存在倾斜时,各落点仍使用各自的局部高度。

下降前统一检查抓取、转移投放和回程路径,包含可达性、碰撞及关节变化。下降结束后读取实际反馈,满足到位条件再闭爪;动作中断时保留执行阶段和恢复信息。桌面槽纹或稀疏异常深度通过局部平面及新视角复核处理,真实障碍继续参与路径判断。

第六轴优先搜索与固定回位

目标不在当前画面内时,搜索先保持前五轴目标不变,仅通过第六轴改变相机观察方向。扫描范围为负 170 度到正 170 度,按不超过 25 度的步长分段执行,每段停稳后采集新画面。扫描结束仍未找到时,才进入机械臂换视角规划。

搜索过程保存当前观察位的腕部参考,记录换视角后的工具姿态和回程关系。新视角使用新采集的图像、深度和变换重新定位,抓取前再次确认骰子是否移动;观察变化后丢弃原候选,重新观察与规划。

固定初始位置保存在 config/dice/fixed_initial_pose.json,包含位置、姿态和六个有符号关节角。搜索参考用于局部扫描恢复,固定原位用于完整任务回位。回位沿检查后的路径执行,并核对工具姿态与第六轴角度,使末端位置和腕部方向一起恢复。

随机投掷的结束控制

随机模式将投放与读数分成两个阶段。完成一次抓取投放后,后面的动作仅用于寻找骰子和读取结果。读数暂时不确定时继续搜索,达到搜索边界仍无法确认则报告点数未确认;已完成的投放次数保持为一次。

得到点数后,任务执行器先处理搜索留下的观察位与腕部恢复,再输出结果事件。随机分支的结束处理如下,result_face 为投放后的读数,home 为固定原位:

# 随机模式只处理本次投掷结果,不进入下一轮重掷
if getattr(self, "random_once", False):
    if result_face not in range(1, 7):
        raise RuntimeError("已投掷一次,点数未确认,不再重掷")
    shifted = self.observation_shifted
    scanned = getattr(self, "wrist_context", None) is not None
    if shifted or scanned:
        self._safe_return_after_stop(home)  # 恢复观察位与第六轴
    result = {"phase": "random_done", "throws": 1, "face": result_face}
    print(json.dumps(result), flush=True)  # 窗口接收结果并触发播报
    return

界面依据 random_done 事件读取点数并调用语音播报,同时结束当前业务步骤。若随机投掷属于连续语音流程,调度器收到完成反馈后再进入下一步;独立随机任务在结果反馈后结束,停止、失败和回位状态仍由同一任务链路维护。
系统使用 FR3C 机械臂、四指夹爪和 DaBai DCW2 深度相机,运行在 ROS 2 Humble 与 MoveIt 2 环境中。语音模块接收操作要求,视觉模块确定骰子位置和顶部点数,任务执行器完成运动规划、抓取、投放及回位。各模块通过明确的动作与状态接口衔接,形成从口头指令到机械臂执行、再到结果播报的完整流程。

系统架构与任务流程

语音、视觉和运动控制分别承担不同职责。语音识别先将音频转换成文字,业务解析再生成动作与参数;视觉输出目标位置、顶面点数及其确认状态;任务执行器根据这些信息选择观察、搜索、抓取、投放或结束。界面按钮与语音调用同一组业务入口,开闭爪、识别和回位等操作能够共用执行状态与反馈。

掷骰分为目标点数和随机投掷两种模式。目标模式按“观察、规划、抓取、投放、重新读数”循环执行,目标点数作为结束条件,并由轮次和超时约束控制整个任务。随机模式完成一次投放后进入结果读取阶段,确认落定后的顶部点数,再通过语音反馈“当前是几点”。

视觉定位贯穿任务过程。每次抓取前重新采集图像和深度,确认目标仍在原位置;发现骰子移动时,旧坐标和旧规划随之失效。骰子移出画面后进入搜索,重新出现在画面内时按新位置定位和规划,因此任务能够响应人工调整后的目标位置。

语音模型接入与指令解析

语音端由关键词唤醒、流式识别和语音合成组成。KWS 负责双重唤醒,Sherpa-ONNX 的预训练 Zipformer 模型负责识别 16 kHz 单声道音频,Matcha 与 Vocos 负责离线播报。骰子业务使用独立的词表、配置和会话,声学模型的输出继续由骰子指令解析器处理,业务适配集中在唤醒词、转写归一化和动作语义上。

用户连续说“小鸿小鸿”,或使用同音的“小红小红”,即可进入唤醒流程。系统先回复“我在,请问需要我做什么?”,问候播报结束后再采集操作指令。识别结果中的“色子”“筛子”统一为“骰子”,数字同音纠错限定在点数等明确上下文中,随后提取动作、目标点数、方向、距离和角度。

解析器将文字映射到受限动作,包括识别、开爪、闭爪、停止、回位、目标投掷、随机投掷、第六轴转动和机械臂平移。例如“向前移动五厘米”生成方向与距离,“第六轴逆时针转三十度”生成旋转方向与角度。参数在进入执行入口前检查单位和范围,点数存在歧义或指令含义不完整时返回澄清信息。

语音面板在完成转写后解析整句,确认动作含义,再交给流程调度器:

from fr3c_voice.dice_voice_intent import fast_dice_intent
# 先解析整句,生成有序动作;此时尚未下发机械臂命令
intent = fast_dice_intent("打开爪子然后关闭爪子最后随机投掷一次骰子")
if intent.action == "clarify":
    reply = intent.reply  # 任一步不明确,整句等待补充
else:
    reply = self.sequence.start(intent)  # 根据完成反馈逐步调度

音频采集通过短帧音量、语音活动和信噪比进行筛选,并参考唤醒声与指令声的强度关系,减少背景交谈带来的无效输入。系统播报期间暂停采集,播报完成后恢复监听,避免把扬声器输出再次识别为指令。

多步骤调度与语音会话

连续指令先解析成有序步骤,再启动第一个动作。例如“打开爪子,然后关闭爪子,接着识别骰子,最后随机投掷一次骰子”,对应四个独立业务步骤。解析阶段检查整句话,任何一步无法明确时先等待补充,避免前半句已经执行、后半句才发现无法理解。

调度器为当前步骤生成独立任务标识,并将该标识与业务反馈关联。只有收到相同标识的成功完成事件,才从队列取出下一步。步骤被拒绝、执行失败或用户要求停止时,队列随即取消。流程推进依赖完成回调,机械臂实际耗时发生变化时,后面的动作仍能保持正确顺序。

空闲会话在唤醒后持续接收指令,连续 10 秒没有有效指令便返回等待唤醒状态。任务运行期间的新操作需要重新唤醒,通过停止接管流程等待原任务退出;存在待完成回位或运动应答时,接管动作继续受恢复状态约束。

识别定稿后,消息携带会话标识与时间信息进入界面队列。界面丢弃旧会话、重复处理和超过时效的指令;定稿到窗口接收使用 2 秒时效限制。识别动作完成后播报点数,随机投掷完成后播报本次结果,执行状态与最终结果分别反馈。

骰子顶面模型训练

视觉定位采用 YOLO11 nano 迁移学习,将检测类别统一为 die_top。标注使用 X-AnyLabeling,矩形框只覆盖朝上的骰面,保留原图及同名 JSON。顶面、侧面和阴影在标注中保持明确边界,让定位模型学习同一种目标区域。

数据整理阶段把矩形标注转换为 YOLO 检测标签,每行依次保存类别编号、中心横坐标、中心纵坐标、宽和高,坐标按原图尺寸归一化。单类别编号统一为 0,空场景使用空标签文件。数据按 data.yaml 组织图像和标签,训练入口检查类别、坐标范围及图像标签对应关系。

train_trial.py 加载已有骰子顶面权重继续微调。核心训练调用如下,路径相对项目根目录,训练产物写入独立实验目录:

from ultralytics import YOLO
root = "evidence/dice/20261016/tabletop_trial"
# 在已有骰子顶面权重上微调,类别由data.yaml定义
model = YOLO("evidence/dice/20261014/models/die_top.pt")
model.train(
    data=f"{root}/trial_top_locator/data.yaml",
    epochs=15, imgsz=960, batch=2, device="cpu",  # CPU训练参数
    workers=0, seed=42, amp=False, cache=False,
    project="runs/dice_article", name="top_locator",  # 独立输出目录
    exist_ok=False,
)

部署进程加载顶面权重,输出候选框和置信度,深度与图像处理模块继续判断顶部平面及圆点。检测模型承担定位,几何与颜色处理承担读数,定位偏差和点数偏差可以在各自环节处理,更新权重也通过明确的模型路径完成。

顶面读数与三维定位

相机斜视骰子时,一个画面可能同时出现顶部和多个侧面。顶面筛选先利用配准深度拟合桌面斜平面,计算候选点相对桌面的高度,再结合区域尺寸、平面朝向和拟合残差确定顶部区域。点数统计使用该区域生成的掩膜,从空间几何上排除侧面圆点。

颜色处理在顶面内分离红蓝圆点,并检查连通区域、形状和粘连情况。深度存在小范围缺失时,掩膜可以保留相应的彩色信息用于数点;缺失深度本身仍保持缺失状态。多个检测框对应同一物理顶面时合并重复候选,存在不同物理目标时保留目标数量的区别。

horizontal_die_top 接收同步彩色图、基坐标点云和候选框,返回顶面位置与点数。调用时分别保留两项结果:

from top_surface import horizontal_die_top
# 输入同步彩色图、基坐标点云和检测候选框
top = horizontal_die_top(image_bgr, cloud_base, detections)
# 位置与点数独立保留;点数未知时仍可保留可靠位置
die_xy = top["center_xy_m"] if top is not None else None
face = top["top_face"] if top is not None else None

在线识别结合至少三个新画面确认位置稳定。位置可靠但点数未知时,保留定位结果并继续检查抓取条件;没有可靠位置时进入搜索。投掷后的结果读取则需要确认具体点数,最终播报只接受 1 到 6。

三维定位使用与彩色图配准的深度和相机内参,将有效顶面像素还原为相机坐标点,再通过手眼外参与该帧时间对应的机械臂变换映射到基坐标。相机随腕部移动时,每个视角都采用对应的变换,图像、深度和姿态作为同一组观察数据参与计算。

四爪居中与抓取深度

视觉输出的是骰子位置,机械臂规划需要的是 TCP 目标,两者通过夹爪几何关系连接。四指夹爪在抓取状态下的操作中心与闭合指尖 TCP 参考分别维护,骰子任务使用独立的平面补偿参数,将视觉中心换算为适合四爪夹持的位置。

操作中心标定使用同一静止骰子的两组数据:记录手动对中时的 TCP 位置和工具姿态,再移开夹爪读取视觉位置。由两者在基坐标下的差值反算工具坐标系中的平面偏移。执行时随工具姿态旋转该偏移,补偿方向能够跟随第六轴与抓取朝向变化。

标定数据与公共 TCP、手眼参数分别保存,加载时校验所依赖的标定信息。每次抓取都基于新识别位置叠加补偿,并重新检查桌面边界,使操作中心修正能够应用到不同落点。

from grasp_alignment import corrected_grasp_xy, grasp_z_at_surface
from tabletop_geometry import table_height_at
# 按工具姿态旋转操作中心偏移,并检查修正后的桌面边界
x, y = corrected_grasp_xy(die_xy, tool_rotation, safe_bounds_xy)
# 使用抓取点处的斜平面高度,长度单位统一为米
surface_z = float(table_height_at((x, y), table_z, table_plane))
target_xyz = (x, y, grasp_z_at_surface(surface_z))  # 组合最终TCP目标

抓取深度按修正后的平面位置求局部桌高,再叠加 4 毫米余量,并保留基坐标下 15 毫米的最低 TCP 高度。这里的下限约束 TCP 坐标,爪指与桌面的间隙由夹爪几何和深度标定确定。桌面存在倾斜时,各落点仍使用各自的局部高度。

下降前统一检查抓取、转移投放和回程路径,包含可达性、碰撞及关节变化。下降结束后读取实际反馈,满足到位条件再闭爪;动作中断时保留执行阶段和恢复信息。桌面槽纹或稀疏异常深度通过局部平面及新视角复核处理,真实障碍继续参与路径判断。

第六轴优先搜索与固定回位

目标不在当前画面内时,搜索先保持前五轴目标不变,仅通过第六轴改变相机观察方向。扫描范围为负 170 度到正 170 度,按不超过 25 度的步长分段执行,每段停稳后采集新画面。扫描结束仍未找到时,才进入机械臂换视角规划。

搜索过程保存当前观察位的腕部参考,记录换视角后的工具姿态和回程关系。新视角使用新采集的图像、深度和变换重新定位,抓取前再次确认骰子是否移动;观察变化后丢弃原候选,重新观察与规划。

固定初始位置保存在 config/dice/fixed_initial_pose.json,包含位置、姿态和六个有符号关节角。搜索参考用于局部扫描恢复,固定原位用于完整任务回位。回位沿检查后的路径执行,并核对工具姿态与第六轴角度,使末端位置和腕部方向一起恢复。

随机投掷的结束控制

随机模式将投放与读数分成两个阶段。完成一次抓取投放后,后面的动作仅用于寻找骰子和读取结果。读数暂时不确定时继续搜索,达到搜索边界仍无法确认则报告点数未确认;已完成的投放次数保持为一次。

得到点数后,任务执行器先处理搜索留下的观察位与腕部恢复,再输出结果事件。随机分支的结束处理如下,result_face 为投放后的读数,home 为固定原位:

# 随机模式只处理本次投掷结果,不进入下一轮重掷
if getattr(self, "random_once", False):
    if result_face not in range(1, 7):
        raise RuntimeError("已投掷一次,点数未确认,不再重掷")
    shifted = self.observation_shifted
    scanned = getattr(self, "wrist_context", None) is not None
    if shifted or scanned:
        self._safe_return_after_stop(home)  # 恢复观察位与第六轴
    result = {"phase": "random_done", "throws": 1, "face": result_face}
    print(json.dumps(result), flush=True)  # 窗口接收结果并触发播报
    return

界面依据 random_done 事件读取点数并调用语音播报,同时结束当前业务步骤。若随机投掷属于连续语音流程,调度器收到完成反馈后再进入下一步;独立随机任务在结果反馈后结束,停止、失败和回位状态仍由同一任务链路维护。

Logo

聚焦RDK与具身智能,汇集机器人开发教程、开源案例和技术交流

更多推荐