Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

面向对象设计的五大原则SOLID

发表于 2019-06-16 | 更新于 2024-03-18

在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则。

当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。 SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用的指南。

SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

SOLID原则简介

首字母 指代 概念
S 单一功能原则 对象应该仅具有一种单一功能
O 开闭原则 软件体应该是对于扩展开放的,但是对于修改封闭的
L 里氏替换原则 程序中对象在不改变程序正确性的前提下被它的子类所替换
I 接口隔离原则 多个特定客户端接口要好于一个宽泛用途的接口
D 依赖反转原则 依赖于抽象而不是一个实例

单一功能原则

在面向对象编程领域中,单一功能原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

这个术语由罗伯特·C·马丁(Robert Cecil Martin)在他的《敏捷软件开发,原则,模式和实践》一书中的一篇名为〈面向对象设计原则〉的文章中给出。 马丁表述该原则是基于的《结构化分析和系统规格》一书中的内聚原则(Cohesion)上。

马丁把功能(职责)定义为:“改变的原因”,并且总结出一个类或者模块应该有且只有一个改变的原因。一个具体的例子就是,想象有一个用于编辑和打印报表的模块。这样的一个模块存在两个改变的原因。第一,报表的内容可以改变(编辑)。第二,报表的格式可以改变(打印)。这两方面会的改变因为完全不同的起因而发生:一个是本质的修改,一个是表面的修改。单一功能原则认为这两方面的问题事实上是两个分离的功能,因此他们应该分离在不同的类或者模块里。把有不同的改变原因的事物耦合在一起的设计是糟糕的。

保持一个类专注于单一功能点上的一个重要的原因是,它会使得类更加的健壮。继续上面的例子,如果有一个对于报表编辑流程的修改,那么将存在极大的危险性,因为假设这两个功能存在于同一个类中,修改报表的编辑流程会导致公共状态或者依赖关系的改变,打印功能的代码会因此不工作。

单一职责原则可以参考数据库表的设计,SQL数据库中主从表设计成两个表就是遵循单一职责原则,而NoSQL数据库设计成一个对象则是违反单一职责原则。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
abstract class Employee {
// This needs to be implemented
abstract calculatePay (): number;
// This needs to be implemented
abstract reportHours (): number;
// let's assume THIS is going to be the
// same algorithm for each employee- it can
// be shared here.
protected save (): Promise<any> {
// common save algorithm
}
}

class HR extends Employee {
calculatePay (): number {
// implement own algorithm
}
reportHours (): number {
// implement own algorithm
}
}

class Accounting extends Employee {
calculatePay (): number {
// implement own algorithm
}
reportHours (): number {
// implement own algorithm
}

}

class IT extends Employee {
...
}

开闭原则

在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

开闭原则的命名被应用在两种方式上。这两种方式都使用了继承来解决明显的困境,但是它们的目的,技术以及结果是不同的。

开闭原则示意图

图2 开闭原则示意图

开闭原则主要利用面向对象的抽象父类和抽象函数。

里氏替换原则

在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。

里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程式中代替其基类(超类)对象。”

简单理解就是子类可以扩展父类的功能,但不要重写父类的方法,从而确保父子行为的一致性。

接口隔离原则

接口隔离原则(英语:interface-segregation principles, 缩写:ISP)指明客户(client)应该不依赖于它不使用的方法。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。

与类设计的单一职责原则类似,接口也要单一职责,不要太庞大。

举例

以商家接入移动支付API的场景举例,支付宝支持收费和退费;微信接口只支持收费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface PayChannel {
void charge();
void refund();
}

class AlipayChannel implements PayChannel {
public void charge() {
...
}

public void refund() {
...
}
}

class WeChatChannel implements payChannel {
public void charge() {
...
}

public void refund() {
// 没有任何代码
}
}

第二种支付渠道,根本没有退款的功能,但是由于实现了PayChannel,又不得不将refund()实现成了空方法。那么,在调用中,这个方法是可以调用的,实际上什么都没有做!

将PayChannel拆成各包含一个方法的两个接口PayableChannel和RefundableChannel。

依赖反转原则

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

  • 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  • 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

注意:依赖反转原则重要的是利用高层抽象类,而不是底层实现类。

该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。

依赖反转原则示意图

左图中高层对象A依赖于底层对象B的实现;右图中把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。
图2 依赖反转原则示意图

在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务。这种对于低层次组件的依赖限制了高层次组件被重用的可行性。

依赖反转原则的目的是把高层次组件从对低层次组件的依赖中解耦出来,这样使得重用不同层级的组件实现变得可能。把高层组件和低层组件划分到不同的包/库(在这些包/库中拥有定义了高层组件所必须的行为和服务的接口,并且存在高层组件的包)中的方式促进了这种解耦。由于低层组件是对高层组件接口的具体实现,因此低层组件包的编译是依赖于高层组件的,这颠倒了传统的依赖关系。众多的设计模式,比如插件,服务定位器或者依赖反转,则被用来在运行时把指定的低层组件实现提供给高层组件。

应用依赖反转原则同样被认为是应用了适配器模式,例如:高层的类定义了它自己的适配器接口(高层类所依赖的抽象接口)。被适配的对象同样依赖于适配器接口的抽象(这是当然的,因为它实现了这个接口),同时它的实现则可以使用它自身所在低层模块的代码。通过这种方式,高层组件则不依赖于低层组件,因为它(高层组件)仅间接的通过调用适配器接口多态方法使用了低层组件,而这些多态方法则是由被适配对象以及它的低层模块所实现的。

