Wetts's blog

Stay Hungry, Stay Foolish.

0%

执行程序时,Python 内部会先将源代码编译成所谓字节码的形式。编译时一个简单的翻译步骤,字节码时一种低级的、与平台无关的形式。这些字节码可以提高执行速度。

如果 Python 进程在机器上拥有写入权限,那么它将把程序的字节码保存为一个以 .pyc 为扩展名的文件。

  • 对 Python 3.2 之前的版本,运行程序之后,你会在那些源代码的附近(也就是说在同一个目录下)看到这些文件。
  • 在 Python 3.2 以及之后的版本,Python 将把 .pyc 字节码存储在名为 __pychache 的子目录中,这个子目录位于与源文件相同的路径下。【新版 Python 的 __pycache__ 子目录中的文件命名中包含了编译它们的 Python 的版本信息。

何时重新编译:

  • 源文件改变。检查源文件和字节码文件的最后一次修改的时间戳。
  • Python 的版本。使用了不同的 Python版本。

如果 Python 无法在机器上写入字节码,程序仍然可以工作:字节码会在内存中生成,并在程序结束时直接被丢弃。

一旦程序编译成字节码(或字节码从已经存在的 .pyc 文件中载入),之后的字节码发送到通常称为 Python 虚拟机(Python Virtual Machine,简称为 PVM)的程序上来执行。

python3 -m venv {环境名}

source venv/bin/activate

deactivate

原始的box文件都会被放在 ~\.vagrant.d 路径下

转自:链接:https://www.zhihu.com/question/51435499/answer/129379006

端到端指的是输入是原始数据,输出是最后的结果,原来输入端不是直接的原始数据,而是在原始数据中提取的特征,这一点在图像问题上尤为突出,因为图像像素数太多,数据维度高,会产生维度灾难,所以原来一个思路是手工提取图像的一些关键特征,这实际就是就一个降维的过程。那么问题来了,特征怎么提?
特征提取的好坏异常关键,甚至比学习算法还重要,举个例子,对一系列人的数据分类,分类结果是性别,如果你提取的特征是头发的颜色,无论分类算法如何,分类效果都不会好,如果你提取的特征是头发的长短,这个特征就会好很多,但是还是会有错误,如果你提取了一个超强特征,比如染色体的数据,那你的分类基本就不会错了。这就意味着,特征需要足够的经验去设计,这在数据量越来越大的情况下也越来越困难。于是就出现了端到端网络,特征可以自己去学习,所以特征提取这一步也就融入到算法当中,不需要人来干预了。

转自:https://zhuanlan.zhihu.com/p/71156910

synchronized与Lock

Java中有两种加锁的方式:一种是用synchronized关键字,另一种是用Lock接口的实现类。

形象地说,synchronized关键字是自动档,可以满足一切日常驾驶需求。但是如果你想要玩漂移或者各种骚操作,就需要手动档了——各种Lock的实现类。

所以如果你只是想要简单的加个锁,对性能也没什么特别的要求,用synchronized关键字就足够了。自Java 5之后,才在java.util.concurrent.locks包下有了另外一种方式来实现锁,那就是Lock。也就是说,synchronized是Java语言内置的关键字,而Lock是一个接口,这个接口的实现类在代码层面实现了锁的功能,具体细节不在本文展开,有兴趣可以研究下AbstractQueuedSynchronizer类,写得可以说是牛逼爆了。

1

其实只需要关注三个类就可以了:ReentrantLock类、ReadLock类、WriteLock类。

ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”,后面会讲它们的用途。

ReadWriteLock其实是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口。

我们停止深究源码,仅从使用的角度看,Lock与synchronized的区别是什么?在接下来的几个小节中,我将梳理各种锁分类的概念,以及synchronized关键字、各种Lock实现类之间的区别与联系。

悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。

悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。

乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。

悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

乐观锁的基础——CAS

说到乐观锁,就必须提到一个概念:CAS

