下位机读取传感器四元数(四元数是一种用于表示三维空间中旋转的方法,它由一个实部和三个虚部组成),通过串口读取数据并更新 Blender 中的骨骼(Armature)的旋转。代码结合了 Blender 的 Python API 和串口通信,主要用于将外部设备(如传感器或运动捕捉系统)的数据映射到 Blender 中的骨骼上。
文章目录
一.功能概述
下位机读取传感器四元数(四元数是一种用于表示三维空间中旋转的方法,它由一个实部和三个虚部组成),通过串口读取数据并更新 Blender 中的骨骼(Armature)的旋转。这段代码结合了 Blender 的 Python API 和串口通信,主要用于将外部设备(如传感器或运动捕捉系统)的数据映射到 Blender 中的骨骼上。跟上一篇帖子不一样的是这一篇处理了10个关节的数据,上一篇只处理了2个。本篇将代码集成在一个python file中,代码解释会比上一篇更加详细。下位机代码在上上篇帖子。
上一篇帖子里面有关于使用四元数控制骨骼旋转的详细解释,在这里就不赘述了,导航:Blender使用脚本控制骨骼旋转1.0
二.代码解析部分
1.导入必要的库
这些库包括Blender的API、时间处理、系统操作、串口通信和科学计算等。
import bpy
import time
import sys
import serial
import glob
import numpy as np
bpy
: Blender的Python API,用于操控Blender环境。time
: 时间相关的操作。sys
: 系统级操作,如修改路径。serial
: 用于串口通信。glob
: 文件路径操作,但在本代码中未实际使用。numpy
: 科学计算库,主要用于四元数运算。
2.串口配置
设置串口端口和波特率,并尝试打开串口。
FPS = 60
# 串口配置
port = 'COM10' # 指定串口号,根据实际情况修改
baudrate = 57600 # 波特率,根据实际情况修改
# 创建串口对象
ser = serial.Serial(port, baudrate, timeout=1)
if ser.isOpen():
print(f"串口 {port} 已打开,波特率 {baudrate}")
else:
print(f"无法打开串口 {port}")
- 设置帧率
FPS
为60。 - 配置串口端口
port
和波特率baudrate
,并尝试打开串口。如果成功打开,打印成功信息,否则打印失败信息。
3.获取Blender场景和骨骼对象与骨骼的具体部分
获取当前Blender场景和名为“骨架”的骨骼对象。
# # Get the whole bge scene
# scene = bge.logic.getCurrentScene()
# # Helper vars for convenience
# source = scene.objects
# # Get the whole Armature
# main_arm = source.get('Armature')
# ob = bge.logic.getCurrentController().owner
# 获取当前场景
scene = bpy.context.scene
# 获取 'Armature' 骨骼对象
armature = bpy.data.objects["骨架"]
# 获取当前对象(这里假设是骨骼对象的所有者,可以根据实际情况修改)
ob = bpy.context.active_object
# get the bones we need
bone_upper_arm_R = armature.pose.bones.get("armUp.R")
bone_lower_arm_R = armature.pose.bones.get("armDown.R")
bone_upper_arm_L = armature.pose.bones.get("armUp.L")
bone_lower_arm_L = armature.pose.bones.get("armDown.L")
bone_trunk = armature.pose.bones.get("trunk.001")
bone_head = armature.pose.bones.get("head.001")
bone_upper_leg_R = armature.pose.bones.get("legUp.R.001")
bone_lower_leg_R = armature.pose.bones.get("legDown.R.001")
bone_upper_leg_L = armature.pose.bones.get("legUp.L.001")
bone_lower_leg_L = armature.pose.bones.get("legDown.L.001")
- 获取当前Blender场景。
- 获取名为
骨架
的骨骼对象。 - 获取当前活动对象。
4.定义四元数运算函数
四元数乘法函数,用于旋转计算。
def multiplyQuaternion(q1, q0):
w0, x0, y0, z0 = q0
w1, x1, y1, z1 = q1
return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
-x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)
5.设置骨骼旋转的函数
根据四元数设置骨骼的旋转。
def setBoneRotation(bone, rotation):
w, x, y, z = rotation
bone.rotation_quaternion[0] = w
bone.rotation_quaternion[1] = x
bone.rotation_quaternion[2] = y
bone.rotation_quaternion[3] = z
关于四元数控制骨骼旋转看上一篇就行,这里不再赘述。导航一下:Blender使用脚本控制骨骼旋转1.0
6.更新角度的函数(!!重点!!)
根据从串口接收到的数据更新骨骼的旋转。该函数 updateAngles 是整个代码的核心之一。它的作用是将从串口接收到的角度数据应用到Blender中的骨骼Armature
上。具体来说,它首先将输入的角度数据转换为适当的格式,然后计算各个骨骼部分的相对旋转,并最终将这些旋转应用到相应的骨骼上。
①将角度数据转换位NumPy数组:
- 以便进行后续的四元数运算。
lowerarmR_out = np.array([angles[0][0], angles[0][1], angles[0][2], angles[0][3]])
... ...
②计算各个骨骼部分的相对旋转:
trunk_inv
是trunk_out
的共轭四元数,用于计算head_rel
。head_rel
是头部相对于躯干的旋转。upperarmR_inv
是右上臂的共轭四元数,用于计算lowerarmR_rel
。- 依此类推,计算其他骨骼部分的相对旋转。
trunk_inv = trunk_out * np.array([1, -1, -1, -1])
head_rel = multiplyQuaternion(trunk_inv, head_out)
upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
... ...
③应用旋转到骨骼:
- 使用
setBoneRotation 函数
,将计算好的四元数旋转应用到各个骨骼部位。
setBoneRotation(bone_trunk, trunk_out)
setBoneRotation(bone_upper_arm_R, upperarmR_out)
setBoneRotation(bone_lower_arm_R, lowerarmR_rel)
... ...
总之,updateAngles 函数从串口接收到的角度数据中提取并转换为NumPy数组。然后,它计算各个骨骼部分的相对旋转,并将这些旋转应用到Blender中的相应骨骼上。通过这种方式,可以将外部设备的运动数据实时映射到Blender中的骨骼动画上,实现动态动画效果。
7.重置骨骼的函数
- 将所有骨骼的旋转重置为默认状态。
def resetBone():
setBoneRotation(bone_upper_arm_R, [1, 0, 0, 0])
# 重置其他骨骼的旋转在这里插入代码片
8.定义Blender操作符类 —— Modal Timer Operator 类(!!重点!!)
处理定时器事件,每次定时器触发时从串口读取数据并更新骨骼。
①字段解释
class ModalTimerOperator(bpy.types.Operator):
# we need these two fields for Blender
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
bl_idname
:这是 Blender 中操作符的唯一标识符,通常以 wm. 开头。bl_label
:这是操作符的名称,会在 Blender 的用户界面中显示。_timer
:这是一个私有字段,用于存储定时器对象。
②modal方法
1)事件处理
def modal(self, context, event):
if event.type == "ESC":
print("BlenderTimer received ESC.")
return self.cancel(context)
if event.type == "TIMER":
modal
函数在 Blender 中通过定时器事件和键盘事件被反复调用。event.type == "ESC"
:如果检测到按下 ESC 键,调用 self.cancel(context) 方法取消操作。event.type == "TIMER"
:定时器事件,用于定期从串口读取数据并更新角度。
2)读取串口数据并验证数据有效性
try:
# Read serial data
s = ser.readline()[:-3].decode('UTF-8') # delete ";\r\n"
except Exception as e:
print(f"Error reading serial data: {e}")
return {"PASS_THROUGH"}
if not s:
print("Invalid joint data")
return {"PASS_THROUGH"}
- 尝试从串口读取一行数据,并去掉末尾的
;\r\n
字符。 - 如果读取失败,打印错误信息并返回
"PASS_THROUGH"
,继续处理其他事件。 - 如果读取的数据为空,打印错误信息并返回
"PASS_THROUGH"
。
3)处理关节角度数据
# Process joint angles
angles = [x.split(',') for x in s.split(';')]
- 将串口读取的数据按
;
分割成多个关节数据,然后再按,
分割成单个角度值。
4)转换数据类型
try:
for i in range(len(angles)):
angles[i] = [float(x) for x in angles[i]]
except ValueError as ve:
print(f"Error converting string to float: {ve}")
return {"PASS_THROUGH"}
- 尝试将每个角度值从字符串转换为浮点数。
- 如果转换失败,打印错误信息并返回
"PASS_THROUGH"
。
5)调用更新函数
if len(angles) == 10:
updateAngles(angles)
#如果刷新率太低,取消这一行的注释以强制Blender渲染视窗
# bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
- 检查解析出的角度数据是否包含 10 个关节的数据。
- 如果数据完整,调用
updateAngles(angles)
函数更新骨骼角度。
总之,
modal
函数通过定时器事件定期从串口读取关节角度数据,并将这些数据应用到 Blender 模型的骨骼上。该过程包括读取和验证数据、解析和转换数据类型、以及调用更新函数。这种方法使得从外部设备(如传感器)获取的实时运动数据能够动态映射到 Blender 中的 3D 模型,实现动画效果。
③execute方法
该方法在操作符启动时调用,初始化定时器并将操作符添加到 Blender 的事件处理器中。
def execute(self, context):
# update rate is 0.01 second
self._timer = context.window_manager.event_timer_add(1./FPS, window=context.window)
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
context.window_manager.event_timer_add(1./FPS, window=context.window)
:添加定时器,更新速率为每秒 FPS 次。context.window_manager.modal_handler_add(self)
:将操作符添加到 Blender 的事件处理器中。- 返回
"RUNNING_MODAL"
表示操作符正在运行。
④cancel方法
该方法在操作符取消时调用,关闭串口,重置关节位置,并移除定时器。
def cancel(self, context):
ser.close()
# reset joint position
resetBone()
context.window_manager.event_timer_remove(self._timer)
print("BlenderTimer Stopped.")
return {"CANCELLED"}
ser.close()
:关闭串口。resetBone()
:重置关节位置。context.window_manager.event_timer_remove(self._timer)
:移除定时器。- 打印消息并返回
"CANCELLED"
表示操作符已取消。
总之,
ModalTimerOperator
类通过定时器事件定期从串口读取关节角度数据,并将这些数据应用到 Blender 模型的骨骼上。该类的实现包括事件处理、定时器管理、数据读取和处理,以及操作符的启动和取消操作。这种方法使得从外部设备(如传感器)获取的实时运动数据能够动态映射到 Blender 中的 3D 模型,实现动画效果。
9.主程序
注册并启动操作符,处理串口数据,捕获异常以进行清理工作。
if __name__ == "__main__":
try:
print("Starting services.")
bpy.utils.register_class(ModalTimerOperator)
# start Blender timer
bpy.ops.wm.modal_timer_operator()
print("All started.")
except KeyboardInterrupt:
resetBone()
ser.close()
print("Received KeyboardInterrupt, stopped.")
- 注册并启动
ModalTimerOperator
操作符。 - 在脚本运行时,启动Blender的定时器操作符并处理串口数据。
- 捕获
KeyboardInterrupt
异常,停止骨骼更新并关闭串口通信。
三.完整代码部分
import bpy
import time
import sys
#sys.path.append("/usr/lib/python3/dist-packages")
import serial
import glob
import numpy as np
FPS = 60
# 串口配置
port = 'COM10' # 指定串口号,根据实际情况修改
baudrate = 57600 # 波特率,根据实际情况修改
# 创建串口对象
ser = serial.Serial(port, baudrate, timeout=1)
if ser.isOpen():
print(f"串口 {port} 已打开,波特率 {baudrate}")
else:
print(f"无法打开串口 {port}")
#Connect the suit first and after a ~second launch the script
# # Get the whole bge scene
# scene = bge.logic.getCurrentScene()
# # Helper vars for convenience
# source = scene.objects
# # Get the whole Armature
# main_arm = source.get('Armature')
# ob = bge.logic.getCurrentController().owner
# 获取当前场景
scene = bpy.context.scene
# 获取 'Armature' 骨骼对象
armature = bpy.data.objects["骨架"]
# 获取当前对象(这里假设是骨骼对象的所有者,可以根据实际情况修改)
ob = bpy.context.active_object
# get the bones we need
bone_upper_arm_R = armature.pose.bones.get("armUp.R")
bone_lower_arm_R = armature.pose.bones.get("armDown.R")
bone_upper_arm_L = armature.pose.bones.get("armUp.L")
bone_lower_arm_L = armature.pose.bones.get("armDown.L")
bone_trunk = armature.pose.bones.get("trunk.001")
bone_head = armature.pose.bones.get("head.001")
bone_upper_leg_R = armature.pose.bones.get("legUp.R.001")
bone_lower_leg_R = armature.pose.bones.get("legDown.R.001")
bone_upper_leg_L = armature.pose.bones.get("legUp.L.001")
bone_lower_leg_L = armature.pose.bones.get("legDown.L.001")
def multiplyQuaternion(q1, q0):
w0, x0, y0, z0 = q0
w1, x1, y1, z1 = q1
return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
-x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)
def setBoneRotation(bone, rotation):
w, x, y, z = rotation
bone.rotation_quaternion[0] = w
bone.rotation_quaternion[1] = x
bone.rotation_quaternion[2] = y
bone.rotation_quaternion[3] = z
def updateAngles(angles):
lowerarmR_out = np.array([angles[0][0],angles[0][1],angles[0][2],angles[0][3]])
upperarmR_out = np.array([angles[1][0],angles[1][1],angles[1][2],angles[1][3]])
lowerarmL_out = np.array([angles[2][0],angles[2][1],angles[2][2],angles[2][3]])
upperarmL_out = np.array([angles[3][0],angles[3][1],angles[3][2],angles[3][3]])
trunk_out = np.array((angles[4][0],angles[4][1],angles[4][2],angles[4][3]))
upperLegR_out = np.array((angles[5][0],angles[5][1],angles[5][2],angles[5][3]))
lowerLegR_out = np.array((angles[6][0],angles[6][1],angles[6][2],angles[6][3]))
upperLegL_out = np.array((angles[7][0],angles[7][1],angles[7][2],angles[7][3]))
lowerLegL_out = np.array((angles[8][0],angles[8][1],angles[8][2],angles[8][3]))
head_out = np.array((angles[9][0],angles[9][1],angles[9][2],angles[9][3]))
# upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
# lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
trunk_inv = trunk_out * np.array([1, -1, -1, -1])
head_rel = multiplyQuaternion(trunk_inv, head_out)
# upperarmR_rel = multiplyQuaternion(trunk_inv, upperarmR_out)
upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
upperarmL_inv = upperarmL_out * np.array([1, -1, -1, -1])
lowerarmL_rel = multiplyQuaternion(upperarmL_inv, lowerarmL_out)
upperLegR_inv = upperLegR_out * np.array([1, -1, -1, -1])
lowerLegR_rel = multiplyQuaternion(upperLegR_inv, lowerLegR_out)
upperLegL_inv = upperLegL_out * np.array([1, -1, -1, -1])
lowerLegL_rel = multiplyQuaternion(upperLegL_inv, lowerLegL_out)
setBoneRotation(bone_trunk, trunk_out)
setBoneRotation(bone_upper_arm_R, upperarmR_out)
setBoneRotation(bone_lower_arm_R, lowerarmR_rel)
setBoneRotation(bone_upper_arm_L, upperarmL_out)
setBoneRotation(bone_lower_arm_L, lowerarmL_rel)
setBoneRotation(bone_head, head_rel)
setBoneRotation(bone_upper_leg_R, upperLegR_out)
setBoneRotation(bone_lower_leg_R, lowerLegR_rel)
setBoneRotation(bone_upper_leg_L, upperLegL_out)
setBoneRotation(bone_lower_leg_L, lowerLegL_rel)
def resetBone():
setBoneRotation(bone_upper_arm_R, [1, 0, 0, 0])
setBoneRotation(bone_lower_arm_R, [1, 0, 0, 0])
setBoneRotation(bone_upper_arm_L, [1, 0, 0, 0])
setBoneRotation(bone_lower_arm_L, [1, 0, 0, 0])
setBoneRotation(bone_trunk, [1, 0, 0, 0])
setBoneRotation(bone_head, [1, 0, 0, 0])
setBoneRotation(bone_upper_leg_R, [1, 0, 0, 0])
setBoneRotation(bone_lower_leg_R, [1, 0, 0, 0])
setBoneRotation(bone_upper_leg_L, [1, 0, 0, 0])
setBoneRotation(bone_lower_leg_L, [1, 0, 0, 0])
class ModalTimerOperator(bpy.types.Operator):
# we need these two fields for Blender
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
def modal(self, context, event):
if event.type == "ESC":
print("BlenderTimer received ESC.")
return self.cancel(context)
if event.type == "TIMER":
try:
# Read serial data
s = ser.readline()[:-3].decode('UTF-8') # delete ";\r\n"
except Exception as e:
print(f"Error reading serial data: {e}")
return {"PASS_THROUGH"}
if not s:
print("Invalid joint data")
return {"PASS_THROUGH"}
# Process joint angles
angles = [x.split(',') for x in s.split(';')]
try:
for i in range(len(angles)):
angles[i] = [float(x) for x in angles[i]]
except ValueError as ve:
print(f"Error converting string to float: {ve}")
return {"PASS_THROUGH"}
if len(angles) == 10:
updateAngles(angles)
# if refresh rate is too low, uncomment this line to force Blender to render viewport
# bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
return {"PASS_THROUGH"}
def execute(self, context):
# update rate is 0.01 second
self._timer = context.window_manager.event_timer_add(1./FPS, window=context.window)
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def cancel(self, context):
ser.close()
# reset joint position
resetBone()
context.window_manager.event_timer_remove(self._timer)
print("BlenderTimer Stopped.")
return {"CANCELLED"}
if __name__ == "__main__":
try:
print("Starting services.")
bpy.utils.register_class(ModalTimerOperator)
# start Blender timer
bpy.ops.wm.modal_timer_operator()
print("All started.")
except KeyboardInterrupt:
resetBone()
ser.close()
print("Received KeyboardInterrupt, stopped.")
代码参考这位大佬:【STM32 MPU6050 动作捕捉完结】
所有代码都在大佬的GIT: GIT
Blender通信脚本可参考:
【笔记 #12: MPU6050 三轴陀螺仪实现手臂关节动作捕捉 + Blender 实时串流同步】
MotioSuit:github
侵删