举例

1
2
3
4
5
6
7
class PasswordReminder {
private $dbConnection;

public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}

上述代码存在问题:首先MySQLConnection是低层次模块,而PasswordReminder处于高层次,但根据S.O.L.I.D.中D的定义,即依赖抽象而不是具体实现,上面这段代码违反这一原则,PasswordReminder类被迫依赖于MySQLConnection类。

以后如果你改变数据库引擎,你还必须编辑PasswordReminder类,因此违反了开闭原则。

PasswordReminder类不应该关心你的应用程序使用什么数据库,为了解决这个问题我们又一次“对接口编程”,因为高层次和低层次模块应该依赖于抽象,我们可以创建一个接口:

1
2
3
interface DBConnectionInterface {
public function connect();
}

接口有一个connect方法,MySQLConnection类实现该接口,在PasswordReminder类的构造函数不使用MySQLConnection类,而是使用接口替换,不用管你的应用程序使用的是什么类型的数据库,PasswordReminder类可以很容易地连接到数据库,没有任何问题,且不违反OCP。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MySQLConnection implements DBConnectionInterface {
public function connect() {
return "Database connection";
}
}

class PasswordReminder {
private $dbConnection;

public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}

根据上面的代码片段,你现在可以看到,高层次和低层次模块依赖于抽象。

迪米特法则

迪米特法则是指软件实体间无需直接通信,那么就不应该发生直接调用,可以通过第三方间接通信。例如软件设计模式当中的门面模式,代理模式。

合成复用原则

合成复用原则是指面向对象设计过程中,尽量使用组合、聚合等关联关系实现,其次才考虑使用继承关系实现。

参考链接

  1. 面向对象的SOLID原则,by wuyuegb2312.
  2. SOLID (面向对象设计),by wikipedia.
  3. SOLID Principles: The Software Developer’s Framework to Robust & Maintainable Code [with Examples],by Khalil Stemmler.
  4. S.O.L.I.D:面向对象设计的头 5 大原则,by 伯乐在线.
  5. 面向对象-软件设计原则-1小时搞懂-波波酱老师,by 波波酱老师.

非专业设计师的基本设计原则

发表于 2019-06-14

绝大部分内容创作者不是专业的设计师,因此在其内容展示时会遇到很大的困难,即如何对展示的内容进行优雅的排版?

遵循如下四个原则,虽然不会让您创造出令人惊艳的设计,但能帮助您创建出色、清晰且易于理解的设计,使每个人都可以轻松理解和互动。

对比

确保所有元素之间有足够的对比度。您的设计元素应该完全相同或显着不同。

对比问题主要表现在4种不同的方面:

  • 颜色:浅色使用深色,反之亦然。

  • 尺寸:仅将彼此相邻的元素放在完全相同的大小或相当大的大小上。

  • 重量:与尺寸相同,只是将元素放在彼此相邻的重量完全相同或重量相当不同的地方。

  • 样式:不要将一个斜体类型放在另一个斜体类型旁边,或者在另一个衬线字体旁边放置一个衬线字体。 结合不同的东西。

一致性

确保类似的元素以类似的方式出现。

为什么? 首先,通过保持事物的一致性(因此,简单),您可以让人们将注意力集中在设计的重要方面,而不是被一直在变化的事物分散注意力。

其次,一致性增加了信任,使事物看起来实际上是设计的,而不是简单地快速抛在一起。

一旦你选择它们就要坚持下去:

  • 字体/字体
  • 调色板/颜色的阴影
  • 表格
  • 对齐
  • 装饰元素的风格

奥卡姆剃刀

奥卡姆剃刀即若无必要,勿增实体,以减少视觉噪音。

您在设计中使用的元素数量越少越好。

为什么? 人类的大脑很难处理信息并在输入过载的情况下做出决策。 使用尽可能少的装饰元素(字体,颜色,阴影,框架,笔画,图标,图案等)。

将奥卡姆剃刀的设计原则应用于所有内容:

如果只用2个元素可以实现某些功能,请不要使用3.如果可以使用10个元素实现某些功能,请不要使用20.您可以使用它。

空间

事物的定位方式会发送关于其含义的元级别消息。

为什么这很重要? 因为了解你如何定位事物以及你在它们周围添加了多少空间有助于降低设计的复杂性,因此,使它更令人愉悦,更容易与之交互。

在设计中使用空间来传达这三个方面的意义:

  1. 接近==相关性

与其他元素相比彼此更接近的事物被认为彼此更多地相互关联而不是与其他元素相关。

这个是最重要的,因为我觉得它经常被忽视(甚至有些人声称是专业设计师,而不是命名任何名字)。

它可以以多种不同的方式应用,例如:

* 行之间应该有一个空格,而不是一行中每个单词之间的空间 - 同样,不同段落之间的空间也比段落内的行之间的空间要大。

* 设计元素应该在彼此之间具有较小的空间,而不是在这些元素和组合物的边缘之间。

* 标签和支持信息应位于其描述/相关的元素附近。

  1. 负空间

与奥卡姆剃刀设计原则结合使用,尽可能地为您的设计提供负面空间,使它们整齐并使其意义更加明显。

将太多元素放入有限的空间就像试着一次听三首不同的歌。 很难理解所说的内容。

  1. 重要性和秩序