什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。

  1. 比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
  2. 设置:如果是,将A更新为B,结束。【这里存在一个问题,就是一个值从A变为B,又从B变回了A。这种情况下,CAS可能会认为值没有发生过变化,但实际上是有变化的。对此,并发包下有AtomicStampedReference提供根据版本号判断的实现。】如果不是,则什么都不做。

上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。

有了CAS,就可以实现一个乐观锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data = 123; // 共享数据

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
oldValue = data; // 保存原始数据
newValue = doSomething(oldValue);

// 下面的部分为CAS操作,尝试更新data的值
if (data == oldValue) { // 比较
data = newValue; // 设置
flag = false; // 结束
} else {
// 啥也不干,循环重试
}
}
/*
很明显,这样的代码根本不是原子性的,
因为真正的CAS利用了CPU指令,
这里只是为了展示执行流程,本意是一样的。
*/

这是一个简单直观的乐观锁实现,它允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
2

Java中真正的CAS操作调用的native方法

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?

不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋。

然而在JDK中并没有自旋锁(SpinLock)这个类,那什么才是自旋锁呢?读完下个小节就知道了。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

感谢评论区酷帅俊靓美的问题:

偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?

线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。

还有人对此有疑惑,我之前确实没有描述清楚,但如果要展开讲,涉及到太多新概念,可以新开一篇了。更何况有些太底层的东西,我没读过源码,没有自信说自己一定是对的。其实在升级为轻量级锁之前,虚拟机会让线程A尽快在安全点挂起,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A之前正在同步代码块中,那么线程B自旋等待即可。如果线程A之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到。这部分内容我之前也没有深入研究过,如果有说的不对的,请多多指教啊!

可重入锁(递归锁)

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。如果你需要不可重入锁,只能自己去实现了。网上不可重入锁的实现真的很多,就不在这里贴代码了。99%的业务场景用可重入锁就可以了,剩下的1%是什么呢?我也不知道,谁可以在评论里告诉我?
3

JDK提供的Lock的实现类都是可重入的

公平锁、非公平锁

如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。
4

ReentrantLock构造器可以指定为公平或非公平

对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。

可中断锁

可中断锁,字面意思是“可以响应中断的锁”。

这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。[2]

回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。

在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Lock接口 */
public interface Lock {

void lock(); // 拿不到锁就一直等,拿到马上返回。

void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

void unlock();

Condition newCondition();
}

读写锁、共享锁、互斥锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。
5

记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改。

读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程,如果仍有疑惑可以再回到第一、二小节,看一下什么是“乐观锁”。

JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。

回到悲观锁和乐观锁

这篇文章经历过一次修改,我之前认为偏向锁和轻量级锁是乐观锁,重量级锁和Lock实现类为悲观锁,网上很多资料对这些概念的表述也很模糊,各执一词。

先抛出我的结论:

我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法

那JDK并发包里到底有没有乐观锁呢?

有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。
6

原子类AtomicInteger的自增方法为乐观锁策略

为什么网上有些资料认为偏向锁、轻量级锁是乐观锁?理由是它们底层用到了CAS?或者是把“乐观/悲观”与“轻量/重量”搞混了?其实,线程在抢占这些锁的时候,确实是循环+CAS的操作,感觉好像是乐观锁。但问题的关键是,我们说一个锁是悲观锁还是乐观锁,总是应该站在应用层,看它们是如何锁住应用数据的,而不是站在底层看抢占锁的过程。如果一个线程尝试获取锁时,发现已经被占用,它是否继续读取数据,等后续要更新时再决定要不要重试?对于偏向锁、轻量级锁来说,显然答案是否定的。无论是挂起还是忙等,对应用数据的读取操作都被“挡住”了。从这个角度看,它们确实是悲观锁。

