简介:这套源码用于实现基于OpenSim的符号肌肉力矩臂计算,面向生物力学研究人员与运动仿真方向学习者,解决肌肉与关节之间力学关系的量化分析与可视化问题。压缩包约2.97MB,共16个文件,包含Python脚本、C++头文件与源文件、OpenSim人体模型、dat数据、csv肌肉坐标、png结果图以及pdf、md说明文档,功能覆盖力矩臂符号计算、多元多项式拟合、可视化出图和结果存储。已有132人学习浏览。资源内提供分别适配OpenSim 3.3与4.0的Python版本,同时包含C++接口实现、multipolyfit多项式拟合工具,并附带示例步态模型与肌肉坐标数据,可直接运行复现计算流程;借助图表和说明文档可快速掌握符号力矩臂矩阵的构建、高阶导数拟合及dat结果存储,涵盖模型导入、参数配置到结果导出的完整链路,适合用于教学演示、课题预研或二次开发。
1. 基于OpenSim的符号肌肉力矩臂计算系统到底解决了什么
做生物力学的人第一次在OpenSim里看到力矩臂曲线时,会觉得这东西就是点两下鼠标的事。但等你真正想把它用进康复外骨骼的关节力矩分配、或者想对肌肉附着点做优化时,会发现GUI导出的离散数值根本不够用——曲线不光滑、步长要反复试、跨关节的肌肉行为黑乎乎一片。基于OpenSim的符号肌肉力矩臂计算系统(源码)做的事情很明确:把OpenSim模型里的肌肉路径用符号变量重新描述,对目标关节角求导,得到力矩臂关于关节角的解析表达式,之后任意角度直接代入求值。
这套源码方向适合三类人:做肌肉驱动运动仿真的课题组,想摆脱数值差分步长猜测的外骨骼工程师,以及在论文里需要光滑可复现力矩臂曲线的研究者。它不是OpenSim的替代品,而是给OpenSim补一层“几何可微”的能力。下面按一套可复现的路径展开:先说清楚力臂的数学到底是什么,再给最小实现代码,最后交代真机模型上的坑。
2. 力矩臂的几何本质:先算L(θ),再决定要不要差分
2.1 力臂不是测出来的,而是肌肉长度对关节角的导数
力矩臂在肌肉骨骼模型里是一个纯几何量。肌肉绕关节产生力矩 ( M = F \times r ),这里的 ( r ) 与力的大小无关,它等于肌肉长度 ( L ) 对关节角 ( \theta ) 的负导数:
( r(\theta) = -\frac{dL}{d\theta} )
负号来自约定:肌肉缩短方向与关节正转方向相反时,力臂记为正。这个公式是所有肌肉力矩臂计算的起点。OpenSim模型里肌肉路径由一串PathPoint组成,每个PathPoint附着在某块骨(body)的局部坐标系上,肌肉长度就是相邻PathPoint之间距离的总和。
当关节转动时,附着在远端骨上的PathPoint会随刚体旋转矩阵移动,( L ) 也随之改变。所以计算力矩臂的本质,就是计算“路径点随关节转动的几何变化率”。这个观点很重要:你不需要知道肌肉产生了多少力,只需要知道路径的几何形状。
2.2 数值差分的截断误差和舍入误差会打架
最常见的力矩臂计算方法是中心差分:
( r(\theta) \approx -\frac{L(\theta+\delta) - L(\theta-\delta)}{2\delta} )
看起来无脑可用,但 ( \delta ) 的选取是个坑。( \delta ) 太大,泰勒展开的高阶项开始污染结果,产生截断误差;( \delta ) 太小,( L(\theta) ) 本身由多体动力学求解器给出,末位数字的舍入误差被 ( \delta ) 放大,曲线出现毛刺。实际操作时,同一个模型里不同肌肉可能要用不同的 ( \delta ),而且换一个关节角度范围又得重新试。
我早期做膝关节力矩臂曲线时,用 ( \delta = 10^{-6} ) 在 30 度附近很平滑,换到 90 度附近就开始抖。这不是OpenSim算错了,是差分步长在模型数值精度底噪面前先天不足。
| 方法 | 输入 | 输出形式 | 主要坑 |
|---|---|---|---|
| 数值中心差分 | 肌肉长度采样 | 离散数列 | δ难调,近极限角抖动 |
| OpenSim自带力矩臂(Point Kinematics) | 模型文件+坐标值 | 单点数值 | 黑匣子,不可求导,难批量 |
| 符号推导 | 模型几何+关节定义 | 解析表达式 | wrap与耦合坐标需额外处理 |
2.3 符号法的假设:路径由刚体上的点和折线段构成
符号法把肌肉路径简化成一张几何拓扑图。每个PathPoint附着在某个body上,坐标是该body局部坐标系的固定向量;肌肉长度是相邻PathPoint距离之和;关节转动时,远端body上的点随刚体旋转矩阵移动。只要肌肉路径没有经过wrapping surface(包裹面),这个模型在数学上是精确的。
带包裹面的肌肉是另一个量级的问题。比如肩三角肌绕圆柱表面走,肌肉路径变成“直线段+圆弧段”,弧长与切点位置都随关节角非线性变化,符号推导需要针对圆柱、球、椭球分别解切点方程。大部分这类源码系统会先声明支持无wrap路径,或者把包裹路径做等效点近似。拿到新模型先检查肌肉是否有PathWrapSet,这是决定符号法可行性的第一道门槛。
2.4 解析表达式带来的额外产物
如果只是要一条曲线,数值法够用。但符号表达式的价值在于可以继续做运算:对 ( r(\theta) ) 再求一次导,得到力臂斜率,用于刚度分析和运动控制里的关节阻抗整形;求 ( r(\theta)=0 ) 的零点,定位关节运动范围内的几何临界点;把力臂解析式直接嵌入基于梯度的肌肉附着点优化循环里——这一步数值法几乎做不到,因为数值求导在优化迭代里会引入噪声。
这也是“符号肌肉力矩臂计算系统”这类源码包值得投入的原因:它把OpenSim从“仿真器”变成了“可微分几何模型”。有了显式表达式,后续所有需要梯度的计算都不再依赖差分。
3. 最小实现:用SymPy对一条肌肉路径做符号求导
3.1 从.osim文件里取出肌肉路径点
先不急着引入OpenSim的Python绑定,很多.osim文件本身就是XML,直接解析就好。下面的函数读入模型文件,按肌肉名找到它的PathPoint列表:
import xml.etree.ElementTree as ET def load_muscle_pts(osim_path, muscle_name): tree = ET.parse(osim_path) root = tree.getroot() for muscle in root.iter('Muscle'): name_el = muscle.findtext('name') if name_el != muscle_name: continue pts = [] for pp in muscle.iter('PathPoint'): loc_text = pp.findtext('location') if loc_text is None: continue loc = loc_text.split() pts.append({ 'body': pp.findtext('body'), 'loc': [float(loc[0]), float(loc[1]), float(loc[2])] }) return pts raise KeyError(f'{muscle_name} not found')这段代码的逻辑是:遍历XML里所有Muscle节点,匹配名字后,进入PathPointSet的子节点,取出每个PathPoint的location和body字段。OpenSim的XML schema里,location是三个由空白分隔的浮点数,body字段表示该点依附的坐标系名称。解析时不要假设顺序,一定按标签名取。
这里有个注意点:有些模型文件里PathPoint同时有location和location_in_parent两个字段,前者是相对于父frame的坐标,后者是绝对坐标。肌肉长度计算用的是相对坐标随关节转动的变化,所以location才是我们要的。如果你发现力臂符号结果和OpenSim自带结果系统性偏差,优先检查是不是取错了字段。
3.2 把旋转几何符号化
假设目标关节只做一个方向的旋转,旋转轴沿Z轴(OpenSim里很多屈伸关节的axis就是0 0 1)。近端骨固定,远端骨绕Z轴旋转 ( \theta ),远端附着点从局部坐标 ( p_{local} ) 变成世界坐标 ( R_z(\theta) \cdot p_{local} ):
import sympy as sp theta = sp.Symbol('theta', real=True) Rz = sp.Matrix([ [sp.cos(theta), -sp.sin(theta), 0], [sp.sin(theta), sp.cos(theta), 0], [0, 0, 1] ]) # 近端附着点(局部坐标,体固定) p_fixed = sp.Matrix([0.05, -0.03, 0.0]) # 远端附着点(局部坐标,随关节转动) p_local = sp.Matrix([-0.02, -0.12, 0.0]) p_rot = Rz * p_local d_vec = p_rot - p_fixed L = sp.sqrt(d_vec.dot(d_vec)) moment_arm = -sp.diff(L, theta)逻辑说明:先构造旋转矩阵,让远端点经历刚体旋转,然后计算两点距离得到肌肉长度表达式 ( L(\theta) ),最后对 ( \theta ) 求导取负。这里的moment_arm是一个SymPy表达式,不是数值。如果你打印出来,会看到带sin(theta)、cos(theta)和根号的分式——这是符号力矩臂的原始形态。
参数说明:旋转轴方向不同,矩阵要换。如果关节axis是1 0 0(绕X轴),把Rz换成绕X轴的旋转矩阵;axis是0 1 0则换绕Y轴。不要想当然认为模型里的轴一定和全局坐标对齐,后面第4章会讲怎么从模型里自动读取。
3.3 化简与生成高性能数值函数:cse和lambdify是核心参数
符号表达式可以直接看,但直接用来做数值计算非常慢。SymPy的simplify会把大量时间花在三角恒等式搜索上,有时一个表达式能化简几分钟还没结果。更实际的路径是先做公共子表达式提取(CSE),再转成NumPy可调用的函数:
from sympy import cse, lambdify import numpy as np repl, reduced = cse(moment_arm, symbols=sp.numbered_symbols('t')) ma_fast = reduced[0] f_ma = lambdify(theta, ma_fast, modules=['numpy']) angles_deg = np.linspace(0, 120, 121) angles_rad = np.deg2rad(angles_deg) r_values = f_ma(angles_rad)逻辑说明:cse把表达式里重复出现的子表达式提取出来,用临时符号t0, t1, ...代替,再代入原式。lambdify把SymPy表达式编译成Python函数,modules=['numpy']表示生成的函数内部用NumPy的cos、sqrt等函数,这样输入NumPy数组时会按向量化计算,而不是逐个循环。
参数说明:numberd_symbols('t')生成t0, t1, t2这样的临时符号,避免和theta冲突。r_values是和angles_rad同长度的数组。如果lambdify时发现表达式里有sin、cos以外的函数(比如atan2),检查是否在modules里指定了对应的NumPy函数,否则会抛NameError。
3.4 最小自检:和中心差分对拍
有了符号结果,第一步验证永远是和中心差分做对比。这个环节可以暴露坐标字段取错、旋转轴方向反了、路径点漏了等基础错误:
def num_moment_arm(theta_val, delta=1e-6): L_plus = float(L.subs(theta, theta_val + delta)) L_minus = float(L.subs(theta, theta_val - delta)) return -(L_plus - L_minus) / (2 * delta) for deg in [10, 30, 60, 90, 110]: rad = np.deg2rad(deg) ana = float(moment_arm.subs(theta, rad)) num = num_moment_arm(rad) print(f'{deg} deg: analytic={ana:.6f}, numeric={num:.6f}, ' f'diff={abs(ana - num):.2e}')逻辑说明:对每个测试角度,分别用符号表达式直接代值,和用中心差分计算长度变化率,对比差异。正常情况差异应该在1e-8量级(有限精度下)。如果差异到了1e-3以上,先检查旋转轴方向,再把差分步长调小一档试试——如果步长调小后差异反而变大,说明符号结果大概率是对的,数值差分自己在舍入误差里挣扎。
参数说明:delta=1e-6是中心差分步长,单位是弧度。这个验证里L是SymPy表达式,subs返回新表达式,float()负责把根式转成浮点数。测试角度覆盖过中点和接近极限的位置,因为极限位置附近力臂绝对值小,相对误差容易被放大。
4. 接上真实模型:把OpenSim的路径点和坐标系变成可批量计算的源码
4.1 用opensim-python读取模型与路径
XML解析适合快速验证,但一旦模型复杂——有多坐标系、有耦合坐标、有wrap——还是得回到OpenSim本身。OpenSim 4.x有官方Python绑定,安装完opensim模块后可以这样拿肌肉路径:
import opensim as os model = os.Model('your_model.osim') state = model.initSystem() muscle = model.getMuscles().get('med_gastrocnemius_r') path = muscle.getGeometryPath() pt_set = path.getPathPointSet() points = [] for i in range(pt_set.getSize()): pt = pt_set.get(i) loc = pt.getLocation() # Vec3 points.append([loc[0], loc[1], loc[2]]) print(points)逻辑说明:initSystem构建多体动力学系统,getMuscles().get()按名字拿肌肉,getGeometryPath()拿到几何路径,getPathPointSet()拿到路径点集合。每个PathPoint的getLocation()返回该点在父坐标系下的坐标。
参数说明:这里的getLocation()返回的是Vec3,下标0/1/2对应x/y/z。如果你的OpenSim版本较老,getLocation()可能返回Vec3而不是SimTK::Vec3,但下标访问方式一样。肌肉名在模型文件里可以用model.getMuscles().get(i).getName()遍历查看,不用硬记。
4.2 坐标轴读取:axis向量和旋转矩阵的关系
符号法要求知道目标关节的旋转轴。在.osim文件里,每个Coordinate定义里有一个<axis>字段,比如膝关节屈曲的<axis>0 0 1</axis>表示绕Z轴旋转。但这里有个细节:axis是旋转轴的方向向量,不是旋转矩阵本身。OpenSim内部对关节的建模是“子body坐标系相对父body坐标系的变换”,这个变换可能是旋转+平移的组合,甚至可能是耦合坐标的函数。
从源码实践的角度,建议分两步走。第一步,用XML解析读出axis向量,确定用绕哪个轴的旋转矩阵;第二步,对存在耦合的坐标(比如膝关节的平移伴随屈曲发生),先固定其它坐标为常数,只对目标坐标求偏导。这种“冻结其它坐标”的做法在处理多关节肌时几乎是必须的:
import xml.etree.ElementTree as ET tree = ET.parse('your_model.osim') root = tree.getroot() coord_map = {} for coord in root.iter('Coordinate'): name = coord.findtext('name') axis_text = coord.findtext('axis') if name and axis_text: axis = axis_text.split() coord_map[name] = [float(axis[0]), float(axis[1]), float(axis[2])] print(coord_map.get('knee_angle_r'))这段代码把模型里所有坐标名和axis向量读到一个字典里。参数说明:<axis>字段存在个别旧模型里可能没有,这时默认按0 0 1处理并打印警告,不要静默通过。axis向量不是单位向量的情况极少,但如果你发现符号结果整体差一个固定倍数,检查axis的模长是否为1。
4.3 批量生成力矩臂矩阵:从单肌肉到全身肌肉
单肌肉的符号推导跑通后,系统才有价值。批量做法的核心是循环:遍历所有肌肉、所有坐标,对每条“肌肉×关节”组合生成对应的符号表达式。这里有个实践经验:不要一条肌肉一条肌肉手工跑,而是把所有肌肉的路径点先解析出来缓存成JSON,再统一做符号推导。
import json def extract_all_muscle_pts(osim_path): tree = ET.parse(osim_path) result = {} for muscle in tree.iter('Muscle'): name = muscle.findtext('name') pts = [] has_wrap = False for wrap in muscle.iter('PathWrap'): if wrap.findtext('wrap_object') is not None: has_wrap = True for pp in muscle.iter('PathPoint'): loc = pp.findtext('location').split() pts.append([float(loc[0]), float(loc[1]), float(loc[2])]) result[name] = {'points': pts, 'has_wrap': has_wrap} with open('muscle_pts_cache.json', 'w') as f: json.dump(result, f, indent=2) return result逻辑说明:这个函数一次性把模型里所有肌肉的路径点和wrap标志导出到JSON缓存。has_wrap标记会在后续符号推导里用到——有wrap的肌肉先跳过,避免符号推导在包裹面上产生不可控的表达式。缓存文件的好处是,后续改符号推导代码时不用重新解析XML,直接读JSON。
参数说明:JSON里保存的是肌肉名到路径点列表的映射。路径点的坐标顺序和OpenSim内部顺序一致,因为PathPointSet的遍历顺序就是XML里的出现顺序。如果你的模型有多个Muscle节点同名,后面会覆盖前面,这是OpenSim模型文件的正常现象——同名肌肉通常是不允许的。
4.4 数值验证协议:三路对拍
批量算出来的力矩臂矩阵必须做一次全量验证,不能只挑一条肌肉看。我一般会把三路结果放一起比较:符号表达式直接代值、中心差分、OpenSim自带的力矩臂计算(如果有的话)。比对时用相对误差而不是绝对误差,因为不同关节的力矩臂量级差异很大(踝关节比腕关节大一个数量级)。
| 角度位置 | 符号值(Nm等效) | OpenSim数值 | 相对误差 |
|---|---|---|---|
| 关节活动范围中间段 | 0.832 | 0.834 | 0.2% |
| 接近活动极限 | 0.124 | 0.131 | 5.3% |
| 肌肉长度极值附近 | 0.008 | 0.015 | 46% |
这个表格是典型的验证输出。中间段误差在1%以内说明符号推导正确;极限段误差放大是正常的,因为力臂绝对值变小,相对误差自然变大;极值附近误差超过40%则意味着目标角度下肌肉力臂接近零,数值方法本身的绝对误差就已经不可忽略了。看到第三种情况不要慌,这不是符号算错了,而是数值参考本身在零值附近不稳定。
验证协议的重点是:全肌肉遍历,不遗漏任何一条;每个坐标至少测5个角度点,覆盖活动范围中间和两端;把验证结果保存下来,作为每次改模型后的回归测试基线。
5. 避坑记录:符号力矩臂在实测中翻车的五个原因
5.1 符号表达式爆炸:cse也救不回来时怎么办
现象:某条肌肉的力矩臂表达式在cse后仍然有几十个中间变量,lambdify生成的函数每次调用要算几千次浮点运算,比OpenSim直接数值计算还慢十倍。
原因:这条肌肉的路径点很多(6个以上),且经过的body之间有复杂的旋转关系,符号推导过程中产生大量重复的三角运算。更多时候,罪魁祸首是wrapping surface参与推导——圆柱切点本身要解反三角函数,切点坐标再参与距离计算,表达式会指数级膨胀。
解决:第一优先检查PathWrapSet,有wrap的肌肉直接排除在符号计算之外,改用等效路径点近似(把绕圆柱的路径等效成一个固定的绕行点)。第二优先对角度做分段,每个分段单独做符号推导,表达式会比全范围公式短得多。实在不行,退回数值法,对这条肌肉单独用中心差分,其它肌肉保持符号化。
5.2 解析结果和OpenSim曲线的系统偏差
现象:符号法计算某个关节的力臂,整体趋势和OpenSim一致,但绝对值系统性偏大或偏小,偏差比例恒定。
原因:路径点坐标取错了参考系。OpenSim的PathPoint有两种坐标表示,一种是相对于父body的局部坐标,一种是世界坐标(或某个中间坐标系的坐标)。如果XML解析时混用了两种坐标,肌肉长度会多出或缺少一个刚体变换,力臂自然按固定比例偏移。
解决:先做一个静态测试。把目标关节固定在某个角度,把所有PathPoint的坐标代入符号表达式,手工计算肌肉长度,再和OpenSim里muscle.getLength(state)的返回值对比。如果长度一致,问题不在路径点;如果长度不一致,逐个PathPoint检查用getLocation()和从XML里读的location字段是否一致,重点看父body有多个坐标系嵌套的模型。
5.3 多关节肌的偏导陷阱
现象:股直肌这种跨髋、膝两个关节的肌肉,符号计算膝关节力臂时结果和OpenSim自带结果对不上,而且误差随髋关节角度改变而变化。
原因:符号推导时只对膝关节角求导,但肌肉长度里同时含有髋关节角的贡献。OpenSim在计算膝关节力矩臂时,内部是把髋关节锁在当前位置的——它求的是偏导数,不是全导数。如果符号实现里把髋关节角当成了独立变量而不固定,求出来的就是全导数,差了耦合项。
解决:对每个关节求偏导前,把所有其它坐标固定为当前值。实现方法是构建符号表达式时,把非目标坐标的Symbol用浮点数替换掉,再对目标坐标求导:
def partial_moment_arm(L_expr, target_theta, fixed_angle, fixed_value): L_fixed = L_expr.subs(fixed_angle, fixed_value) return -sp.diff(L_fixed, target_theta)参数说明:fixed_angle是其它关节的Symbol,fixed_value是它在验证时刻的取值。如果模型里有耦合坐标(比如膝关节的伴随平移),同样要把耦合平移量固定为常数,否则导数里会多出耦合项的贡献。
5.4 单位陷阱:力矩臂突然差了一千倍
现象:某个模型算出来的力臂数值在0.001量级,和文献里的力矩臂值差了一千倍,肌肉长度却看起来正常。
原因:模型文件是用毫米建模的。OpenSim内部默认单位是米,但很多从CT/MRI重建的模型在导出时长度单位是mm,路径点坐标数值都放大了1000倍。肌肉长度因此放大1000倍,对角度求导后力臂也差1000倍。最迷惑的是长度数据本身看起来“合理”,因为肌肉长度的绝对值落在几十到几百的范围内,看不出是毫米还是米。
解决:在解析模型后先做单位检查:
L_test = muscle.getLength(state) if L_test > 10: # 正常人体肌肉长度在0.05~1.0米之间 scale = 0.001 print('模型疑似毫米单位,长度缩放系数=0.001')一个简单经验:正常成年人体的肌肉长度很少超过1米,如果你在初始姿态下拿到一条长度是47的量,几乎可以断定是毫米模型。处理方式是把所有PathPoint坐标乘以0.001再做符号推导,力矩臂结果会自动回到米制量级。
5.5 关节极限位置的NaN和分母爆炸
现象:符号函数在关节角接近解剖极限(比如膝关节完全伸直)时返回NaN或一个天文数字,导致整条力矩臂曲线在末端断裂。
原因:表达式分母里有sqrt(...)或cos(theta)项。关节角接近极限时,肌肉长度导数趋于零,而力臂表达式是长度导数除以长度相关项,分母趋零导致数值溢出。这是几何本身的性质——力臂在肌肉长度极值点附近应该趋近零,但浮点运算会在分母足够小时先爆炸。
解决:设置合理的角度裁剪区间,超出解剖范围的输入直接返回NaN或者用numpy.errstate压制警告再填充边界值:
with np.errstate(divide='ignore', invalid='ignore'): r_raw = f_ma(angles_rad) r_clean = np.where(np.abs(r_raw) > 10 * np.nanmax(r_raw), np.nan, r_raw)逻辑说明:先算原始值,再用np.where把绝对值超过正常范围10倍的异常值替换为NaN。后续绘图时,NaN会自然断裂,避免曲线出现一个刺眼的尖峰。参数说明:10 * np.nanmax(r_raw)这个阈值不是写死的物理含义,它只是用来筛掉分母趋零时的异常尖峰,实际使用中根据你模型的力臂正常范围调整倍数。
6. 进阶技巧:把解析结果锁死在OpenSim的数值上
源码系统跑通一批肌肉后,最重要的事是把验证自动化,做成每次改模型都要过的安全锁。我自己会写一个verify_moment_arms()函数,对每一组“肌肉-关节”组合做三路对拍:符号表达式代入求值、SymPy数值差分、OpenSim的getLength采样差分。脚本输出一个汇总表,任何一条肌肉的相对误差超过阈值就标红。
这个自检还有一个隐藏价值:它能验证你对模型的理解。每当我怀疑“这条肌肉是不是经过了某个我没注意到的wrap”时,跑一遍三路对拍,差异会直接告诉我有个几何项没建模。尤其是处理那些带wrap的肌肉,如果实在想在源码里支持,先对每条wrap肌肉单独标注has_wrap=True,输出到一个“不支持清单”里,而不是让它们在批量计算里静默产生错误结果——这样做的好处是,模型升级后哪些肌肉可以纳入符号计算一目了然。
另一个常用技巧是批量结果落盘。把所有肌肉在所有关节角度的力臂值存成CSV,每行一条肌肉,列是不同角度,这样后续做控制仿真可以直接查表,不用每次重新求值。存储之前,先用cse的表达式做一次lambdify,把生成的Python函数序列化到磁盘,下次加载时直接调用,能省掉数分钟的符号推导时间。至于要不要把符号推导结果直接嵌入C++代码——只有当你的控制器运行环境不依赖Python时才值得做,否则维护成本和收益不成正比。
我自己的习惯是:每次拿到新模型,第一件事不是看肌肉列表,而是先跑一遍单位检查和wrap肌肉清单,然后才敢让符号推导开工。这套“先排查、再推导、后验证”的流程帮我避开过两次单位翻车,也替课题组省下过好几轮重算的返工。希望帮到你。
本文还有配套的精品资源,点击获取