七月 20

神经骨骼肌肉建模:通过神经信号估测肌肉力和关节扭矩以及运动

摘要

本文提供了正向神经骨骼肌肉建模的概览。这种模型的目标是通过神经信号来估计或者预测肌肉力,关节扭矩,以及关节运动学参数。这个过程总共分成四步。第一步,肌肉激活动力学主要研究的是从神经信号到肌肉激活信号的转换 – 肌肉激活信号是一个变化的介于0-1之间的参数。第二步,肌肉收缩动力学表征了肌肉激活信号是如何转换为肌肉力的。第三步,我们需要使用肌肉骨骼的几何模型来将肌肉力转换成关节扭矩。最后,运动方程允许我们将关节扭矩转换成关节运动参数。这其中每一步都包含了复杂的非线性关系。本文的重点在于详细的描述前两步的过程,而这两步也是生物力学研究者最感到头疼的部分。本文还会通过预测肘部和膝部动力学的实际应用案例来讲述整个建模过程。

关键词

Hill模型,EMG,肌腱,肌肉跟腱复合体,肌纤维与肌腱所成的角度

本文标题中的神经肌肉骨骼这个词意味着我们需要对由神经控制的肌肉骨骼系统进行建模来预测运动。神经肌肉骨骼建模对研究肌肉瘫痪的电刺激、设计肌肉电控制的假肢以及一般意义上神经系统如何在健康或者有病变的人体中控制肢体运动都是非常重要的。

研究人体运动的生物力学有两种截然不同的方法:正向动力学和反向动力学。任何一种方法都可以被用来获得关节运动学参数(例如预测关节运动时的扭矩)。了解这两种方法的不同之处是非常重要的。

基于EMG的正向动力学

在使用正向动力学研究人体运动的过程中,输入的参数是神经指令(图1)。神经指令限定了肌肉激活信号的强度。神经信号可以像本文介绍的那样从EMG信号中获得,也可以通过优化或者神经网络模型进行估算。

当神经指令需要增加或者减少肌肉力时,EMG信号的强度也会随之改变。然而,要直接比较不同肌肉的EMG信号的绝对数值还是很困难的。这是因为EMG信号的强度是由很多因素决定的,例如放大器的幅值,测量电极的种类,电极放置的位置,电极和肌肉之间组织的多少。因此,为了使用EMG信号来建立神经肌肉骨骼模型,我们首先应该把它们转换成一个被称作肌肉激活信号的参数ai(这里的i代表肌肉的编号)。这个过程就被称作是肌肉激活动力学。这个过程的输出参数ai在数学上表示一个介于0-1之间的变化的数值。

肌肉收缩动力学主要描述从肌肉激活信号ai到肌肉力Fi之间的转换。一旦肌肉开始产生力,肌腱便开始承担载荷并且将力从肌肉传递给骨骼。这种力被称作肌腱肌肉力。由于关节运动学结构的差异,肌腱和肌肉之间的相对长度变化可能差异很大。例如,当我们进行静态拉伸的时候。

关节扭矩是所有肌肉肌腱力乘以它们对应的力臂长度之和。在每个肌肉肌腱单元上的力都对最后的关节总力矩产生影响。肌肉肌腱的几何形状决定了肌肉力臂的长度。值得注意的是,肌肉的力臂长度并不是一个定值,它是随着关节运动变化的。另外,我们需要注意许多关节都有多个自由度,由于几何形状不同,肌肉也可能对关节有不同的作用。例如,肱二头肌可以同时作为肘关节的曲肌以及前臂的旋后肌;股四头肌可以同时作为膝关节的伸肌以及髋关节的曲肌,诸如此类。最后,我们还需要注意到关节扭矩Mj(j代表对应的关节)是由各个肌肉的力矩共同决定的。如果在某种程度上某些肌肉没有被包含在计算过程中,关节扭矩就会被低估。这一转换之后我们就可以得到各个关节的扭矩。更加精确的说,是关节各个自由度的扭矩。

我们可以利用多关节动力学,以关节扭矩作为输入参数来计算我们感兴趣的各个关节的加速度,速度以及关节角。至于负反馈方面,神经命令会受到肌肉长度(肌梭)以及肌腱力(腱器官)的影响。还有其他一些感受器官也在负反馈环节中起着作用,但是肌梭和腱器官是最重要的两种。

正向动力学存在的问题

由EMG信号驱动的不同复杂程度的模型曾被用来估测膝关节,腰背部,以及肘关节的关节力矩。然而,使用正向动力学方法也伴随着不少问题。

首先,正向动力学方法需要我们估算肌肉激活信号。而EMG信号的高度变化性让这一过程变得非常困难,这一特点在动态过程中尤其明显。第二,从肌肉激活信号转换成肌肉力的过程也是非常困难的,这主要是由于其中的机理我们还不能完全理解。这其中的大部分模型都是基于Hill所建立的基于现象的经典模型,以及由Huxley建立的更加复杂的生物物理模型。

要绕开从EMG信号获得肌肉力的问题,方法之一是使用优化方法来直接预测肌肉力,这样就绕开了前面所描述的两个问题。然而,如何选择正确的成本函数还是一个广受争议的问题。研究神经控制人体运动的科学家惊奇的发现生物力学工程师们将他们所有的工作,也就是整个中枢神经系统,简单的替换成一个未经证实的方程。然而,在研究一些特定问题是,有些成本函数还是提供了较为合理的近似的。尽管优化方法更常用于反向动力学模型,使用基于成果的成本函数选择肌肉,例如通过最大化跳跃高度或者最小化代谢能量,也曾被用于正向运动学中。

正向运动学的另外一个难点就是决定肌肉-肌腱的力臂长度以及作用线。这些在尸体中都很难进行测量,在活体中就更难精确测量了。最后,由于我们难以准确估算每个肌肉上的肌肉力,估算关节力矩非常容易引入误差。更糟糕的是,当我们使用正向动力学时,关节力矩上的微小误差会成为关节位置上的大误差。

正向动力学与反向动力学比较

反向动力学方法从反方向来解决我们前面描述的问题。此时我们从测量人体的位置和作用于人体的外力开始(图2)。例如,在步态分析中,位置标记被放置在参与者的肢体上,并使用基于录像机的视频系统来采集为之一,同时使用力学记录系统来记录外力。

在相邻的肢体上的标记被用来计算不同肢体的相对的位置和方向,并据此计算关节角。这些数据通过差分后可以获得速度和加速度。加速度和施加在人体上的外力信息可以被用来构建运动方程,最终获得对应的关节反作用力和力矩。

如果我们同时也拥有肌肉骨骼几何形状,肌肉力在理论上也可以从关节力矩中估测,并且我们还可能据此估测韧带和关节的压缩力。然而,将这些力分解到各个肌肉并不简单。

反向动力学存在的问题

和正向动力学一样,反向动力学也存在非常重要的局限。首先,如果我们想要正确预估关节力矩,我们就必须知道每个人体部分的质量以及惯量。这些参数难以测量因此只能估算。通常这些参数都是通过测量的尸体数据并且使用简单的缩放法获得的,其准确性很难保证。

第二,我们需要将位置数据进行差分以获得肢体各个部分的角速度,线速度,角加速度,线加速度。然而这一过程在现实中是非常难以实现的,也就是说测量中的误差会在差分的过程中被放大。

第三,我们计算得到的关节反力以及关节扭矩都是净值。如果我们使用反向动力学来预测肌肉力的话,这一点就非常重要。例如,如果一个人让他的大腿后肌产生30Nm的曲缩扭矩,同时让他的股四头肌产生25Nm的伸展扭矩,那么我们用反向动力学最终得到的膝关节曲缩扭矩结果就会是5Nm。由于实际的膝关节曲缩肌所做的贡献是计算值的六倍,我们可以知道这种方法是非常不准确的。因此这种方法也不适合用于估算屈膝时曲缩肌所起的作用。我们上面的例子并不夸张,因为肌肉协作是非常常见的。然而这种方法还是常常被用来估测肌肉在运动中起的作用。

第四,当我们试着估测肌肉力时,反向动力学的另外一个局限就体现出来了。由于每个关节都连接着多块肌肉,因此从关节扭矩到肌肉力的变化就会产生许多可能的结果。通常,肌肉对关节扭矩的作用都是通过某些优化模型来估算的。除此之外,我们还可以将肌肉分组成肌群。例如我们可以将肌肉分成拉伸集群和曲缩肌群来分别应对外界的拉伸扭矩和曲缩扭矩。这些模型都比较难以验证因为他们都预先做了一个关于肌肉如何工作的假设:要么他们作为一个协作肌群,要么他们遵循某一个成本函数。这两种假设都被证实不适用于较为复杂的肌肉活动。

最后,如果我们想要研究肌肉激活,我们并不能找到一个现成的模型来将肌肉力反向转换到肌肉激活信号。因此,如果我们想要在研究中包含神经控制的部分,反向动力学就不是一个好方法。然而,神经控制往往不是反向动力学研究所关注的部分。不过神经控制却是我们这篇文章的重点。因此本文剩下的部分会专注于不同形式的正向动力学方法,除了在一个混合模型的案例中我们会考虑使用反向动力学校正以及验证正向动力学计算的结果。

在本文剩余的部分,我们会讨论图1所展示转换的各个步骤:肌肉激活动力学,肌肉收缩动力学,肌肉骨骼几何形状,以及关节扭矩和关节角的计算。我们随后会讨论如何根据特定的测量对象调整模型并且展示本方法在肘关节和膝关节建模上的应用。

肌肉激活动力学

从EMG到肌肉激活信号的转换并不是一件容易的事情。在本节内容中,我们会仔细讲解进行这一变换所需要的许多步骤。值得注意的是,大多数研究者并不会使用我们讲述的所有方法,而只是使用一部分我们描述的方法。这个过程的基础步骤在图3中有所描述。尽管我们需要进行一些数学变换,然而肌肉激活动力学这一步往往是和下一步周肌肉收缩动力学一起进行计算的。

EMG信号处理

EMG信号处理的目的是发现每个肌肉的激活信号轮廓。一个原始的EMG信号包含正电压和负电压,而肌肉激活信号则被表示成一个介于0-1之间的数字,这一信号同时还被平滑或者滤波处理过以便更加符合EMG和肌肉力的关系。

第一个任务是将EMG信号处理成一种可以后续用来估测肌肉激活信号的信号。要达到这一目的,我们首要要移除信号中的直流分量。如果我们使用低质量的信号放大器或者测试的电极有移动,我们就有可能看到原始EMG信号的均值随时间变化。这并不是一个好现象因为这并不是肌肉发出的真正的信号。我们可以通过使用高通滤波器来消除低频噪音。高通滤波必须在整流之前进行,且截止频率应当在5-30Hz之间,具体数字取决于滤波器以及电极的种类。滤波器可以用软件实现,且滤波器的象限延迟应为0(例如正向和反向通过的四阶巴特沃兹滤波器),这样滤波就不会让信号在时域内发生偏移。一旦这一步完成,我们就可以放心的将信号整流,也就是每一个信号点都取信号值的绝对值。这样我们就有了整流后的EMG信号。

将整流后的EMG信号转换成肌肉激活信号的最简单的方法就是将EMG信号标准化,标准化的方法就是用整流后的EMG信号除以最大肌肉收缩时获得的最大整流后信号,然后再向处理后的信号施加一个低通滤波器。由于EMG的最大值非常难以获得,因此标准化也变得比较困难。人们关于如何定义最大肌肉收缩一直都是有争议的:是不是每个肌肉都应当使用不同的测量方法以保证可以取得最大值,或者是不是需要在关节力矩最大的时候记录数值?测量是不是应当在动态情况下进行?是不是每个肌肉都应该在长度-拉力曲线的最大值处进行测量?这些问题都有自己的合理性,也收到一些争议。

我们建议在肌肉测量时,每个肌肉分别测量最大值。当完成这一步时,记录时关节扭矩是否位于最大值并不重要因为关节扭矩是各个肌肉共同作用的结果。如果可以保证记录的位置位于肌肉的长度-拉力曲线的最大值位置,我们就可以确保此时测量到的肌肉力是最大值。但是我们在测量最大EMG信号时这一点并不重要。对我们来说最重要的是,如果标准化后的EMG信号值超过1.0,我们就清楚的知道我们之前用来标准化的数值并不是EMG的最大值。如果参与者积极的参与研究,那么测量到这个最大值是不难的。

整流后的EMG信号需要经过低筒滤波器处理因为肌肉自然的有低通滤波器的效果,而我们希望在EMG到肌肉力的转化过程中体现这一特性。也就是说,尽管通过肌肉的电信号有超过100Hz的成分,肌肉产生的肌肉力却只有较低的频率,也就是说肌肉力的曲线比原始EMG曲线要平滑。这也是一般机械马达的特性。在肌肉中,有很多机制都可以导致这种低通滤波效果;例如,钙动力,有限时间的肌肉运动信号传递,以及肌肉和肌腱的粘弹性特性。因此,为了让EMG信号和肌肉力对应起来,我们需要过滤掉高频部分。截止频率随着滤波器的锐化特性不同而不同,但是通常是在3-10Hz范围内。

激活动力学

标准化的,整流的,滤波的EMG信号是否适合作为肌肉激活信号用?对于某些处于静态状态的肌肉来说,这或许是合理的,但是一般来说我们需要一个具备更多细节的肌肉激活动力学模型来表征EMG信号的时变特性。

微分方程

EMG是一种分布于肌肉的,用于激活肌肉的电信号。EMG会导致肌肉产生肌肉力。然而,产生肌肉力需要时间 – 这个过程并不是瞬间发生的。因此,在肌肉激活过程中存在延时效应,这个延时效应可以用一个时间常数τact来表达。这个过程就被称为肌肉激活动力学,而肌肉激活动力学可以通过一个一节线性微分方程来建模。我们将标准化的,整流的,滤波的EMG信号标记为e(t)。值得注意的是每个肌肉的 e(t) 都是不一样的,不过为了方便,现在我们只认为它属于单个肌肉。将处理后的EMG信号,也就是 e(t) 转换为神经激活信号u(t)的过程被称作激活动力学。Zajac用下面的微分方程描述激活动力学。

公式中的β是一个介于0-1之间的常数。如果我们仔细研究括号中的项就会发现当肌肉被完全激活时,也就是e(t) = 1时,时间常数为 τact 。然而,当肌肉完全没有被激活时,也就是 e(t) = 0时,时间常数为 τact /β。这就意味着,当e(t)为常数时,我们会发现肌肉力在激活时会比放松时速度更快,这一特性在很多文献中都有记载。

你或许会注意到,上面的公式是一个微分方程。也就是说,u(t)是其导数du(t)/dt共同构成了方程。这意味着如果我们的输入信号e(t)是离散信号,那么上述方程最好使用数值积分的方式来解,龙格库塔算法就是一个不错的例子。

尽管上面的这个一阶微分方程可以不错的归纳激活动力学的特性。我们发现对于离散数据来说,二阶的关系有更高的计算效率。

离散递归滤波器

当肌纤维被单个执行电势激活时,肌肉会产生一个抽搐反应。这个反应可以很好的被一个临界阻尼的线性二阶微分系统表示。这种反应是表达从EMG信号e(t)到神经激活信号u(t)转换的微分方程的基石。

公式中的M,B和K都是定义二阶系统的常数。

上述公式是一个连续的二阶微分方程。然而,当我们在实验室采集连续数据时,数据会被采样成离散的点。我们得到的结果是离散EMG时间序列。因此,我们应该建立一个离散版本的二阶偏微分方程来处理离散的EMG数据。我们可以使用反向差分法我们可以从上面的公式得到下面的用于求解u(t)的离散方程。

公式中的d表示机电延迟,α,β1,β2都是定义二阶动态系统的常数。这些参数(d,α,β1,β2)将EMG信号e(t)映射到神经激活信号u(t)。正确的选择 β1,β2 的数值对于方程的稳定性至关重要,因此我们需要保证方程满足以下条件:

我们应该意识到上面的差分方程是一个递归滤波器,其中当前的u(t)数值由之前的两个数值u(t-1)和u(t-2)决定。也就是说,神经激活不仅仅基于当前的神经激活信号,也基于历史信号。这个滤波器的增益应该为1,这样神经激活就不会超过1。要保证这一点,公式必须满足下列条件:

因此,如果我们你知道γ1和γ2,我们就可以利用上面的约束条件获得β1和β2,同时利用上面的公式获得α。这样,我们就只需要三个参数就可以来描述这种转换了(d,γ1和γ2)。

方程中包含机电延迟d,这一项用来描述从发出神经信号到开始抽搐反应的时间差。先前的研究发现机电延迟的数值在10ms到100ms之间。这个延迟包含了两部分的内容:(a)运输所耗费的时间。这一部分取决于诸如钙在肌肉膜中传递的时间以及肌纤维传到速率。(b)肌肉力的产生动态过程依赖于肌肉去极化的动态化学过程以及肌肉收缩的动态过程。

上面的四个约束条件的作用是保证递归系统或者二阶系统的稳定性。一个不稳定的滤波器会输出的结果u(t)按照滤波器的自然频率震荡,甚至会导致u(t)的值逐渐趋向于无穷。这些约束条件都是从传递函数的脉冲响应的z变换中得到的。z变换后的传递函数如下:

分母中多项式的根分别为z = -γ1和z = – γ2。如果我们想要设计一个稳定的滤波器,那么根的绝对值就不能超过1.

先前对于猫的研究揭示了为什么许多研究人员都会难以从整流和低通处理后的EMG信号预测肌肉力。主要原因就是下面几方面的困难:(a)难以获得EMG和肌肉力之间的时间延迟。(b)难以表征观察到的EMG信号的持续时间要比肌肉力的持续时间短的现象。而这两个问题都可以轻松的被上面描述的离散二阶系统解决。

激活与EMG信号之间存在非线性关系

很多研究者都认为u(t)是肌肉激活的一个合理的近似,但事实真的是这样吗?和之前说过的一样,这取决于要我们要研究的肌肉。这是因为EMG和肌肉力之间不一定是线性关系。在研究单个肌肉单元时,一个刺激会产生一个抽搐反应,多个刺激会产生多个抽搐反应。如果刺激间隔的时间缩短,刺激的频率增加,抽搐反应会逐渐融合在一起,肌肉单元产生的平均力也会平稳上升。然而,随着刺激频率逐渐增加,抽搐反应会达到稳定状态。此时即使在增加刺激频率,肌肉也不会再产生更多力。这意味着对于单个肌肉单元而言,刺激频率和肌肉力之间并不是线性关系。这种非线性关系会因为其他因素而偏移。例如,当需要的肌肉力较小时,会激活较小的肌肉,而需要的肌肉力较大时则会激活较大的肌肉。

这种非线性特性并未归纳在u(t)之中。研究人员发现尽管有一些肌肉的EMG信号和肌肉力之间存在线性关系,但是在另外一些信号中这种关系却显现出非线性,尤其是在肌肉力较小的情况下(到30%)。研究人员用下列幂函数来表征这一关系,

公式中的横线代表两个量都用最大值标准化过。其中a和b两个参数是通过实验获得的。这个关系适用于曲线的开始阶段(前30-40%),在剩下的部分我们使用线性关系。尽管这种数学形式和数据较为吻合,然而它却存在两个问题。首先,这个方程在两部分连接时并不平滑,也就是说两段曲线在连接处的曲率并不相等。从数学意义上讲,一阶导数连续的方程更具有优势。第二,这个公式需要两个参数(a和b),而实际上我们只需要一个。

这些缺点可以通过使用对数函数来取代密函数来解决。使用对数函数表示的公式如下:

公式中的u(t)为神经激活,而a(t)则是肌肉激活。中间的系数c,d,m和b可以被轻松的获得,同时,它们可以用一个参数A代替,如图4所示。中间的转折点(也就是大约30%时)处的u(t)并不是一个定值,而是如图中所示那样变化的。参数A被用来描述EMG和激活信号关系的曲线,它和EMG和激活信号之间关系的非线性程度有关。A的数值通常在大约0.0-0.12之间。

我们还有另外一个形式的公式,这个公式更加简单并且也能得到不错的结果。

公式中A为形状因素。注意这里的A和前面所用的A是不一样的。这里的非线性形状因素A的可用范围在-3到0之间。当A = -3时,曲线高度非线性,而当A = 0时,曲线为完全线性。

注意上述两个公式都是只有一个参数A来表示非线性程度。而A的实际值需要在校正或者微调的过程中获得。

肌肉收缩动力学

一旦我们获得了肌肉激活参数,下一步就是确定肌肉力。这就需要肌肉收缩动力学模型。描述这种关系的生理学模型是存在一些问题的。例如,Huxley模型就非常的复杂,并且由多个微分方程所控制。解这些微分方程需要数值积分。这就使得这些模型在计算上成本过高,难以用来模拟多肌肉情况下的肌肉力。正是因为这个原因,许多做大型神经肌肉模型的研究员都会使用Hill模型。这些模型在本质上都是现象模型。也就是说,它表征的是系统的外在行为现象而不是内在生理学解释。然而,Hill模型仍然是强大的工具而且对于大多数应用来说可以给出合理的解释。Hill肌肉模型的一个显著优势就是,在大多数情况下,每个肌肉都是由一个微分方程所控制,这就使得使用肌肉系统进行计算变得可能。

Hill模型

肌肉肌腱的一般模型为肌肉纤维与弹性或者粘弹性的肌腱串联(如图5A所示)。肌肉纤维同时又有一个收缩的部分和弹性部分相并联(如图5B所示)。

Hill肌肉模型被用来估测肌纤维中收缩单元能够产生的力,模型基本形式为:

公式中的Fm(t)为时变的肌肉纤维力,之后的文中我们统一使用Fm。f(v)表示标准化的与速度相关的肌纤维力。f(l)表示标准化的与长度相关的肌纤维力。a(t)表示时变的肌肉激活参数。最后,F0m表示最大肌肉纤维力。

前面我们讲述了如何如何获取肌肉激活序列。下面我们就来讲述如何与肌肉长度和速度相关的肌肉力,然后在讲述如何计算肌肉-肌腱单元产生的力。

肌肉力随着肌肉长度的变化

要理解肌肉收缩动力学,我们必须从描述肌肉力和肌肉长度的关系开始。我们可以认为肌肉中有一个可以被主动激活的部分,就像一个马达一样。此外还有一个被动的部分,这部分被拉长时就会产生阻力,就像橡皮筋一样。

主动的部分就是我们之前说的收缩单元。这些收缩单元在肌小节位于最适当的长度时会产生最大的力,此时当肌动蛋白和肌凝蛋白之间有着最佳的交叠尺寸。当肌肉长度超过最佳长度时,肌肉不再能产生很多的力,因为肌动蛋白和肌凝蛋白之间的交叠较少,产生力的能力也较小。与此类似,当肌肉长度小于最佳长度时,能够产生的力也会减少。

当肌小节长度为2.8μm时,人类肌肉能产生最大的力。当肌肉纤维中的肌小节长度为这个尺寸时,我们就说这个肌纤维位于最佳纤维长度l0m。

从数学上讲,使用无量纲的单位来描述肌肉力和长度之间的关系会更加有效,如图6所示。当肌肉长度小于最佳长度的50%或者大于最佳长度的150%时,肌肉不会产生任何力。尽管这个曲线被建模为二阶多项式,实际情况却要比这个复杂一些。最好的建模策略还是使用Gordon等人建立的肌肉力-长度关系模型。我们使用对Gordon定义的点进行三阶样条插值得到的曲线来获得图6中的曲线。这条曲线中的肌肉力和肌肉长度都经过标准化。标准化后的肌肉力fAm和标准化后的肌肉长度lm都由下面的公式得出:

公式中的FAm是肌肉力-肌肉长度曲线中产生肌肉力的部分。注意,在公式中也考虑了a(t),因为肌肉激活水平会决定肌肉产生的最大力的大小。

肌肉的肌肉力-长度关系同时也受到肌肉激活水平的影响。之前的研究表明最佳纤维长度随着肌肉激活水平的降低而增加,如图6所示。这种肌肉激活和最佳纤维长度之间的耦合现象也被我们考虑进我们的肌肉模型中:

l0m(t)表示时变的最佳肌肉长度,l则表示相对于完全激活状态下(a = 1)肌肉最佳长度变长的百分比。例如,如果l = 15%,那么在完全没有激活的状态下最佳肌纤维长度就为1.15。

肌肉中的被动力是因为和收缩单元并行的组织的弹性作用引起的。当肌纤维长度小于最佳肌纤维长度时,被动力非常小。但是当纤维长度超过最佳纤维长度后,被动力就剧烈增加。我们使用一个指数关系来描述这一被动肌肉力:

这里的fpm是标准化后的被动肌肉力而lm是则是标准化后的肌肉长度。实际的被动肌肉力还和最大肌肉里相关,公式如下:

总的标准化的肌肉力是主动部分和被动部分之和,这个肌肉力可以缩放到不同的肌肉来估算总的静态肌肉力Fm

注意这里Fm中包含的仅仅是肌肉力-长度关系中的那部分,此外,还有肌肉纤维速度也会对肌肉力产生影响。下面,我们就将描述这一部分。

肌肉力随着肌肉速度的变化

尽管大多数研究者都使用Hill方程来描述肌肉纤维力,很少有人记得Hill最早使用这个公式来研究和热相关的肌肉收缩。他通过实验发现当肌肉缩短一定长度x,肌肉会释放出一些收缩热H,收缩热公式为:

此处的a为和肌肉截面面积相关的热常数。Hill认为a/F0m也是一个常数(~0.25),这里F0m是指肌肉在最佳肌肉长度时产生的最大肌肉力。

Hill接着研究了系统单总能量,也就是肌肉收缩产生的热和所作的功(力x距离)的总和:

从公式中我们可以看出通过积分我们就可以得到能量释放的功率:

最后,Hill提出这个能量释放的功率一定和肌肉力的变化成正比。由于肌肉力是由最大值开始的,F0m,这个关系可以表示为:

这里的常数b定义的是能量释放的速率。这就是Hill方程。这个方程也可以重写成下面的形式:

公式中vm就是肌肉收缩的速度,在这个公式中也就是肌肉变短的速度。

Hill模型考虑了肌肉力-长度关系以及肌肉力-速度关系。想要将两者结合起来是有一定难度的,这是因为肌肉力-速度关系是在最佳肌肉长度下推导出来的,而肌肉力-长度关系则允许非最优长度。然而,我们必须修改上面的公式才能将其用在不同的肌肉长度上。要想把肌肉力-长度和肌肉力-速度关系很好的结合起来,有两种方式。第一种是下面的方法:

在第一种方法中,我们将根据肌肉力-长度曲线将最佳长度下的肌肉力替换成其他长度下的肌肉力Fm(l)。这种方法究竟是否正确呢?有研究者指出如果上述公式是正确的,那么在最大速度时(此时肌肉力为0),上述公式可以重写为:

由于a和b都是常数,这就意味着最大肌肉纤维速度取决于肌肉长度。然而,研究人员发现,对于大多数肌肉来说,v0m是一个常数。正式出于这个原因,研究人员提出了下面的公式:

公式中的fA(l)表示标准化的肌肉力-肌肉长度关系。

之前推导的公式只适用于肌肉缩短的情况,肌肉伸长的情况必须另做考虑。一个描述这一现象的普遍公式可以表示为:

公式中的a’和b’分别是a和b的修正值。而FEccm则表示F0m的一个数乘因子,用来设定最大的修正肌肉力的值。FEccm的值一般在1.1-1.8之间。我们通常使用的值为1.8。这里的a’和b’都是修正参数。从数学上讲我们仍然认为应该标准化肌肉速度值,将他们表示为无量纲的单位f(v)。

肌腱建模

由于肌腱和肌肉是相互串联的,因此通过肌肉传递的力也会传过肌肉,反之亦然。正是出于这个原因,要研究整个肌肉中的力我们就必须研究这种力是如何影响肌腱的。

肌腱是像橡皮筋那样的被动元素。在肌腱松弛长度下,肌腱不承受任何载荷。然而,当寄件长度超过松弛长度后,肌腱就会产生和伸长长度成比例的力。研究人员发现当肌肉产生最大静态力时,韧带的应变为3.3%。当施加的力为3.5倍的最大静态肌肉力时,肌腱的应变为10%,此时韧带会失效。我们可以定义肌腱的应变为:

当然,肌腱产生的力随着应变变化这一现象只有在肌腱超过松弛长度后才会出现。否则肌腱产生的力为零。

很多人都将肌腱模拟成一个简单的在超过松弛长度后斜率为正的直线。然而,肌腱是由在未加载阶段呈蜷曲状态的胶原蛋白构成。当我们刚开始给未加载的胶原蛋白施加拉力时,蜷曲的胶原蛋白会展开。在这种模型中,肌腱就有较低的刚度并且具有非线性的力-应变关系。然而,当这些蜷曲的胶原蛋白完全展开时,肌腱会展示出更大的刚度以及线性的力-应变关系,弹性模量能达到1.2GPa。再一次,如果我们想将这条曲线标准化以便使用在不同的肌肉上,弹性模量必须除以产生最大肌肉力时的肌腱应力。这个值大约为32MPa,因此标准化后的弹性模量为37.5。据此我们就可以知道标准化的肌腱力遵循如下公式(如图7所示):

由于随着肌肉力量的增加,肌腱也变得更加粗壮,因此研究者提出最终的肌腱力可以用标准化的肌腱力乘以最大静态肌肉力,也就是:

肌纤维与肌腱所成的角度

肌纤维和肌腱之间并不是平行的,他们之间存在一个夹角(如图5A所示)。尽管对于许多肌肉来说,这个夹角是可以忽略的,但是对有些肌肉来说,这个夹角却大到不容忽略。对于夹角大于0的肌肉来说,肌肉力的方向和肌腱的方向之间就有呈一定角度。由于肌腱和肌肉纤维是串联在一起的,肌腱中的力Ft应当表示为:

对于那些夹角较小的肌肉,夹角并不会对肌肉肌腱单元产生的力有多大的影响。然而,对于那些夹角较大的肌肉(例如在小腿三头肌的夹角超过20°),夹角就会对力产生很大的影响。

这个夹角的值是定值吗?不幸的是,对于我们这些研究仿真的人来说,答案是否定的。研究人员利用超声波研究发现内侧腓肠肌的夹角随着关节角和肌肉激活程度不同可以从22°变到67°。尽管目前有一些描述夹角随着肌肉激活变化的简单模型,然而却几乎没有人在做过人体影像学实验(例如超声波)。无论如何,从对动物肌肉的研究我们可以知道,一些聪明的研究人员已经创建了相当一些精细的模型以及一些简单的可以用来预测收缩肌肉模型中夹角的模型。我们当然更喜欢简单的模型,因为他们计算起来速度更快,并且有效性也得到了证实。这些模型假设肌肉在收缩时厚度和体积都是不变的。一个计算夹角的常用公式如下:

这里的lm(t)代表的是在时间t时候的肌肉纤维长度,而φ0则表示肌肉位于最佳肌肉长度时的夹角。

可以调整的生理学参数

我们上面推导的所有标准化方程都是用来描述肌肉肌腱单元的动态产生力的能力。如果我们要缩放这些方程以适用于不同的肌肉,我们就必须在模型中包含生理学参数来体现单个肌肉的特性。这些参数为:最大肌肉力F0m,最佳肌肉长度l0m,肌腱松弛长度lst以及肌肉最佳长度时的夹角φ0。

最佳肌肉长度,肌腱松弛长度,以及夹角都是从尸体上测量获得的。Yamaguchi等人的研究总结了大量研究结果的数据。对于这三个参数,肌腱松弛长度时最难测量的,但是这个长度可以使用数值方法来估测。然而,最大肌肉力的获得方法却不太相同。

最大肌肉力对应的是肌肉在最佳长度时可以产生的最大力。这个最大肌肉力和肌肉的截面积是相关的。简单的说,有更多肌小节并联的肌肉能够产生更多的力。描述肌肉中并联的肌小节数量的最佳参数是肌肉中生理界面的面积,英文简写为PCSA。PCSA定义为肌肉的体积除以最佳肌肉长度。通常来讲,肌肉体积都是通过质量除以肌肉密度(1.06g/cm3)获得的。

我们通常认为肌肉的最大应力为定值。因此,如果我们知道PCSA,并用它乘以最大肌肉应力,我们就可以估测最大肌肉力了。然而这种方法也是存在问题的,因为大家所报告的最大肌肉应力的数值变化相当大(从35-137N/cm2)。研究者指出当他们使用文献中的PCSA数值,并且记录肘关节最大扭矩时,曲肌的最大应力和伸肌的最大应力明显不同。这可能仅仅是因为测量的PCSA值是从年老的尸体上获得的,而在应用中又将这些数值应用在了年轻人建立的模型上。当不使用肌肉时,曲肌和伸肌的萎缩速度是不同的。这一现象在卧床不起的老人身上非常常见,而这些老人很可能就是我们解剖的样本。无论原因是否是我们前面分析的这样,这都是对肌肉进行精确建模的一个潜在问题。也就是说,如果将一个单一的最大肌肉应力值应用在基于尸体的PCSA来获得最大肌肉力时可能会导致曲肌和伸肌过于强壮或者衰弱。然而,通过准确测量每个肌肉的最大关节扭矩,我们可以获得更好的缩放比例。这个比例可以根据相对PCSA来分布给不同的肌肉。

另外一个需要我们考虑的参数就是最大肌肉收缩速度v0m。这个速度的数值在快速和慢速抽搐肌肉中是不同的。表示最大肌肉收缩速度的通常方法是将其表示为每秒多少个最佳肌肉长度,也就是标准化的最大肌肉收缩速度。慢速抽搐肌肉标准化的最大肌肉收缩速度通常小于等于8l0m/s, 快速抽搐肌肉标准化的最大肌肉收缩速度通常为14l0m/s。 先前的研究人员建议对于混合快速抽搐纤维和慢速抽出纤维的肌肉来说,使用10l0m/s为一个比较合适的近似值。我们现在肌肉模型中使用的也是这一数值。也就是说,我们认为v0m是一个常数然而,vom可以随着肌肉中纤维的相对比例变化而变化。如果要进行更加仔细的研究,Yamaguchi列出来的肌肉混合比例是一个很好的参考,不过大家也都知道人体中的快速抽搐和慢速抽搐比例是因人而异的。

模型综合

当我们将生理学参数结合进Hill的模型中后,我们就会发现肌肉-肌腱力是一个有多个参数构成的方程。这个方程形式如下:

也就是说,肌肉肌腱力是由肌肉肌腱激活a,长度lmt,速度vmt构成的函数。这些变量都是随着时间变化的变量,同时也是肌肉肌腱模型的输入。从上面的方程中我们可以看出,肌肉力同时也依赖于其他骨骼肌肉参数,不过这些参数通常被认为是不随时间变化的:最大静态肌肉力(F0m),最佳肌肉长度(l0m),肌腱松弛长度(lst),以及最佳纤维长度时的夹角(φ0)。这个方程非常复杂,且高度非线性。它不仅包含了力-长度以及力-速度关系,也包含了肌肉中的力要和肌腱中的力相匹配。上面的公式可以写成另外一种形式,以便我们能够更加清晰的看到肌肉肌腱力是如何计算的:

肌肉纤维中的总受力可以用括号中的公式表示,如图5B所示。尽管看起来可能不像,然而这却确实是一个非线性的一阶微分方程。

由于肌肉肌腱力方程式非线性微分方程,而方程的输入又是离散信号,因此方程必须使用数值积分来解算(我们使用的是龙格-库塔-费尔博格法 )。下面就具体介绍一下具体方法。首先对于某个肌肉长度lm,可以用前面的公式计算出夹角φ。接下来,我们使用lt = lmt – lmcos(φ)计算出肌腱的长度,而肌肉-肌腱长度lmt是输入肌肉肌腱模型的参数之一(具体方法我们会在后面讨论)。一旦我们有了肌腱长度,我们就可以根据前面的公式计算肌腱力。之后,我们就可以计算标准化的速度相关的肌肉力f(v)。如果我们将上面的公式整理一下,我们就可以得到:

因为我们已经知道lm,我们就可以计算出fA(l)和fP(l)。同时肌肉激活a(t)是一个输入参数。一旦我们计算出了f(v),我们就可以根据前面的公式解算vm。一旦我们知道了vm,我们就可以数值积分得出下一个时刻的lm。由于我们得到了新的lm,我们就可以开始新的循环了。这个循环将一直进行下去,直到我们将输入参数a(t)和lmt(t)用尽为止。在骨骼肌肉系统的每个肌肉中都将进行这个过程,这样所有的肌肉肌腱力都可以被估算出来。

骨骼肌肉几何模型

在上述方法中重要的变量有肌肉肌腱单元的总长度和速度。因为它们通过肌肉力-长度关系和肌肉力-速度关系直接影响产生的肌肉力。但是我们应该如何计算肌肉长度以及速度呢?另外一旦我们计算出了肌肉力,我们就要计算肌肉力对关节扭矩的贡献。这就需要我们知道肌肉力臂的长度,而这个力臂的长度正是肌肉长度的函数。

如果我们要计算肌肉肌腱单元的长度和力臂,我们就需要一个骨骼肌肉模型。这个模型需要告诉我们肌肉肌腱长度以及力臂是如何随着关节角度改变而改变的。更好的骨骼肌肉模型包含了骨骼几何信息以及关节运动学的复杂关系信息。例如,绝大多数关节都并不是简单的铰链。有些关节可以进行复杂的平动和转动。因此关节中心并不是固定的。这也意味着力臂(也就是关节中心到肌肉作用力的距离)也会改变。另外,我们还需要骨骼肌肉模型来描述肌肉并不是一条直线这一现象。肌肉的路径要复杂的多,如果要定义解剖学上恰当的模型则需要使用高级的计算机图形学。即使我们可以创建一个骨骼肌肉模型,模型的真实性也很难验证,验证过程可能需要大量的解剖学研究来保证模型在解剖学上足够精确。最后,肌肉的力臂以及肌肉肌腱长度也难以缩放后用到不同个体身上。

肌肉肌腱长度以及力臂长度

当我们使用骨骼肌肉模型来求解肌肉的长度,我们通常是将肌肉和肌腱的长度放在一起的。这样做主要是因为从几何上讲,肌肉和肌腱共同作为一个肌肉肌腱单元来工作。这个肌肉肌腱单元可以被当成一条直线,一组相互连接到线段或者一条曲线。正如我们之前提到的那样,将肌肉肌腱系统描述成从原点到终点的一条直线过于简单,会引发一些问题。这是因为几乎每条肌肉都会弯曲包绕在其他一些关节结构上。例如,大多数的伸肌都会包绕在骨头上(例如肱三头肌就包绕在肱骨的肢端)。与此类似,大多数曲肌都通过表面组织固定在关节上。由于这些约束结构随着关节不同而不同,因此对骨骼肌肉系统进行几何建模就非常困难。

肌肉肌腱系统的力臂r(θ)可以直接从肌肉肌腱系统的长度 lmt(θ) 和关节角θ中获得。具体公式如下:

这个公式可以使用虚功原理轻松的推导出。值得注意的是,力臂长度并不是一个定制,它随着关节角的改变而改变。对于双关节肌肉来说,肌肉的力臂是关于两个关节角的函数,这也就是为什么我们不建议从文献书记中寻找力臂长度的数据,除非这个模型之在特定的关节构型下有效。

计算关节扭矩和关节角

一旦我们计算好了所有的肌肉力也估算好了它们对应的力臂,它们对关节的扭矩就可以通过乘法获得了。如果某个关节上所有的肌肉产生的力矩都计算完成了,对应的关节力矩就可以表示为:

这了的肌肉力是和力臂都可以从前面介绍的公式中获得。注意角标中的i表示肌肉的编号。加和项目的每一项都对应某个肌肉产生的关节扭矩。

一旦单个肌肉产生的扭矩可以计算出来,其他关节的扭矩也可以用同样的方法计算并相加。除了肌肉产生的力之外,我们还需要考虑一些外载荷或者重力或者节简相互作用引起的扭矩。要计算关节总扭矩,这些所有的因素都要考虑进去。

关节扭矩反过来也会引起关节运动。运动引起的关节角也必须再使用一般动力学方法考虑进计算中(拉格朗日或者欧拉动力学)。这些动力学方程都依赖于关节数量已经每个关节的自由度数目。当模型中不只有一个关节时,整个模型就会变得非常复杂。注意如果我们要解算这些方程,各个运动的肢体部分的惯量参数需要合理的估算出来。

模型调整和验证:实例

让我们首先考虑一个例子:人体肘关节屈伸时的神经肌肉骨骼模型。我们首先从和肘关节屈伸相关的七块主要肌肉采集EMG信号。为了校正模型,我们记录从位于前臂末端的一个加载单元上的力,这样加载单元到肘关节中心的距离就已知了。参与者的前臂末端会被固定在模具中,并且固定到加载单元上。这样就可以保证测量的准确性。加载单元可以测量三种力和三种力矩,从中我们就可以确定肘关节运动。加载单元上的力和力矩以及EMG信号同时以1000Hz的频率采集。

EMG信号采用肌肉间电极对或者间距2cm放置的双击表面电极。EMG信号使用前面描述的方法进行信号处理。EMG信号在硬件电路中进行放大并使用带通滤波器(30Hz – 10KHz)进行滤波消除高频和低频噪音。之后信号在经过第二级放大,将信号调整在信号收集系统限定的±10v范围内。数据采集开始时参与者要产生最大肌肉激活。这些信号稍后会用来标准化EMG信号。在实际采集数据的过程中,参与者被要求产生时变的肘关节屈伸扭矩。

使用模型

数字化的EMG信号都经过整流和滤波(四阶巴特沃兹低通滤波器,截止频率4Hz)。所有的EMG信号都除以对应肌肉的最大EMG值,获得处理后的EMG信号,e(t)。每条肌肉的e(t)的数值在0-1之间。

随后,我们用前面介绍的公式将e(t)转换为u(t),然后再将u(t)转换成a(t)。注意我们还不知道转换中所需要的一些参数:γ1,γ2,d和A。我们可以先猜测一个值( γ1 = 0.5,γ2 = 0.5,d = 40ms和A = 0.1 ),然后再稍后进行优化。

肌肉收缩动力学的步骤需要使用前面的步骤来计算每个肌肉中的肌肉力。正如我们之前描述的那样,这一步并不容易,因为我们需要平衡肌肉和肌腱中的力。注意这一步需要下一步中的输入,也就是肌肉骨骼肌肉模型。因为我们计算肌肉力时需要肌肉肌腱组织的长度以及其他的一些参数。我们会使用Murray建立的骨骼肌肉模型来定义肌肉肌腱长度以及力臂长度与关节角度的关系。这一步输出的结果是肘关节屈伸的力矩。此时并没有必要进行多关节动力学分析,因为我们在做进行检测的时候,参与者的胳膊是固定在加载单元上的。尽管假设肌肉速度为0是对系统的一种简化,在这个例子中我们会做速度为0的假设。

在我们回头优化系数之前,让我们来回忆下整个过程。关节力矩是基于EMG输入信号的。然而,我们知道实际的关节扭矩因为我们可以用加载单元测量这个扭矩。因此,通过比较计算出来的关节扭矩数值和通过实验确定的数值,我们就可以知道我们的模型在多大程度上和实际情况相匹配。

调整参数

我们现在可以回到系数调整上来了。我们的初始值完全靠猜测,并没有指望模型输出的扭矩和实际测量的扭矩非常一致。现在我们可以看一下模型的输出结果,看一下预测的结果和实际结果是否一致,并据此调整参数。也就是说,我们要讲参数调整的适合我们的参与者。这一步在数学上可以通过非线性优化来完成。如果我们希望,我们也可以调整一些肌肉骨骼模型的参数。例如,我们可以预估韧带松弛长度并用其做初始猜想,但是这些数值可以在合理的范围内变动以便更加符合我们参与者的生理参数。这样,模型就可以被调整的符合每个个体。调整的数学公式如下如下:

我们使用最小二乘法来优化我们的数值。在一个5s的1000Hz的数据中,相加的数据差有5000个。系数和参数应该尽可能的保持在合理的生理范围内。例如,时间延迟项,di,一般都在10-100ms之间,通常为40ms。非线性形状参数A应该在0-0.12之间。这可以产生符合生理情况的从线性到非线性的肌肉力-EMG曲线。除此之外,我们还有其他约束方程以保证肌肉激活参数小于等于1。

通过调整各个肌肉的参数我们可以最小化上面的函数。这个过程是在线下进行的,一般几分钟内就可以收敛。为了加速这一过程,在优化之前我们用100Hz将原始数据重采样。优化的流程图如图8所示。随机选择的参数或者使用针对其他参与者优化的参数往往难以得到能和测量数值匹配的预测结果。于此相反,当我们针对实验参与者调整模型后,往往可以得到不错的预测结果。

参数太多不是好事

参数越多,参数允许变化的范围越大,我们预测的关节扭矩就更容易和测量到的关节扭矩一致。然而,这并不意味着参数变化越多越好。

参数太多的模型往往预测能力较差。例如,曾经有研究人员创造了一个从EMG信号中预测肌肉力的模型,模型预测的关节扭矩结果和利用逆向动力学得到的非常一致。但是他们的模型需要每时每刻都重新调整参数。也就是说,每个时间步都需要调整模型以便符合观测到的数据。但是对于时间步较多的情况,即使只是一个简单的动作,研究人员也需要调整成百上千个参数,那么预测的结果当然可能非常准确。但是问题是这样获得的模型过拟合了,这就意味着这个模型不能用于任何预测。如果我们需要在每一个时间点都重新预测参数,这个模型就不能预测任何新数据。这样的模型或许在有些场合还是有用的,但是这个模型肯定几乎不具备预测能力的。

一个具备预测能力的模型是可以通过一些数据来校正模型,使得参数可以落在合理的范围内。那些对应测量数据的参数不应该被调整至超过正常范围。随后,一旦我们调整好参数,我们就可以利用这个模型预测新的数据了。此时我们不再需要调整参数。这样,模型预测正确结果的稳定性就能得到保证了。

理想情况下,模型应该尽可能简单。调整的参数越少,我们就对支撑模型生物力学基础更有信心。大家也不太会把整个过程当做是一个简单的数学曲线拟合。研究人员应该在获得合适的拟合和使用适当数量的参数之间权衡,因为增加参数就意味着减少模型的可信性,让模型不再强大。

动态案例:使用混合方法

上面描述的方法被应用在四肢动力学的研究中。一旦关节运动可以确定了,使用多关节动力学来计算对应的肢体运动就不是一件难事儿了。随着模型中身体部分的增加,模型也变得越来越复杂,解这些动力学方程也在计算上变得昂贵。

除了上面描述的方法,我们还可以使用混合方法将正向运动学和逆向运动学结合起来。我们使用这个方法来研究膝关节肌肉和肌腱在运动时的载荷。在这个研究中,我们从十块肌肉中采集EMG信号,并且估算肌肉力以及关节扭矩。用来推导肌肉力的数据测量于步态实验室,研究人员使用基于视觉的采集系统追踪固定在人体上的标记的位置,同时使用力学传感器测量被测试者脚部受力。然后使用运动学模型来确定关节运动参数和地面的反力。接着我们在使用反向动力学来估测关节屈伸时的扭矩。同样的关节屈伸时的扭矩也通过正向动力学获得,使用EMG信号,踝关节,膝关节以及髋关节的运动学参数作为输入。

校正EMG驱动的模型意味着将正向动力学和反向动力学得到的结果相比较。两者结果之间的差的平方被用来调整模型参数,如上文所述。三次步态数据以及两次等力测试的数据被用来校正模型。模型中一共有18个参数,利用来自五次测试的500多个数据点来进行优化。这样就不会存在过拟合问题。这18个参数被分成两组:肌肉肌腱参数和EMG到激活参数。肌肉肌腱参数包括每个肌肉的肌腱松弛长度,以及屈伸时的最大肌肉应力。EMG到激活参数包括γ1,γ2,以及A,这些参数在所有肌肉中都使用相同的数值。

混合方法的优势就在于,正向动力学和反向动力学获得的关节扭矩结果可以交叉验证正向建模方法。当然,这一切都是在反向动力学误差范围内。校正后的正向模型可以 非常好的预测其他两百多次逆向运动学预测的结果。平均的R2 = 0.91 ± 0.4。此外,如果我们保存肌肉肌腱参数而只调整EMG到激活的参数,模型也可以很好的预测两周后同一参与者的结果。一旦我们校正好了模型,确认模型可以很好的预测关节扭矩,我们就对模型预测的肌肉力和关节扭矩更有信心了。接下来我们就可以使用校正后的模型来估测当参与者执行牟星动作时膝关节韧带中载荷。我们使用这个方法来预测人在走路,跑步等动作时的关节接触力。注意,这些力和从反向动力学中得到的地面反作用力不同。

总结

在本文中我们给大家展示了如何使用EMG信号通过正向动力学方法来预测关节扭矩。我们使用Hill模型来归纳肌肉力-长度关系以及肌肉力-速度关系。模型预测的结果通过比较预测的结果和测量的结果来进行验证。这些模型在预测不同任务时的肌肉力时起着巨大的作用,因为肌肉力时很难通过其他模型获得的。例如,基于优化的模型或许可以预测肌肉力,但是这种方法无法考虑不同个体间神经肌肉控制系统的差异,而这可能引起不准确的问题。

这些模型的准确性很大程度上受到解剖学数据精确程度的影响。这种方法需要一个完整的肌肉骨骼几何模型。这种方法也是非常严谨的。先前的许多实用EMG预测肌肉力和关节扭矩的模型都被证明是非常不准确的。只有基于严格的生物力学和解剖学基础的模型可以得到合理的结果。

附录

本文由安静辛苦整理,发布于安静笔记:www.ajnote.com 转载请注明出处。

七月 15

wpf之文本控件

TextBox:文本框控件

存储字符串格式的文本

MaxLength:文本框控件最多可以输入的字符长度

TextWrapping:自动换行属性。如果想要自动换行,则设置为Wrap

VerticalScrollBarVisibility:纵向滚动条设置。设置为Auto则自动调整

SelectionStart:选中文本的开始位置

SelectionLength:选中文本的长度

SelectedText:选中的文本

SpellCheck.IsEnabled:拼写检查,设置为True则开启拼写检查。目前这个功能不能检查中文。检查英文时也要求键盘设置为英文键盘。

LineUp()方法:向上一行。还有类似的LineDown()、PageUp()、PageDown()、ScrollToHome()、ScrollToEnd()等方法。

PasswordBox:密码框控件

通过显示实心点等方式来屏蔽实际输入的字符串。密码框控件不能复制粘贴。

PasswordChar:设置加密的字符。在密码框中输入的所有字符都将以设置的字符形式显示。

七月 12

wpf之带标题的内容控件

GroupBox

GroupBox就是带有圆角和标题的方框。

Header:标题。如果想在Header中添加多种元素,那么就需要设置成属性元素。

Content:内容,通常用属性元素形式来设置。由于带标题的内容控件也是继承自内容控件,因此Content中只能有一个元素。如果想要多个元素,需要先放置一个布局元素,然后再在布局元素中添加多个元素。

TabItem

TabItem是TabControl中的一页内容。

Header:标题。如果想在Header中添加多种元素,那么就需要设置成属性元素。

Content:内容,通常用属性元素形式来设置。由于带标题的内容控件也是继承自内容控件,因此Content中只能有一个元素。如果想要多个元素,需要先放置一个布局元素,然后再在布局元素中添加多个元素。

TabStripPlacement:Tab的放置位置。可以放置在上下左右四个位置。

选择哪个TabItem可以在C#代码中通过 this.TabItem1.IsSelected属性进行设置。

Expander

通过一个小箭头来显示或者隐藏某些内容。

Header:标题。如果想在Header中添加多种元素,那么就需要设置成属性元素。

Content:内容,通常用属性元素形式来设置。由于带标题的内容控件也是继承自内容控件,因此Content中只能有一个元素。如果想要多个元素,需要先放置一个布局元素,然后再在布局元素中添加多个元素。

ExpandDirection:展开方向。

七月 10

正则表达式 – 元字符

字符描述
\将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\\’ 匹配 “\” 而 “\(” 则匹配 “(“。
^匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
$匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
?当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。
.匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像”(.|\n)”的模式。
(pattern)匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 ‘\(‘ 或 ‘\)’。
(?:pattern)匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。
(?=pattern)正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95|98|NT|2000)”能匹配”Windows2000″中的”Windows”,但不能匹配”Windows3.1″中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95|98|NT|2000)”能匹配”Windows3.1″中的”Windows”,但不能匹配”Windows2000″中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?<=pattern)反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”(?<=95|98|NT|2000)Windows”能匹配”2000Windows”中的”Windows”,但不能匹配”3.1Windows”中的”Windows”。
(?<!pattern)反向否定预查,与正向否定预查类似,只是方向相反。例如” (?<!95|98|NT|2000)Windows “能匹配” 3.1Windows “中的” Windows “,但不能匹配” 2000Windows “中的” Windows “。
x|y匹配 x 或 y。例如,’z|food’ 能匹配 “z” 或 “food”。'(z|f)ood’ 则匹配 “zood” 或 “food”。
[xyz]字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
[^xyz]负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’、’l’、’i’、’n’。
[a-z]字符范围。匹配指定范围内的任意字符。例如,'[a-z]’ 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。
[^a-z]负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]’ 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。
\b匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
\B匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
\cx匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\d匹配一个数字字符。等价于 [0-9]。
\D匹配一个非数字字符。等价于 [^0-9]。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。
\w匹配字母、数字、下划线。等价于'[A-Za-z0-9_]’。
\W匹配非字母、数字、下划线。等价于 ‘[^A-Za-z0-9_]’。
\xn匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,’\x41′ 匹配 “A”。’\x041′ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。
\num匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)\1′ 匹配两个连续的相同字符。
\n标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
\nm标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
\nml如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
\un匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。
七月 9

wpf之内容控件

内容控件简介

wpf中的内容控件是一种特殊的控件类型。它可以包含并且显示一块内容。从技术的角度讲,内容控件是可以嵌套单个元素的控件。这一点与可以嵌套多个元素的布局控件不同。 所有的内容控件都是继承自ContentControl类。常见的Label以及Button控件都是内容控件。

Content属性

内容控件都包含Content属性。Content属性只能设置为一个元素,虽然元素的类型可以是多种多样的。Content不仅仅可以是字符串,还可以是图片等。那么如果想要同时在Content中显示图片和字符串怎么办呢?答案很简单,我们可以在Content属性中设置一个布局容器,然后再在布局容器中添加多个元素就可以了。

如果要设置Content的对齐方式,可以使用HorizontalContentAlignment或者VerticalContentAlignment。

Label控件(标签控件)

在所有控件中,最简单的就是Label控件(标签控件)。由于Label标签控件常常用来说明其他控件,因此它和它所说明的控件往往是可以绑定在一起的。这种绑定用的是Target属性。

Target = "{Binding ElementName=textBox1}"

设置快捷键需要使用_,而不是之前winForm中的&。所有设置的快捷键都默认和alt一起使用。

Button控件(按钮控件)

wpf中提供了三种Button按钮控件。包括我们熟悉的Button控件,还有CheckBox(复选框)控件以及RadioButton(单选钮)控件。

当我们设置IsCancel为True的时候,当我们在键盘上按ESC的时候,就会触发这个按键的Click事件。

当我们设置IsDefault属性为True的时候,当我们鼠标没有指向任何按钮时,按下回车键就会触发IsDefault设置为True的按钮。

CheckBox控件包含一个叫做IsChecked的属性。这个属性有三种选择,除了True和False,还有一个为{x:Null}, 表示未确定。如果需要可以选择 {x:Null} 状态,我们需要将IsThreeState设置为True。

RadioButton在一个布局容器中有多个可以RadioButton,但是我们只能从中选择一个。如果不在同一个容器下的RadioButton想要有互斥的效果,那么我们需要给不同容器下的RadioButton设置相同的GroupName属性。

ToolTip控件( 工具提示 )

我们可以为元素添加ToolTip工具提示属性。工具提示中可以放置任何需要的可视化元素。比较复杂的ToolTip需要设置为元素属性,例如Button.ToolTip。如果需要设置工具提示的背景色以及放置位置,那么可以在Button.ToolTip中添加ToolTip元素并且设置BackGround,Placement,HorzontalOffset以及VerticalOffset等属性。

七月 8

wpf之控件类

控件类(Control)简介

wpf中充满了各种元素,但是这些元素中只有一部分是控件。wpf中的控件指的是那些和用户交互的元素。也就是那些能够接受焦点并接受用户输入的元素。例如文本框,按钮等。所有的控件都继承自Control类。

背景画刷和前景画刷

所有的控件都包括背景和前景。背景是指控件的表面,而前景则是指控件上的文字。背景和前景用的都是Brush对象。示例:

this.button1.Background = new SolidColorBrush(Colors.AliceBlue);
this.button1.Background = new SolidColorBrush(Colors.FromRgb(255, 0, 0));

还可以赋予系统自带的颜色。示例:

this.button1.Background = System.Windows.SystemColors.ControlDarkBrush;

也可以使用RGB值来进行设置。示例:

字体属性

FontFamily:选择字体

FontSize:字体大小

FontStyle:字体样式

FontWeight:文本的粗细

FontStrech:问题的拉伸或压缩

TextDecoration:字体装饰。有BaseLIne,StrikeThrough,UnderLine,OverLine。

示例:加载计算机上拥有的所有字体

foreach (FontFamily fontFamily in Fonts.SystemFontFamilies)
{
    this.listBox1.Items.Add(fontFamily.Source)
}

字体继承:一旦我们在某个元素中设置了字体,那么默认的包含在其中的元素也使用相同的字体,除非被包含的元素自己指定字体。

对于小尺寸的文字,可能会出现模糊的问题。此时可以在XAML中设置

TextOptions.TextFormattingMode = "Display"

对于大尺寸文字则不需要如此设置。

鼠标光标

Windows中自带许多类型的光标,例如常见的箭头,等待时的沙漏,还有帮助菜单中的问号。我们可以在C#和XAML中设置光标的形状。代码示例:

C#: this.Cursor = Cursors.Wait
XAML: Cursor = "Help"
七月 8

wpf之理解鼠标输入事件

MouseEnter事件:当鼠标移动到某个元素上时,就触发MouseEnter事件。需要注意的是,这是一个直接事件。

MouseLeave事件:当鼠标指针离开某个元素时,就触发MouseLeave事件。这个事件也是一个直接事件。

PreviewMouseMove事件:移动鼠标时就会触发这个隧道事件。事件会提供一个MouseEventArgs对象。这个对象包含鼠标此时状态的一些属性,还有一个叫做GetPosition()的方法可以获得鼠标的坐标。GetPosition()方法中的参数为坐标所在的坐标系,一般情况下用this即可。GetPosition()返回的为一个类型为Point数据。因此,使用GetPosition()方法的一个案例为:

Point pt = e.GetPosition(this);

MouseMove事件:与PreviewMouseMove对应的冒泡事件。

鼠标单击事件

PreviewMouseLeftButtonDown以及PreviewMouseRightButtonDown事件:按下鼠标时触发的隧道事件。

MouseLeftButtonDown以及MouseRightButtonDown事件:按下鼠标时触发的冒泡事件。

PreviewMouseLeftButtonUp以及PreviewMouseRightButtonUp事件:释放鼠标时触发的隧道事件。

MouseLeftButtonUp以及MouseRightButtonUp事件:释放鼠标时触发的隧道事件。

这些鼠标单击事件都会提供一个MouseButtonEventArgs的对象。值得注意的是,Windows程序一般是对Up类事件进行相应,而不是对Down事件进行相应。

鼠标捕获事件

绝大多数时候,在一个鼠标按键按下的动作之后,紧接着会有一个鼠标按键释放的动作。然而,在有的时候也并非如此。例如,我们可能在按下鼠标按键之后先把鼠标移动出这个点击的元素再释放。这样,接收鼠标按下动作的元素就无法接收鼠标释放的动作了。如果我们希望这个元素仍然能够接收鼠标释放的动作,就需要使用鼠标捕获事件,让这个元素捕获鼠标。这样,无论鼠标在按下后移动到哪里,这个元素都可以接收到鼠标释放的动作。需要注意的是,一旦鼠标被某一个元素捕获之后,其他的元素就无法接收鼠标的信息了。

示例:Mouse.Capture(this.button1); //让button1捕获鼠标。

鼠标拖放事件

鼠标拖放事件一般首先在拖拽源的MouseDown函数中定义:

DragDrop.DoDragDrop(dragSource, dragContent, DragDropEffects.(对应的枚举))

接收拖拽的元素首先要在XAML中设定AllowDrop为True。这样才能接收拖拽过来的数据。然后,要在接收拖拽的元素上添加Drop事件,并进行设置:

((将Object转化为的类型)sender).Content = e.Data.GetData(DataFormats.Text);

温馨提示:TextBox自带拖放功能。

七月 7

wpf之理解键盘输入事件

wpf的事件类型

在wpf中,最重要的事件类型有五种:生命周期事件,鼠标事件,键盘事件,手写笔事件以及多点触控事件。生命周期事件是在元素被初始化加载或者卸载的时候发生的事件。而剩下的四种事件则都是输入事件,也就是说,由用户对操作系统进行输入是发生的事件。

wpf中的键盘输入

当用户按下键盘上的按键时,最先触发的是PreviewKeyDown事件。通过前面的学习我们知道,所有带有Preview的事件都是隧道路由事件,这一个也不例外。.

随后触发的事件是KeyDown事件。这个这也是一个路由事件,只不过是一个冒泡路由事件,而不是一个隧道路由事件而已。

接下来触发的事件是PreviewTextInput。这个事件在按键过程已经完成,元素正在接受文本输入的时候发生。对于不能产生文本输入的按键,例如Ctrl,Shift等,并不能触发这个事件。

再之后是TextInput事件。和前面介绍的PreviewTextInput一样,它也只能由产生文本输入的按键触发。

接下来是PreviewKeyUp。从名字中我们就可以知道,这个事件是在释放按键的时候触发的。

最后是KeyUp。这个是在释放按键的时候触发的事件。

七月 6

wpf之如何理解路由事件(Routed Event)

初学者看到路由事件这个名字的时候都会不禁好奇:这个路由指的是什么?和我们家里平时用的路由器有关系吗?

事实上,无论是我们家里用的路由器,还是路由事件,名字中的路由的概念是一样的,他们都表示起点与终点间有若干个中转站,从起点出发后经过每个中转站时要做出选择,最终以正确(比如最短或者最快)的路径到达终点。

作为程序员来说,我们编程的本质是通过编译我们所写的代码来扩展操作系统的功能,所以,程序的基本运行不可能脱离操作系统。要正确理解路由事件首先需要正确理解wpf所依赖的操作系统,Windows操作系统。

Windows操作系统本身就是一种消息驱动的操作系统,所以我们的程序注定都是消息驱动的,程序运行的时候也要把合己的消息系统与整个操作系统的消息系统“连通”才能够被执行和响应。

纵观几代 Windows 平各理序开发,最早的windowsAPI开发(C语言)和MFC开发我们可以直接看到各种消息并可以定义自己的消息。到了COM和VB时代,消息被封装为事件(Event)并一直沿用至.NET平台开发。

无论怎么说,程序间模块使用消息互相通信的本质是没有改变的。从Windows API开发到传统的.NET开发,消息的传递(或者说事件的激发与响应)都是直接模式的,即消息直接由发送者交给接收者(或者说事件宿主发生的事件直接由事件响应者的事件处理器来处理)。WPF把这种直接消息模型升级为可传递的消息模型。

前面我们已经知道WPF的UI是由布局组件和控件构成的树形结构,当这棵树上的某个结点激发出某个事件时,程序员可以选择以传统的直接事件模式让响应者来响应之,也可以让这个事件在UI组件树沿着一定的方向传递且路过多个中转结点,并在这个路由过程中被恰当地处理。你可以把WPF的路由事件看成是一只小蚂蚁,它可以从树的基部向顶部(或反向)目标爬行,每路过一个树枝的分又点就会把消息带给这个分叉点。

关于wpf中的路由事件就给大家介绍到这里。希望通过我的简单描述,大家能够更加直观的理解wpf中的路由事件。

七月 6

C#中Enum .GetValues (typeof(EnumName ))为什么要加typeof

当我们需要获取某个枚举类型中的所有值时,就需要用到Enum.GetValues方法。一个常见的错误就是直接把枚举的名称当做参数来使用,例如下面的代码:

Enum .GetValues (EnumName);

然而这并不是正确的做法。正确的做法是,还需要在参数中加一个typeof()运算符。你没看错,我也没有写错,typeof()是一个运算符,不是一个方法。正确的写法如下:

也就是Enum .GetValues(typeof(EnumName));

安静本人比较喜欢刨根问底,因此就很好奇为什么要多此一举的加上typeof运算符呢?于是我找了很多的中英文的文档,最终也么找到,大家绝大多数用下面的解释:

GetValues()方法的参数信息显示是这样的:

public static Array GetValues( Type enumType )

要求参数是enumType的类型为Type,而不是Enum。因此在我们要使用typeof获取Enum的类型。

这个方法只是解释了表象,最终还是没能解释深层次的原因。如果有哪位大神碰巧知道,欢迎留言赐教。