退一步讲,也没有必要在这些术语上狠钻牛角尖,最重要的是理解它们的运行机制。想写得尽量简单一些,却发现洋洋洒洒近万字,只讲了个皮毛。深知自己水平有限,不敢保证完全正确,只能说路漫漫其修远兮,望指正。

  1. 安装 VBox 和 vagrant
  2. 下载系统的 box 文件
  3. vagrant box add box名称 box文件地址
  4. vagrant init box名称
  5. vagrant up:启动
  6. vagrant ssh:登陆
  7. vagrant halt:关机
  8. vagrant logout:退出登陆
  • vagrant box list:列出 vagrant 当前 box 列表
  • vagrant box remove box名称
  • vagrant destroy:停止当前正在运行的虚拟机并销毁所有创建的资源
  • vagrant package:把当前的运行的虚拟机环境进行打包为 box 文件
  • vagrant plugin:安装卸载插件
  • vagrant reload:重新启动虚拟机,重新载入配置文件
  • vagrant resume:恢复被挂起的状态
  • vagrant status:获取当前虚拟机的状态
  • vagrant suspend:挂起当前的虚拟机
  • vagrant global-status:查看当前 vagrant 管理的所有 vm 信息

virtualenv --no-site-packages venv

参数--no-site-packages,这样,已经安装到系统 Python 环境中的所有第三方包都不会复制过来,这样,我们就得到了一个不带任何第三方包的“干净”的 Python 运行环境。

source venv/bin/activate

deactivate

转自:https://www.jianshu.com/p/a3fbb3af9ab7

经常会看到在修改了某些环境变量之后会执行source命令,到底source命令做了那些事?让我们来探究一下。

source的作用

source命令:

source命令也称为“点命令”,也就是一个点符号(.),是bash的内部命令。

功能:使 Shell 读入指定的 Shell 程序文件并依次执行文件中的所有语句

使用范例:

source filename

. filename(中间有空格)

注意:上述两种表达方式都是正确的并且等效的

为什么在修改了环境变量后执行source命令

例如当我修改了 /etc/profile 文件,我想让它立刻生效,而不用重新登录;这时就想到用 source 命令,如:source /etc/profile。这样就重新执行刚修改的初始化文件,使之立即生效

source filenamesh filename./filename 执行脚本的区别在那里呢?

  1. 当shell脚本具有可执行权限时,用 sh filename./filename 执行脚本是没有区别得。./filename 是因为当前目录没有在 PATH 中,所有”.”是用来表示当前目录的。

  2. sh filename 重新建立一个子 shell,在子 shell 中执行脚本里面的语句,该子 shell 继承父 shell 的环境变量,但子 shell 新建的、改变的变量不会被带回父 shell,除非使用 export。

  3. source filename:这个命令其实只是简单地读取脚本里面的语句依次在当前 shell 里面执行,没有建立新的子 shell。那么脚本里面所有新建、改变变量的语句都会保存在当前 shell 里面。(当这个 shell 关闭后就失效了)

举例说明:

  1. 新建一个 test.sh 脚本,内容为:A=1
  2. 然后使其可执行 chmod +x test.sh
  3. 运行 sh test.sh 后,echo $A,显示为空,因为 A=1 并未传回给当前 shell
  4. 运行 ./test.sh 后,也是一样的效果
  5. 运行 source test.sh 或者 . test.sh,然后 echo $A,则会显示 1,说明 A=1 的变量在当前 shell 中。

矩阵的等价(只有秩相同),合同(秩和正负惯性指数相同),相似(秩,正负惯性指数,特征值均相同)是矩阵亲密关系的一步步深化。

矩阵等价

若存在可逆矩阵 $P、Q$,使 $PAQ=B$,则 $A$ 与 $B$ 等价。所谓矩阵 $A$ 与矩阵 $B$ 等价,即 $A$ 经过初等变换可得到 $B$。秩是矩阵等价的不变量,两同型矩阵相似的本质是秩相似。

矩阵合同

$C^TAC=B$。原来是同一个向量双线性运算在不同坐标系下的过渡关系啊。换句话说,无论你怎么换坐标系,向量双线性运算的值只差一个合同矩阵。矩阵合同是说同一个向量双线性运算在不同坐标系下的表示。

我们经常要进行相似变换,来简化矩阵的计算,两个双线性型在进行相似变换前后算出的值不一样,多少会给我们带来麻烦。举个例子:我们知道在力学里面,力和位移的内积表示能量,我们换了一组基之后再计算新的力和新的位移的内积,得到了不同的能量,显然不符合能量守恒对吧!于是我们就想找出新旧坐标系下双线性型的关系。

矩阵相似

$P^-1AP=B$。针对方阵而言;本质是二者有相等的不变因子;可看作是同一线性变换在不同基下的矩阵;矩阵相似必等价,但等价不一定相似;

线性变换就是电影院中播放的电影,不同的基坐在不同的位置观看。同一部“电影”,不同基“看到”的就是不同的矩阵。两个矩阵相似。

坐标变换1
坐标变换2

  • 线性变换(linear transformation):在两个向量空间(包括由函数构成的抽象的向量空间)之间的一种保持向量加法和标量乘法的特殊映射。【就要求变换前后的加法和数乘运算不变】【旋转(rotation)、镜像(翻转)(reflection)、伸缩(缩放)(scaling),推移(错切)(shear)】
    • 变换前是直线的,变换后依然是直线
    • 直线比例保持不变
    • 变换前是原点的,变换后依然是原点
  • 刚体变换(rigid transformation)或欧式变换(Euclidean transformation):在三维空间中,当物体不发生形变时,对一个几何物体作旋转、平移的运动,称为刚体变换。【可以理解为保持长度,角度,面积等不变的仿射变换, 即保持内积和度量不变】【平移(translation)、旋转(rotation)】
  • 仿射变换:指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。【线性变换 + 平移(translation)】【保证物体形状的“平直性”和“平行性”】
    • 变换前是直线的,变换后依然是直线
    • 直线比例保持不变
  • 透视变换(perspective transformation)或投影变换(projective transformation):利用透视中心、像点、目标点三点共线的条件,按透视旋转定律使承影面(透视面)绕迹线(透视轴)旋转某一角度,破坏原有的投影光线束,仍能保持承影面上投影几何图形不变的变换。【仿射变换是透视变换的子集,仿射变换是特殊的透射变换】【不能保证物体形状的“平行性”】
  • 相似变换(similarity transformation):【刚体变换 + uniform scaling】

基础概念

放缩(scaling)可进一步分为 uniform scaling 和 non-uniform scaling,前者每个坐标轴放缩系数相同(各向同性),后者不同;如果放缩系数为负,则会叠加上反射(reflection)—— reflection可以看成是特殊的 scaling。

旋转
翻转
伸缩
推移
基础概念

线性变换

线性变换-旋转1
线性变换-旋转2
线性变换-推移1
线性变换-推移2
线性变换-旋转+推移

仿射变换

仿射变换

仿射变换与线性变换

为了涵盖平移,引入齐次坐标,在原有2维坐标的基础上,增广1个维度,如下所示:
$$
\left[\begin{array}{l}{x^{\prime}} \ {y^{\prime}} \ {1}\end{array}\right]=\left[\begin{array}{lll}{a} & {b} & {c} \ {d} & {e} & {f}\end{array}\right]\left[\begin{array}{l}{x} \ {y} \ {1}\end{array}\right]
$$
所以,仿射变换的变换矩阵统一用 $\left[\begin{array}{lll}{a} & {b} & {c} \ {d} & {e} & {f}\end{array}\right]$ 来描述,不同基础变换的 $a, b, c, d, e, f$ 约束不同,如下所示:

仿射变换2

仿射变换

透视变换

透视变换

仿射变换与透视变换

仿射变换:保证物体形状的“平直性”和“平行性”
透视变换:不能保证物体形状的“平行性”

仿射变换-例子
透视变换-例子