最重要的事情,你先放,和/或让它们占用最多的空间。 使用一系列事物来传达秩序。 等等。你绝对可以弄明白其余的。

参考链接

  1. Fundamental design principles for non-designers,by freecodecamp.
  2. 奥卡姆剃刀,by wikipedia.

MAVLink协议解析

发表于 2019-06-13

MAVLink是一种非常轻量级的消息传递协议,用于与无人机(以及板载无人机组件之间)进行通信。

MAVLink遵循现代混合发布-订阅和点对点设计模式:数据流作为主题发送/发布,而配置子协议(如任务协议或参数协议)是点对点重传。

消息在XML文件中定义。每个XML文件定义特定MAVLink系统支持的消息集,也称为“方言”。由大多数地面控制站和自动驾驶仪实现的参考消息集在common.xml中定义(大多数方言构建在此定义的顶部)。

MAVLink工具链使用XML消息定义为每种受支持的编程语言生成MAVLink库。无人机,地面控制站和其他MAVLink系统使用生成的库进行通信。这些通常是MIT许可的,因此可以在任何闭源应用程序中无限制地使用,而无需发布闭源应用程序的源代码。

MAVLink于2009年初由Lorenz Meier首次发布,现在已经有相当数量的贡献者。

MAVLink消息格式

MAVLink消息格式如图1所示。

MAVLink消息格式

图1 MAVLink消息格式

MAVLink消息中各字段含义如图2所示。

MAVLink消息格式描述

图2 MAVLink消息格式描述

参考链接

  1. MAVLink Developer Guide,by mavlink homepage.
  2. 无人机通讯协议 –Mavlink 学习, by Theshy.
  3. Pixhawk原生固件PX4之MAVLink协议解析,by FantasyJXF.

轨迹相似度度量方法总结

发表于 2019-06-10 | 更新于 2019-06-11

轨迹相似度度量有广泛的用途,如语音识别分类、模板匹配、信息检索等。下面介绍各种轨迹相似度度量方法。

轨迹定义

轨迹可由时间域到空间域的映射函数表示,如:

$$t\stackrel{F}{\longrightarrow}R^d, d>1$$

度量方法

轨迹相似度度量方法主要有:

  • 基于点方法: EDR,LCSS,DTW等

  • 基于形状的方法: Frechet, Hausdorff

  • 基于分段的方法:One Way Distance, LIP distance

  • 基于特定任务的方法:TRACLUS, Road Network,grid等

基于点的方法

DTW

DTW(Dynamic Time Warping, 动态时间规整)可以计算两个时间序列的相似度,尤其适用于不同长度、不同节奏的时间序列(比如不同的人读同一个词的音频序列)。DTW将自动warping扭曲 时间序列(即在时间轴上进行局部的缩放),使得两个序列的形态尽可能的一致,得到最大可能的相似度。

Dynamic Time Warping(DTW)诞生有一定的历史了(日本学者Itakura提出),它出现的目的也比较单纯,是一种衡量两个长度不同的时间序列的相似度的方法。应用也比较广,主要是在模板匹配中,比如说用在孤立词语音识别(识别两段语音是否表示同一个单词),手势识别,数据挖掘和信息检索等中。

设 $P=<p_1,p_2,…,p_m>$ 和 $Q=<q_1,q_2,…,q_n>$ 是两个时间序列,则 $P$ 和 $Q$的距离 $DTW(P,Q)$ 定义如下:

$$DTW(P,Q)= \left{
\begin{array}{lcl}
0 & &if\ m=n=0\
\infty & &if\ m=0\ or\ n=0\
dist(p_1,q_1)+min
\left{
\begin{aligned}
DTW(Rest(P),Rest(Q))\
DTW(Rest(P),Q)\
DTW(P,Rest(Q))\
\end{aligned}
\right} & & otherwise
\end{array}
\right.
$$

参考链接

  1. 如何判断两条轨迹(或曲线)的相似度?,by zhihu.
  2. 动态时间规整(DTW)算法简介,by 文均.
  3. DTW(Dynamic Time Warping)动态时间规整,by X-猪.
  4. Dynamic Time Warping 动态时间规整算法,by 阿凡卢.

经典控制与现代控制理论的区别与联系

发表于 2019-06-09 | 更新于 2021-01-22

控制理论是工程学与数学的跨领域分支,主要处理在有输入信号的动力系统的行为。系统的外部输入称为“参考值”,系统中的一个或多个变量需随着参考值变化,控制器处理系统的输入,使系统输出得到预期的效果。

控制理论一般的目的是借由控制器的动作让系统稳定,也就是系统维持在设定值,而且不会在设定值附近晃动。

控制论简介

控制理论是

  • 一个研究如何调整动态系统特性的理论。
  • 科学中跨学科的领域,起源于工程及数学,逐渐的应用在许多社会科学中,例如心理学、社会学(社会学中的控制理论)、犯罪学及金融系统。

控制系统可以视为具有四种机能的系统:量测、比较、计算及修正。这四个机能可以用五种元素来实现:感测器、换能器、发送器、控制器及最终控制元件。量测机能是由感测器、换能器及发送器执行,在实务应用上,这三个元素会整合在一个单体内,像是电阻温度计。比较和计算的机能是由控制器执行,可能是电子式的比例控制(P控制)、PI控制、PID控制、双稳态的迟滞控制,也可能是可编程逻辑控制器(PLC)。早期的控制器也可能是机械式的,像是离心式调速器或是化油器。修正机能是由最终控制元件执行,最终控制元件改变系统的输出,因此影响操纵或控制的变量。

范例

车辆的巡航定速系统是让车辆维持在由驾驶者设定的固定参考速度。此时控制器为巡航定速系统,车辆为受控体(plant),而系统是由控制器和车辆所组成,而控制变量是引擎节流阀的位置.会决定引擎可以产生的功率。

一种最单纯的作法是当驾驶者启动巡航定速系统时,固定引擎节流阀的位置。但是若驾驶者在平坦的路面启动巡航定速系统,车辆在上坡时速度会较慢,车辆在下坡时速度又会较快。这种的控制器称为开环控制器,因为没有去量测系统输出(车辆速度)并且影响控制变量(节流阀位置),因此此系统无法去针对车辆遇到的变化(像路面坡度的变动)去进行调整。

在闭环控制系统中,利用感测器量测系统输出(车辆速度),并将资料送入控制器中,控制器依资料调整控制变量(节流阀位置),来达到维持理想系统输出(使车辆速度和驾驶者设定的参考速度一致)。此时若车辆在上坡时,感测器会量到车辆的速度变慢,因此会调整节流阀位置,加大引擎输出功率,使马达加速。因为有量测车辆速度的回授,因此控制器可以配合车辆速度的变化进行动态调整。因此产生了控制系统中的“环”范式:控制变量影响系统输出,而再根据量测到的系统输出去调整控制变量。

经典控制理论

为了克服开环控制器的限制,在控制理论中导入了反馈。闭环的控制器利用回授来控制动态系统的状态或输出。其名称来自系统中的讯息路径:程序输入(例如马达的电压)影响程序输出(例如马达的电流或转矩),利用感测器量测输出,再将量测资料送到控制器中处理,结果送回控制器作为输入信号之一,因此成为一闭环。

相对于开环控制器,闭环控制器有以下的优点:

  • 噪声抑制能力(像巡航定速中的路面坡度)。
  • 即使在数学模型有一些不确定性的情形下(如模型结构和实际系统不是完全符合,或是模型参数和实际数值不是完全一致),仍有一定程度的性能。
  • 可以稳定不稳定的系统
  • 减少对于参数变动的灵敏度
  • 提升命令追随(命令变化时,系统配合命令变化)的性能
  • 有些系统中,同时出现开环及闭环的控制,此时的开环会称为前馈,目的是为了提升命令追随的性能。

PID控制器是常见的闭回路控制器架构。

闭环传递函数

系统的输出y(t)借由感测器F量测后,和参考值r(t)相减,控制器C根据参考值和输出值的误差e调整受控体P的输入u,如图1所示,这类的控制器称为闭环控制器。

由于只有一个输入和输出,此系统会称为SISO(单一输入单一输出)控制系统。MIMO(多重输入多重输出)控制系统是指输入或输出不只一个,在实际应用上也很常见,其输入变量和输出变量会用向量表示,而不是单一数值的标量。在分布参量系统中,向量可能是无限维的,即一般的函数。

闭环传递函数示意图

图1 闭环传递函数示意图

若假设控制器C、受控体P及感测器F都是线性及非时变的(各模组输入和输出的关系不随时间改变),可以将上述系统用拉普拉斯转换来分析,因此可以得到以下的关系:

$$Y(s)=P(s)U(s),!$$
$$U(s)=C(s)E(s),!$$
$$E(s)=R(s)-F(s)Y(s),!$$

其中 s为拉普拉斯转换中的复变量,若要求解Y(s)用R(s)表示,可得:

$$Y(s)=\left({\frac {P(s)C(s)}{1+F(s)P(s)C(s)}}\right)R(s)=H(s)R(s),!$$

表示式 $H(s)={\frac {P(s)C(s)}{1+F(s)P(s)C(s)}},!$即为系统的闭环传递函数,分子是从r到y的前馈(开环)增益,分母是1加上经过反馈环的增益.即闭环增益,若$|P(s)C(s)|\gg 1,!$,,也就是说在各s下,其范数都很大,且 $|F(s)|\approx 1,!$,则Y(s)近似于R(s),此时输出会紧密的追随参考输入。

PID

请参考链接[4]。

现代控制理论

经典控制理论以频域分析为主,而现代控制理论利用时域的状态空间表示法,将系统中的输入、输出及状态变量之间的关系用一阶的微分方程表示。为了抽象化输入、输出及状态变量的数量,这些变量一般会用向量来表示,而微分方程或代数方程(当系统是线性时)则会以矩阵形式表示。状态空间表示法也称为时域分析,提供一个方便且简洁的方式针对多重输入及输出的系统建模及分析,在有输入和输出时,也可以利用拉氏转换,将系统所有的资料包括在其中。现代控制理论不同于频域分析,可以分析非线性或不是零初始条件的系统。状态空间就是指坐标轴为状态变量的空间,系统的状态可以表示为状态空间中的一个向量。

控制理论主题

稳定性

在控制理论中的稳定性是指控制系统的状态在特定条件下,可以维持在一定的范围内,不会发散,而在什么范围内才算是稳定则依系统种类而不同。

  • 没有输入信号的动力系统,其稳定性是用李雅普诺夫稳定性来描述,也就是任何初始条件在 $x_{0}$ 附近的轨迹均能维持在 $x_{0}$ 附近
  • 有输入信号的线性系统,其稳定性是用有界输入有界输出稳定性(BIBO 稳定性)来描述,针对任何有界的输入信号,其输出也是有界。
  • 有输入信号的非线性系统,其稳定性是用输入-状态稳定性(input-to-state stability),结合了李雅普诺夫稳定性及类似有界输入有界输出稳定性的表示方式。

可控制性及可观测性

可控制性和可观测性分别是输入和状态,输出和状态之间的性质。是在分析控制系统,决定控制策略或判断是否可以使系统稳定时所需要的重要性质。

可控制性是指是可以用适当的控制信号作为输入,使特定状态变量的数值变成0,和利用输入调整状态变量的能力有关,若一个状态变量是不可控制的,表示没有输入可以调整这一个状态,若一系统中所有不可控制的状态变量,其动态特性都是稳定的,则此系统称为可稳定的(stabilizable)。

可观测性是指可以用输出的量测及计算得到状态变量的值,若一个状态变量是不可观测的,表示无法确认此一状态是否稳定,也就无法用此状态来稳定整个系统。若一系统中所有不可控制的观测变量都是稳定的,则此系统称为可检测的(detectable)。

控制规格

在控制原理的基础下,已发展出许多不同的控制策略,从非常通用的(PID控制器),到针对特殊系统的控制,尤其是机器人或是航空器的巡航定速控制。

一个控制问题会有许多的规格,其中稳定性是必要条件的,不论系统开环稳定性如何,控制器需确保在闭环下是稳定的。性能不佳或是调整不当的控制器可能使系统变的不稳定,甚至可能比开环还要不稳定,这是应尽量要避免的。

模型识别及鲁棒性

控制系统一定会有一定程度的鲁棒性。控制器一般是依照一个假设的受控系统模组再进行设计,鲁棒性是指一控制器配合的受控系统和原来假设的系统有一点不同,控制器的特性不会有太大的变化。这个规格在实际的控制器中相当重要,因为很少实际系统会完全符合描述它的微分方程,在选择系统数学模型时,一般会进行简化,否则数学模型会非常复杂,甚至无法求得一个完整的模型。

系统分类

线性系统控制

针对MIMO的系统,极点的指定可以用开环系统的状态空间,再将极点放在指定位置,计算对应的回授矩阵。若在复杂的系统中,上述的程序需要用电脑辅助计算才能达到,而且不保证其鲁棒性。而且一般而言无法量到所有的系统状态,在极点指定的设计时需加入观测器(observer)的设计。

非线性系统控制

像机器人学及航天产业中的程序一般都有高度非线性的动态,在控制理论中有时可以用线性化的方式转换为线性系统,再依线性系统的方式控制。但有时需要用一些可以配合非线性系统使用的非线性控制理论,例如回授线性化、反推控制、滑动模式控制等。轨迹线性化控制一般利用李亚普诺夫稳定性的基础。微分几何用做为一数学工具,将许多广为人知的线性控制概念扩展到非线性控制中,但其中又有其微妙之处,因此变成一个更有挑战性的问题。

分散式系统

分散控制系统是指一个系统由多个控制器来控制。分散控制有几个好处,例如可以控制一个位在广大地理区域的系统,各控制器之间可以用通讯网络彼此交换资料,并协调彼此的行动。

参考链接

  1. 控制理论,by wikipedia.
  2. 「珂学原理」精选:经典控制和现代控制理论有何本质区别?,by 王珂.
  3. 拉普拉斯变换,by wikipedia.
  4. PID控制算法原理分析,by jackhuang.
  5. Matlab中margin函数使用,by jk_101.
  6. 伯德图中的相角裕量和幅值裕量有什么物理意义?,by zhihu.

ROS中Package安装方法

发表于 2019-06-08

ROS(机器人操作系统,Robot Operating System),是专为机器人软件开发所设计出来的一套电脑操作系统架构。它是一个开源的元级操作系统(后操作系统),提供类似于操作系统的服务,包括硬件抽象描述、底层驱动程序管理、共用功能的执行、程序间消息传递、程序发行包管理,它也提供一些工具和库用于获取、建立、编写和执行多机融合的程序。

ROS的Package资源非常丰富,官方库中就有两千多Package,这对扩充ROS的功能十分重要。下面即介绍ROS中Package的安装方法,主要分成两种方法:

Deb安装方式

deb方式安装方法十分简单,根据ROS版本,直接运行apt-get命令,例如:

1
$ sudo apt-get install ros-kinetic-camera-calibration

源码安装方式

源码安装方式稍微复杂,安装方法如下:

  1. 创建catkin工作空间
  2. 在catkin工作空间的src文件夹下,下载ROS的Package源代码
  3. 使用catkin build命令编译安装

参考链接

  1. 安装ROS软件包,by ferstar.

ROS编译命令catkin简析

发表于 2019-06-06

目前编译ROS的Package有两种方法:

  • catkin_make
  • catkin build

catkin_make

catkin_make 是一个命令行工具,它简化了catkin的标准工作流程。你可以认为catkin_make是在CMake标准工作流程中依次调用了cmake 和 make。

使用方法如下:

1
2
# 在catkin工作空间下
$ catkin_make [make_targets] [-DCMAKE_VARIABLES=...]

catkin

catkin是一个用于处理catkin元构建系统和catkin工作区的命令行工具。其用法如下:

1
2
3
4
5
6
7
8
9
10
11
`catkin VERB -h` for help on each verb listed below:

build Builds a catkin workspace.
clean Deletes various products of the build verb.
config Configures a catkin workspace's context.
create Creates catkin workspace resources like packages.
env Run an arbitrary command in a modified environment.
init Initializes a given folder as a catkin workspace.
list Lists catkin packages in the workspace or other arbitray folders.
locate Get the paths to various locations in a workspace.
profile Manage config profiles for a catkin workspace.

同样可使用catkin build命令编译ROS的package。

catkin_make与catkin build的区别

与catkin_make不同,catkin命令行工具不仅仅是围绕cmake和make命令的瘦包装器。 catkin build命令隔离地在工作空间的源空间中构建每个包,以防止构建时串扰。 因此,在其最简单的用法中,catkin构建的行为类似于catkin_make_isolated的并行化版本。

参考链接

  1. Migrating from catkin_make,by catkin_tools homepage.
  2. 编译ROS程序包,by ros wiki.

卡尔曼滤波入门

发表于 2019-06-04 | 更新于 2023-01-31

卡尔曼滤波(Kalman filter)是一种高效率的递归滤波器(自回归滤波器),它能够从一系列的不完全及包含噪声的测量中,估计动态系统的状态。卡尔曼滤波会根据各测量量在不同时间下的值,考虑各时间下的联合分布,再产生对未知变数的估计,因此会比只以单一测量量为基础的估计方式要准。卡尔曼滤波得名自主要贡献者之一的鲁道夫·卡尔曼。

卡尔曼滤波在技术领域有许多的应用。常见的有飞机及太空船的导引、导航及控制。卡尔曼滤波也广为使用在时间序列的分析中,例如信号处理及计量经济学中。卡尔曼滤波也是机器人运动规划及控制的重要主题之一,有时也包括在轨迹最佳化。卡尔曼滤波也用在中轴神经系统运动控制的建模中。因为从给与运动命令到收到感觉神经的回授之间有时间差,使用卡尔曼滤波有助于建立符合实际的系统,估计运动系统的目前状态,并且更新命令。

卡尔曼滤波原理解析

卡尔曼滤波器的状态矩阵方程如图1所示。

卡尔曼滤波器的状态矩阵方程

图1 卡尔曼滤波器的状态矩阵方程

其中,下角标上的k是状态。此处我们将其视为离散时间间隔,比如说k=1 代表 1ms, k=2 代表2ms。

我们的目的是找到信号 $x$ 的估值 $\hat{x} $,并且希望能对所有的k值都能找到对应的估值。

另外此处的 $Z_k$ 是实际测量值,记住我们对该值并不完全信任,否则我们也不用费这么多事了。 $K_k$ 称为卡尔曼增益(也是最重要的量), $\hat{x}_k$ 是前一状态下的信号估值。

现在我们有了测量值,前一状态的信号估值。该方程中唯一未知的量就是卡尔曼增益 $K_k$ 了。对于每个状态,我们都需要计算对应的。这事不简单,但好在我们有所需的计算工具。

另一方面,假设 $K_k$ 等于0.5,我们会发现该式变成了一个简单的求平均值公式。换句话说,随着状态的变化,我们的 $K_k$ 值将越来越“聪明”。

卡尔曼滤波器的构造

建立模型

此步最为关键,你必须确保卡尔曼滤波器适用于你要解决的问题。

卡尔曼滤波器的两个方程如下:

$$x_k=Ax_{k-1}+Bu_k+w_{k-1} \tag{1}$$
$$z_k=Hx_k+v_k \tag{2}$$

式(1)表达的是每个 $x_k$ 都可以通过一个线性随机方程估计出来。任意 $x_k$ 都是其前一时刻的值与过程噪音的线性组合(这个很难概念化)。请记住,大部分情况下该式没有控制信号 $u_k$ 项。

式(2)告诉我们任何测量值 $z_k$ (无法确定精确与否的测量值)都是信号值与测量噪声的线性组合。这两个分量符合高斯分布。

过程噪声与测量噪声互相统计独立。

$A, B, H$ 是一般形式的矩阵。但在大多数信号处理问题中,这些量仅为数值。而且虽然这些值在状态变换时会改变,大多数情况下我们都可以假设他们为定值。

如果我们十分确定我们的系统符合此模型,那么唯一剩下要做的事就是估计噪音函数 $w_{k-1}$ 和 $v_k$ 的平均值以及标准差。我们知道,在实际生活中没有信号满足高斯分布,但我们可以近似其为高斯分布。

该近似问题不大,因为我们将看到卡尔曼滤波器算法会逐渐向正确的(噪音函数的)估计值收敛,即使高斯噪声参数估计不佳。

唯一需要记住的是:你估计出来的噪音参数越好(越接近实际),你估计的(输出真实值)就越好。

开始卡尔曼滤波

如果你的模型适用于卡尔曼滤波器,那么接下来的步骤就是决定一些必要的参数以及初始值。

卡尔曼滤波器包含的方程可分为两个方程集:时间更新方程组(用于预测)以及测量更新方程组(用于修正)。这两个方程组在滤波器运行的每一步(每个状态)下都会执行,如图2所示。

卡尔曼滤波的两个步骤

图2 卡尔曼滤波的两个步骤

建模部分已经在步骤一完成了,所以矩阵A,B和H已知。这些矩阵很可能是一个常数,而且大部分情况下会等于1。

剩下的最让人难受的部分就是决定R和Q的值了。R的值还是很容易找的,因为一般情况下我们对环境中的噪音还是能够确认的。(起码能用仪器测一下)。但是找Q的值就没那么直观了。

为了使滤波器能够运行,我们需要知道 $x_0$ 和 $P_0$ 的估计值。

迭代

在获得了滤波器运行所需的所有信息后,我们就可以估值迭代了。记住:前一状态的估值将成为当前状态的输入。

卡尔曼滤波的迭代运行

图3 卡尔曼滤波的迭代运行

此处 $\hat{x}_k^-$ 是预估值,从某种角度来说是第二部分运行前对 x 的一个粗略估计值。

同时 $P_k^-$ 叫做预估误差协方差。在第二步“测量更新”中我们将会用到这两个预估值。

$\hat{x}_k$为在时间 k 时的 x 的估计值。(也是我们最想获得的值)。同时,我们得到了用于k+1时刻计算的 $P_k$ 值。

下一次迭代不会用到我们求得的卡尔曼增益 $K_k$ 的值,该值隐藏而神秘,并且是这些方程集的最重要的部分。

我们在第二步“测量更新”中求得的值也叫做后部值(posterior values)。这个名称也很说得通。

卡尔曼滤波器的应用示例

参考链接

  1. 卡尔曼滤波:从入门到精通,by David LEE.
  2. 傻瓜也能懂的卡尔曼滤波器(翻译自外网博客),by 彦鑫.
  3. 说说卡尔曼滤波,by 李阳.
  4. 卡尔曼滤波,by wikipedia.
  5. 图说卡尔曼滤波,一份通俗易懂的教程,by 论智.
  6. 时间序列基本概念、任务、预测方法,by 带你聊技术.

求解射线与三角形交点的算法

发表于 2019-06-04

求解射线与三角形的交点,在光线追踪、碰撞检测、目标拾取等场景中经常使用,是计算机图形学中最基本的操作。下面介绍常用的求解射线与三角形交点的算法。

问题定义

求解射线与三角形交点示意图如图1所示。

求解射线与三角形交点示意图 求解射线与三角形交点示意图

图1 求解射线与三角形交点示意图

射线的参数方程如下,其中O是射线的起点,D是射线的方向,t是常数。

$$O+Dt$$

该方程的含义是一个点从起点O开始,沿着方向D移动任意长度,得到终点R,根据t值的不同,得到的R值也不同,所有这些不同的R值便构成了整条射线,比如下面的射线,起点是P0,方向是u,p0 + tu也就构成了整条射线。

射线方程示意图

图2 射线方程示意图

三角形的参数方程如下,其中$V_0$,$V_1$和$V_2$是三角形的三个点,$u, v$是$V_1$和$V_2$的权重,$1-u-v$是$V_0$的权重,并且满足$u>=0, v >= 0,u+v<=1$。

$$(1-u-v)V_0+uV_1+vV_2$$

三角形方程示意图

图3 三角形方程示意图

直观方法

求解射线与三角形的交点最直观的方法如下:

  1. 判断射线是否与平面相交
  2. 判断点是否在三角形内

但该方法需要额外计算三角形所在平面,效率不高。

Moller-Trumbore方法(Journal of Graphic Tools, 1997)

Moller-Trumbore方法中,求射线与三角形的交点即求解如下方程:

$$O+Dt=(1-u-v)V_0+uV_1+vV_2$$

其中t,u,v是未知数,其他都是已知的。

移项并整理,将t,u,v提取出来作为未知数,得到下面的线性方程组:

$$\begin{bmatrix}
-D& V_1-V_0 &V_2-V_0
\end{bmatrix}\begin{bmatrix}
t\u\v
\end{bmatrix}=O-V_0$$

现在开始解这个方程组,这里要用到两个知识点,一是克莱姆法则,二是向量的混合积。

令$E_1 = V_1 - V_0,E_2 = V_2 - V_0,T = O - V_0$上式可以改写成:

$$\begin{bmatrix}
-D& E_1 & E_2
\end{bmatrix}\begin{bmatrix}
t\u\v
\end{bmatrix}=T$$

根据克莱姆法则,可得到t,u,v的解为:

$$
\begin{bmatrix}
t\u\v
\end{bmatrix}
=\frac{1}{\begin{vmatrix}
-D & E_1 & E_2
\end{vmatrix} }
\begin{vmatrix}
T&E_1&E_2\
-D& T& E_2\
-D & E_1& T
\end{vmatrix}
$$

根据混合积公式:

$$\begin{vmatrix}
a&b&c
\end{vmatrix}
=a\times{b}\cdot{c}$$

上式改写为:

$$
\begin{bmatrix}
t\u\v
\end{bmatrix}
=\frac{1}{\begin{bmatrix}
-D \times E_2 \cdot E_1
\end{bmatrix} }
\begin{vmatrix}
T \times E_1 \cdot E_2\
D \times E_2 \cdot T\
T \times E_1 \cdot D
\end{vmatrix}
$$

令$P=D \times E_2$,$Q=T \times E_1$,得到最终的公式:

$$\begin{bmatrix}
t\u\v
\end{bmatrix}
=\frac{1}{\begin{bmatrix}
P \cdot E_1
\end{bmatrix} }
\begin{vmatrix}
Q \cdot E_2\
P \cdot T\
Q \cdot D
\end{vmatrix}
$$

之所以提炼出P和Q是为了避免重复计算。

参考链接

  1. 射线和三角形的相交检测(ray triangle intersection test),by zdd.
  2. 光线-三角形求交测试算法[译], by PKUWWT.
  3. 克莱姆法则,by wikipedia.
  4. 混合积,by wikipedia.
  5. 重心坐标,by wikipedia.
  6. 重心坐标(Barycentric coordinates),by 杨超.

PLY文件格式分析

发表于 2019-06-04

PLY文件是一种存储3D模型的文件格式,全名为多边形档案(Polygon File Format)或 史丹佛三角形档案(Stanford Triangle Format)。

该格式主要用以储存立体扫描结果的三维数值,透过多边形片面的集合描述三维物体,与其他格式相较之下这是较为简单的方法。它可以储存的资讯包含颜色、透明度、表面法向量、材质座标与资料可信度,并能对多边形的正反两面设定不同的属性。

在档案内容的储存上PLY有两种版本,分别是纯文字(ASCII)版本与二元码(binary)版本,其差异在储存时是否以ASCII编码表示元素资讯。

通过分析PLY文件格式,我们可以进而初窥存储3D模型的文件奥秘。

文件格式

各种文件格式通常分成文件头和文件内容,PLY格式也不例外。

每个PLY档都包含档头(header),用以设定网格模型的“元素”与“属性”,以及在档头下方接着一连串的元素“数值资料”。一般而言,网格模型的“元素”就是顶点(vertices)、面(faces),另外还可能包含有边(edges)、深度图样本(samples of range maps)与三角带(triangle strips)等元素。无论是纯文字与二元码的PLY档,档头资讯都是以ASCII编码编写,接续其后的数值资料才有编码之分。

PLY档案以此行作为开头,以识别PLY格式:

1
ply

接着第二行是版本资讯,目前有三种写法:

1
2
3
format ascii 1.0
format binary_little_endian 1.0
format binary_big_endian 1.0

其中ascii, binary_little_endian, binary_big_endian是档案储存的编码方式,而1.0是遵循的标准版本(现阶段仅有PLY 1.0版)。在档头中可使用’comment’作为一行的开头以编写注解,例如:

1
comment This is a comment!

描述元素及属性,必须使用’element’及’property’的关键字,一般的格式为element下方接着属性列表,例如:

1
2
3
4
element <element name> <number in file>
property <data_type> <property name 1>
property <data_type> <property name 2>
property <data_type> <property name 3>

‘property’不仅定义了资料的型态,其出现顺序亦定义了资料的顺序。内定的资料形态有两种写法:一种是char uchar short ushort int uint float double,另外一种是具有位元长度的int8 uint8 int16 uint16 int32 uint32 float32 float64。 例如,描述一个包含12个顶点的物体,每个顶点使用3个单精度浮点数 (x,y,z)代表点的座标,使用3个unsigned char代表顶点颜色,颜色顺序为 (B, G, R),则档头的写法为:

1
2
3
4
5
6
7
element vertex 12
property float x
property float y
property float z
property uchar blue
property uchar green
property uchar red

其中vertex是内定的元素类型,接续的6行property描述构成vertex元素的数值字段顺序代表的意义,及其资料形态。

另一个常使用的元素是面。由于一个面是由3个以上的顶点所组成,因此使用一个“顶点列表”即可描述一个面, PLY格式使用一个特殊关键字’property list’定义之。 例如,一个具有10个面的物体,其PLY档头可能包含:

1
2
element face 10
property list uchar int vertex_indices

‘property list’表示该元素face的特性是由一行的顶点列表来描述。列表开头以uchar型态的数值表示列表的项目数,后面接着资料型态为int的顶点索引值(vertex_indices),顶点索引值从0开始。

最后,标头必须以此行结尾:

1
end_header

档头后接着的是元素资料(端点座标、拓朴连结等)。在ASCII格式中各个端点与面的资讯都是以独立的一行描述,而二元编码格式则连续储存这些资料,载入时须以’element’定义的元素数目以及’property’中设定的资料形态计算各笔字段的长度。

示例

一个典型的PLY文件结构分成三部分:

1
2
3
文件头 (从ply开始到end_header)
顶点元素列表
面元素列表

其中的顶点元素列表一般以x y z方式排列,形态如文件头所定义;而面元素列表是以下列格式表示。

1
<组成面的端点數N> <端点#1的索引> <端点#2的索引> ... <端点#N的索引>

以存储一个立方体模型的PLY文件为例,其内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ply
format ascii 1.0
comment made by anonymous
comment this file is a cube
element vertex 8
property float32 x
property float32 y
property float32 z
element face 12
property list uint8 int32 vertex_index
end_header
0 0 0
0 25.8 0
18.9 0 0
18.9 25.8 0
0 0 7.5
0 25.8 7.5
18.9 0 7.5
18.9 25.8 7.5
3 5 1 0
3 5 4 0
3 4 0 2
3 4 6 2
3 7 5 4
3 7 6 4
3 3 2 1
3 1 2 0
3 5 7 1
3 7 1 3
3 7 6 3
3 6 3 2

PLY文件可用blender软件打开。

参考链接

  1. PLY,by wikipedia.
  2. Opengl学习笔记:(一).Ply文件文件格式和文件读取,by 为何走到这里.
上一页1…394041…53下一页

Jack Huang

521 日志
67 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse